#!/usr/bin/env python3
"""Generate ``MGF_LIBRARIES.md`` + run the federation release-consistency gate.

This is the mechanical core of the mgf-common supervisor's
release-consistency duty (RELEASING REL-13). It scans the on-disk
federation (sibling ``mgf-*`` repos next to mgf-common), builds the
dependency graph from each repo's manifest, and:

  - emits ``MGF_LIBRARIES.md`` at the mgf-standard repo root — the
    always-current registry + high-level guide to every library. It has
    five sections: an **At a glance** index table, a **Release &
    compatibility** matrix (each library's version + the mgf-common
    window it accepts + whether the current cornerstone satisfies it),
    a **Release order (waves)** list (the dependency-ordered roots->leaves
    walk for a federation-wide update, from ``fed_waves.compute_waves``),
    a **Per-library detail** table for every library (version, Python,
    install, deps with pins, extras, third-party footprint, consumers,
    purpose), and a **Consistency** verdict;
  - checks **consistency**: every mgf-* → mgf-* dependency pin must
    accept the dependency's *current* version. A dependent pinned to a
    window that excludes the current version is a hard failure
    (exit 1) — that is the gate.
  - checks **currency** (``--check``): the committed registry MUST match
    a fresh render. "A library was released but the registry wasn't
    regenerated" is the drift this catches — it is what makes the
    "update on every release" rule (REL-13 / DOC-18) enforceable.

Usage::

    python scripts/gen_mgf_libraries.py            # WRITE the registry (+ consistency)
    python scripts/gen_mgf_libraries.py --check     # release gate: consistency + currency

The ``--check`` form is the supervisor's pre-release gate — it writes
nothing and exits 1 if the federation is inconsistent OR the committed
registry is stale. Run the no-arg form to regenerate, then commit.

Stdlib-only (tomllib + json + re + pathlib). The generator itself is a
supervisor/dev tool (not shipped), but its machine-readable sibling
output ``MGF_LIBRARIES.json`` IS shipped as mgf-standard wheel data
and resolved through ``mgf.standard.libraries()`` — that is how an
agent becomes federation-aware (the roster + dependency graph) from the
dependency it already pins. Assumes the federation-on-disk layout the
cohort-release recipe also assumes (siblings under one parent dir).
"""

from __future__ import annotations

import argparse
import datetime
import json
import re
import sys
import tomllib
from dataclasses import dataclass, field
from pathlib import Path

# The dependency-wave algorithm lives in the sibling ``fed_waves.py`` so the
# registry render and the standalone ``scripts/fed_waves.py`` CLI compute the
# release order from one source of truth. Add this script's own directory to
# the path so the import resolves whether run as ``python scripts/...`` or
# loaded by file path (e.g. under pytest's ``spec_from_file_location``).
_SCRIPTS_DIR = Path(__file__).resolve().parent
if str(_SCRIPTS_DIR) not in sys.path:
    sys.path.insert(0, str(_SCRIPTS_DIR))
from fed_waves import compute_waves  # noqa: E402  (after the sys.path shim)

CODEBERG_ORG = "magogi-admin"
DEPS_URL = "https://codeberg.org/{org}/{repo}/src/branch/main/DEPS.md"

# A federation member is a buildable/renderable artifact: it carries one
# of these manifests at its root. An ``mgf-*`` directory with none of them
# is NOT a member — e.g. an architectural-review deliverable (``mgf-*-review``)
# or other scratch repo that happens to sit in the federation parent dir.
# Keying on the manifest (not on a name pattern) keeps the gate principled
# and self-maintaining as new member kinds appear.
MEMBER_MANIFESTS = ("pyproject.toml", "build.zig.zon", "cookiecutter.json")

# Repos explicitly excluded from the registry even though they carry a member
# manifest. ``mgf-vm`` was retired and DELETED by the vm-vmanager-app team
# (2026-06; the umbrella drain is complete — every former consumer now depends
# on ``mgf-vm-core`` instead, and it was never published to the Codeberg
# registry, PACKAGE_DISTRIBUTION.md / PKG-01). The checkout is gone, so this is
# now a defensive guard: it keeps a stray/resurrected ``mgf-vm`` working copy
# from re-entering the roster or any member's dependency graph. Safe to drop
# once you're confident no such checkout can reappear.
_EXCLUDED_REPOS = frozenset({"mgf-vm"})


