#!/usr/bin/env python3
"""Fleet ownership-boundary audit — Item 2 of the libraries review.

Makes each library's ownership boundary EXPLICIT and VERIFIED, not implied.
`.importlinter` only guards *intra-repo* imports; this is the missing
*cross-repo* check: who is the SOLE owner (importer / credential-reader /
domain-holder) of each capability, proven by fleet-wide evidence.

Two engines:
  1. Import matrix — AST-parse every repo's src/, classify each import as
     stdlib / mgf-sibling / third-party, and build {third-party root -> repos}
     and {mgf.sibling -> repos}. The backbone: a heavyweight external dep
     imported by >1 repo is a boundary smell; the federation rule is one
     owning wrapper per external dep (FEDERATION §5 federation-first).
  2. Owned-resource verification — a curated map of marquee boundaries
     {resource: expected owner, patterns}. For each, find ALL referencing
     repos (imports + string greps for credentials/markers) and grade:
        SOLE-CONFIRMED  - only the owner references it
        ENCROACHED      - a non-owner references it (boundary violation)
        OWNER-MISSING   - the owner doesn't reference it (wrong owner?)
     This reproduces the mgf-cloud (sole openstacksdk) vs mgf-fed (zero
     OpenStack) reference case the review is modelled on.

Read-only. Output -> docs/inprogress/FLEET_AUDIT/item2-*.{md,json}.
"""
from __future__ import annotations

import argparse
import ast
import json
import re
import subprocess
import sys
from collections import defaultdict
from dataclasses import dataclass, field
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent          # mgf-standard/
ROOTS = [REPO_ROOT.parent]                                  # the on-disk federation
OUT_DIR = REPO_ROOT / "docs" / "inprogress" / "FLEET_AUDIT"
STDLIB = set(sys.stdlib_module_names) | {"__future__"}

# ---- curated owned-resource map: the marquee boundaries to verify ----------
# patterns are python regexes grepped over src/ (+ pyproject for deps/creds).
@dataclass
class Owned:
    resource: str
    owner: str
    import_roots: tuple[str, ...] = ()      # third-party top-level modules
    grep: tuple[str, ...] = ()              # extra string patterns (creds/markers)
    kind: str = "dep"                       # dep | credential | domain

OWNED = [
    Owned("OpenStack compute cloud", "mgf-cloud",
          ("openstack", "openstacksdk", "keystoneauth1", "novaclient"),
          (r"clouds\.ya?ml", r"OS_AUTH_URL", r"application_credential"), "credential"),
    Owned("libvirt / KVM / QEMU", "mgf-vm-libvirt",
          ("libvirt",), (r"qemu[-_]img", r"qemu:///", r"\bvirsh\b"), "dep"),
    Owned("Firecracker microVM", "mgf-vm-firecracker",
          (), (r"\bfirecracker\b", r"\bjailer\b", r"firecracker\.sock"), "dep"),
    Owned("pcap / packet forensics", "mgf-vm-forensics",
          ("dpkt", "scapy"), (r"\.pcap\b", r"top.?talkers"), "dep"),
    Owned("OVA / OVF interchange", "mgf-vm-interchange",
          (), (r"\bOVF\b", r"\.ova\b", r"\.ovf\b"), "domain"),
    Owned("gRPC VM transport", "mgf-vm-sdk",
          ("grpc", "grpc_tools"), (r"VmManagerClient",), "dep"),
    Owned("httpx HTTP client", "mgf-http", ("httpx",), (), "dep"),
    Owned("SQLAlchemy ORM/core", "mgf-sqlalchemy", ("sqlalchemy",), (), "dep"),
    Owned("Alembic migrations", "mgf-alembic", ("alembic",), (), "dep"),
    Owned("FastAPI web framework", "mgf-fastapi", ("fastapi", "starlette"), (), "dep"),
    Owned("Django web framework", "mgf-django", ("django",), (), "dep"),
    Owned("Redis / SSE livepush", "mgf-livepush", ("redis",), (r"text/event-stream",), "dep"),
    Owned("Clerk / Svix auth verify", "mgf-auth", ("svix",), (r"\bclerk\b", r"svix-id"), "dep"),
    Owned("Codeberg / Woodpecker dev-infra", "mgf-fed",
          (), (r"woodpecker", r"codeberg", r"\btea\b", r"gitea"), "credential"),
    Owned("Vault secrets backend", "mgf-common", ("hvac",), (r"vault", r"VAULT_ADDR"), "credential"),
    Owned("Teltonika codec", "mgf-tele-teltonika", (), (r"[Cc]odec ?8", r"[Tt]eltonika"), "domain"),
]

