#!/usr/bin/env python3
"""Fleet conformance audit — Item 1 of the libraries review.

Runs ``mgf-fed catch-up`` (read-only) across every on-disk ``mgf-*`` repo,
parses the per-check drift matrix, captures each repo's DECLARED conformance
level + git state, and writes a durable tracker (markdown + JSON) under
``docs/inprogress/FLEET_AUDIT/``.

Design goals
------------
* **Reproducible in any container.** Search roots are derived from this
  repo's parent directory (override with ``--root``). The ``mgf-fed`` binary
  is auto-discovered (PATH, then ``<root>/mgf-fed/.venv/bin/mgf-fed``).
* **Read-only.** This script never mutates a target repo. Mechanical fixes
  are applied separately with ``mgf-fed catch-up --apply`` per repo.
* **Idempotent output.** Re-running regenerates the JSON + markdown so the
  tracker always reflects current on-disk truth.

Usage
-----
    python3 scripts/fleet_conformance_audit.py            # audit + write tracker
    python3 scripts/fleet_conformance_audit.py --root DIR # extra search root
    python3 scripts/fleet_conformance_audit.py --json-only
"""

from __future__ import annotations

import argparse
import json
import os
import re
import shutil
import subprocess
import sys
from dataclasses import asdict, dataclass, field
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent  # mgf-standard/
OUT_DIR = REPO_ROOT / "docs" / "inprogress" / "FLEET_AUDIT"

STATUS_GLYPHS = {
    "✅": "ok",
    "⚠": "warn",
    "\U0001f6ab": "declined",
    "❌": "fail",
    "\U0001f512": "pin",
}
GLYPH_RE = re.compile(r"^\s+([✅⚠\U0001f6ab❌\U0001f512])️?\s+(.+?)\s*$")
LEVEL_RE = re.compile(r"\bL([0-3])\b")

# Shape false-positives: catch-up isn't shape-aware, so it flags non-Python repos
# for Python-only rules. These are N/A, not real gaps — exclude from the count.
PYTHON_ONLY_CHECKS = (
    "pyproject.toml — mgf-common pin",
    "pyproject.toml — four gates",
    "Federation test supervisor",
    "Federation marker taxonomy",
)
# Per-repo structural / declared N/A (title-substring match).
PER_REPO_NA = {
    # mgf-standard IS the federation root: no mgf-common dep by design, and it
    # declines TS-22..27 (CFM-04 pure-leaf — adding it would close a dep cycle).
    "mgf-standard": ("mgf-common pin", "Federation test supervisor", "Federation marker taxonomy"),
    # mgf-common importing its OWN `mgf.common._*` internals is not a closed-box
    # leak (the rule is about *consumers*).
    "mgf-common": ("closed-box leaks",),
    # mgf-fed HOUSES the catch-up detector: the v0.1-pattern table and the
    # private-import examples are literal strings in mgf/fed/cli/_catch_up.py, so
    # catch-up self-matches its own source. mgf-fed's real closed-box reaches were
    # retired in mgf-common 0.49 (_DEFERRED emptied) and it has no v0.1 calls.
    "mgf-fed": ("closed-box leaks", "v0.1-pattern"),
    # mgf-brand is a non-code assets/docs repo (no package) — the WF-13 template
    # renders a Python pipeline with nothing to run. Declared decline, documented
    # in mgf-brand/MGF_STANDARDS_CONFORMANCE.md.
    "mgf-brand": ("Woodpecker",),
}


def is_na(repo_name: str, lang: str, title: str) -> bool:
    """True if a flagged check is a known shape false-positive / declared N/A."""
    if lang != "python" and any(k in title for k in PYTHON_ONLY_CHECKS):
        return True
    return any(sub in title for sub in PER_REPO_NA.get(repo_name, ()))