@dataclass
class Library:
    repo: str  # on-disk dir name == Codeberg repo slug (hyphen form)
    lang: str  # "python" | "zig" | "template" | "other"
    version: str
    purpose: str
    # name -> pin string (mgf-* deps only).
    deps: dict[str, str] = field(default_factory=dict)
    used_by: list[str] = field(default_factory=list)
    requires_python: str = ""  # e.g. ">=3.11" (python libs only)
    extras: tuple[str, ...] = ()  # optional-dependency group names
    third_party_count: int = 0  # distinct non-mgf runtime deps (footprint proxy)


def _zon_name_to_repo(zon_name: str) -> str:
    """`mgf_hrb_model` -> `mgf-hrb-model` (zon uses underscores)."""
    return zon_name.replace("_", "-")


def _purpose_from_readme(repo_dir: Path, limit: int = 200, *, skip: str = "") -> str:
    """First contentful README line. ``skip`` ignores a title line that is
    just the repo's own name (an H1 like ``# mgf-website-template`` carries
    no information the row doesn't already have)."""
    readme = repo_dir / "README.md"
    if not readme.is_file():
        return ""
    for line in readme.read_text(encoding="utf-8", errors="replace").splitlines():
        s = line.strip().lstrip("#").strip()
        if s and not s.startswith(("<", "!", "[![", ">")) and s != skip:
            return s[:limit]
    return ""


def _parse_python(repo_dir: Path, repo: str) -> Library | None:
    pyproject = repo_dir / "pyproject.toml"
    if not pyproject.is_file():
        return None
    data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
    project = data.get("project", {})
    version = project.get("version", "?")
    requires_python = str(project.get("requires-python", "") or "").strip()
    purpose = (project.get("description") or _purpose_from_readme(repo_dir)).strip()
    runtime = list(project.get("dependencies", []))
    optional = project.get("optional-dependencies", {}) or {}
    extras = tuple(sorted(optional.keys()))

    deps: dict[str, str] = {}
    third_party: set[str] = set()
    for spec in [*runtime, *(s for grp in optional.values() for s in grp)]:
        m = re.match(r"^\s*(mgf-[a-z0-9-]+)\s*(.*)$", spec)
        if m:
            deps[m.group(1)] = (m.group(2) or "").strip() or "(any)"
    # Third-party footprint = distinct non-mgf RUNTIME (non-extra) deps.
    for spec in runtime:
        if re.match(r"^\s*mgf-[a-z0-9-]+", spec):
            continue
        name = re.match(r"^\s*([A-Za-z0-9._-]+)", spec)
        if name:
            third_party.add(name.group(1).lower())
    return Library(
        # Slug = the published package name (== dir name for every repo except one
        # mid-rename, where pyproject `name` is the source of truth for the slug).
        repo=project.get("name", repo) or repo,
        lang="python",
        version=version,
        purpose=purpose,
        deps=deps,
        requires_python=requires_python,
        extras=extras,
        third_party_count=len(third_party),
    )


_CHANGELOG_VERSION_RE = re.compile(r"^##\s*\[(\d[0-9A-Za-z.]*)\]", re.MULTILINE)


def _version_from_changelog(repo_dir: Path) -> str:
    """Latest released version from a Keep-a-Changelog ``## [X.Y.Z]``
    heading. The first *versioned* heading wins (``[Unreleased]`` doesn't
    match the leading-digit pattern); ``?`` if none — which the integrity
    check turns into a hard failure."""
    changelog = repo_dir / "CHANGELOG.md"
    if not changelog.is_file():
        return "?"
    m = _CHANGELOG_VERSION_RE.search(changelog.read_text(encoding="utf-8", errors="replace"))
    return m.group(1) if m else "?"


def _parse_cookiecutter(repo_dir: Path, repo: str) -> Library | None:
    """A cookiecutter-template member (e.g. ``mgf-website-template``).

    Not a Python package — there is no manifest version to read. The
    template's release contract is its CHANGELOG (Keep a Changelog +
    SemVer over the *generated project shape*), so the version is the
    latest released ``## [X.Y.Z]`` heading there.
    """
    if not (repo_dir / "cookiecutter.json").is_file():
        return None
    return Library(
        repo=repo,
        lang="template",
        version=_version_from_changelog(repo_dir),
        purpose=_purpose_from_readme(repo_dir, skip=repo),
    )


