#!/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


@dataclass
class RepoAudit:
    name: str
    path: str
    lang: str
    declared_level: str | None
    git_branch: str | None
    dirty: bool
    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]:
    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"))
    return branch, dirty


def parse_checks(output: str) -> list[Check]:
    checks: list[Check] = []
    for line in output.splitlines():
        m = GLYPH_RE.match(line)
        if m:
            checks.append(Check(status=STATUS_GLYPHS[m.group(1)], title=m.group(2)))
    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]) -> list[Path]:
    seen: dict[str, Path] = {}
    for root in roots:
        if not root.is_dir():
            continue
        for child in sorted(root.glob("mgf-*")):
            if child.is_dir() and (child / ".git").exists():
                seen.setdefault(child.name, child)
    return list(seen.values())


def audit(fed: str, repos: list[Path]) -> list[RepoAudit]:
    results: list[RepoAudit] = []
    for repo in repos:
        lang = detect_lang(repo)
        branch, dirty = 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, 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) -> None:
    payload = {
        "generated": generated,
        "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) -> 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")

    a("\n## Summary\n")
    a("| Repo | Lang | Decl | Branch | 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 ""
        a(f"| `{r.name}` | {r.lang} | {r.declared_level or '—'} | "
          f"{r.git_branch or '—'} | {dirty} | {ok} | {warn} | {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")


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).")
    args = ap.parse_args()

    roots = [REPO_ROOT.parent] + [Path(r).resolve() for r in args.root]
    fed = find_fed_binary(roots)
    repos = discover_repos(roots)
    print(f"mgf-fed: {fed}")
    print(f"Auditing {len(repos)} repos across roots: "
          f"{', '.join(str(r) for r in roots)}\n")

    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)
    if not args.json_only:
        write_markdown(results, generated)
    print(f"\nWrote {OUT_DIR}/item1-conformance.{{json,md}}")


if __name__ == "__main__":
    main()
