#!/usr/bin/env python3
"""rights_audit — which sample banks does a release actually contain, and who owns them.

PLN: *"cant push the single tracks when samples hit, but full mix might well
pass!"* — the measurement agrees (an isolated track was flagged where the
87-minute mix passed). So before any per-track release, the question is per
BANK: is this ours, is it licensed, or is it somebody's record.

The machine's job here is enumeration and joining, not judgement. It answers
"what is in this release and where did each bank come from"; the verdict is
PLN's and is stored once in `rights_ledger.json` so it is never re-litigated.

## The discriminator that matters

"Lives in the Dirt-Samples folder" is NOT "is a Dirt-Samples bank". 56 of
OPAL-26's 60 banks resolve to that directory, including `the_revolution`,
`desire` and `crimewave` — because local banks were dropped in beside the
quark's own. Classifying by location would have returned a confident,
comprehensively wrong low-risk verdict on the most obviously sampled material
in the set.

Dirt-Samples is a git checkout, so `git ls-files` gives the exact upstream set:
**217 upstream banks, 509 added locally.** Tracked = shipped with the quark
under its own license. Untracked = PLN's, provenance unknown to the machine,
and therefore a question for him.

    python3 rights_audit.py judge_specs/opal26.json          # report
    python3 rights_audit.py judge_specs/opal26.json --init   # seed the ledger
"""
from __future__ import annotations

import argparse
import json
import subprocess
import sys
from datetime import date
from pathlib import Path

HERE = Path(__file__).parent
LEDGER = HERE / "rights_ledger.json"
WWW = Path("/home/pln/Work/Web/www")

# Where SuperDirt actually loads from — read off start_and_midi.scd. If that
# boot file changes, this must too; a bank that resolves nowhere is reported
# as UNRESOLVED rather than silently treated as safe.
ROOTS = {
    "dirt": Path("/home/pln/.local/share/SuperCollider/downloaded-quarks/Dirt-Samples"),
    "extra": Path("/home/pln/Work/Sound/Samples/extra"),
    "machines": Path("/home/pln/Work/Sound/Samples/tidal-drum-machines/machines"),
}

# The verdicts PLN can record. Only `original` and `cleared` are releasable
# per-track; `unknown` is the honest default and blocks by design.
STATUSES = ("original", "cleared", "dirt_samples", "third_party", "unknown")
RELEASABLE = ("original", "cleared", "dirt_samples")


def norm(name: str) -> str:
    """SuperDirt strips hyphens when naming banks (see the boot's namingFunction)."""
    return name.replace("-", "").lower()


def upstream_banks(root: Path) -> set[str]:
    """The banks the quark itself ships, straight from git."""
    r = subprocess.run(["git", "-C", str(root), "ls-files"],
                       capture_output=True, text=True)
    if r.returncode != 0:
        return set()
    return {norm(p.split("/")[0]) for p in r.stdout.splitlines() if "/" in p}


def index() -> dict[str, dict]:
    """bank -> {root, origin, dir, files}. Origin is the answer that matters."""
    up = upstream_banks(ROOTS["dirt"])
    out: dict[str, dict] = {}
    for label, root in ROOTS.items():
        if not root.exists():
            continue
        for d in sorted(root.iterdir()):
            if not d.is_dir() or d.name.startswith("."):
                continue
            key = norm(d.name)
            if key in out:
                continue                      # first root wins, as SuperDirt does
            out[key] = {
                "root": label,
                "origin": "upstream" if (label == "dirt" and key in up) else "local",
                "dir": str(d),
                "files": len(list(d.glob("*.wav")) + list(d.glob("*.flac"))),
            }
    return out


def load_ledger() -> dict:
    if LEDGER.exists():
        return json.loads(LEDGER.read_text())
    return {"_comment": [], "banks": {}}