def _parse_zig(repo_dir: Path, repo: str) -> Library | None:
    zon = repo_dir / "build.zig.zon"
    if not zon.is_file():
        return None
    text = zon.read_text(encoding="utf-8")
    vm = re.search(r"\.version\s*=\s*\"([^\"]+)\"", text)
    version = vm.group(1) if vm else "?"
    deps: dict[str, str] = {}
    # Each mgf-* dependency carries a Codeberg url + a hash whose prefix
    # encodes the pinned version, e.g. `mgf_hrb_model-0.1.0-<b32>`.
    for dep_repo, ver in re.findall(
        r"codeberg\.org/" + re.escape(CODEBERG_ORG) + r"/(mgf-[a-z0-9-]+)\.git#[0-9a-f]+\"[^}]*?"
        r"\.hash\s*=\s*\"[a-z0-9_]+-([0-9][0-9A-Za-z.]*)-",
        text,
        re.DOTALL,
    ):
        deps[dep_repo] = f"=={ver} (git-pinned)"
    purpose = _purpose_from_readme(repo_dir)
    return Library(repo=repo, lang="zig", version=version, purpose=purpose, deps=deps)


def discover(federation_root: Path) -> dict[str, Library]:
    libs: dict[str, Library] = {}
    for repo_dir in sorted(federation_root.glob("mgf-*")):
        if not (repo_dir / ".git").is_dir():
            continue
        if repo_dir.name in _EXCLUDED_REPOS:
            # Explicitly retired/excluded (see _EXCLUDED_REPOS) — e.g. mgf-vm,
            # being deleted + never published to the Codeberg registry.
            continue
        if not any((repo_dir / m).is_file() for m in MEMBER_MANIFESTS):
            # No recognised manifest — not a federation member (e.g. a
            # ``mgf-*-review`` audit deliverable). Skip so it never lands
            # in the registry.
            continue
        repo = repo_dir.name
        lib = (
            _parse_python(repo_dir, repo)
            or _parse_zig(repo_dir, repo)
            or _parse_cookiecutter(repo_dir, repo)
        )
        if lib is None:
            # Has a member manifest but no dedicated parser (a future
            # member kind) — a member, just not py/zig/template. Its "?"
            # version will trip the integrity check, which is the signal
            # to teach the generator the new shape.
            lib = Library(
                repo=repo,
                lang="other",
                version="?",
                purpose=_purpose_from_readme(repo_dir),
            )
        libs[lib.repo] = lib  # key by the package-name slug (== dir for all but a renamed repo)
    # Excluded repos (e.g. retired mgf-vm) must not appear as a dep of any
    # member — and neither must the member itself: a PEP 508 self-referential
    # extra (e.g. mgf-fed's `standards = ["mgf-fed[cli]"]` back-compat alias)
    # is legitimate packaging, but it is NOT a dependency edge. A library
    # must never list itself in deps/used-by (F-M6).
    for lib in libs.values():
        lib.deps = {
            d: p for d, p in lib.deps.items() if d not in _EXCLUDED_REPOS and d != lib.repo
        }
    # Reverse edges (mgf-* -> mgf-* only).
    for repo, lib in libs.items():
        for dep in lib.deps:
            if dep in libs:
                libs[dep].used_by.append(repo)
    for lib in libs.values():
        lib.used_by.sort()
    return libs


def _ver_tuple(v: str) -> tuple[int, ...]:
    parts = re.findall(r"\d+", v)
    return tuple(int(p) for p in parts) or (0,)


def _window_accepts(pin: str, version: str) -> bool | None:
    """Does a `>=A,<B` pin window accept `version`? None = can't tell."""
    lo = re.search(r">=\s*([0-9][0-9.]*)", pin)
    hi = re.search(r"<\s*([0-9][0-9.]*)", pin)
    if not lo and not hi:
        return None  # exact/git pin or unparseable — handled elsewhere
    v = _ver_tuple(version)
    if lo:
        lov = _ver_tuple(lo.group(1))
        if v[: len(lov)] < lov:
            return False
    if hi:
        hiv = _ver_tuple(hi.group(1))
        if v[: len(hiv)] >= hiv:
            return False
    return True


