"""In-memory runtime contract hot-swap helpers."""

from __future__ import annotations

import asyncio
from pathlib import Path
from typing import Any, cast

from fastapi import FastAPI, Request, Response
from starlette.routing import Route

from app.api.registry import (
    FallbackMode,
    HandlerRegistry,
    register_contract_routes,
    register_legacy_handler_routes,
    register_unbound_handler_routes,
)
from app.compatibility import (
    CompatibilityDimension,
    CompatibilityReport,
    build_report,
    load_evidence_manifest,
    resolve_evidence_path,
)
from app.config import Settings
from app.contracts.model import Snapshot

_ADMIN_ROUTE_NAMES = frozenset(
    {
        "admin:compatibility",
        "admin:compatibility.md",
        "admin:compatibility.html",
    }
)


def clear_contract_routes(app: FastAPI) -> None:
    """Drop previously registered contract (and optional admin) routes for rebuild."""

    app.router.routes = [route for route in app.router.routes if not _is_swappable_route(route)]
    app.openapi_schema = None


def _is_swappable_route(route: object) -> bool:
    name = getattr(route, "name", None)
    if not isinstance(name, str):
        return False
    return name.startswith("contract:") or name in _ADMIN_ROUTE_NAMES


def build_compatibility_for_snapshot(
    snapshot: Snapshot,
    handlers: HandlerRegistry,
    settings: Settings,
    *,
    require_evidence_match: bool = False,
) -> CompatibilityReport:
    """Build a compatibility report for the active primary snapshot.

    Evidence is resolved per ``snapshot.source_version``
    (``evidence/pve-{version}.json``). When ``require_evidence_match`` is true
    (cold start) a missing or mismatched ledger raises.
    """

    declared = frozenset(
        (path.path, method.verb.upper()) for path in snapshot.paths for method in path.methods
    )
    dimensions: dict[CompatibilityDimension, frozenset[tuple[str, str]]] = {
        CompatibilityDimension.ROUTE_METHOD: declared,
    }
    observed: frozenset[tuple[str, str]] = frozenset()
    verified: frozenset[tuple[str, str]] = frozenset()
    evidence_path = resolve_evidence_path(snapshot.source_version, settings)
    if evidence_path is not None:
        evidence = load_evidence_manifest(evidence_path)
        if evidence.source_version != snapshot.source_version:
            if require_evidence_match:
                raise ValueError("compatibility evidence version does not match contract")
        else:
            dimensions.update(evidence.dimension_map())
            dimensions[CompatibilityDimension.ROUTE_METHOD] = declared
            observed = evidence.observed_methods() & declared
            verified = evidence.verified_methods() & declared
    implemented_all = frozenset(handlers.keys())
    return build_report(
        snapshot,
        implemented=implemented_all & declared,
        observed=observed,
        verified=verified,
        dimensions=dimensions,
    )


def apply_runtime_contract(
    app: FastAPI,
    snapshot: Snapshot,
    *,
    handlers: HandlerRegistry,
    store_root: Path,
    fallback: FallbackMode,
    settings: Settings,
    require_evidence_match: bool = False,
    register_admin: bool = True,
) -> CompatibilityReport:
    """Replace `/api2/*` contract routes and refresh runtime app.state fields."""

    clear_contract_routes(app)
    registered = register_contract_routes(app, snapshot, handlers, fallback)
    registered = register_legacy_handler_routes(
        app,
        handlers,
        store_root,
        fallback,
        primary_version=snapshot.source_version,
        existing=registered,
    )
    register_unbound_handler_routes(app, handlers, fallback, existing=registered)
    report = build_compatibility_for_snapshot(
        snapshot,
        handlers,
        settings,
        require_evidence_match=require_evidence_match,
    )
    implemented_all = frozenset(handlers.keys())
    app.state.runtime_snapshot = snapshot
    app.state.runtime_source_version = snapshot.source_version
    app.state.handlers = handlers
    app.state.contract_store_root = store_root
    app.state.implemented_methods = implemented_all
    app.state.compatibility_report = report
    if register_admin:
        _ensure_admin_compatibility_routes(app)
    return report


async def apply_runtime_contract_locked(
    app: FastAPI,
    snapshot: Snapshot,
    *,
    handlers: HandlerRegistry,
    store_root: Path,
    fallback: FallbackMode,
    settings: Settings,
    require_evidence_match: bool = False,
    register_admin: bool = True,
) -> CompatibilityReport:
    """Serialize concurrent Apply calls to avoid a torn route table."""

    lock = getattr(app.state, "contract_swap_lock", None)
    if lock is None:
        lock = asyncio.Lock()
        app.state.contract_swap_lock = lock
    async with lock:
        return apply_runtime_contract(
            app,
            snapshot,
            handlers=handlers,
            store_root=store_root,
            fallback=fallback,
            settings=settings,
            require_evidence_match=require_evidence_match,
            register_admin=register_admin,
        )


def contract_store_root(settings: Settings) -> Path:
    """Resolve the revision store root next to ``CONTRACT_SNAPSHOT``."""

    if settings.contract_snapshot is None:
        return Path("contracts")
    snapshot_path = settings.contract_snapshot.resolve()
    if snapshot_path.name == "snapshot.json" and (snapshot_path.parent / "manifest.json").is_file():
        return snapshot_path.parent.parent
    return snapshot_path.parent


def runtime_version_payload(request: Request) -> dict[str, str]:
    """Proxmox-shaped version payload derived from the active runtime contract."""

    version = getattr(request.app.state, "runtime_source_version", None) or "0.0"
    release = str(version).split("-", 1)[0]
    if release.count(".") >= 2:
        release = ".".join(release.split(".")[:2])
    return {"version": str(version), "release": release, "repoid": "simulator"}


def _ensure_admin_compatibility_routes(app: FastAPI) -> None:
    existing = {
        getattr(route, "name", None) for route in app.router.routes if isinstance(route, Route)
    }
    if "admin:compatibility" in existing:
        return

    @app.get("/admin/compatibility", include_in_schema=False, name="admin:compatibility")
    async def compatibility_report(request: Request) -> dict[str, Any]:
        report = getattr(request.app.state, "compatibility_report", None)
        if report is None:
            return {}
        return cast(dict[str, Any], report.as_json())

    @app.get("/admin/compatibility.md", include_in_schema=False, name="admin:compatibility.md")
    async def compatibility_report_markdown(request: Request) -> Response:
        report = getattr(request.app.state, "compatibility_report", None)
        body = report.as_markdown() if report is not None else ""
        return Response(body, media_type="text/markdown")

    @app.get("/admin/compatibility.html", include_in_schema=False, name="admin:compatibility.html")
    async def compatibility_report_html(request: Request) -> Response:
        report = getattr(request.app.state, "compatibility_report", None)
        body = report.as_html() if report is not None else ""
        return Response(body, media_type="text/html")