@dataclass
class Check:
    status: str  # ok | warn | declined | fail | pin
    title: str
    # WHY the check fired. Recorded because the tracker without it was undiagnosable:
    # a reader could see 43x "[warn] AGENTS.md" and not tell *missing* from *drifted*
    # from *unreadable* without re-running the whole fleet audit. A finding you cannot
    # act on from the artifact is a finding the artifact did not really record.
    detail: str = ""


@dataclass
class RepoAudit:
    name: str
    path: str
    lang: str
    declared_level: str | None
    git_branch: str | None
    dirty: bool
    # Commits HEAD is behind origin/main. >0 means this row describes a checkout that
    # is not what the fleet actually ships. None = no origin/main to compare (unknown).
    behind_origin: int | None
    catch_up_ran: bool
    catch_up_note: str = ""
    checks: list[Check] = field(default_factory=list)

    @property
    def warns(self) -> list[Check]:
        return [c for c in self.checks if c.status in ("warn", "fail")]

    @property
    def na(self) -> list[Check]:
        return [c for c in self.checks if c.status == "na"]

    @property
    def n_ok(self) -> int:
        return sum(1 for c in self.checks if c.status == "ok")

    @property
    def n_warn(self) -> int:
        """Real gaps only — shape false-positives are reclassified to 'na'."""
        return len(self.warns)

    @property
    def n_na(self) -> int:
        return len(self.na)


def find_fed_binary(roots: list[Path]) -> str:
    onpath = shutil.which("mgf-fed")
    if onpath:
        return onpath
    for root in roots:
        cand = root / "mgf-fed" / ".venv" / "bin" / "mgf-fed"
        if cand.exists():
            return str(cand)
    sys.exit("ERROR: mgf-fed not found on PATH or in <root>/mgf-fed/.venv/bin")


def detect_lang(repo: Path) -> str:
    if (repo / "pyproject.toml").exists():
        return "python"
    if (repo / "build.zig").exists():
        return "zig"
    if (repo / "package.json").exists():
        return "node"
    return "other"


def declared_level(repo: Path) -> str | None:
    conf = repo / "docs" / "inprogress" / "MGF_STANDARDS_CONFORMANCE.md"
    if not conf.exists():
        return None
    for line in conf.read_text(errors="replace").splitlines():
        low = line.lower()
        if "conformance level" in low or "declare" in low or "target" in low:
            m = LEVEL_RE.search(line)
            if m:
                return "L" + m.group(1)
    # fallback: first L\d anywhere
    m = LEVEL_RE.search(conf.read_text(errors="replace"))
    return ("L" + m.group(1)) if m else None


def git_state(repo: Path) -> tuple[str | None, bool, int | None]:
    """Branch, dirty, and **how many commits HEAD is behind ``origin/main``**.

    The third value is the one that was missing, and it matters more than the other
    two. This audit reads **working trees** — so if the checkout is stale, the audit
    measures a fleet that no longer exists and reports it as current truth.

    The sibling family already learned this and wrote it down: sooth-standard's
    ``verify_family_conformance.py`` reads each member's *remote default branch* and
    labels the working-tree mode ``lies more``. MGF's audit never compared the two, so
    it could not even tell you it might be lying. Reading remote refs outright is the
    better end-state; reporting the divergence is the honest step that needs no network
    and no credentials. ``None`` = no ``origin/main`` ref to compare against, which is
    *unknown*, never *current*.
    """

    def run(*args: str) -> str:
        return subprocess.run(
            ["git", "-C", str(repo), *args], capture_output=True, text=True
        ).stdout.strip()

    branch = run("rev-parse", "--abbrev-ref", "HEAD") or None
    dirty = bool(run("status", "--porcelain"))
    behind: int | None = None
    if run("rev-parse", "--verify", "--quiet", "origin/main"):
        raw = run("rev-list", "--count", "HEAD..origin/main")
        behind = int(raw) if raw.isdigit() else None
    return branch, dirty, behind