def _pinned_version(pin: str) -> str | None:
    """Extract the exact version a NON-window pin pins to, or None.

    Covers both manifest dialects:
      - Zig zon (as this generator encodes it): ``==X.Y.Z (git-pinned)``.
      - Python PEP 508 direct git refs: ``@ git+...@v1.2.3`` /
        ``...@1.2.3`` / ``...#v1.2.3``. A bare-SHA ref (``#<sha>``, no
        dotted version) returns None — there's no semantic version to
        compare, so the gate abstains on it.
    """
    m = re.search(r"==\s*([0-9][0-9A-Za-z.]*)", pin)
    if m:
        return m.group(1)
    if "git+" in pin or pin.lstrip().startswith("@"):
        # First `@vX.Y` / `#vX.Y` whose token is a dotted version.
        m = re.search(r"[@#]v?([0-9]+(?:\.[0-9]+)+)\b", pin)
        if m:
            return m.group(1)
    return None


def _deps_by_repo(libs: dict[str, Library]) -> dict[str, list[str]]:
    """The ``{repo: [mgf-* dep, ...]}`` graph the wave computer consumes."""
    return {repo: list(lib.deps) for repo, lib in libs.items()}


def check_consistency(libs: dict[str, Library]) -> list[str]:
    """Return a list of hard inconsistencies (empty == federation consistent)."""
    problems: list[str] = []
    for repo, lib in libs.items():
        for dep, pin in lib.deps.items():
            target = libs.get(dep)
            if target is None:
                continue
            accepts = _window_accepts(pin, target.version)
            if accepts is False:
                problems.append(
                    f"{repo} pins `{dep} {pin}` but {dep} is at "
                    f"{target.version} — pin EXCLUDES the current version "
                    f"(walk it forward + verify)."
                )
            elif accepts is None:
                # Not a `>=,<` window — an exact / git-ref pin (Zig hash
                # or Python `@ git+...@vX`). Lag-check the pinned version.
                pinned = _pinned_version(pin)
                if pinned and _ver_tuple(pinned) < _ver_tuple(target.version):
                    kind = "git-ref" if ("git+" in pin or "git-pinned" in pin) else "exact"
                    problems.append(
                        f"{repo} {kind}-pins `{dep}` at {pinned} but {dep} "
                        f"is at {target.version} — pin LAGS (re-pin to the "
                        f"current version + verify; prefer a devpi-index window over "
                        f"a git direct ref where the dep is published)."
                    )
    return sorted(problems)


def check_integrity(libs: dict[str, Library]) -> list[str]:
    """Structural defects in the roster itself (not pin windows).

    A registry row that lists a library as its own dependency (self-edge)
    or whose version could not be determined (``?``) is a generator or
    metadata defect, never publishable truth (F-M6). ``discover`` drops
    self-edges at build time; this check is the belt-and-braces gate that
    keeps the whole class from returning through any future code path.
    """
    problems: list[str] = []
    for repo, lib in libs.items():
        if repo in lib.deps:
            problems.append(
                f"{repo} lists itself as a dependency (self-edge) — a "
                f"library must never appear in its own deps; fix the "
                f"generator/graph build (a PEP 508 self-referential extra "
                f"is not a dependency edge)."
            )
        if repo in lib.used_by:
            problems.append(
                f"{repo} lists itself in used-by (self-edge) — reverse "
                f"edges must exclude the library itself."
            )
        if lib.version == "?":
            problems.append(
                f"{repo} has version '?' — the generator could not "
                f"determine a version from the repo's manifests/CHANGELOG; "
                f"teach the generator the repo's shape or fix the source "
                f"metadata."
            )
    return sorted(problems)


def _cell(text: str, limit: int | None = None) -> str:
    """Make text safe for a markdown table cell (escape pipes, flatten
    newlines), optionally truncating to ``limit`` chars."""
    t = (text or "—").replace("|", "\\|").replace("\n", " ").strip() or "—"
    if limit is not None and len(t) > limit:
        t = t[: limit - 1].rstrip() + "…"
    return t


def _accepts_cornerstone(lib: Library, repo: str, mc_version: str) -> tuple[str, str]:
    """(verdict-glyph, pin-display) for the compatibility matrix."""
    if repo == "mgf-common":
        return "—", "_(cornerstone)_"
    pin = lib.deps.get("mgf-common")
    if pin is None:
        return "n/a", "—"
    verdict = _window_accepts(pin, mc_version)
    if verdict is True:
        return "✅", f"`{pin}`"
    if verdict is False:
        return "❌", f"`{pin}`"
    return "🔒", f"`{pin}`"  # exact / git-ref pin — see Consistency notes