def gig_banks(spec: dict) -> dict[str, list[str]]:
    """bank -> the tracks that use it, from the CANONICAL tracklist."""
    tj = json.loads(Path(spec["tracksJson"]).read_text())
    ts = tj.get("tracks", tj) if isinstance(tj, dict) else tj
    out: dict[str, list[str]] = {}
    for t in ts:
        for b in t.get("samples", []):
            out.setdefault(norm(b), []).append(t["name"])
    return out


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__,
                                 formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("spec")
    ap.add_argument("--init", action="store_true",
                    help="add this gig's banks to the ledger as `unknown`")
    ap.add_argument("--show", default="open", choices=["open", "all"],
                    help="`open` lists only banks still needing a verdict")
    a = ap.parse_args()

    spec = json.loads(Path(a.spec).read_text())
    idx = index()
    used = gig_banks(spec)
    ledger = load_ledger()
    banks = ledger.setdefault("banks", {})

    if a.init:
        added = 0
        for b in sorted(used):
            if b in banks:
                continue
            info = idx.get(b, {})
            # An upstream bank gets its verdict for free: it ships with the
            # quark under the quark's license. Everything else starts UNKNOWN,
            # which is the honest default and the one that blocks.
            banks[b] = {
                "status": "dirt_samples" if info.get("origin") == "upstream" else "unknown",
                "origin": info.get("origin", "unresolved"),
                "files": info.get("files", 0),
                "note": "",
                "decided": date.today().isoformat() if info.get("origin") == "upstream" else None,
            }
            added += 1
        ledger["_comment"] = [
            "Per-BANK rights verdicts. The machine enumerates; PLN adjudicates.",
            "`origin` is measured (upstream = tracked in the Dirt-Samples git",
            "checkout; local = added beside it, provenance unknown to the tool).",
            "`status` is a human verdict: " + " | ".join(STATUSES) + ".",
            "Only " + ", ".join(RELEASABLE) + " are releasable per-track.",
            "A bank answered here is answered for every release that uses it.",
        ]
        LEDGER.write_text(json.dumps(ledger, indent=1, ensure_ascii=False) + "\n")
        print(f"ledger: +{added} bank(s) -> {LEDGER}")

    # -- the report ---------------------------------------------------------
    rows = []
    for b in sorted(used):
        e = banks.get(b, {})
        info = idx.get(b, {})
        rows.append((b, e.get("status", "unknown"),
                     info.get("origin", "UNRESOLVED"),
                     info.get("files", 0), used[b]))

    by_status: dict[str, int] = {}
    for _, st, *_ in rows:
        by_status[st] = by_status.get(st, 0) + 1
    print(f"\n{spec['gig']} · {len(used)} distinct banks")
    for st in STATUSES:
        if by_status.get(st):
            print(f"  {st:<14} {by_status[st]:>3}")
    unresolved = [r for r in rows if r[2] == "UNRESOLVED"]
    if unresolved:
        print(f"  {'UNRESOLVED':<14} {len(unresolved):>3}  (in the score, not on disk)")

    # Sorted by LEVERAGE: how many tracks each verdict unblocks. Alphabetical
    # makes a 41-item list look like 41 equal chores, when in fact five banks
    # gate almost the whole record — answer those and the shape of the release
    # changes. Keystone first.
    open_rows = sorted((r for r in rows if r[1] not in RELEASABLE),
                       key=lambda r: (-len(r[4]), r[0]))
    if a.show == "open":
        show = open_rows
        print(f"\nBanks still needing a verdict ({len(show)}), most leverage first:")
    else:
        show = sorted(rows, key=lambda r: (-len(r[4]), r[0]))
        print(f"\nAll banks ({len(show)}):")
    for b, st, origin, files, tracks in show:
        print(f"  {b:<22} {st:<13} {origin:<10} {files:>4} files  "
              f"{len(tracks)} track(s): {', '.join(tracks[:3])}"
              + (" …" if len(tracks) > 3 else ""))

    # -- the per-track verdict, which is what gates the release -------------
    blocked: dict[str, list[str]] = {}
    for b, st, *_rest in rows:
        if st in RELEASABLE:
            continue
        for t in used[b]:
            blocked.setdefault(t, []).append(b)
    print(f"\n{len(blocked)} track(s) contain an unadjudicated or third-party bank:")
    for t, bs in sorted(blocked.items()):
        print(f"  {t[:38]:<38} {', '.join(sorted(bs))}")
    clean = [t for t in {x for v in used.values() for x in v} if t not in blocked]
    print(f"\n{len(clean)} track(s) clear for per-track release: "
          + (", ".join(sorted(clean)) if clean else "none yet"))
    print("\nThe continuous mix is a separate question and a lower-risk one — "
          "the long form has passed where isolated tracks were flagged.")
    return 1 if blocked else 0


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