def parse_checks(output: str) -> list[Check]:
    """Parse catch-up's drift report into checks, INCLUDING each check's detail.

    catch-up prints a glyph line then indents the explanation beneath it::

        ⚠️  AGENTS.md
            exists but its content has drifted from the canonical pointer. …

    The detail lines used to be discarded, which is how a tracker full of
    ``[warn] AGENTS.md`` could not tell anyone *why*.
    """
    checks: list[Check] = []
    detail: list[str] = []

    def flush() -> None:
        if checks and detail:
            checks[-1].detail = " ".join(" ".join(detail).split())[:400]
        detail.clear()

    for line in output.splitlines():
        m = GLYPH_RE.match(line)
        if m:
            flush()
            checks.append(Check(status=STATUS_GLYPHS[m.group(1)], title=m.group(2)))
        elif checks and line.startswith("      ") and line.strip():
            detail.append(line.strip())
        elif not line.strip():
            flush()
    flush()
    return checks


def run_catch_up(fed: str, repo: Path) -> tuple[bool, str, str]:
    proc = subprocess.run(
        [fed, "catch-up", "--path", str(repo)], capture_output=True, text=True, timeout=180
    )
    out = proc.stdout + ("\n" + proc.stderr if proc.stderr else "")
    # catch-up needs a pyproject (or docs-kind detection); on hard failure it
    # exits non-zero with a usage-style message and no drift report.
    ran = "Drift report:" in out
    note = "" if ran else out.strip().splitlines()[-1] if out.strip() else "no output"
    return ran, out, note


def discover_repos(roots: list[Path]) -> tuple[list[Path], list[str]]:
    """Federation members on disk, plus the ``mgf-*`` directories deliberately skipped.

    **Membership test: does it have an ``origin`` remote?** The fleet is what lives on the
    forge; a local ``mgf-*`` directory with no remote is a scaffold someone ran ``git init``
    in, not a member. It is also precisely the case this audit cannot assess — with no
    ``origin/main`` there is nothing to compare a working tree against, so it yields a
    permanently *unknown* row that can never resolve and would wedge ``--check`` forever on
    a stray directory.

    Found live 2026-07-30: ``mgf-uitest-desktop`` — one commit, **no remote**, absent from
    ``MGF_LIBRARIES.json``, 13 uncommitted files. Enrolling it is a membership event
    (LOCK §2 EXTEND), not something a sweep decides.

    Skipped names are RETURNED, never swallowed — the tracker names them. Silently dropping
    something from a sweep is how "clean" quietly starts meaning "narrower than you think";
    a skip you can read is a skip you can argue with.
    """
    seen: dict[str, Path] = {}
    skipped: list[str] = []
    for root in roots:
        if not root.is_dir():
            continue
        for child in sorted(root.glob("mgf-*")):
            if not (child.is_dir() and (child / ".git").exists()):
                continue
            if child.name in seen or child.name in skipped:
                continue
            remote = subprocess.run(
                ["git", "-C", str(child), "remote", "get-url", "origin"],
                capture_output=True,
                text=True,
            )
            if remote.returncode == 0 and remote.stdout.strip():
                seen[child.name] = child
            else:
                skipped.append(child.name)
    return list(seen.values()), skipped


def audit(fed: str, repos: list[Path]) -> list[RepoAudit]:
    results: list[RepoAudit] = []
    for repo in repos:
        lang = detect_lang(repo)
        branch, dirty, behind = git_state(repo)
        ran, out, note = run_catch_up(fed, repo)
        checks = parse_checks(out) if ran else []
        for c in checks:  # reclassify shape false-positives
            if c.status in ("warn", "fail") and is_na(repo.name, lang, c.title):
                c.status = "na"
        ra = RepoAudit(
            name=repo.name,
            path=str(repo),
            lang=lang,
            declared_level=declared_level(repo),
            git_branch=branch,
            dirty=dirty,
            behind_origin=behind,
            catch_up_ran=ran,
            catch_up_note=note,
            checks=checks,
        )
        results.append(ra)
        na = f" (+{ra.n_na} N/A)" if ra.n_na else ""
        flag = f"{ra.n_warn} gaps{na}" if ran else f"catch-up N/A ({note[:40]})"
        print(f"  {repo.name:<26} {lang:<7} {ra.declared_level or '-':<3} {flag}")
    return results


