"""``mgf-fed ledger-seed`` — account for newly-declared MUSTs in consumer ledgers.

WHY THIS EXISTS, AND WHY IT IS NOT HARDCODED
--------------------------------------------
DOC-15 requires every repo's ``MGF_STANDARDS_CONFORMANCE.md`` to carry a row for
every current MUST, and ``app-doctor`` fails the repo when one is unaccounted
for. So each time the corpus gains a MUST, **every member's ledger must gain a
row** — a fleet-wide follow-up wave.

The corpus has recorded that obligation, in its own words, every single time:

    Fleet-wide §Z conformance-ledger re-baseline (265 → 268, every member's
    MGF_STANDARDS_CONFORMANCE.md gains three rows) is a follow-up wave, tracked
    separately — not landed in this release.

and every single time it was deferred. The debt is invisible while it
accumulates, because `app-doctor` reads the corpus from the **installed** wheel:
a MUST merged to `mgf-standard`'s main binds nobody until someone publishes. On
2026-08-03 a release cut for an unrelated reason published five releases' worth
at once — **33 new MUSTs, no ledger carrying any of them, the whole fleet red
within minutes**.

There is a precedent for the repair: ``mgf-standard/scripts/
seed_ledger_rebaseline.py``, written for PAPER-100's identical situation (40 new
MUSTs, every ledger short). It hardcodes that cohort's ids and their verdicts. It
ran once. It cannot help with this cohort, and a third one would be needed for
the next. That is this federation's central failure class — knowledge captured,
enforcement not — so this derives the cohort from the corpus instead of naming
it.

WHY IT SEEDS ``⏳`` AND NEVER GUESSES
-------------------------------------
DOC-15 accepts ✅ / 🚫 / ⏳ / N/A, and only requires that a row exist. It would be
easy to mark the new rows ``N/A`` and go green everywhere. That would be **worse
than the gap it closes**: a false conformance claim about 33 rules across 43
repos, asserted by a script that assessed nothing. ``rules.json`` carries id,
family, doc and kind — it does *not* carry ``applies_to``, so shape-scoping is
genuinely not derivable here.

``⏳`` is the truthful status for *not yet assessed*. Seeding it converts silent,
invisible debt into recorded, per-rule, greppable debt — and leaves the judgment
where it belongs. ``--status`` exists for the reviewed case, where a human has
decided a family's verdict and wants it applied.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from mgf.fed._audit import _LEDGER_PATHS, index_of_musts
from mgf.fed._roster import federation_root

if TYPE_CHECKING:
    import argparse

#: Appended, never spliced into an existing table: this file is authored, and a
#: rewriter that reaches into someone's prose is how 295-line diffs happen.
_SECTION = "### §Z.seeded — pending assessment"

_VALID_STATUS = ("⏳", "✅", "🚫", "N/A")


def add_ledger_seed_args(parser: argparse.ArgumentParser) -> None:
    parser.add_argument("repos", nargs="*", help="Repos to seed (default: every registry member).")
    parser.add_argument(
        "--apply",
        action="store_true",
        help="Write the rows. Read-only without it — the report is the default.",
    )
    parser.add_argument(
        "--status",
        default="⏳",
        choices=_VALID_STATUS,
        help=(
            "Verdict for seeded rows. Defaults to ⏳ (not yet assessed) — the only "
            "honest answer a script can give. Pass another only when a human has "
            "actually decided."
        ),
    )
    parser.add_argument("--root", help="Federation root (default: resolved).")


def run_ledger_seed(args: argparse.Namespace) -> int:
    from pathlib import Path

    from mgf.fed._audit import _default_ref, _git, _registry_names, _tree_paths
    from mgf.fed._roster import is_git_repo

    root = Path(args.root) if getattr(args, "root", None) else federation_root()
    std = root / "mgf-standard"
    std_ref = _default_ref(std) if is_git_repo(std) else ""
    if not std_ref:
        print(f"no mgf-standard checkout at {std} — cannot read the declared MUSTs")
        return 1
    # The COMMITTED corpus, never the installed wheel: the wheel on any given
    # bench is whatever that machine last pulled (0.1.66 on the one that wrote
    # this — nine releases stale), and seeding against it would under-account.
    must_ids = index_of_musts(_git(std, "show", f"{std_ref}:docs/standards/README.md"))
    if not must_ids:
        print("mgf-standard's 'Index of MUSTs' resolved empty — refusing to seed anything")
        return 1

    names = list(args.repos) or sorted(_registry_names(std / "MGF_LIBRARIES.json"))
    if not names:
        print("no repos to seed")
        return 1

    print(f"corpus declares {len(must_ids)} MUSTs (from {std_ref})")
    touched = seeded = 0
    for name in names:
        repo = root / name
        if not is_git_repo(repo):
            print(f"  · {name:24} not on disk — skipped")
            continue
        ref = _default_ref(repo)
        ledger = next((p for p in _LEDGER_PATHS if ref and p in _tree_paths(repo, ref)), None)
        if ledger is None:
            print(f"  ! {name:24} no MGF_STANDARDS_CONFORMANCE.md — needs one before seeding")
            continue
        path = repo / ledger
        if not path.is_file():
            print(f"  ! {name:24} {ledger} is committed but absent from the working tree")
            continue
        missing = missing_musts(path.read_text(encoding="utf-8"), must_ids)
        if not missing:
            continue
        touched += 1
        seeded += len(missing)
        print(f"  {'+' if args.apply else '?'} {name:24} {len(missing):>3} unaccounted")
        if args.apply:
            path.write_text(
                append_rows(path.read_text(encoding="utf-8"), missing, args.status),
                encoding="utf-8",
            )

    verb = "seeded" if args.apply else "would seed"
    print(f"\n{verb} {seeded} row(s) across {touched} repo(s)")
    if not args.apply and touched:
        print("re-run with --apply to write them")
    # Read-only mode reports a real gap as non-zero: "nothing written" and
    # "nothing to write" must not share an exit code.
    return 1 if (touched and not args.apply) else 0


def missing_musts(ledger_text: str, must_ids: set[str]) -> list[str]:
    """Declared MUSTs with no row in this ledger, in corpus-id order."""
    from mgf.fed.cli._doctor_standards import _RULE_ID_RE

    accounted = set(_RULE_ID_RE.findall(ledger_text))
    return sorted(must_ids - accounted)


def append_rows(text: str, ids: list[str], status: str) -> str:
    """Append the seeded rows, reusing the section when it already exists.

    Idempotent by construction: callers pass only ids the ledger does not already
    mention, and a second run finds none. The rows go in their own section rather
    than inside §Z's table — appending to authored prose is safe, editing it is
    how a tool destroys 295 lines it did not understand.
    """
    rows = "\n".join(
        f"| {i} | {status} | — | seeded by `mgf-fed ledger-seed`; not yet assessed |" for i in ids
    )
    if _SECTION in text:
        body = text.rstrip("\n")
        return f"{body}\n{rows}\n"
    head = text if text.endswith("\n") else text + "\n"
    return (
        f"{head}\n{_SECTION}\n\n"
        "Rows added because the corpus declared these MUSTs and this ledger had no\n"
        "entry for them. **⏳ means not yet assessed — it is not a conformance\n"
        "claim.** Replace each with a real verdict and evidence as it is reviewed.\n\n"
        "| Rule | Status | Evidence | Notes |\n"
        "|---|---|---|---|\n"
        f"{rows}\n"
    )
