"""The routes: federation overview (sync matrix) + per-project lenses + sync actions.

``GET`` is read-only — it reads the spine's ``generated/`` through ``DiskProvider``
and renders. M3 adds the **sync actions** (spec §5.5):

- ``POST /p/{slug}/check`` / ``POST /check-all`` — run the deep, authoritative
  ``deep_check`` (regenerate into memory + diff, **non-mutating**) and re-render
  with the true state + a result line. The only writes anywhere are force-sync's.
- ``POST /p/{slug}/force-sync`` / ``POST /force-sync-all`` — ``generate(write=True)``,
  the one sanctioned mutation, and only of the working tree's ``generated/``. Never
  git, never an auto-commit (governed-repo default).

A requested project that isn't in the federation renders an honest ``UNKNOWN`` page
(HTTP 200) — for GET and for an action POST alike — never a 500 (spec §5.7).
"""

from __future__ import annotations

from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any
from urllib.parse import parse_qs

from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse, Response

from ..discovery import discover_projects, find_project
from ..lenses import (
    DEFAULT_LENS,
    LENS_LABELS,
    LENS_ORDER,
    ProjectMeta,
    assemble_meta,
    build_lens,
    compose_and_deliver,
    deep_meta,
    federation_state,
    feedback_thread,
    next_round_number,
    open_round_count,
    run_check,
    run_force_sync,
    sync_matrix,
    unknown_meta,
)
from ..live import follow_live

if TYPE_CHECKING:
    from fastapi.templating import Jinja2Templates
    from mgf.fedres import SyncReport

# An empty thread for the not-wired / unknown-project render paths (honest-absence).
_EMPTY_THREAD: dict[str, Any] = {"rounds": [], "has_rounds": False}


async def _form(request: Request) -> dict[str, str]:
    """Parse an ``application/x-www-form-urlencoded`` body (no python-multipart dep)."""
    raw = await request.body()
    parsed = parse_qs(raw.decode("utf-8"), keep_blank_values=True)
    return {key: values[0] for key, values in parsed.items() if values}


# The state language pinned in the rail legend (spec §5.2) — the full vocabulary,
# transient CHECKING/SYNCING included (spec §1.5), so colours/icons are never
# ambiguous. Kept here so the template stays declarative.
STATE_LEGEND: tuple[tuple[str, str], ...] = (
    ("IN_SYNC", "committed view == regenerate output"),
    ("STALE", "project moved ahead of its dashboard"),
    ("TAMPERED", "generated/ edited by hand"),
    ("UNKNOWN", "not found / not wired"),
    ("UNREADABLE", "manifest / schema unknown"),
    ("CHECKING", "deep check in flight"),
    ("SYNCING", "regenerate in flight"),
    ("ERROR", "check / generate failed"),
)


def _now() -> str:
    return datetime.now(UTC).strftime("%Y-%m-%d %H:%M UTC")


def _now_iso() -> str:
    """UTC stamp for a force-sync's manifest ``generated_at`` (canonical ``…Z`` form)."""
    return datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")


def _chrome(metas: list[ProjectMeta], active: str, check_mode: str = "shallow") -> dict[str, Any]:
    """The shared topbar + rail context (global pill, project list, legend, check tier)."""
    return {
        "projects": metas,
        "pill": federation_state(metas),
        "active": active,
        "last_checked": _now(),
        # "shallow" on load / "deep" after a Check action — labels the last-check chip.
        "check_mode": check_mode,
        "legend": STATE_LEGEND,
    }