def write_json(results: list[RepoAudit], generated: str, skipped: list[str] | None = None) -> None:
    payload = {
        "generated": generated,
        "skipped_not_members": sorted(skipped or []),
        "tool": "mgf-fed catch-up",
        "n_repos": len(results),
        "repos": [
            {
                **asdict(r),
                "checks": [asdict(c) for c in r.checks],
                "n_ok": r.n_ok,
                "n_warn": r.n_warn,
                "n_na": r.n_na,
            }
            for r in results
        ],
    }
    (OUT_DIR / "item1-conformance.json").write_text(
        json.dumps(payload, indent=2, ensure_ascii=False) + "\n"
    )


def write_markdown(
    results: list[RepoAudit], generated: str, skipped: list[str] | None = None
) -> None:
    lines: list[str] = []
    a = lines.append
    a("# Item 1 — Fleet conformance audit (declared-target + catch-up)\n")
    a(
        f"> **Generated** {generated} by `scripts/fleet_conformance_audit.py`. "
        "Read-only; regenerate any time. Source of truth for Item 1 progress "
        "across sessions/containers.\n"
    )
    a(
        "> Each repo is audited against its **declared** conformance level via "
        "`mgf-fed catch-up`. ✅ = clean check · ⚠️ = **real** gap · N/A = shape "
        "false-positive (catch-up isn't shape-aware) or declared decline — "
        "excluded from the gap count. Mechanical gaps are fixed with "
        "`mgf-fed catch-up --apply` (+ hand-fixes for prose drift).\n"
    )

    total_warn = sum(r.n_warn for r in results)
    total_na = sum(r.n_na for r in results)
    clean = [r for r in results if r.catch_up_ran and r.n_warn == 0]
    a(
        f"\n**{len(results)} repos** · {len(clean)} clean · "
        f"{len([r for r in results if r.n_warn])} with real gaps · "
        f"**{total_warn} real gaps** (+{total_na} N/A shape/declined)\n"
    )

    # State the INPUT, not only the verdict. This audit reads working trees; if those
    # are stale it is describing a fleet that no longer exists. Saying so is the whole
    # difference between a measurement and an assertion.
    stale = [r for r in results if (r.behind_origin or 0) > 0]
    unknown = [r for r in results if r.behind_origin is None]
    off_main = [r for r in results if r.git_branch not in (None, "main")]
    a(
        "\n> **What was measured.** Working trees on disk — *not* each repo's remote "
        "default branch. A stale checkout makes every row below describe a fleet that "
        "is not the one shipping. (sooth-standard's `verify_family_conformance.py` "
        "reads remote refs for exactly this reason and calls the working-tree mode "
        '*"lies more"*.)\n'
    )
    if stale or unknown or off_main:
        bits = []
        if stale:
            worst = max(r.behind_origin or 0 for r in stale)
            bits.append(f"**{len(stale)} behind `origin/main`** (up to {worst} commits)")
        if off_main:
            bits.append(f"**{len(off_main)} not on `main`**")
        if unknown:
            bits.append(f"**{len(unknown)} with no `origin/main`** to compare (unknown)")
        a(
            f"> ⚠️ **This run's input was not clean:** {' · '.join(bits)}. "
            "Refresh the fleet and re-run before treating these numbers as the fleet's "
            "state. Note remote-tracking refs are themselves only as fresh as the last "
            "`git fetch`.\n"
        )
    else:
        a("> ✅ Every repo is on `main` and level with `origin/main` — input was clean.\n")
    if skipped:
        a(
            f"\n> **Skipped ({len(skipped)}):** `" + "`, `".join(sorted(skipped)) + "` — a `.git` "
            "directory with **no `origin` remote**: a scaffold someone ran `git init` in, not a "
            "fleet member. Named rather than dropped: a silent skip is how *clean* quietly starts "
            "meaning *narrower than you think*.\n"
        )

    a("\n## Summary\n")
    a("| Repo | Lang | Decl | Branch | Behind | Dirty | ✅ | ⚠️ | N/A | Status |")
    a("|---|---|---|---|---|---|---|---|---|---|")
    for r in sorted(results, key=lambda x: (not x.catch_up_ran, -x.n_warn, x.name)):
        if not r.catch_up_ran:
            status = f"catch-up N/A — {r.catch_up_note[:30]}"
            ok = warn = "—"
        elif r.n_warn == 0:
            status = "✅ clean" if r.n_na == 0 else f"✅ clean ({r.n_na} N/A)"
            ok, warn = r.n_ok, 0
        else:
            status = f"{r.n_warn} real gap(s)"
            ok, warn = r.n_ok, r.n_warn
        dirty = "✱" if r.dirty else ""
        if r.behind_origin is None:
            behind = "?"
        elif r.behind_origin:
            behind = f"**-{r.behind_origin}**"
        else:
            behind = ""
        a(
            f"| `{r.name}` | {r.lang} | {r.declared_level or '—'} | "
            f"{r.git_branch or '—'} | {behind} | {dirty} | {ok} | {warn} | "
            f"{r.n_na or ''} | {status} |"
        )

    a("\n## Real gaps by repo\n")
    for r in sorted(results, key=lambda x: (-x.n_warn, x.name)):
        if not r.catch_up_ran or r.n_warn == 0:
            continue
        a(f"\n### `{r.name}` ({r.declared_level or '—'}) — {r.n_warn} real gap(s)\n")
        for c in r.warns:
            a(f"- ⚠️ {c.title}")

    na_repos = [r for r in results if r.n_na]
    if na_repos:
        a("\n## N/A (shape false-positives / declared declines — not gaps)\n")
        for r in sorted(na_repos, key=lambda x: x.name):
            a(f"- `{r.name}` ({r.lang}): {', '.join(c.title for c in r.na)}")

    not_run = [r for r in results if not r.catch_up_ran]
    if not_run:
        a("\n## catch-up did not run\n")
        for r in not_run:
            a(f"- `{r.name}` ({r.lang}): {r.catch_up_note}")

    (OUT_DIR / "item1-conformance.md").write_text("\n".join(lines) + "\n")


