import hmac
import re
import uuid
from contextlib import asynccontextmanager
from typing import Annotated

from fastapi import Depends, FastAPI, Header, Request
from fastapi.responses import JSONResponse, Response

from .capabilities import verify_computer_capability
from .config import get_settings
from .errors import ComputerRuntimeError
from .runtime import ComputerRuntime
from .schemas import (
    ActionResult,
    BrowserActionInput,
    CommandInput,
    HumanKeyboardInput,
    HumanPointerInput,
    SessionState,
    StartSessionInput,
    StreamFrame,
)

REQUEST_ID_PATTERN = re.compile(r"^[A-Za-z0-9._:-]{1,80}$")
runtime = ComputerRuntime(get_settings())


@asynccontextmanager
async def lifespan(_app: FastAPI):
    get_settings().require_production_configuration()
    await runtime.startup()
    try:
        yield
    finally:
        await runtime.shutdown()


app = FastAPI(title="Hayva Computer Agent", version="0.1.0", lifespan=lifespan)


async def require_service_token(
    x_service_token: Annotated[str | None, Header()] = None,
) -> None:
    expected = get_settings().read_service_token()
    if not expected:
        raise ComputerRuntimeError(
            "COMPUTER_SERVICE_AUTH_UNCONFIGURED",
            "Computer service authentication is unconfigured.",
            503,
        )
    if not x_service_token or not hmac.compare_digest(x_service_token, expected):
        raise ComputerRuntimeError(
            "COMPUTER_SERVICE_FORBIDDEN", "Service authentication failed.", 401
        )


ServiceAuth = Annotated[None, Depends(require_service_token)]


def request_id(request: Request) -> str:
    value = request.headers.get("x-request-id", "")
    if not REQUEST_ID_PATTERN.fullmatch(value):
        raise ComputerRuntimeError(
            "COMPUTER_REQUEST_ID_INVALID", "A valid request identifier is required.", 422
        )
    return value


def require_capability(
    request: Request, *, scope: str, session_id: uuid.UUID | None = None,
    workspace_id: uuid.UUID | None = None, fencing_token: int | None = None,
) -> None:
    verify_computer_capability(
        request.headers.get("x-computer-capability"), settings=get_settings(),
        expected_scope=scope, request_id=request_id(request), session_id=session_id,
        workspace_id=workspace_id, fencing_token=fencing_token,
    )


@app.exception_handler(ComputerRuntimeError)
async def runtime_error_handler(request: Request, error: ComputerRuntimeError):
    safe_request_id = request.headers.get("x-request-id", "")
    if not REQUEST_ID_PATTERN.fullmatch(safe_request_id):
        safe_request_id = "unavailable"
    return JSONResponse(
        status_code=error.status_code,
        content={
            "success": False,
            "error": {
                "code": error.code,
                "message": error.message,
                "request_id": safe_request_id,
            },
        },
    )


@app.get("/health")
async def health(_auth: ServiceAuth):
    configured = (
        get_settings().read_service_token() is not None
        and get_settings().read_capability_public_key() is not None
        and runtime.playwright is not None
    )
    payload = {
        "status": "ok" if configured else "unconfigured",
        "service": "computer-agent",
        "browser": "configured" if configured else "unconfigured",
    }
    return payload if configured else JSONResponse(status_code=503, content=payload)


@app.post("/v1/sessions", response_model=SessionState)
async def start_session(payload: StartSessionInput, request: Request, _auth: ServiceAuth):
    require_capability(
        request, scope="session:start", session_id=payload.session_id,
        workspace_id=payload.workspace_id, fencing_token=payload.fencing_token,
    )
    return await runtime.start_session(payload, request_id=request_id(request))


@app.post("/v1/sessions/{session_id}/pause", response_model=SessionState)
async def pause(
    session_id: uuid.UUID, payload: CommandInput, request: Request, _auth: ServiceAuth,
):
    require_capability(
        request, scope="session:pause", session_id=session_id,
        fencing_token=payload.fencing_token,
    )
    return await runtime.command(
        session_id, "pause", payload.fencing_token, request_id=request_id(request)
    )


