#!/usr/bin/env python3
"""kitgate — the release gate for a freshly cut sample pack. Run before linking.

Cutting a pack produces audio; this decides whether it is fit to load into a running
rig. Four checks, three of them things a single earlier stage cannot answer alone:

1. **grade, on the shipped bytes.** `engine.grade` re-run over the written files, not
   the candidate windows the finder scored. It is a separate code path reaching the same
   verdict, so a disagreement means one of them is wrong — which is the point of running
   it twice (`feedback_assembling_checks_revalidates`).
2. **kitcheck.** Dead bars, bar-multiple exactness, per-family periodicity, duplicates.
3. **Bank shadowing.** `loadSoundFiles` defaults to appendToExisting = false, so a new
   folder whose basename matches an existing bank frees that bank's buffers and takes
   the name — silently, with the only symptom being that an old track now plays the
   wrong sound. Checked BEFORE anything is linked, via `tools/pvbanks.py` so this agrees
   with `bank-check.py` by construction (`feedback_one_parser_per_concept`).
4. **Rights.** A kit cut from someone else's stems must not be releasable per-track by
   default. Reported here, ledgered by `armada/tide-table/register_fred_rights.py`.

    python3 kitgate.py <cuts.json> [--link] [--source "..."]

`--link` performs the Dirt-Samples symlink for every kit, but only if 1-3 pass.
"""
from __future__ import annotations

import argparse
import json
import subprocess
import sys
from collections import Counter
from pathlib import Path

HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
TIDAL_TOOLS = HERE.parent            # tools/


def check_grades(cuts: list[dict]) -> tuple[bool, list[str]]:
    """Re-grade the written files and compare with what the finder recorded."""
    from engine import grade as G
    lines, bad = [], []
    tiers = Counter()
    for c in cuts:
        g = G.grade(c["path"], role=c.get("family"))
        tiers[g.tier] += 1
        # the finder graded the same audio in memory; a real gap means one path is wrong
        if abs(g.grade - c["grade"]) > 0.02:
            bad.append(f"  {c['kit']}/{c['name']}: finder said {c['grade']:.3f}, "
                       f"file grades {g.grade:.3f}")
        for f in g.flags:
            if f in ("clipping", "near-silent", "mono-incompatible"):
                bad.append(f"  {c['kit']}/{c['name']}: {f}")
    lines.append("  tiers " + " ".join(f"{t}x{tiers[t]}" for t in "SABCD" if tiers[t]))
    lines += bad[:20]
    if len(bad) > 20:
        lines.append(f"  … and {len(bad) - 20} more")
    return not bad, lines


def check_kits(cuts: list[dict]) -> tuple[bool, list[str]]:
    from engine import kitcheck as K
    checks = K.check_cuts(cuts)
    hard = [c for c in checks if not c.ok]
    soft = [c for c in checks if c.ok and c.problems]
    dupes = [c for c in checks if c.dupe_of]
    lines = [f"  {len(checks) - len(hard) - len(soft)} clean, {len(soft)} advisory, "
             f"{len(hard)} failing · {len(checks) - len(dupes)} distinct sounds"]
    lines += [f"  ✗ {c.kit}/{c.name}: {'; '.join(c.problems)}" for c in hard[:20]]
    return not hard, lines


def check_shadowing(cuts: list[dict]) -> tuple[bool, list[str]]:
    """Would linking these kits steal a name an existing bank already answers to?"""
    sys.path.insert(0, str(TIDAL_TOOLS))
    try:
        from pvbanks import scan
    except Exception as e:
        return True, [f"  (skipped: {e})"]
    existing = scan()
    kits = sorted({c["kit"] for c in cuts})
    clash = [k for k in kits if k in existing]
    lines = [f"  {len(kits)} kits vs {len(existing)} known banks"]
    lines += [f"  ✗ {k} already exists: "
              f"{', '.join(str(e[0]) for e in existing[k])}" for k in clash]
    return not clash, lines


def check_rights(cuts: list[dict], source: str) -> tuple[bool, list[str]]:
    """Are these banks recorded as NOT releasable per-track? Advisory, never blocking."""
    ledger = (TIDAL_TOOLS.parent / "armada/tide-table/rights_ledger.json")
    kits = sorted({c["kit"] for c in cuts})
    if not ledger.exists():
        return True, ["  (no ledger)"]
    banks = json.loads(ledger.read_text()).get("banks", {})
    unregistered = [k for k in kits if k not in banks]
    releasable = [k for k in kits
                  if banks.get(k, {}).get("status") in ("original", "cleared", "dirt_samples")]
    lines = [f"  {len(kits) - len(unregistered)} of {len(kits)} in the ledger"]
    if unregistered:
        lines.append(f"  ! {len(unregistered)} unregistered — run register_fred_rights.py "
                     f"so they block per-track release with their provenance attached")
    if releasable:
        lines.append(f"  ! {len(releasable)} marked releasable per-track: "
                     f"{', '.join(releasable[:5])}")
    return True, lines


def main(argv=None) -> int:
    ap = argparse.ArgumentParser(prog="kitgate", description=__doc__,
                                formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("cuts_json", type=Path)
    ap.add_argument("--link", action="store_true", help="symlink into Dirt-Samples if clean")
    ap.add_argument("--source", default="third-party producer stem pack")
    a = ap.parse_args(argv)

    cuts = json.loads(a.cuts_json.read_text())
    print(f"kitgate — {len(cuts)} samples in {len({c['kit'] for c in cuts})} kits\n")

    results = {}
    for label, fn in [("grade (re-run on the written files)", lambda: check_grades(cuts)),
                      ("kitcheck", lambda: check_kits(cuts)),
                      ("bank shadowing", lambda: check_shadowing(cuts)),
                      ("rights", lambda: check_rights(cuts, a.source))]:
        ok, lines = fn()
        results[label] = ok
        print(f"{'✓' if ok else '✗'} {label}")
        for l in lines:
            print(l)
        print()

    blocking = [k for k, v in results.items() if not v and k != "rights"]
    if blocking:
        print(f"BLOCKED by: {', '.join(blocking)}")
        return 1
    print("all blocking checks pass")

    if a.link:
        from engine import publish as P
        acted = Counter()
        for kit in sorted({c["kit"] for c in cuts}):
            try:
                acted[P.link_kit(kit).action] += 1
            except Exception as e:
                print(f"  ! {kit}: {e}")
                acted["failed"] += 1
        print("linked into Dirt-Samples: " +
              ", ".join(f"{v} {k}" for k, v in sorted(acted.items())))
        if acted.get("skipped-real-dir"):
            print("  (a real directory was left untouched — we never delete what we did "
                  "not create)")
    return 0


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