#: A tracker older than this is not evidence, it is a memory. Two weeks is long
#: enough that a normal review cadence never trips it, short enough that the
#: 25-day silence this gate exists to prevent cannot recur.
MAX_TRACKER_AGE_DAYS = 14


def check_tracker_freshness(max_age_days: int = MAX_TRACKER_AGE_DAYS) -> int:
    """Gate the COMMITTED tracker: is it fresh, stamped, and built on a clean input?

    Why this exists. The audit is expensive (it drives ``mgf-fed catch-up`` across
    the fleet), so it is run by hand — and on 2026-07-29 the committed tracker was
    found still asserting *"39 repos, 39 clean, 0 real gaps"* from **2026-07-04**.
    Twenty-five days of a stale artifact reading as current, because nothing was
    ever going to notice. A checker nobody runs silently becomes a liar.

    This is the cheap half of the fix and it needs no clones, no network and no
    credentials, so it rides the ordinary pipeline. It does not re-audit anything —
    it audits the *audit*, on three questions the artifact can answer about itself:

      1. Is it stamped at all?  (``--generated`` omitted writes "UNSTAMPED")
      2. Is it younger than ``max_age_days``?
      3. Was its input clean, or was it computed over stale checkouts?

    (3) matters as much as (2): a tracker generated today from a fleet 26 commits
    behind ``origin/main`` is fresh and wrong.
    """
    import datetime

    path = OUT_DIR / "item1-conformance.json"
    if not path.is_file():
        print(f"CONFORMANCE TRACKER: MISSING — {path}")
        print("  Run `mgf-fed conformance --generated $(date -u +%F)` and commit the result.")
        return 1
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, ValueError) as exc:
        print(f"CONFORMANCE TRACKER: UNREADABLE — {exc}")
        return 1

    problems: list[str] = []
    stamp = str(data.get("generated") or "")
    try:
        gen = datetime.date.fromisoformat(stamp[:10])
    except ValueError:
        problems.append(
            f"not dated — generated={stamp!r}. Regenerate with `--generated YYYY-MM-DD`; "
            "an unstamped tracker can never be shown to be stale."
        )
        gen = None
    if gen is not None:
        age = (datetime.date.today() - gen).days
        if age > max_age_days:
            problems.append(
                f"{age} days old (generated {gen}, limit {max_age_days}). "
                "It describes a fleet that has moved."
            )

    repos = data.get("repos") or []
    stale = [r for r in repos if (r.get("behind_origin") or 0) > 0]
    unknown = [r for r in repos if r.get("behind_origin") is None]
    if stale:
        worst = max(r.get("behind_origin") or 0 for r in stale)
        problems.append(
            f"built on a stale input — {len(stale)} of {len(repos)} repos were behind "
            f"origin/main (up to {worst} commits) when it ran. Fresh but wrong is still wrong."
        )
    if unknown:
        problems.append(
            f"{len(unknown)} of {len(repos)} repos had no origin/main to compare — "
            "their rows are unknown, not clean."
        )

    if problems:
        print("CONFORMANCE TRACKER: STALE")
        for p in problems:
            print(f"  - {p}")
        print("\n  Fix: refresh the fleet, then `mgf-fed conformance --generated $(date -u +%F)`")
        print("  and commit docs/inprogress/FLEET_AUDIT/item1-conformance.{json,md}.")
        return 1

    print(
        f"CONFORMANCE TRACKER: FRESH — generated {gen}, "
        f"{len(repos)} repos, input clean (every repo level with origin/main)."
    )
    return 0


