"""FastAPI service for the public sandbox and authenticated tenant runtime."""

from __future__ import annotations

import os
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Annotated, Any

from fastapi import Depends, FastAPI, Header, HTTPException, Request, Query
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, ConfigDict, Field

from .contracts import GenerationRequest
from .demo import build_demo_runtime
from .errors import (
    AuthenticationError,
    BackplaneError,
    RequestLimitError,
    UnsupportedCapability,
)
from .runtime import BackplaneRuntime
from .security import TokenBucket


class DemoRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    tenant: str = Field(pattern=r"^(support|operations|research)$")
    prompt: str = Field(min_length=1, max_length=2_000)


class ChatMessage(BaseModel):
    model_config = ConfigDict(extra="forbid")

    role: str
    content: str


class ChatCompletionRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    model: str = "backplane"
    messages: list[ChatMessage] = Field(min_length=1, max_length=64)
    max_tokens: int = Field(default=96, ge=1, le=512)


def create_app(
    runtime: BackplaneRuntime | None = None,
    *,
    state_path: str | Path | None = None,
    admin_token: str | None = None,
) -> FastAPI:
    state_path = state_path or os.environ.get(
        "BACKPLANE_STATE", "./work/backplane-state.sqlite3"
    )
    runtime = runtime or build_demo_runtime(state_path)
    limiter = TokenBucket(
        rate_per_minute=int(os.environ.get("BACKPLANE_RATE_PER_MINUTE", "30")),
        burst=int(os.environ.get("BACKPLANE_RATE_BURST", "10")),
    )

    @asynccontextmanager
    async def lifespan(_: FastAPI):
        try:
            yield
        finally:
            await runtime.close()

    app = FastAPI(
        title="Backplane Runtime",
        version="0.1.0",
        docs_url="/api/docs",
        openapi_url="/api/openapi.json",
        lifespan=lifespan,
    )
    origins = [
        value.strip()
        for value in os.environ.get(
            "BACKPLANE_ALLOWED_ORIGINS",
            "https://backplane.cyberelf.link,http://localhost:3000",
        ).split(",")
        if value.strip()
    ]
    app.add_middleware(
        CORSMiddleware,
        allow_origins=origins,
        allow_credentials=False,
        allow_methods=["GET", "POST"],
        allow_headers=["Authorization", "Content-Type", "X-Backplane-Tenant"],
    )

    @app.middleware("http")
    async def enforce_limits(request: Request, call_next):
        length = int(request.headers.get("content-length", "0") or 0)
        if length > 65_536:
            return _error_response(413, "request body exceeds 64 KiB")
        try:
            limiter.consume(request.client.host if request.client else "unknown")
        except RequestLimitError as exc:
            return _error_response(429, str(exc))
        response = await call_next(request)
        response.headers["X-Content-Type-Options"] = "nosniff"
        response.headers["Cache-Control"] = "no-store"
        return response

    @app.exception_handler(BackplaneError)
    async def handle_backplane_error(_: Request, exc: BackplaneError):
        status = 400
        if isinstance(exc, AuthenticationError):
            status = 401
        elif isinstance(exc, UnsupportedCapability):
            status = 422
        return _error_response(status, str(exc))

    @app.exception_handler(Exception)
    async def handle_unexpected(_: Request, __: Exception):
        return _error_response(500, "runtime request failed")

    @app.get("/api/health")
    async def health() -> dict[str, Any]:
        return {
            "status": "ok",
            "product": "Backplane Runtime",
            "version": "0.1.0",
            "backend": runtime.backend.capabilities.model_dump(mode="json"),
        }

    @app.get("/api/capabilities")
    async def capabilities() -> dict[str, Any]:
        return {
            "backend": runtime.backend.capabilities.model_dump(mode="json"),
            "cards": [
                card.package.model_dump(mode="json")
                for card in sorted(runtime.cards.values(), key=lambda c: c.manifest.id)
            ],
        }

    @app.post("/api/demo/run")
    async def demo_run(body: DemoRequest) -> dict[str, Any]:
        result = await runtime.run(
            GenerationRequest(tenant_id=body.tenant, prompt=body.prompt),
            authenticate=False,
        )
        return result.model_dump(mode="json")

    async def tenant_auth(
        authorization: Annotated[str | None, Header()] = None,
        x_backplane_tenant: Annotated[str | None, Header()] = None,
    ) -> tuple[str, str]:
        if not authorization or not authorization.startswith("Bearer "):
            raise AuthenticationError("bearer tenant credential required")
        if not x_backplane_tenant:
            raise AuthenticationError("X-Backplane-Tenant is required")
        return x_backplane_tenant, authorization.removeprefix("Bearer ").strip()

    @app.post("/v1/chat/completions")
    async def chat(
        body: ChatCompletionRequest,
        identity: tuple[str, str] = Depends(tenant_auth),
    ) -> dict[str, Any]:
        tenant_id, key = identity
        prompt = "\n".join(
            f"{message.role}: {message.content}" for message in body.messages
        )
        result = await runtime.run(
            GenerationRequest(
                tenant_id=tenant_id,
                prompt=prompt,
                max_new_tokens=body.max_tokens,
            ),
            tenant_key=key,
        )
        return {
            "id": result.receipt.request_id,
            "object": "chat.completion",
            "model": result.receipt.model,
            "choices": [
                {
                    "index": 0,
                    "message": {"role": "assistant", "content": result.text},
                    "finish_reason": "stop",
                }
            ],
            "usage": result.receipt.token_usage.model_dump(),
            "backplane_receipt": result.receipt.model_dump(mode="json"),
        }

    @app.get("/api/receipts")
    async def receipt_history(
        limit: Annotated[int, Query(ge=1, le=200)] = 50,
        before: Annotated[str | None, Query(max_length=128)] = None,
        identity: tuple[str, str] = Depends(tenant_auth),
    ) -> dict[str, Any]:
        tenant_id, key = identity
        runtime.store.verify_tenant_key(tenant_id, key)
        try:
            return runtime.store.list_receipts(tenant_id, limit=limit, before=before)
        except ValueError as exc:
            raise HTTPException(status_code=400, detail=str(exc)) from exc

    @app.get("/api/receipts/{receipt_id}")
    async def receipt(
        receipt_id: str,
        identity: tuple[str, str] = Depends(tenant_auth),
    ) -> dict[str, Any]:
        tenant_id, key = identity
        runtime.store.verify_tenant_key(tenant_id, key)
        body = runtime.store.get_receipt(receipt_id, tenant_id)
        if body is None:
            raise HTTPException(status_code=404, detail="receipt not found")
        return body

    @app.get("/api/admin/status")
    async def admin_status(
        x_admin_token: Annotated[str | None, Header()] = None,
    ) -> dict[str, Any]:
        if not admin_token or x_admin_token != admin_token:
            raise AuthenticationError("management credential rejected")
        return {
            "registered_cards": len(runtime.cards),
            "backend": runtime.backend.capabilities.model_dump(mode="json"),
        }

    return app


def _error_response(status: int, message: str):
    from fastapi.responses import JSONResponse

    return JSONResponse(status_code=status, content={"error": message})
