"""
FastAPI router exposing Mizan's HTTP endpoints:

    POST /call/                — RPC dispatch
    GET  /ctx/{context_name}/  — bundled context fetch

    from fastapi import FastAPI
    from mizan_fastapi import router, mizan_exception_handler, MizanError

    app = FastAPI()
    app.include_router(router, prefix="/api/mizan")
    app.add_exception_handler(MizanError, mizan_exception_handler)
"""

from __future__ import annotations

from typing import Any

from fastapi import APIRouter, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field

from mizan_core.registry import get_context_groups, get_function

from .executor import (
    ErrorCode,
    MizanError,
    NotFound,
    compute_invalidation,
    compute_merges,
    execute_function,
)


router = APIRouter()


def _no_store(payload: Any, status_code: int = 200) -> JSONResponse:
    return JSONResponse(payload, status_code=status_code, headers={"Cache-Control": "no-store"})


# ─── Endpoints ──────────────────────────────────────────────────────────────


@router.get("/session/")
async def session_init() -> JSONResponse:
    """Session-init probe. Parity with mizan-django's session endpoint.

    CSRF is a Django-only concern at the protocol level; FastAPI surfaces a
    null token so the response shape stays uniform across backends. The
    wire-parity harness uses this endpoint as its readiness probe.
    """
    return _no_store({"csrfToken": None})


class CallBody(BaseModel):
    fn: str = Field(..., min_length=1)
    args: dict[str, Any] = Field(default_factory=dict)


@router.post("/call/")
async def function_call(body: CallBody, request: Request) -> JSONResponse:
    """RPC dispatch — `{"fn": "...", "args": {...}}` → `{"result": ..., "invalidate": [...], "merge"?: [...]}`."""
    fn_class = get_function(body.fn)
    result = await execute_function(request, body.fn, body.args)
    invalidate = compute_invalidation(fn_class, body.args)
    merges = compute_merges(fn_class, body.args, result)
    payload: dict[str, Any] = {"result": result, "invalidate": invalidate}
    if merges:
        payload["merge"] = merges
    return _no_store(payload)


@router.get("/ctx/{context_name}/")
async def context_fetch(context_name: str, request: Request) -> JSONResponse:
    """Bundled context fetch — `{function_name: result, ...}` for every function in the context."""
    fn_names = get_context_groups().get(context_name)
    if not fn_names:
        raise NotFound(f"Context '{context_name}' not found")

    params = dict(request.query_params)
    bundled = {fn: await execute_function(request, fn, params) for fn in fn_names}
    return _no_store(bundled)


# ─── Exception handler ──────────────────────────────────────────────────────


async def mizan_exception_handler(_request: Request, exc: MizanError) -> JSONResponse:
    """FastAPI exception handler — renders MizanError to the protocol's error envelope."""
    body: dict[str, Any] = {"error": {"code": exc.code.value, "message": exc.message}}
    if exc.details:
        body["error"]["details"] = exc.details
    return _no_store(body, status_code=exc.status_code)


async def mizan_validation_handler(_request: Request, exc: RequestValidationError) -> JSONResponse:
    """Maps malformed request bodies (invalid JSON, missing top-level fields) to BAD_REQUEST."""
    return _no_store(
        {
            "error": {
                "code": ErrorCode.BAD_REQUEST.value,
                "message": "Invalid request body",
                "details": {"errors": exc.errors()},
            }
        },
        status_code=400,
    )
