"""/api/registry — the satellite registry API (SATELLITE-CONTRACT.md;
ADR #014). Server-to-server ONLY: these routes carry NO app session — the
default-deny session gate deliberately bypasses ``/api/registry/*``
(``middleware.REGISTRY_PREFIX``) and this router enforces its OWN service
token instead (``Authorization: Bearer <token>``, timing-safe, fails
closed). A browser session is NEVER accepted here.

Thin HTTP layer over ``services/registry``: it shapes requests, gates the
token, and translates ``ServiceError`` -> the structured ApiError triple.
"""

from __future__ import annotations

import hmac
import uuid

from fastapi import APIRouter, Depends, Request
from pydantic import BaseModel
from starlette.responses import JSONResponse

from app.config import settings
from app.routers.deps import ApiError, SessionDep
from app.services import registry as registry_svc
from app.services.errors import ServiceError


def require_registry_token(request: Request) -> None:
    """Service-token gate for every ``/api/registry/*`` route (ADR #014).

    Fails CLOSED: an unconfigured token => 503 (the surface is OFF), never
    an open door. A missing/malformed bearer => 401 REGISTRY_AUTH_REQUIRED;
    a present-but-wrong token => 401 REGISTRY_AUTH_INVALID. The comparison
    is timing-safe and the token is NEVER logged or echoed.

    Registered as a router-level dependency so it is solved BEFORE the
    per-route ``SessionDep`` — a denied request never opens a DB session.
    """
    configured = settings.registry_service_token
    if not configured:
        raise ApiError(
            503,
            "REGISTRY_DISABLED",
            "The registry API is not enabled.",
            "No service token is configured on the server.",
        )
    header = request.headers.get("Authorization", "")
    scheme, _, token = header.partition(" ")
    if scheme.lower() != "bearer" or not token:
        raise ApiError(
            401,
            "REGISTRY_AUTH_REQUIRED",
            "A bearer service token is required.",
            "Send the header `Authorization: Bearer <token>`.",
        )
    if not hmac.compare_digest(token.encode("utf-8"), configured.encode("utf-8")):
        raise ApiError(401, "REGISTRY_AUTH_INVALID", "Invalid service token.")


router = APIRouter(dependencies=[Depends(require_registry_token)])


def _run(fn, *args, **kwargs):
    try:
        return fn(*args, **kwargs)
    except ServiceError as exc:
        raise ApiError(exc.status, exc.code, exc.summary, exc.detail) from exc


def _uuid_or_404(raw: str, code: str, summary: str) -> uuid.UUID:
    """Parse an opaque path id. A malformed id can't name anything, so it's
    a clean 404 in the endpoint's own not-found vocabulary — never a 500,
    and never a leak of parser internals (ids are opaque to satellites)."""
    try:
        return uuid.UUID(raw)
    except (ValueError, AttributeError) as exc:
        raise ApiError(404, code, summary, "The id is not a valid identifier.") from exc


class CreateOrgBody(BaseModel):
    name: str
    allow_duplicate: bool = False


# ---------------------------------------------------------------- resolve


@router.get("/api/registry/parties/{party_id}")
def get_party(party_id: str, session: SessionDep) -> dict:
    pid = _uuid_or_404(party_id, "PARTY_NOT_FOUND", "No party with that id.")
    return _run(registry_svc.resolve_party, session, pid)


# ---------------------------------------------------------------- search


@router.get("/api/registry/parties")
def search_parties(
    session: SessionDep,
    query: str | None = None,
    kind: str | None = None,
    limit: str | None = None,
) -> dict:
    parsed_limit: int | None = None
    if limit is not None and limit.strip() != "":
        try:
            parsed_limit = int(limit)
        except ValueError as exc:
            raise ApiError(
                422, "BAD_LIMIT", "limit must be an integer.", f"Got {limit!r}."
            ) from exc
    return _run(
        registry_svc.search_parties,
        session,
        query=query,
        kind=kind,
        limit=parsed_limit,
    )


# ---------------------------------------------------------------- create


@router.post("/api/registry/organizations", status_code=201)
def create_organization(body: CreateOrgBody, session: SessionDep):
    try:
        return registry_svc.create_organization(
            session, name=body.name, allow_duplicate=body.allow_duplicate
        )
    except registry_svc.OrgExistsError as exc:
        # 409 carries the extra `existing` key alongside the triple so the
        # satellite can LINK the existing org instead of duplicating it.
        return JSONResponse(
            status_code=409,
            content={
                "error_code": exc.code,
                "summary": exc.summary,
                "detail": exc.detail,
                "existing": exc.existing,
            },
        )
    except ServiceError as exc:
        raise ApiError(exc.status, exc.code, exc.summary, exc.detail) from exc


# ---------------------------------------------------------------- projects


@router.get("/api/registry/projects/{project_id}")
def get_project(project_id: str, session: SessionDep) -> dict:
    pid = _uuid_or_404(project_id, "PROJECT_NOT_FOUND", "No project with that id.")
    return _run(registry_svc.get_project, session, pid)


# ---------------------------------------------------------------- org reference


@router.get("/api/registry/organizations/{party_id}/reference")
def get_org_reference(party_id: str, session: SessionDep) -> dict:
    pid = _uuid_or_404(party_id, "ORG_NOT_FOUND", "No organization with that id.")
    return _run(registry_svc.get_org_reference, session, pid)