def render(libs: dict[str, Library]) -> str:
    today = datetime.datetime.now(tz=datetime.UTC).date().isoformat()
    cornerstone = libs.get("mgf-common")
    mc_version = cornerstone.version if cornerstone else "?"
    out: list[str] = []
    add = out.append

    # ---- Header / intro -------------------------------------------------
    add("# MGF_LIBRARIES — the Magogi Foundation library registry")
    add("")
    add(
        "> **The authoritative, always-current index of every Magogi "
        "Foundation library.** Maintained by the mgf-common supervisor; "
        "**generated** by `scripts/gen_mgf_libraries.py` and regenerated "
        "as part of every release (RELEASING **REL-13**), so it MUST be "
        "up to date at all times. Do not hand-edit — run the generator."
    )
    add(">")
    add(
        "> **Each library name links to that library's `DEPS.md`** (its "
        "bidirectional dependency manifest — what it depends on + who "
        "depends on it)."
    )
    add(">")
    add(
        "> **Who may change a library:** only the **mgf-common** "
        "supervisor cuts releases or edits another library. Any other "
        "project that wants a change files a request in the **target "
        "library's `FEEDBACK.md`** — it MUST NOT edit the library "
        "itself (AGENTS **AG-13**)."
    )
    add("")
    add(
        f"_Generated {today} from the on-disk federation. Versions are the "
        "working-tree (about-to-release) values; the devpi index is the contract once "
        "published (**AG-09/PKG-11**) — see docs/standards/PACKAGE_DISTRIBUTION.md._"
    )
    add("")
    add(f"**{len(libs)} libraries** · cornerstone **mgf-common {mc_version}**.")
    add("")
    add(
        "**Contents:** [At a glance](#at-a-glance) · "
        "[Release & compatibility](#release--compatibility) · "
        "[Release order (waves)](#release-order-waves) · "
        "[Per-library detail](#per-library-detail) · "
        "[Consistency](#consistency)"
    )
    add("")

    # ---- At a glance ----------------------------------------------------
    add("## At a glance")
    add("")
    add("| Library | Lang | Version | Depends on (mgf-*) | Used by (mgf-*) | Purpose |")
    add("|---|---|---|---|---|---|")
    for repo in sorted(libs):
        lib = libs[repo]
        url = DEPS_URL.format(org=CODEBERG_ORG, repo=repo)
        name = f"[`{repo}`]({url})"
        deps = ", ".join(f"`{d}`" for d in sorted(lib.deps)) or "—"
        used = ", ".join(f"`{u}`" for u in lib.used_by) or "—"
        add(
            f"| {name} | {lib.lang} | {lib.version} | {deps} | {used} | {_cell(lib.purpose, 140)} |"
        )
    add("")
    add("---")
    add("")

    # ---- Release & compatibility ---------------------------------------
    add("## Release & compatibility")
    add("")
    add(
        f"Latest version of each library, the **mgf-common** window it "
        f"accepts, and whether the current cornerstone (**mgf-common "
        f"{mc_version}**) satisfies it. Legend: ✅ accepts · ❌ excludes "
        f"(needs a pin-walk before they can be installed together) · n/a no "
        f"`mgf-common` dependency · 🔒 exact/git pin (see Consistency)."
    )
    add("")
    add("| Library | Version | Python | Accepts mgf-common | mgf-common pin | Other mgf-* pins |")
    add("|---|---|---|---|---|---|")
    for repo in sorted(libs):
        lib = libs[repo]
        glyph, pin_disp = _accepts_cornerstone(lib, repo, mc_version)
        others = (
            ", ".join(f"`{d}` {p}" for d, p in sorted(lib.deps.items()) if d != "mgf-common") or "—"
        )
        add(
            f"| `{repo}` | {lib.version} | {lib.requires_python or '—'} "
            f"| {glyph} | {pin_disp} | {_cell(others)} |"
        )
    add("")
    add("---")
    add("")

    # ---- Release order (waves) -----------------------------------------
    add("## Release order (waves)")
    add("")
    add(
        "The dependency-ordered order to walk the federation for any "
        "federation-wide update (a cornerstone bump, a standards-corpus "
        "change, a cohort pin-walk). Walk **roots → leaves**: a library may "
        "ship only once every `mgf-*` dependency below it is published + live. "
        "**Within a wave** there are no inter-deps — parallelizable; **between "
        "waves** a hard barrier (the lower wave must be REST-200 live first). "
        "The `mgf-test-supervisor` (dev/test) and `mgf-standard` "
        "(`[standards]` extra) edges are deliberately not treated as "
        "build-order constraints."
    )
    add("")
    add(
        "Generated from the roster below — regenerate or inspect with "
        "`python scripts/fed_waves.py`. The full procedure (scope rings, the "
        "two engines, the close-out proof) is "
        "[`docs/recipes/federation-update-strategy.md`](docs/recipes/federation-update-strategy.md)."
    )
    add("")
    for i, wave in enumerate(compute_waves(_deps_by_repo(libs))):
        members = " · ".join(f"`{r}`" for r in wave)
        add(f"- **Wave {i}:** {members}")
    add("")
    add("---")
    add("")

    # ---- Per-library detail --------------------------------------------
    add("## Per-library detail")
    add("")
    for repo in sorted(libs):
        lib = libs[repo]
        deps_url = DEPS_URL.format(org=CODEBERG_ORG, repo=repo)
        repo_url = f"https://codeberg.org/{CODEBERG_ORG}/{repo}"
        add(f"### `{repo}`")
        add("")
        add("| Field | Value |")
        add("|---|---|")
        add(f"| Version | {lib.version} |")
        add(f"| Language | {lib.lang} |")
        if lib.requires_python:
            add(f"| Requires | Python {lib.requires_python} |")
        if lib.lang == "python":
            add(f"| Install | `pip install {repo}` |")
        deps = (
            ", ".join(f"`{d}` {p}" for d, p in sorted(lib.deps.items())) or "— _(federation leaf)_"
        )
        add(f"| Depends on (mgf-*) | {_cell(deps)} |")
        if lib.lang == "python":
            add(f"| Third-party runtime deps | {lib.third_party_count} |")
        if lib.extras:
            add(f"| Extras | {', '.join(f'`{e}`' for e in lib.extras)} |")
        used = ", ".join(f"`{u}`" for u in lib.used_by) or "— _(no federation consumers yet)_"
        add(f"| Used by (mgf-*) | {_cell(used)} |")
        add(f"| Purpose | {_cell(lib.purpose)} |")
        add(f"| Repo | [{repo}]({repo_url}) · [DEPS.md]({deps_url}) |")
        add("")

    # ---- Consistency ----------------------------------------------------
    add("---")
    add("")
    add("## Consistency")
    add("")
    problems = check_consistency(libs)
    if not problems:
        add(
            "✅ **Consistent.** Every mgf-* → mgf-* dependency pin accepts "
            "the dependency's current version (release-gate check passes)."
        )
    else:
        add("⚠️ **Inconsistencies — the release gate is RED:**")
        add("")
        for p in problems:
            add(f"- {p}")
    add("")
    return "\n".join(out)