def build_router(templates: Jinja2Templates) -> APIRouter:
    """Build the router; handlers close over ``templates`` (no module globals)."""
    router = APIRouter()

    def render(request: Request, name: str, **ctx: object) -> Response:
        return templates.TemplateResponse(request, name, ctx)

    # ---- federation overview (the sync matrix) --------------------------- #
    def _render_overview(request: Request, metas: list[ProjectMeta], check_mode: str) -> Response:
        pill = federation_state(metas)
        summary = (
            "Everything is in sync."
            if pill["all_synced"]
            else (f"Drift — {pill['counts']}." if metas else "No projects discovered yet.")
        )
        return render(
            request,
            "overview.html",
            chrome=_chrome(metas, "overview", check_mode),
            matrix=sync_matrix(metas),
            summary=summary,
            check_mode=check_mode,
        )

    @router.get("/", response_class=HTMLResponse)
    def overview(request: Request) -> Response:
        # Shallow on load (instant, manifest-only) — the first verdict (spec §1.4).
        metas = [assemble_meta(p.slug, p.provider) for p in discover_projects()]
        return _render_overview(request, metas, "shallow")

    @router.post("/check-all", response_class=HTMLResponse)
    def check_all(request: Request) -> Response:
        # Deep-check every discovered project (authoritative, non-mutating); the
        # matrix + global pill then reflect the true state, not the load-time guess.
        metas = [deep_meta(p.slug, p.provider) for p in discover_projects()]
        return _render_overview(request, metas, "deep")

    @router.post("/force-sync-all", response_class=HTMLResponse)
    def force_sync_all(request: Request) -> Response:
        # Force-sync only what the matrix shows as drifting (shallow STALE/TAMPERED);
        # each regenerate is the sanctioned working-tree write, never a commit. Then
        # re-read shallow so the matrix reflects the settled state.
        for p in discover_projects():
            if assemble_meta(p.slug, p.provider).sync_state in ("STALE", "TAMPERED"):
                run_force_sync(p.provider, _now_iso())
        metas = [assemble_meta(p.slug, p.provider) for p in discover_projects()]
        return _render_overview(request, metas, "shallow")

    # ---- per-project (header + lenses + the per-project sync actions) ----- #
    @router.get("/p/{slug}", response_class=HTMLResponse)
    def project_root(request: Request, slug: str) -> Response:
        return _render_project(request, slug, DEFAULT_LENS)

    # M4 — feedback (the closed loop) + M5 — the review pack. These fixed segments
    # are registered BEFORE the catch-all /p/{slug}/{lens} GET so they win the match.
    @router.get("/p/{slug}/feedback", response_class=HTMLResponse)
    def project_feedback(request: Request, slug: str) -> Response:
        return _render_feedback(request, slug)

    @router.post("/p/{slug}/feedback", response_class=HTMLResponse)
    async def project_feedback_deliver(request: Request, slug: str) -> Response:
        active = find_project(slug)
        if active is None:
            return _render_feedback(request, slug)  # honest UNKNOWN, never a 500
        form = await _form(request)
        # Compose -> validate -> DELIVER into the target's feedback/ inbox (uncommitted).
        _, result = compose_and_deliver(
            active.provider,
            slug,
            authored_by=form.get("authored_by") or "founder",
            authored_at=_now_iso(),
            kind=form.get("kind") or "direction",
            priority=form.get("priority") or "medium",
            title=form.get("title") or "",
            body=form.get("body") or "",
            view=form.get("applies_view") or None,
            ref=form.get("applies_ref") or None,
        )
        return _render_feedback(request, slug, result=result)

    @router.get("/p/{slug}/review-pack", response_class=HTMLResponse)
    def project_review(request: Request, slug: str) -> Response:
        return _render_review(request, slug)

    # M6 — Follow-live: the In-progress lens' live activity mode (telemetry, never gated).
    # ``?session=`` follows a named past session; default follows live/CURRENT.
    @router.get("/p/{slug}/live", response_class=HTMLResponse)
    def project_live(request: Request, slug: str, session: str | None = None) -> Response:
        return _render_live(request, slug, session)

    @router.get("/p/{slug}/{lens}", response_class=HTMLResponse)
    def project_lens(request: Request, slug: str, lens: str) -> Response:
        return _render_project(request, slug, lens)

    @router.post("/p/{slug}/check", response_class=HTMLResponse)
    def project_check(request: Request, slug: str, lens: str = DEFAULT_LENS) -> Response:
        active = find_project(slug)
        if active is None:
            return _render_project(request, slug, lens)  # honest UNKNOWN, never a 500
        report, result = run_check(active.provider)
        return _render_project(request, slug, lens, action_report=report, sync_result=result)

    @router.post("/p/{slug}/force-sync", response_class=HTMLResponse)
    def project_force_sync(request: Request, slug: str, lens: str = DEFAULT_LENS) -> Response:
        active = find_project(slug)
        if active is None:
            return _render_project(request, slug, lens)  # honest UNKNOWN, never a 500
        report, result = run_force_sync(active.provider, _now_iso())
        return _render_project(request, slug, lens, action_report=report, sync_result=result)

    def _render_project(
        request: Request,
        slug: str,
        lens: str,
        *,
        action_report: SyncReport | None = None,
        sync_result: dict[str, Any] | None = None,
    ) -> Response:
        projects = discover_projects()
        active = next((p for p in projects if p.slug == slug), None)
        active_lens = lens if lens in LENS_ORDER else DEFAULT_LENS

        if active is None:
            # Honest-absence: a project not in the federation renders UNKNOWN, not 500
            # (true for a stray GET and for an action POST against a bad slug alike).
            metas = [assemble_meta(p.slug, p.provider) for p in projects]
            return render(
                request,
                "project.html",
                chrome=_chrome(metas, slug),
                meta=unknown_meta(slug),
                lenses=LENS_ORDER,
                lens_labels=LENS_LABELS,
                active_lens=active_lens,
                lens_template=None,
                view=None,
                sync_result=sync_result,
            )

        # After an action the acted project carries its authoritative (deep) verdict;
        # the others stay shallow — one action doesn't silently re-check the fleet.
        metas = [
            assemble_meta(p.slug, p.provider, report=action_report)
            if (p.slug == slug and action_report is not None)
            else assemble_meta(p.slug, p.provider)
            for p in projects
        ]
        meta = next(m for m in metas if m.slug == slug)
        view = build_lens(active_lens, active.provider) if meta.wired else None
        return render(
            request,
            "project.html",
            chrome=_chrome(metas, slug, "deep" if action_report is not None else "shallow"),
            meta=meta,
            lenses=LENS_ORDER,
            lens_labels=LENS_LABELS,
            active_lens=active_lens,
            lens_template=f"lenses/_{active_lens}.html",
            view=view,
            sync_result=sync_result,
        )

    # ---- M4 feedback (the closed loop) + M5 review pack ------------------- #
    def _render_feedback(
        request: Request,
        slug: str,
        *,
        result: dict[str, Any] | None = None,
    ) -> Response:
        """The feedback surface: compose form + the round thread. ``result`` is the
        outcome of a just-attempted delivery (shown as a result bar). An unknown /
        unwired project renders the honest-absence header; the compose form + thread
        are gated on ``meta.wired`` in the template, so nothing false is offered."""
        projects = discover_projects()
        active = next((p for p in projects if p.slug == slug), None)
        metas = [assemble_meta(p.slug, p.provider) for p in projects]

        if active is None:
            return render(
                request,
                "feedback.html",
                chrome=_chrome(metas, slug),
                meta=unknown_meta(slug),
                lenses=LENS_ORDER,
                lens_labels=LENS_LABELS,
                active_lens=DEFAULT_LENS,
                thread=_EMPTY_THREAD,
                next_round=1,
                open_rounds=0,
                feedback_result=result,
                sync_result=None,
            )

        meta = next(m for m in metas if m.slug == slug)
        # The thread + next-round are read straight from the target's feedback/ inbox
        # through the spine's honest-absence reader — never asserted, never cached.
        return render(
            request,
            "feedback.html",
            chrome=_chrome(metas, slug),
            meta=meta,
            lenses=LENS_ORDER,
            lens_labels=LENS_LABELS,
            active_lens=DEFAULT_LENS,
            thread=feedback_thread(active.provider) if meta.wired else _EMPTY_THREAD,
            next_round=next_round_number(active.provider) if meta.wired else 1,
            open_rounds=open_round_count(active.provider) if meta.wired else 0,
            feedback_result=result,
            sync_result=None,
        )

    def _render_review(request: Request, slug: str) -> Response:
        """The review pack: every lens + the feedback thread on one printable page
        (spec §5.5). Each section carries its lens view-model + the lens partial, so the
        pack renders identically to the live lenses. Unknown / unwired → honest-absence
        header with no sections (the template gates the body on ``meta.wired``)."""
        projects = discover_projects()
        active = next((p for p in projects if p.slug == slug), None)
        metas = [assemble_meta(p.slug, p.provider) for p in projects]

        if active is None or not (meta := next(m for m in metas if m.slug == slug)).wired:
            return render(
                request,
                "review_pack.html",
                chrome=_chrome(metas, slug),
                meta=unknown_meta(slug) if active is None else meta,
                lenses=LENS_ORDER,
                lens_labels=LENS_LABELS,
                active_lens=DEFAULT_LENS,
                sections=[],
                thread=_EMPTY_THREAD,
                open_rounds=0,
                sync_result=None,
            )

        sections = [
            {
                "label": LENS_LABELS[lid],
                "view": build_lens(lid, active.provider),
                "template": f"lenses/_{lid}.html",
            }
            for lid in LENS_ORDER
        ]
        return render(
            request,
            "review_pack.html",
            chrome=_chrome(metas, slug),
            meta=meta,
            lenses=LENS_ORDER,
            lens_labels=LENS_LABELS,
            active_lens=DEFAULT_LENS,
            sections=sections,
            thread=feedback_thread(active.provider),
            open_rounds=open_round_count(active.provider),
            sync_result=None,
        )

    def _render_live(request: Request, slug: str, session_id: str | None) -> Response:
        """The Follow-live surface — the current (or a named) session, composed by the
        ``live`` data layer. ``active_tab`` stays ``inprogress`` so the lens it belongs to
        keeps its highlight. ``live/`` is telemetry, read independently of the manifest, so
        the pane is honest even for an unwired project (the header still says why)."""
        projects = discover_projects()
        active = next((p for p in projects if p.slug == slug), None)
        metas = [assemble_meta(p.slug, p.provider) for p in projects]

        if active is None:
            live: dict[str, Any] = {
                "telemetry": True,
                "active": False,
                "ended": False,
                "session": None,
                "timeline": [],
                "title": "Project not in the federation",
                "reason": "Nothing to follow — this project isn't wired into the federation.",
            }
            return render(
                request,
                "live.html",
                chrome=_chrome(metas, slug),
                meta=unknown_meta(slug),
                lenses=LENS_ORDER,
                lens_labels=LENS_LABELS,
                active_lens="inprogress",
                live=live,
                sync_result=None,
            )

        meta = next(m for m in metas if m.slug == slug)
        return render(
            request,
            "live.html",
            chrome=_chrome(metas, slug),
            meta=meta,
            lenses=LENS_ORDER,
            lens_labels=LENS_LABELS,
            active_lens="inprogress",
            live=follow_live(active.provider, session_id),
            sync_result=None,
        )

    return router
