#!/usr/bin/env python3
"""CHECK -- ledger directory census 2026-11-02.

Exit 0 iff the committed
wizard-work/ledger/2026-11-02-ledger-dir-census.csv is the re-derivation of the
tracked population at the rev pinned by
wizard-work/ledger/2026-11-02-ledger-dir-census.manifest.json, read from git and
never from the working tree.  Exit 1 on any row drift/extra/missing; exit 2 if
the rev or artifacts cannot be read.
"""
import csv
import hashlib
import json
import os
import subprocess
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.abspath(os.path.join(HERE, "..", ".."))
CSV_PATH = os.path.join(HERE, "2026-11-02-ledger-dir-census.csv")
MANIFEST_PATH = os.path.join(HERE, "2026-11-02-ledger-dir-census.manifest.json")
SCHEMA = ["rel_path", "size_bytes", "sha256"]
EXCLUDE = {
    os.path.join("wizard-work", "ledger", "2026-11-02-ledger-dir-census.csv"),
    os.path.join("wizard-work", "ledger", "2026-11-02-ledger-dir-census.manifest.json"),
}


def git(repo, *args, input_bytes=None):
    proc = subprocess.run(
        ["git", "-C", repo] + list(args),
        stdout=subprocess.PIPE, stderr=subprocess.PIPE, input=input_bytes,
    )
    if proc.returncode != 0:
        raise RuntimeError(
            "git %s -> rc %d: %s" % (
                " ".join(args), proc.returncode,
                proc.stderr.decode("utf-8", "replace").strip()[:400]))
    return proc.stdout


def resolve_rev(repo, rev):
    out = git(repo, "rev-parse", "--verify", "%s^{commit}" % rev,
              input_bytes=None).decode("utf-8", "replace").strip()
    if len(out) != 40:
        raise RuntimeError("rev %r is not a full sha" % rev)
    return out


def derive_rows(repo, rev):
    raw = git(repo, "ls-tree", "-r", "-l", "-z", rev, "--",
              "wizard-work/ledger")
    wanted = []
    for chunk in raw.split(b"\x00"):
        if not chunk.strip():
            continue
        meta, _, path = chunk.partition(b"\t")
        parts = meta.decode("utf-8", "surrogateescape").split()
        if len(parts) < 4 or parts[1] != "blob":
            continue
        rel = path.decode("utf-8", "surrogateescape")
        if rel in EXCLUDE:
            continue
        wanted.append((parts[2], rel))
    query = ("\n".join(sha for sha, _ in wanted) + "\n").encode()
    out = git(repo, "cat-file", "--batch", input_bytes=query)
    rows, pos = [], 0
    for sha, rel in wanted:
        nl = out.index(b"\n", pos)
        header = out[pos:nl].decode("utf-8", "replace").split()
        if len(header) < 3 or header[1] != "blob":
            raise RuntimeError("bad cat-file header for %s" % rel)
        n = int(header[2])
        body = out[nl + 1:nl + 1 + n]
        rows.append((rel, len(body), hashlib.sha256(body).hexdigest()))
        pos = nl + 1 + n + 1
    rows.sort(key=lambda r: r[0])
    return rows


def read_committed(path):
    with open(path, "r", newline="", encoding="utf-8") as fh:
        reader = csv.reader(fh)
        header = next(reader)
        if header != SCHEMA:
            raise RuntimeError("bad header %r in %s" % (header, path))
        rows = []
        for row in reader:
            if not row:
                continue
            if len(row) != 3:
                raise RuntimeError(
                    "line in %s has %d fields, want 3" % (path, len(row)))
            rows.append((row[0], int(row[1]), row[2]))
    return rows


def main():
    try:
        with open(MANIFEST_PATH, "r", encoding="utf-8") as fh:
            man = json.load(fh)
        rev = resolve_rev(REPO, man["rev"])
    except Exception as exc:
        print("UNGRADABLE: %s" % exc)
        return 2

    try:
        derived = derive_rows(REPO, rev)
        committed = read_committed(CSV_PATH)
    except Exception as exc:
        print("UNGRADABLE: %s" % exc)
        return 2

    by_c = {p: (s, h) for p, s, h in committed}
    by_d = {p: (s, h) for p, s, h in derived}
    drift, extra, missing = [], [], []
    match = 0
    for p, v in by_c.items():
        if p not in by_d:
            extra.append(p)
        elif by_d[p] == v:
            match += 1
        else:
            drift.append(p)
    for p in by_d:
        if p not in by_c:
            missing.append(p)
    counts = {
        "match": match, "drift": len(drift), "extra": len(extra),
        "missing": len(missing),
        "committed": len(committed), "derived": len(derived),
    }

    print("subject: wizard-work/ledger at %s (%s)" % (rev, man.get("rev_subject", "")))
    print("rows committed: %d   rows derived: %d" % (counts["committed"],
          counts["derived"]))
    print("match %d   drift %d   extra %d   missing %d"
          % (counts["match"], counts["drift"], counts["extra"], counts["missing"]))
    for label in ("drift", "extra", "missing"):
        for p in sorted(locals()[label])[:5]:
            print("  %s: %s" % (label, p))

    faults = []
    if drift:
        faults.append("count drift moved 0 -> %d" % counts["drift"])
    if extra:
        faults.append("count extra moved 0 -> %d" % counts["extra"])
    if missing:
        faults.append("count missing moved 0 -> %d" % counts["missing"])
    if man.get("rows") != len(committed):
        faults.append("manifest rows %s != CSV rows %d"
                      % (man.get("rows"), len(committed)))

    try:
        head = resolve_rev(REPO, "HEAD")
        if head == rev:
            print("currency: HEAD is the pinned rev -- 0 added, 0 removed, 0 changed")
        else:
            d2 = {p: (s, h) for p, s, h in derive_rows(REPO, head)}
            d = {"added": 0, "removed": 0, "changed": 0}
            for p in by_c:
                if p not in by_d:
                    d["removed"] += 1
                elif by_d[p] != by_c[p]:
                    d["changed"] += 1
            for p in by_d:
                if p not in by_c:
                    d["added"] += 1
            print("currency: HEAD %s is past the pin %s -- %d added, %d removed,"
                  " %d changed (advisory only)" % (
                      head[:12], rev[:12], d["added"], d["removed"], d["changed"]))
    except RuntimeError as exc:
        print("currency: SKIP (%s)" % exc)

    if faults:
        for f in faults:
            print("FAIL: %s" % f)
        print("VERDICT: rc 1 -- %d fault(s)" % len(faults))
        return 1
    print("VERDICT: rc 0 -- the committed CSV is the derivation at the pinned rev")
    return 0


if __name__ == "__main__":
    sys.exit(main())