def _drop_date_line(text: str) -> str:
    """Strip the volatile ``_Generated <date> …_`` line so currency
    comparison reacts only to SUBSTANTIVE drift (versions, deps, new
    libraries, descriptions), not the regeneration date."""
    return "\n".join(ln for ln in text.splitlines() if not ln.startswith("_Generated "))


def check_currency(libs: dict[str, Library], out_path: Path) -> str | None:
    """Return a message if the on-disk registry is STALE vs a fresh render
    (ignoring the date line), else None. This is what makes "a library was
    released but the registry wasn't regenerated" a hard, catchable error
    (RELEASING **REL-13**)."""
    if not out_path.is_file():
        return f"{out_path.name} is missing — run `python scripts/gen_mgf_libraries.py`."
    on_disk = out_path.read_text(encoding="utf-8")
    fresh = render(libs) + "\n"
    if _drop_date_line(on_disk) != _drop_date_line(fresh):
        return (
            f"{out_path.name} is STALE — a library's version / dependency / "
            "description changed without regenerating the registry. Run "
            "`python scripts/gen_mgf_libraries.py` and commit the result "
            "(REL-13 / DOC-18)."
        )
    return None


# ----------------------------------------------------------------------
# Machine-readable registry (shipped as mgf-standard wheel data)
# ----------------------------------------------------------------------
# The Markdown above is for humans; this JSON is what an *agent* reads to
# become federation-aware. ``pyproject.toml`` force-includes it into the
# wheel (``mgf/standard/_libraries.json``) and
# ``mgf.standard.libraries()`` resolves it, so any installed consumer
# learns the current roster + dependency graph from the version it pins —
# no copied project-lists to drift (the failure mode catch-up/sync chased).

_REGISTRY_SCHEMA = 1


