"""Engine entity routes (Phase 16).

Read-only HTTP shape over engine entities. Mounted at /api by main.py.

  GET  /api/projects                                       — list_projects
  GET  /api/projects/{project_id}                          — get_project
  GET  /api/projects/{pid}/episodes                        — list_episodes (P3)
  GET  /api/projects/{pid}/episodes/{eid}/scenes           — list_scenes (P3)
  GET  /api/projects/{pid}/episodes/{eid}/scenes/{sid}/beats — list_beats
  GET  /api/beats/{beat_id}/takes                          — list_takes
  GET  /api/beats/{beat_id}/lineage                        — get_lineage
  GET  /api/events                                         — list_events
  GET  /api/memory                                         — list_memory

LegacyProjectFormatError raised by the projects adapter is caught here and
mapped to 410 Gone with detail; the same projects appear as warning events
in /api/events. The Console v2 omits them from the project tree.
"""

from __future__ import annotations

from typing import Optional

from fastapi import APIRouter, HTTPException, Query, status

from recoil.api.adapters._ids import validate_hierarchy_id, validate_project_id
from recoil.api.adapters.beats import (
    list_beats,
    list_episodes as _list_episodes,
    list_scenes as _list_scenes,
    list_takes,
)
from recoil.api.adapters.events import list_events
from recoil.api.adapters.lineage import get_lineage as _get_lineage
from recoil.api.adapters.memory import list_memory
from recoil.api.adapters.projects import (
    LegacyProjectFormatError,
    get_project as _get_project,
    list_projects,
)
from recoil.api.schemas.engine import (
    Beat,
    EngineEvent,
    Episode,
    EventSeverity,
    Lineage,
    MemoryEntry,
    MissingCanonicalField,
    MissingCanonicalFieldError,
    Project,
    Scene,
    Take,
)

router = APIRouter()


def _missing_canonical_422(exc: MissingCanonicalFieldError) -> HTTPException:
    """Map MissingCanonicalFieldError to a 422 HTTPException with the
    MissingCanonicalField body. Routes that call into adapters/beats.py or
    adapters/projects.py wrap their adapter calls with this helper so the
    error becomes a structured 422 the frontend can render as a fix-CLI hint
    instead of a generic 500."""
    return HTTPException(
        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        detail=MissingCanonicalField(
            error="missing_canonical_field",
            field=exc.field,
            project_id=exc.project_id,
            remediation=exc.remediation,
            fix_cli=exc.fix_cli,
        ).model_dump(),
    )


@router.get("/projects", response_model=list[Project])
def get_projects() -> list[Project]:
    try:
        return list_projects()
    except MissingCanonicalFieldError as exc:
        raise _missing_canonical_422(exc) from exc


@router.get("/projects/{project_id}", response_model=Project)
def get_project(project_id: str) -> Project:
    try:
        p = _get_project(project_id)
    except ValueError as exc:
        # Debug R1: path-traversal guard — malformed IDs become 400.
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=str(exc),
        ) from exc
    except LegacyProjectFormatError as exc:
        raise HTTPException(
            status_code=status.HTTP_410_GONE,
            detail=f"legacy project format: {exc.reason}",
        ) from exc
    except MissingCanonicalFieldError as exc:
        raise _missing_canonical_422(exc) from exc
    if p is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"unknown project {project_id!r}",
        )
    return p


@router.get("/projects/{project_id}/episodes", response_model=list[Episode])
def get_episodes(project_id: str) -> list[Episode]:
    """P3 — synthesized list of Episodes for a project (one per distinct
    episode_id across the project's shot files; ``synthesized=True`` on each)."""
    try:
        validate_project_id(project_id)
        return _list_episodes(project_id)
    except FileNotFoundError:
        # Unknown project (ProjectPaths.for_project raises when the project
        # dir does not exist) → contracted empty list, not a 500.
        return []
    except ValueError as exc:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=str(exc),
        ) from exc
    except MissingCanonicalFieldError as exc:
        raise _missing_canonical_422(exc) from exc


@router.get(
    "/projects/{project_id}/episodes/{episode_id}/scenes",
    response_model=list[Scene],
)
def get_scenes(project_id: str, episode_id: str) -> list[Scene]:
    """P3 — synthesized scene list (one synthetic scene per episode).

    ``KeyError`` (bogus episode_id) → 404. Path-traversal-shaped IDs → 400.
    """
    try:
        validate_project_id(project_id)
        validate_hierarchy_id("episode_id", episode_id)
        # REC-231 Phase 4: _list_scenes is the synthetic per-episode identity
        # adapter (api/adapters/beats.list_scenes) — it returns flat active scene
        # identities, never enumerates version bodies, so it is pointer-safe
        # (consistent with the Phase-1 persistence.list_scenes sidecar exclusion).
        return _list_scenes(project_id, episode_id)
    except ValueError as exc:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=str(exc),
        ) from exc
    except KeyError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=str(exc),
        ) from exc
    except MissingCanonicalFieldError as exc:
        raise _missing_canonical_422(exc) from exc


@router.get(
    "/projects/{project_id}/episodes/{episode_id}/scenes/{scene_id}/beats",
    response_model=list[Beat],
)
def get_beats(project_id: str, episode_id: str, scene_id: str) -> list[Beat]:
    """List beats for a (project, episode, scene).

    Under the P3 synthesis model bogus ``scene_id`` (or ``episode_id``)
    raises ``KeyError`` in the adapter and surfaces here as 404.
    """
    try:
        return list_beats(project_id, episode_id, scene_id)
    except ValueError as exc:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=str(exc),
        ) from exc
    except KeyError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=str(exc),
        ) from exc
    except MissingCanonicalFieldError as exc:
        raise _missing_canonical_422(exc) from exc


@router.get("/beats/{beat_id}/takes", response_model=list[Take])
def get_takes(
    beat_id: str,
    project_id: str = Query(..., alias="projectId"),
) -> list[Take]:
    try:
        return list_takes(beat_id, project_id=project_id)
    except ValueError as exc:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=str(exc),
        ) from exc
    except MissingCanonicalFieldError as exc:
        raise _missing_canonical_422(exc) from exc


@router.get("/beats/{beat_id}/lineage", response_model=Lineage)
def get_lineage(
    beat_id: str,
    project_id: str = Query(..., alias="projectId"),
    take_id: Optional[str] = Query(default=None, alias="takeId"),
) -> Lineage:
    """Beat-rooted lineage by default; take-rooted when `takeId` is provided.

    Take-rooted mode filters the graph to the parent_take_id chain producing
    that take — so the inspector shows only what made the selected take,
    not the take plus its siblings.
    """
    try:
        L = _get_lineage(beat_id, project_id=project_id, take_id=take_id)
    except ValueError as exc:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=str(exc),
        ) from exc
    except MissingCanonicalFieldError as exc:
        raise _missing_canonical_422(exc) from exc
    if L is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"no lineage for beat {beat_id!r}",
        )
    return L


@router.get("/events", response_model=list[EngineEvent])
def get_events(
    severities: Optional[list[EventSeverity]] = Query(default=None),
    scope_prefix: Optional[str] = Query(default=None, alias="scopePrefix"),
    since_id: Optional[str] = Query(default=None, alias="sinceId"),
    limit: int = Query(default=200, ge=1, le=500),
) -> list[EngineEvent]:
    return list_events(
        severities=list(severities) if severities else None,
        scope_prefix=scope_prefix,
        since_id=since_id,
        limit=limit,
    )


@router.get("/memory", response_model=list[MemoryEntry])
def get_memory() -> list[MemoryEntry]:
    return list_memory()


__all__ = ["router"]