# Heavyweight third-party roots that should have exactly ONE owning wrapper.
# (Anything here imported by >1 repo is surfaced as a boundary smell.)
SINGLE_OWNER_EXPECTED = {
    "openstack", "openstacksdk", "libvirt", "dpkt", "scapy", "httpx",
    "sqlalchemy", "alembic", "fastapi", "starlette", "django", "redis",
    "svix", "hvac", "grpc",
}
# Deps every repo legitimately shares (test/build/lint infra) — never findings.
SHARED_INFRA = {
    "pytest", "hypothesis", "ruff", "mypy", "coverage", "_pytest", "click",
    "typer", "rich", "yaml", "tomllib", "tomli", "packaging", "setuptools",
    "hatchling", "pydantic", "pydantic_settings", "anyio", "typing_extensions",
    "structlog", "opentelemetry", "pytest_timeout",
}


@dataclass
class RepoScan:
    name: str
    has_src: bool
    py_files: int
    thirdparty: dict[str, int] = field(default_factory=dict)   # root -> count
    siblings: dict[str, int] = field(default_factory=dict)     # mgf.X -> count
    declared_deps: list[str] = field(default_factory=list)
    has_scope: bool = False


def repos(roots: list[Path]) -> list[Path]:
    seen: dict[str, Path] = {}
    for root in roots:
        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 own_pkg_roots(repo: Path) -> set[str]:
    src = repo / "src"
    out: set[str] = set()
    if src.is_dir():
        for child in src.iterdir():
            if child.is_dir() and not child.name.startswith("."):
                out.add(child.name)
    return out


def extract_imports(repo: Path) -> tuple[dict[str, int], dict[str, int], int]:
    """Return (thirdparty root->count, mgf.sibling->count, py_file_count)."""
    tp: dict[str, int] = defaultdict(int)
    sib: dict[str, int] = defaultdict(int)
    src = repo / "src"
    n = 0
    if not src.is_dir():
        return {}, {}, 0
    for py in src.rglob("*.py"):
        n += 1
        try:
            tree = ast.parse(py.read_text(errors="replace"))
        except SyntaxError:
            continue
        for node in ast.walk(tree):
            mods: list[str] = []
            if isinstance(node, ast.Import):
                mods = [a.name for a in node.names]
            elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module:
                mods = [node.module]
            for m in mods:
                root = m.split(".")[0]
                if root == "mgf":
                    second = ".".join(m.split(".")[:2])  # mgf.common
                    sib[second] += 1
                elif root in STDLIB:
                    continue
                else:
                    tp[root] += 1
    return dict(tp), dict(sib), n


def declared_deps(repo: Path) -> list[str]:
    pp = repo / "pyproject.toml"
    if not pp.is_file():
        return []
    try:
        import tomllib
        data = tomllib.loads(pp.read_text())
    except Exception:
        return []
    deps: list[str] = list(data.get("project", {}).get("dependencies", []) or [])
    for grp in (data.get("project", {}).get("optional-dependencies", {}) or {}).values():
        deps.extend(grp)
    # normalise to the distribution name
    out = []
    for d in deps:
        name = (d.split("[")[0].split(">=")[0].split("==")[0].split("<")[0]
                .split(">")[0].split("~=")[0].split(";")[0].split("@")[0].strip())
        if name:
            out.append(name)
    return sorted(set(out))


def grep_repo(repo: Path, pattern: str) -> int:
    """Count matches of a regex across the repo's src/ (+ pyproject). rg if present."""
    src = repo / "src"
    targets = [str(src)] if src.is_dir() else []
    pp = repo / "pyproject.toml"
    if pp.is_file():
        targets.append(str(pp))
    if not targets:
        return 0
    r = subprocess.run(["grep", "-rIiE", "--include=*.py", "--include=*.toml",
                        pattern, *targets], capture_output=True, text=True)
    return len([ln for ln in r.stdout.splitlines() if ln.strip()])


def scan(repo: Path) -> RepoScan:
    tp, sib, n = extract_imports(repo)
    return RepoScan(
        name=repo.name, has_src=(repo / "src").is_dir(), py_files=n,
        thirdparty=tp, siblings=sib, declared_deps=declared_deps(repo),
        has_scope=(repo / "SCOPE.md").is_file(),
    )