@app.post("/v1/sessions/{session_id}/resume", response_model=SessionState)
async def resume(
    session_id: uuid.UUID, payload: CommandInput, request: Request, _auth: ServiceAuth,
):
    require_capability(
        request, scope="session:resume", session_id=session_id,
        fencing_token=payload.fencing_token,
    )
    return await runtime.command(
        session_id, "resume", payload.fencing_token, request_id=request_id(request)
    )


@app.post("/v1/sessions/{session_id}/stop", response_model=SessionState)
async def stop(
    session_id: uuid.UUID, payload: CommandInput, request: Request, _auth: ServiceAuth,
):
    require_capability(
        request, scope="session:stop", session_id=session_id,
        fencing_token=payload.fencing_token,
    )
    return await runtime.command(
        session_id, "stop", payload.fencing_token, request_id=request_id(request)
    )


@app.post("/v1/sessions/{session_id}/snapshot", response_model=SessionState)
async def snapshot(
    session_id: uuid.UUID, payload: CommandInput, request: Request, _auth: ServiceAuth,
):
    require_capability(
        request, scope="session:snapshot", session_id=session_id,
        fencing_token=payload.fencing_token,
    )
    return await runtime.snapshot(
        session_id, payload.fencing_token, request_id=request_id(request)
    )


@app.post("/v1/sessions/{session_id}/actions", response_model=ActionResult)
async def browser_action(
    session_id: uuid.UUID, payload: BrowserActionInput, request: Request,
    _auth: ServiceAuth,
):
    require_capability(
        request, scope=f"browser:{payload.tool}", session_id=session_id,
        fencing_token=payload.fencing_token,
    )
    return await runtime.action(session_id, payload, request_id=request_id(request))


@app.post("/v1/sessions/{session_id}/human/pointer", response_model=SessionState)
async def human_pointer(
    session_id: uuid.UUID, payload: HumanPointerInput, request: Request,
    _auth: ServiceAuth,
):
    require_capability(
        request, scope="human:pointer", session_id=session_id,
        fencing_token=payload.fencing_token,
    )
    return await runtime.human_pointer(session_id, payload, request_id=request_id(request))


@app.post("/v1/sessions/{session_id}/human/keyboard", response_model=SessionState)
async def human_keyboard(
    session_id: uuid.UUID, payload: HumanKeyboardInput, request: Request,
    _auth: ServiceAuth,
):
    require_capability(
        request, scope="human:keyboard", session_id=session_id,
        fencing_token=payload.fencing_token,
    )
    return await runtime.human_keyboard(session_id, payload, request_id=request_id(request))


@app.post("/v1/sessions/{session_id}/frame", response_model=StreamFrame)
async def frame(
    session_id: uuid.UUID, payload: CommandInput, request: Request, _auth: ServiceAuth,
):
    require_capability(
        request, scope="session:frame", session_id=session_id,
        fencing_token=payload.fencing_token,
    )
    return await runtime.frame(
        session_id, payload.fencing_token, request_id=request_id(request)
    )


@app.get("/v1/artifacts/{workspace_id}/{session_id}/{artifact_id}")
async def artifact(
    workspace_id: uuid.UUID, session_id: uuid.UUID, artifact_id: uuid.UUID,
    request: Request, _auth: ServiceAuth,
):
    require_capability(
        request, scope="artifact:read", session_id=session_id, workspace_id=workspace_id,
    )
    content = runtime.read_artifact(workspace_id, session_id, artifact_id)
    return Response(
        content=content, media_type="image/jpeg",
        headers={"cache-control": "no-store", "x-content-type-options": "nosniff"},
    )


@app.delete(
    "/v1/artifacts/{workspace_id}/{session_id}/{artifact_id}", status_code=204
)
async def delete_artifact(
    workspace_id: uuid.UUID, session_id: uuid.UUID, artifact_id: uuid.UUID,
    request: Request, _auth: ServiceAuth,
):
    require_capability(
        request, scope="artifact:delete", session_id=session_id,
        workspace_id=workspace_id,
    )
    runtime.delete_artifact(workspace_id, session_id, artifact_id)
    return Response(status_code=204)