def _lib_to_dict(lib: Library) -> dict[str, object]:
    return {
        "repo": lib.repo,
        "lang": lib.lang,
        "version": lib.version,
        "requires_python": lib.requires_python,
        "purpose": lib.purpose,
        "deps": dict(sorted(lib.deps.items())),
        "used_by": list(lib.used_by),
        "extras": list(lib.extras),
        "third_party_count": lib.third_party_count,
    }


def render_json(libs: dict[str, Library]) -> str:
    """Render the registry as deterministic JSON (the agent-facing form)."""
    today = datetime.datetime.now(tz=datetime.UTC).date().isoformat()
    cornerstone = libs.get("mgf-common")
    payload = {
        "schema": _REGISTRY_SCHEMA,
        "generated": today,
        "cornerstone": "mgf-common",
        "cornerstone_version": cornerstone.version if cornerstone else None,
        "count": len(libs),
        # Dependency-ordered release waves (roots first) — the order to walk
        # the federation for any cross-cutting update. Derived from the deps
        # graph via the shared wave computer (scripts/fed_waves.py), so an
        # agent reading the shipped registry gets the walk order, not just the
        # roster. See docs/recipes/federation-update-strategy.md.
        "release_waves": compute_waves(_deps_by_repo(libs)),
        "libraries": [_lib_to_dict(libs[r]) for r in sorted(libs)],
    }
    return json.dumps(payload, indent=2, sort_keys=True) + "\n"


def _json_drop_date(text: str) -> str:
    """Normalize registry JSON for the currency check: drop the volatile
    ``generated`` date so only SUBSTANTIVE drift (versions, deps, new
    members) trips staleness."""
    try:
        data = json.loads(text)
    except (ValueError, TypeError):
        return text
    if isinstance(data, dict):
        data.pop("generated", None)
    return json.dumps(data, indent=2, sort_keys=True)


def check_json_currency(libs: dict[str, Library], json_path: Path) -> str | None:
    """Like :func:`check_currency`, for the machine-readable
    ``MGF_LIBRARIES.json`` shipped as wheel data."""
    if not json_path.is_file():
        return f"{json_path.name} is missing — run `python scripts/gen_mgf_libraries.py`."
    if _json_drop_date(json_path.read_text(encoding="utf-8")) != _json_drop_date(render_json(libs)):
        return (
            f"{json_path.name} is STALE — the registry data changed without "
            "regenerating. Run `python scripts/gen_mgf_libraries.py` and commit "
            "(REL-13 / DOC-18)."
        )
    return None


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--check",
        action="store_true",
        help="Don't write MGF_LIBRARIES.md; instead run the release gate — "
        "the consistency check AND a staleness check that the committed "
        "registry matches a fresh render. Exit 1 if either fails.",
    )
    args = parser.parse_args(argv)

    repo_root = Path(__file__).resolve().parent.parent  # mgf-standard/ (this repo)
    federation_root = repo_root.parent
    md_path = repo_root / "MGF_LIBRARIES.md"
    json_path = repo_root / "MGF_LIBRARIES.json"
    libs = discover(federation_root)
    if not libs:
        print(f"error: no mgf-* repos found under {federation_root}", file=sys.stderr)
        return 2

    problems = check_consistency(libs)
    integrity = check_integrity(libs)

    stale: str | None = None
    if args.check:
        stale = check_currency(libs, md_path) or check_json_currency(libs, json_path)
    else:
        md_path.write_text(render(libs) + "\n", encoding="utf-8")
        json_path.write_text(render_json(libs), encoding="utf-8")
        print(f"wrote {md_path.name} + {json_path.name} ({len(libs)} libraries)")

    failed = bool(problems) or bool(integrity) or bool(stale)
    if stale:
        print(f"REGISTRY CURRENCY: STALE\n  - {stale}", file=sys.stderr)
    if integrity:
        print("REGISTRY INTEGRITY: RED", file=sys.stderr)
        for p in integrity:
            print(f"  - {p}", file=sys.stderr)
    if problems:
        print("FEDERATION CONSISTENCY GATE: RED", file=sys.stderr)
        for p in problems:
            print(f"  - {p}", file=sys.stderr)
    if failed:
        return 1
    print(f"FEDERATION CONSISTENCY GATE: GREEN ({len(libs)} libraries consistent)")
    if args.check:
        print("REGISTRY CURRENCY: CURRENT")
    return 0


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