def main() -> None:
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument(
        "--root",
        action="append",
        default=[],
        help="Extra search root for mgf-* repos (repeatable).",
    )
    ap.add_argument("--json-only", action="store_true")
    ap.add_argument(
        "--generated", default=None, help="Override generated timestamp (for reproducible output)."
    )
    ap.add_argument(
        "--check",
        action="store_true",
        help=(
            "Gate the COMMITTED tracker (stamped? fresh? clean input?) and exit 1 if not. "
            "Cheap: no clones, no network — safe to run in ordinary CI."
        ),
    )
    ap.add_argument(
        "--max-age-days",
        type=int,
        default=MAX_TRACKER_AGE_DAYS,
        help=f"Staleness limit for --check (default {MAX_TRACKER_AGE_DAYS}).",
    )
    args = ap.parse_args()

    if args.check:
        sys.exit(check_tracker_freshness(args.max_age_days))

    roots = [REPO_ROOT.parent] + [Path(r).resolve() for r in args.root]
    fed = find_fed_binary(roots)
    repos, skipped = discover_repos(roots)
    print(f"mgf-fed: {fed}")
    print(f"Auditing {len(repos)} repos across roots: {', '.join(str(r) for r in roots)}")
    if skipped:
        print(f"Skipped (no origin remote — a scaffold, not a fleet member): {', '.join(skipped)}")
    print()

    results = audit(fed, repos)

    OUT_DIR.mkdir(parents=True, exist_ok=True)
    generated = args.generated or os.environ.get("AUDIT_TS") or "UNSTAMPED (pass --generated)"
    write_json(results, generated, skipped)
    if not args.json_only:
        write_markdown(results, generated, skipped)
    print(f"\nWrote {OUT_DIR}/item1-conformance.{{json,md}}")


if __name__ == "__main__":
    main()