def verify_owned(repo_by_name: dict[str, Path], scans: dict[str, RepoScan]) -> list[dict]:
    """Grade each owned resource.

    Primary (rigorous) signal = IMPORTS. A non-owner *importing* an owned
    third-party module is a real encroachment. Secondary signal = string
    greps (creds/markers/prose) — non-owners here are flagged for *triage*,
    not auto-failed (the mgf-fed/openstack reference case proved these are
    usually benign wrapper-references or prose).
    """
    results = []
    for o in OWNED:
        importers: dict[str, int] = {}
        ref_only: dict[str, int] = {}      # string hits but no import (triage)
        for name, repo in repo_by_name.items():
            ih = sum(scans[name].thirdparty.get(r, 0) for r in o.import_roots)
            gh = sum(grep_repo(repo, p) for p in o.grep) if o.grep else 0
            if ih > 0:
                importers[name] = ih
            elif gh > 0:
                ref_only[name] = gh
        non_owner_imports = sorted(k for k in importers if k != o.owner)
        if o.import_roots:                 # a python-importable resource
            if not importers:
                verdict = "NO-IMPORT"      # nobody imports it (grep-only / non-py)
            elif non_owner_imports:
                verdict = "MULTI-IMPORTER"  # real boundary question
            elif o.owner in importers:
                verdict = "SOLE-IMPORTER"   # ✅ verified
            else:
                verdict = "OWNER-NOT-IMPORTER"
        else:                              # non-python resource (binary/CLI/cred)
            verdict = "GREP-ONLY"
        results.append({
            "resource": o.resource, "owner": o.owner, "kind": o.kind,
            "verdict": verdict, "importers": importers,
            "non_owner_importers": non_owner_imports,
            "string_refs_triage": ref_only,
        })
    return results


# --- boundary stanza: the verified-ownership block, shared by the central doc,
#     the per-repo SCOPE.md injection, and the --check drift gate. The stanza
#     carries only module *names* (never counts), so it changes ONLY on a real
#     boundary change (a new/lost sole-owner, a new shared dep, a new sibling
#     import) — never on import-count noise. That makes --check meaningful.
BOUNDARY_BEGIN = ("<!-- BEGIN BOUNDARY (generated by mgf-standard/scripts/"
                  "fleet_ownership_audit.py --inject; do not hand-edit) -->")
BOUNDARY_END = "<!-- END BOUNDARY -->"
BOUNDARY_HEADING = "## Boundary — verified ownership (generated)"
BOUNDARY_INTRO = (
    "> Generated from the fleet-wide import matrix — the **verified** ownership "
    "boundary (who is the SOLE importer of each third-party dep across the whole "
    "federation). Regenerate with `fleet_ownership_audit.py --inject`; CI-gated "
    "against code drift by `--check`. **Do not hand-edit the block below.**"
)


def repo_stanza(name: str, s: RepoScan, tp_matrix: dict[str, dict[str, int]]) -> list[str]:
    """The verified-boundary stanza lines for one python repo (or [] if it has
    no python in `src/` — i.e. a non-python/asset/zig repo, or the mgf-standard
    root). A python repo with no third-party imports still gets a stanza (it
    states 'owns nothing exclusive; pure federation consumer')."""
    if s.py_files == 0:
        return []
    sole = sorted(r for r in s.thirdparty
                  if r not in SHARED_INFRA and len(tp_matrix.get(r, {})) == 1)
    shared = sorted(r for r in s.thirdparty
                    if r not in SHARED_INFRA and len(tp_matrix.get(r, {})) > 1)
    sibs = sorted(s.siblings)
    out = [
        "- **Owns (sole third-party importer, fleet-verified):** "
        + (", ".join(f"`{x}`" for x in sole) if sole
           else "— (no exclusive third-party dep; domain/seam owner)")
    ]
    if shared:
        sh = ", ".join(f"`{x}` (+{', '.join(r for r in sorted(tp_matrix[x]) if r != name)})"
                       for x in shared)
        out.append(f"- **Shared third-party (intentional, boundary-checked):** {sh}")
    out.append("- **Federation deps (imports):** "
               + (", ".join(f"`{x}`" for x in sibs) if sibs else "— (none; leaf/cornerstone)"))
    return out


def boundary_block(name: str, s: RepoScan, tp_matrix: dict[str, dict[str, int]]) -> str:
    """The marker-delimited block embedded in a repo's SCOPE.md ('' if N/A)."""
    stanza = repo_stanza(name, s, tp_matrix)
    if not stanza:
        return ""
    return "\n".join([BOUNDARY_BEGIN, *stanza, BOUNDARY_END])


