from __future__ import annotations

import os
import re
import secrets
from collections.abc import Callable
from datetime import UTC, datetime
from pathlib import Path
from typing import Annotated, Any, Final

from fastapi import APIRouter, Header, HTTPException, Query
from fastapi import Path as PathParameter
from pydantic import BaseModel, ConfigDict, Field

from k1link.simulation import (
    QualificationRun,
    QualificationRunIntegrityError,
    QualificationRunNotFoundError,
    QualificationRunStore,
)
from k1link.simulation.worker_gateway import (
    STATUS_SCHEMA,
    PolygonWorkerGateway,
    SimulationWorkerGatewayError,
    SimulationWorkerRejectedError,
    SimulationWorkerUnavailableError,
    UnixSocketWorkerGateway,
)

POLYGON_RUNS_ROOT_ENV: Final = "MISSIONCORE_POLYGON_RUNS_ROOT"
POLYGON_WORKER_SOCKET_ENV: Final = "MISSIONCORE_POLYGON_WORKER_SOCKET"
POLYGON_WORKER_CONTROL_ENV: Final = "MISSIONCORE_POLYGON_WORKER_CONTROL"
MISSION_CORE_COMMIT_ENV: Final = "MISSIONCORE_COMMIT"
CATALOG_SCHEMA: Final = "missioncore.polygon-run-catalog/v1"
DETAIL_SCHEMA: Final = "missioncore.polygon-run-detail/v1"
COMMIT_PATTERN: Final = re.compile(r"^[a-f0-9]{40}$")
READ_ONLY_LIMITATIONS: Final[tuple[str, ...]] = (
    "Архивный UI-0 публикует только квалификационные доказательства; "
    "содержимое команд и mutation-маршруты в архиве отсутствуют.",
    "Артефакты представлены относительными метаданными; их содержимое не публикуется этим API.",
    "Терминальное состояние прогона не означает приёмку навигации, "
    "восприятия или физической безопасности.",
)

RootProvider = Callable[[], Path | None]
WorkerProvider = Callable[[], PolygonWorkerGateway | None]
ControlProvider = Callable[[], bool]
CommitProvider = Callable[[], str | None]


class StartStockRoverRequest(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)

    scenario_id: str


class AckermannCommandRequest(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)

    speed_mps: float = Field(ge=-1.5, le=1.5, allow_inf_nan=False)
    steering_normalized: float = Field(ge=-1.0, le=1.0, allow_inf_nan=False)


def configured_polygon_runs_root() -> Path | None:
    raw = os.environ.get(POLYGON_RUNS_ROOT_ENV)
    if raw is None or not raw.strip():
        return None
    return Path(raw.strip()).expanduser()


def configured_polygon_worker() -> PolygonWorkerGateway | None:
    raw = os.environ.get(POLYGON_WORKER_SOCKET_ENV)
    if raw is None or not raw.strip():
        return None
    return UnixSocketWorkerGateway(Path(raw.strip()).expanduser())


def configured_polygon_control() -> bool:
    return os.environ.get(POLYGON_WORKER_CONTROL_ENV, "").strip() == "internal-virtual-only"


def configured_mission_core_commit() -> str | None:
    raw = os.environ.get(MISSION_CORE_COMMIT_ENV)
    if raw is None:
        return None
    normalized = raw.strip()
    return normalized if COMMIT_PATTERN.fullmatch(normalized) else None


