"""``fedres session-close`` — the end-of-session hygiene gate + state-at-exit scaffold.

Verifies the mechanically-checkable Session-Close MUSTs (durable state committed, no branch
left silently ahead, no live op still running) and scaffolds the state-at-exit record so an
agent *edits* rather than *authors*. Cross-family by construction: both the MGF and Sooth
fleets reach it through this one ``mgf.fedres`` entry point (Sooth via its ``tools/fedres.py``
wrapper). The canonical standard text ships as package data (``docs/SESSION_CLOSE.md``) so it
is byte-identical in both families.

Stdlib only (VAL-04). Every probe is local and deterministic — no network.
"""

from __future__ import annotations

import subprocess
from dataclasses import dataclass
from importlib.resources import files
from typing import TYPE_CHECKING

from . import live

if TYPE_CHECKING:
    from .provider import DiskProvider

_GIT_TIMEOUT = 5


def _git(root: str, *args: str, strip: bool = True) -> str | None:
    """Run ``git -C root <args>`` and return its stdout, or ``None`` on any failure.

    ``strip`` whole-strips the output (right for ``rev-parse``); porcelain callers pass
    ``strip=False`` — a whole-string strip would eat the *leading space* of the first
    ``status --porcelain`` line (` M path`), corrupting the parsed path. A successful
    command with empty output returns ``""`` — distinct from the ``None`` failure sentinel.
    """
    try:
        out = subprocess.run(  # noqa: S603
            ["git", "-C", root, *args],  # noqa: S607
            capture_output=True,
            text=True,
            check=True,
            timeout=_GIT_TIMEOUT,
        )
    except (OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired):
        return None
    return out.stdout.strip() if strip else out.stdout


@dataclass
class CloseState:
    """The end-of-session facts, read passively from git + the fed-resources live channel."""

    is_git: bool
    branch: str | None
    head: str | None
    dirty: list[str]  # `git status --porcelain` lines (uncommitted + untracked)
    ahead: int | None  # commits ahead of upstream; None when there is no upstream to compare
    live_session: str | None  # an open live/CURRENT session, or None


def collect_state(provider: DiskProvider) -> CloseState:
    root = str(provider.project_root)
    is_git = _git(root, "rev-parse", "--is-inside-work-tree") == "true"
    branch = _git(root, "rev-parse", "--abbrev-ref", "HEAD") if is_git else None
    porcelain = _git(root, "status", "--porcelain", strip=False) if is_git else None
    dirty = [ln for ln in (porcelain or "").splitlines() if ln.strip()]
    ahead_raw = _git(root, "rev-list", "--count", "@{u}..HEAD") if is_git else None
    ahead = int(ahead_raw) if (ahead_raw is not None and ahead_raw.isdigit()) else None
    session = live.current_session(provider) if provider.has_fedres() else None
    return CloseState(
        is_git=is_git,
        branch=branch,
        head=provider.head_commit(),
        dirty=dirty,
        ahead=ahead,
        live_session=session,
    )


@dataclass
class Finding:
    level: str  # "fail" | "warn" | "info"
    code: str
    message: str


def check(provider: DiskProvider, *, allow_dirty: bool = False) -> tuple[list[Finding], bool]:
    """Verify the cross-family Session-Close invariants. Returns ``(findings, ok)``;
    ``ok`` is false iff any finding is a ``fail``."""
    st = collect_state(provider)
    findings: list[Finding] = []

    if not st.is_git:
        findings.append(
            Finding("warn", "no-git", "not a git work tree — durable-state checks skipped")
        )
    else:
        if st.dirty:
            findings.append(
                Finding(
                    "warn" if allow_dirty else "fail",
                    "uncommitted",
                    f"{len(st.dirty)} uncommitted change(s) — commit durable state before "
                    "close, or declare each with `# session-close: allow <path> <reason>` (MUST-1)",
                )
            )
        if st.ahead:
            findings.append(
                Finding(
                    "warn",
                    "unpushed",
                    f"branch {st.branch!r} is {st.ahead} commit(s) ahead of upstream — "
                    "push it or note it in the close (MUST-2/3)",
                )
            )
        elif st.ahead is None:
            findings.append(
                Finding("info", "no-upstream", f"branch {st.branch!r} has no upstream to compare")
            )

    if st.live_session:
        findings.append(
            Finding(
                "fail",
                "live-session",
                f"live session {st.live_session!r} is still open — a traced op did not "
                "finish (MUST-3/5)",
            )
        )

    ok = not any(f.level == "fail" for f in findings)
    return findings, ok


_SCAFFOLD = """\
# SESSION CLOSE — {date}
## What happened        — 2-5 lines; link the decision log for depth
## State at exit
  - Merged/released:    <what landed live>
  - Open PRs:           #n <title> · <branch>→<base> · CI <state> · held/ready   [MUST]
  - Local branches:     {branch}{ahead} — <intent>                               [MUST]
  - Uncommitted:        {uncommitted}   [MUST]
  - Processes started:  {live_session}   [MUST]
  - Pending decisions:  <question> — blocked on <who>                            [MUST]
## Next steps           — ordered; note merge/release order where PRs interact
## Health at exit       — the key invariants, each with its verified state
## Lessons              — what to carry forward
"""


def render_template(provider: DiskProvider, *, date: str) -> str:
    """The state-at-exit record, pre-filled with what git + the live channel can detect."""
    st = collect_state(provider)
    branch = st.branch or "<branch>"
    ahead = f" ({st.ahead} unmerged commit(s))" if st.ahead else ""
    if st.dirty:
        uncommitted = (
            f"{len(st.dirty)} path(s): "
            + ", ".join(ln[3:] or ln for ln in st.dirty[:8])
            + (" …" if len(st.dirty) > 8 else "")
            + " — why not committed?"
        )
    else:
        uncommitted = "(clean)"
    live_session = (
        f"live session {st.live_session!r} STILL OPEN — stop it"
        if st.live_session
        else "(none this session? name any daemon + how to stop it)"
    )
    return _SCAFFOLD.format(
        date=date, branch=branch, ahead=ahead, uncommitted=uncommitted, live_session=live_session
    )


def canonical_doc() -> str:
    """The canonical Session-Close standard text, shipped as package data."""
    return files("mgf.fedres").joinpath("docs/SESSION_CLOSE.md").read_text(encoding="utf-8")