_BLOCK_RE = re.compile(re.escape(BOUNDARY_BEGIN) + r".*?" + re.escape(BOUNDARY_END), re.DOTALL)


def extract_block(text: str) -> str | None:
    m = _BLOCK_RE.search(text)
    return m.group(0) if m else None


def inject_scope(repo: Path, block: str) -> str:
    """Write/refresh the boundary block in repo/SCOPE.md.
    Returns: created | updated | unchanged | skip-no-scope | skip-no-block."""
    if not block:
        return "skip-no-block"
    scope = repo / "SCOPE.md"
    if not scope.is_file():
        return "skip-no-scope"
    text = scope.read_text()
    if _BLOCK_RE.search(text):
        new = _BLOCK_RE.sub(lambda _m: block, text)
        if new == text:
            return "unchanged"
        scope.write_text(new)
        return "updated"
    sep = "" if text.endswith("\n\n") else ("\n" if text.endswith("\n") else "\n\n")
    scope.write_text(text + sep + f"\n{BOUNDARY_HEADING}\n\n{BOUNDARY_INTRO}\n\n{block}\n")
    return "created"


def render_central(scans: dict[str, RepoScan],
                   tp_matrix: dict[str, dict[str, int]]) -> str:
    """Pure render of the central per-library boundary doc (no side effects)."""
    lines: list[str] = [
        "# Item 2 — per-library verified boundary statements\n",
        "> Auto-generated by `scripts/fleet_ownership_audit.py` from the import "
        "matrix — **regenerable, not hand-asserted**. 'Owns (sole importer)' means "
        "the third-party module is imported by **only** this repo across the whole "
        "federation. The same stanza is injected into each repo's `SCOPE.md` "
        "(`--inject`) and drift-gated (`--check`).\n",
    ]
    for name in sorted(scans):
        stanza = repo_stanza(name, scans[name], tp_matrix)
        if not stanza:
            continue
        lines.append(f"\n### `{name}`")
        lines.extend(stanza)
    return "\n".join(lines) + "\n"


def write_boundaries(scans: dict[str, RepoScan],
                     tp_matrix: dict[str, dict[str, int]]) -> None:
    (OUT_DIR / "item2-boundaries.md").write_text(render_central(scans, tp_matrix))


def _scan_fleet() -> tuple[list[Path], dict[str, Path], dict[str, RepoScan],
                          dict[str, dict[str, int]], dict[str, dict[str, int]]]:
    rps = repos(ROOTS)
    repo_by_name = {r.name: r for r in rps}
    scans = {r.name: scan(r) for r in rps}
    tp_matrix: dict[str, dict[str, int]] = defaultdict(dict)
    sib_matrix: dict[str, dict[str, int]] = defaultdict(dict)
    for name, s in scans.items():
        for root, c in s.thirdparty.items():
            tp_matrix[root][name] = c
        for sibmod, c in s.siblings.items():
            sib_matrix[sibmod][name] = c
    return rps, repo_by_name, scans, dict(tp_matrix), dict(sib_matrix)


def cmd_audit(inject: bool) -> int:
    """Default: regen the central item2-*.{md,json}. With --inject also write
    the verified boundary block into every repo's SCOPE.md."""
    rps, repo_by_name, scans, tp_matrix, sib_matrix = _scan_fleet()
    print(f"Scanning {len(rps)} repos for imports + ownership ...")
    owned = verify_owned(repo_by_name, scans)
    smells = [{"module": root, "repos": sorted(by_repo)}
              for root, by_repo in sorted(tp_matrix.items())
              if root not in SHARED_INFRA and len(by_repo) > 1]
    OUT_DIR.mkdir(parents=True, exist_ok=True)
    payload = {
        "n_repos": len(rps),
        "scans": {n: {"has_src": s.has_src, "py_files": s.py_files,
                      "has_scope": s.has_scope,
                      "thirdparty": s.thirdparty, "siblings": s.siblings,
                      "declared_deps": s.declared_deps} for n, s in scans.items()},
        "thirdparty_matrix": dict(sorted(tp_matrix.items())),
        "sibling_matrix": dict(sorted(sib_matrix.items())),
        "owned_verification": owned,
        "multi_importer_smells": smells,
    }
    (OUT_DIR / "item2-ownership.json").write_text(json.dumps(payload, indent=2) + "\n")
    write_boundaries(scans, tp_matrix)

    if inject:
        print("\n== Injecting boundary blocks into SCOPE.md ==")
        tally: dict[str, list[str]] = defaultdict(list)
        for name in sorted(scans):
            res = inject_scope(repo_by_name[name], boundary_block(name, scans[name], tp_matrix))
            tally[res].append(name)
        for res in ("created", "updated", "unchanged", "skip-no-scope", "skip-no-block"):
            if tally[res]:
                print(f"  {res:<14} ({len(tally[res])}): {', '.join(tally[res])}")

    print("\n== Owned-resource verification (IMPORTS = rigorous) ==")
    mark = {"SOLE-IMPORTER": "✅", "MULTI-IMPORTER": "⚠️", "NO-IMPORT": "·",
            "GREP-ONLY": "·", "OWNER-NOT-IMPORTER": "❓"}
    for o in owned:
        print(f"  {mark[o['verdict']]} {o['resource']:<30} {o['owner']:<18} {o['verdict']}")
        if o["non_owner_importers"]:
            print(f"       ⚠️ NON-OWNER IMPORTERS: {', '.join(o['non_owner_importers'])}")
    no_scope = [n for n, s in scans.items() if not s.has_scope]
    print(f"\n== SCOPE.md missing ({len(no_scope)}): {', '.join(no_scope)}")
    print(f"Wrote {OUT_DIR}/item2-ownership.json + item2-boundaries.md")
    return 0