def build_polygon_router(
    *,
    root_provider: RootProvider = configured_polygon_runs_root,
    worker_provider: WorkerProvider = configured_polygon_worker,
    control_provider: ControlProvider = configured_polygon_control,
    commit_provider: CommitProvider = configured_mission_core_commit,
) -> APIRouter:
    router = APIRouter(prefix="/api/v1/polygon", tags=["polygon"])

    @router.get("/runs")
    def list_polygon_runs(
        limit: Annotated[int, Query(ge=1, le=100)] = 20,
    ) -> dict[str, Any]:
        store = _open_read_only_store(root_provider)
        try:
            ordered = sorted(
                store.list_runs(),
                key=lambda run: (run.created_at_utc, run.run_id),
                reverse=True,
            )
        except QualificationRunIntegrityError as exc:
            raise HTTPException(
                status_code=500,
                detail="Журнал прогонов Полигона не прошёл проверку целостности.",
            ) from exc
        return {
            "schema_version": CATALOG_SCHEMA,
            "access": "read-only",
            "items": [_run_summary(run) for run in ordered[:limit]],
            "total": len(ordered),
            "limitations": list(READ_ONLY_LIMITATIONS),
        }

    @router.get("/runs/{run_id}")
    def get_polygon_run(
        run_id: Annotated[
            str,
            PathParameter(pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"),
        ],
        event_limit: Annotated[int, Query(ge=1, le=500)] = 200,
    ) -> dict[str, Any]:
        store = _open_read_only_store(root_provider)
        try:
            run = store.load(run_id)
            events = store.list_events(run_id)
            commands = store.list_commands(run_id)
        except QualificationRunNotFoundError as exc:
            raise HTTPException(status_code=404, detail="Прогон Полигона не найден.") from exc
        except QualificationRunIntegrityError as exc:
            raise HTTPException(
                status_code=500,
                detail="Доказательства прогона не прошли проверку целостности.",
            ) from exc
        visible_events = events[-event_limit:]
        return {
            "schema_version": DETAIL_SCHEMA,
            "access": "read-only",
            "run": {
                **_run_summary(run),
                "scenario_sha256": run.scenario_sha256,
                "profile_sha256": run.profile_sha256,
                "host_profile_sha256": run.host_profile_sha256,
                "seed": run.seed,
                "parent_run_id": run.parent_run_id,
                "providers": [provider.to_dict() for provider in run.providers],
                "authority": run.authority.to_dict(),
            },
            "events": [event.to_dict() for event in visible_events],
            "events_total": len(events),
            "events_truncated": len(visible_events) != len(events),
            "commands": {
                "count": len(commands),
                "content_exposed": False,
            },
            "artifacts": [artifact.to_dict() for artifact in run.artifacts],
            "limitations": list(READ_ONLY_LIMITATIONS),
        }

    @router.get("/worker")
    def get_polygon_worker() -> dict[str, Any]:
        gateway = worker_provider()
        if gateway is None:
            return _unavailable_worker_status()
        try:
            status = gateway.status()
        except SimulationWorkerGatewayError:
            return _unavailable_worker_status()
        return {
            **status,
            "control_available": bool(status["control_available"] and control_provider()),
        }

    @router.get("/worker/live")
    def get_polygon_worker_live() -> dict[str, Any]:
        gateway = _required_worker(worker_provider)
        try:
            return gateway.live()
        except SimulationWorkerUnavailableError as exc:
            raise HTTPException(
                status_code=503,
                detail="Simulation Worker недоступен.",
            ) from exc
        except SimulationWorkerRejectedError as exc:
            raise HTTPException(status_code=409, detail=str(exc)) from exc
        except SimulationWorkerGatewayError as exc:
            raise HTTPException(
                status_code=502,
                detail="Simulation Worker вернул некорректное состояние.",
            ) from exc

    @router.post("/worker/runs")
    def start_polygon_worker_run(
        request: StartStockRoverRequest,
        idempotency_key: Annotated[
            str,
            Header(alias="Idempotency-Key", min_length=1, max_length=160),
        ],
    ) -> dict[str, Any]:
        if request.scenario_id != "stock-rover-ackermann":
            raise HTTPException(status_code=422, detail="Сценарий Полигона не поддерживается.")
        gateway, commit = _control_context(
            worker_provider,
            control_provider,
            commit_provider,
        )
        run_id = _new_run_id(commit)
        try:
            return gateway.start(
                run_id=run_id,
                mission_core_commit=commit,
                idempotency_key=idempotency_key,
            )
        except SimulationWorkerUnavailableError as exc:
            raise HTTPException(status_code=503, detail="Simulation Worker недоступен.") from exc
        except SimulationWorkerRejectedError as exc:
            raise HTTPException(status_code=409, detail=str(exc)) from exc
        except SimulationWorkerGatewayError as exc:
            raise HTTPException(
                status_code=502,
                detail="Simulation Worker не подтвердил запуск.",
            ) from exc

    @router.post("/worker/runs/{run_id}/stop")
    def stop_polygon_worker_run(
        run_id: Annotated[
            str,
            PathParameter(pattern=r"^[a-z0-9][a-z0-9-]{0,63}$"),
        ],
        idempotency_key: Annotated[
            str,
            Header(alias="Idempotency-Key", min_length=1, max_length=160),
        ],
    ) -> dict[str, Any]:
        gateway, _ = _control_context(
            worker_provider,
            control_provider,
            commit_provider,
        )
        try:
            return gateway.stop(run_id=run_id, idempotency_key=idempotency_key)
        except SimulationWorkerUnavailableError as exc:
            raise HTTPException(status_code=503, detail="Simulation Worker недоступен.") from exc
        except SimulationWorkerRejectedError as exc:
            raise HTTPException(status_code=409, detail=str(exc)) from exc
        except SimulationWorkerGatewayError as exc:
            raise HTTPException(
                status_code=502,
                detail="Simulation Worker не подтвердил остановку.",
            ) from exc

    @router.post("/worker/runs/{run_id}/commands")
    def command_polygon_worker_run(
        run_id: Annotated[
            str,
            PathParameter(pattern=r"^[a-z0-9][a-z0-9-]{0,63}$"),
        ],
        request: AckermannCommandRequest,
        idempotency_key: Annotated[
            str,
            Header(alias="Idempotency-Key", min_length=1, max_length=160),
        ],
    ) -> dict[str, Any]:
        gateway, _ = _control_context(
            worker_provider,
            control_provider,
            commit_provider,
        )
        try:
            return gateway.command(
                run_id=run_id,
                speed_mps=request.speed_mps,
                steering_normalized=request.steering_normalized,
                idempotency_key=idempotency_key,
            )
        except SimulationWorkerUnavailableError as exc:
            raise HTTPException(status_code=503, detail="Simulation Worker недоступен.") from exc
        except SimulationWorkerRejectedError as exc:
            raise HTTPException(status_code=409, detail=str(exc)) from exc
        except SimulationWorkerGatewayError as exc:
            raise HTTPException(
                status_code=502,
                detail="Simulation Worker не подтвердил команду ровера.",
            ) from exc

    return router


def _open_read_only_store(root_provider: RootProvider) -> QualificationRunStore:
    root = root_provider()
    if root is None:
        raise HTTPException(
            status_code=503,
            detail="Источник журналов Полигона не настроен на этом экземпляре Mission Core.",
        )
    if not root.is_absolute():
        raise HTTPException(
            status_code=503,
            detail="Источник журналов Полигона настроен некорректно.",
        )
    try:
        return QualificationRunStore(root, read_only=True)
    except QualificationRunIntegrityError as exc:
        raise HTTPException(
            status_code=503,
            detail="Источник журналов Полигона недоступен или настроен некорректно.",
        ) from exc


def _run_summary(run: QualificationRun) -> dict[str, Any]:
    return {
        "run_id": run.run_id,
        "episode_id": run.episode_id,
        "kind": run.kind.value,
        "state": run.state.value,
        "created_at_utc": run.created_at_utc,
        "started_at_utc": run.started_at_utc,
        "ended_at_utc": run.ended_at_utc,
        "terminal_reason": run.terminal_reason,
        "scenario_generation": run.scenario_generation,
        "profile_generation": run.profile_generation,
        "mission_core_commit": run.mission_core_commit,
        "host_profile_id": run.host_profile_id,
        "reproducibility_tier": run.reproducibility_tier.value,
        "clock_domain": run.clock_domain,
        "revision": run.revision,
        "provider_ids": [provider.identifier for provider in run.providers],
        "artifact_count": len(run.artifacts),
    }


def _required_worker(provider: WorkerProvider) -> PolygonWorkerGateway:
    gateway = provider()
    if gateway is None:
        raise HTTPException(
            status_code=503,
            detail="Simulation Worker не зарегистрирован на этом экземпляре Mission Core.",
        )
    return gateway


def _control_context(
    worker_provider: WorkerProvider,
    control_provider: ControlProvider,
    commit_provider: CommitProvider,
) -> tuple[PolygonWorkerGateway, str]:
    if not control_provider():
        raise HTTPException(
            status_code=403,
            detail="Lifecycle Полигона отключён на этом экземпляре Mission Core.",
        )
    gateway = _required_worker(worker_provider)
    commit = commit_provider()
    if commit is None:
        raise HTTPException(
            status_code=503,
            detail="Mission Core не привязан к точной Git-ревизии.",
        )
    return gateway, commit


def _unavailable_worker_status() -> dict[str, Any]:
    return {
        "schema_version": STATUS_SCHEMA,
        "worker_id": "mission-gpu-s1",
        "transport": "unix",
        "mode": "simulation",
        "available": False,
        "control_available": False,
        "active_run_id": None,
        "run_state": None,
        "provider_ids": [],
        "isolation": {
            "network": "unavailable",
            "process_identity": "missioncore",
            "artifact_policy": "d-only",
        },
        "authority": {
            "scope": "virtual-only",
            "actuator_authority": False,
            "direct_actuator_setpoints_allowed": False,
        },
    }


def _new_run_id(commit: str) -> str:
    stamp = datetime.now(UTC).strftime("%Y%m%dt%H%M%Sz").lower()
    return f"s1c-{commit[:7]}-{stamp}-{secrets.token_hex(3)}"