def cmd_check() -> int:
    """Drift gate (Item 3 seed): the committed central boundary doc AND every
    repo's injected SCOPE.md block MUST match the live import reality. The
    stanzas carry only module names, so this fires on a real boundary change
    (a new/lost sole-owner, a new shared dep, a new sibling import) — not on
    import-count noise. Exit 1 on any drift."""
    _, repo_by_name, scans, tp_matrix, _ = _scan_fleet()
    drifts: list[str] = []
    central = OUT_DIR / "item2-boundaries.md"
    if not central.is_file() or central.read_text() != render_central(scans, tp_matrix):
        drifts.append("docs/inprogress/FLEET_AUDIT/item2-boundaries.md is STALE "
                      "— run `fleet_ownership_audit.py` and commit")
    missing_block: list[str] = []
    for name in sorted(scans):
        block = boundary_block(name, scans[name], tp_matrix)
        if not block:
            continue
        scope = repo_by_name[name] / "SCOPE.md"
        if not scope.is_file():
            continue  # missing SCOPE.md reported as a non-fatal note below
        cur = extract_block(scope.read_text())
        if cur is None:
            missing_block.append(name)        # not-yet-adopted: a note, not a failure
        elif cur.strip() != block.strip():
            drifts.append(f"{name}: SCOPE.md boundary block is STALE vs live imports "
                          "— run `--inject` and commit")  # an adopted block drifted: RED
    # The authoritative, always-current boundary is the central doc; per-repo
    # SCOPE.md blocks are an opt-in local mirror. So the gate is RED only on a
    # STALE central doc or a STALE adopted block — never on a not-yet-injected one.
    if drifts:
        print("OWNERSHIP-BOUNDARY GATE: RED")
        for d in drifts:
            print(f"  - {d}")
        return 1
    with_block = sum(1 for n in scans
                     if boundary_block(n, scans[n], tp_matrix)
                     and (repo_by_name[n] / "SCOPE.md").is_file()
                     and extract_block((repo_by_name[n] / "SCOPE.md").read_text()))
    print(f"OWNERSHIP-BOUNDARY GATE: GREEN — central boundary doc current; "
          f"{with_block} repos carry an in-sync SCOPE.md block")
    if missing_block:
        print(f"  note: {len(missing_block)} python repos haven't adopted the SCOPE.md "
              f"block yet (`--inject`): {', '.join(missing_block)}")
    no_scope = sorted(n for n, s in scans.items() if not s.has_scope)
    if no_scope:
        print(f"  note: {len(no_scope)} repos have no SCOPE.md (non-python/asset): "
              + ", ".join(no_scope))
    return 0


def main() -> int:
    ap = argparse.ArgumentParser(description="Fleet ownership-boundary audit (Item 2).")
    ap.add_argument("--inject", action="store_true",
                    help="write the verified boundary block into each repo's SCOPE.md")
    ap.add_argument("--check", action="store_true",
                    help="drift gate: committed boundaries MUST match live imports (exit 1 on drift)")
    args = ap.parse_args()
    if args.check:
        return cmd_check()
    return cmd_audit(inject=args.inject)


if __name__ == "__main__":
    raise SystemExit(main())
