#!/usr/bin/env python3
"""emotion_corpus — grade the CLAP emotion engine against training-knowledge priors.

PLN's idea: take famous tracks whose feeling/structure/genre I (the model) already
know, and use that knowledge as PSEUDO-GROUND-TRUTH to grade emotion_timeline — does
CLAP-on-audio agree with the cultural prior? Disagreements then DRIVE tuning (fusion
weights, ontology, window) and tell us which V/A quadrants / genres / structures are
under-covered, so the next wave (30→100→300) fills the gaps. Self-correcting by design.

Pipeline (each step cached in _corpus_cache, gitignored):
    fetch     yt-dlp each track (android client + bestaudio/best, the 403 workaround)
    analyze   Foundry demucs → emotion_timeline.analyze_stems  (--mix = whole-mix, fast)
    grade     predicted vs GT → emotion-hit, V/A error, quadrant, structure/vocal
    gaps      coverage histogram → what to add next
    status    what's fetched / analyzed

    python3 emotion_corpus.py fetch [slug…]
    python3 emotion_corpus.py analyze [slug…] [--mix]
    python3 emotion_corpus.py grade [slug…] [--mix]
    python3 emotion_corpus.py gaps
See [[project_hexa_emotion_convergence]], [[feedback_build_katana_first]]."""
import json
import subprocess
import sys
from pathlib import Path

import numpy as np

import emotion_ontology as EMO

HERE = Path(__file__).resolve().parent
GT = HERE / "emotion_corpus_gt.json"
CACHE = HERE / "_corpus_cache"
AUDIO = CACHE / "audio"
ANALYSIS = CACHE / "analysis"


def _tracks(slugs=None):
    data = json.loads(GT.read_text())["tracks"]
    if slugs:
        keep = set(slugs)
        data = [t for t in data if t["slug"] in keep]
    return data


def _audio_path(slug):
    hits = list(AUDIO.glob(f"{slug}.*"))
    return hits[0] if hits else None


# ── fetch ───────────────────────────────────────────────────────────────────────
def fetch_one(t):
    if _audio_path(t["slug"]):
        return _audio_path(t["slug"])
    AUDIO.mkdir(parents=True, exist_ok=True)
    cmd = ["yt-dlp", "-q", "--no-warnings",
           "--extractor-args", "youtube:player_client=android",
           "-f", "bestaudio/best", "-x", "--audio-format", "mp3", "--audio-quality", "5",
           "-o", str(AUDIO / (t["slug"] + ".%(ext)s")), f"ytsearch1:{t['query']}"]
    try:
        subprocess.run(cmd, timeout=240, check=False,
                       stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    except Exception:
        pass
    return _audio_path(t["slug"])


def cmd_fetch(slugs):
    ts = _tracks(slugs)
    print(f"⛵ fetching {len(ts)} tracks…")
    ok = 0
    for t in ts:
        p = fetch_one(t)
        ok += bool(p)
        print(f"  {'✓' if p else '✗'} {t['slug']:<28} {t['artist']} – {t['title']}", flush=True)
    print(f"\n{ok}/{len(ts)} present in {AUDIO}")


# ── analyze ───────────────────────────────────────────────────────────────────────
def analyze_one(t, mix=False):
    ANALYSIS.mkdir(parents=True, exist_ok=True)
    out = ANALYSIS / f"{t['slug']}{'_mix' if mix else ''}.json"
    if out.exists():
        return json.loads(out.read_text())
    audio = _audio_path(t["slug"])
    if not audio:
        return None
    import emotion_timeline as ET
    if mix:
        res = ET.analyze(audio)
    else:
        import foundry_stems as FS
        res = ET.analyze_stems(FS.ensure_stems(audio, t["slug"]))
    if res:
        res["file"] = str(audio)
        out.write_text(json.dumps(res, ensure_ascii=False, indent=1))
    return res


def cmd_analyze(slugs, mix=False):
    ts = _tracks(slugs)
    mode = "whole-mix" if mix else "stem-aware (demucs)"
    print(f"⛵ analyzing {len(ts)} tracks — {mode}…")
    for t in ts:
        r = analyze_one(t, mix=mix)
        if r:
            s = r["summary"]
            print(f"  ✓ {t['slug']:<28} {s['dominant']:<11} V/A {s['valence']:+.2f}/{s['arousal']:+.2f}  {s['arc']}", flush=True)
        else:
            print(f"  ✗ {t['slug']:<28} (no audio / failed)", flush=True)


# ── grade ───────────────────────────────────────────────────────────────────────
def _quadrant(v, a):
    return ("+" if v >= 0 else "-") + ("+" if a >= 0 else "-")


def grade_one(t, res):
    """Predicted res vs GT prior → per-track scorecard row."""
    s = res["summary"]
    pv, pa = s["valence"], s["arousal"]
    gv, ga = t["valence"], t["arousal"]
    pred_emos = {sec["emotion"] for sec in res["sections"]} | {s["dominant"]}
    gt_emos = set(t["emotions"])
    return {
        "slug": t["slug"],
        "emotion_hit": s["dominant"] in gt_emos,            # top-1 prior match
        "emotion_overlap": bool(pred_emos & gt_emos),       # any section matches a prior
        "va_err": round(float(np.hypot(pv - gv, pa - ga)), 3),
        "quadrant_hit": _quadrant(pv, pa) == _quadrant(gv, ga),
        "pred": f"{s['dominant']} {pv:+.2f}/{pa:+.2f}",
        "gt": f"{'/'.join(t['emotions'])} {gv:+.2f}/{ga:+.2f}",
        "n_sec_pred": len(res["sections"]),
        "n_sec_gt": len(t.get("structure", [])),
        "vocal_ratio": s.get("vocal_ratio"),
        "vocal_gt": round(np.mean([1.0 if seg[2] else 0.0 for seg in t.get("structure", [])]), 2)
        if t.get("structure") else None,
    }


def cmd_grade(slugs, mix=False):
    ts = _tracks(slugs)
    rows = []
    for t in ts:
        r = analyze_one(t, mix=mix)
        if r:
            rows.append(grade_one(t, r))
    if not rows:
        sys.exit("nothing analyzed — run fetch + analyze first")
    print(f"⛵ grading {len(rows)} tracks vs training-knowledge priors "
          f"({'whole-mix' if mix else 'stem-aware'})\n")
    print(f"  {'slug':<28} {'emo':>3} {'quad':>4} {'vaErr':>6}  {'predicted':<22} {'← prior'}")
    for r in rows:
        print(f"  {r['slug']:<28} {'✓' if r['emotion_hit'] else ('~' if r['emotion_overlap'] else '✗'):>3} "
              f"{'✓' if r['quadrant_hit'] else '✗':>4} {r['va_err']:>6.2f}  {r['pred']:<22} {r['gt']}")
    n = len(rows)
    print(f"\n  top-1 emotion hit : {sum(r['emotion_hit'] for r in rows)}/{n} "
          f"({sum(r['emotion_hit'] for r in rows)/n*100:.0f}%)   "
          f"(+overlap {sum(r['emotion_overlap'] for r in rows)}/{n})")
    print(f"  V/A quadrant hit  : {sum(r['quadrant_hit'] for r in rows)}/{n} "
          f"({sum(r['quadrant_hit'] for r in rows)/n*100:.0f}%)")
    print(f"  mean V/A error    : {np.mean([r['va_err'] for r in rows]):.3f}  "
          f"(median {np.median([r['va_err'] for r in rows]):.3f})")
    vt = [r for r in rows if r["vocal_ratio"] is not None and r["vocal_gt"] is not None]
    if vt:
        err = np.mean([abs(r["vocal_ratio"] - r["vocal_gt"]) for r in vt])
        print(f"  vocal-presence err: {err:.2f}  (pred vs GT vocal fraction, {len(vt)} tracks)")


# ── gaps (self-correcting coverage) ───────────────────────────────────────────────
def cmd_gaps():
    ts = _tracks()
    from collections import Counter
    quad = Counter(_quadrant(t["valence"], t["arousal"]) for t in ts)
    emo = Counter(e for t in ts for e in t["emotions"])
    genre = Counter(g for t in ts for g in t.get("genre", []))
    print(f"⛵ corpus coverage — {len(ts)} tracks\n")
    print("  V/A quadrants (want ≥5 each):")
    for q in ("++", "+-", "-+", "--"):
        names = {"++": "euphoric/energetic", "+-": "calm/pleasant",
                 "-+": "tense/aggressive", "--": "sad/dark-calm"}[q]
        flag = "  ⚠ thin" if quad[q] < 5 else ""
        print(f"    {q} {names:<22} {quad[q]}{flag}")
    print(f"\n  emotions covered ({len(emo)}/{len(EMO.NAMES)}): " + ", ".join(sorted(emo)))
    missing = [e for e in EMO.NAMES if e not in emo]
    if missing:
        print(f"    ⚠ NOT yet represented: {', '.join(missing)}")
    print(f"\n  genres ({len(genre)}): " + ", ".join(f"{g}×{n}" for g, n in genre.most_common()))


def cmd_status():
    ts = _tracks()
    f = sum(bool(_audio_path(t["slug"])) for t in ts)
    a = sum((ANALYSIS / f"{t['slug']}.json").exists() for t in ts)
    am = sum((ANALYSIS / f"{t['slug']}_mix.json").exists() for t in ts)
    print(f"⛵ corpus: {len(ts)} tracks · fetched {f} · stem-analyzed {a} · mix-analyzed {am}")


def main():
    args = sys.argv[1:]
    cmd = args[0] if args else "status"
    slugs = [a for a in args[1:] if not a.startswith("--")]
    mix = "--mix" in args
    if cmd == "fetch":
        cmd_fetch(slugs)
    elif cmd == "analyze":
        cmd_analyze(slugs, mix=mix)
    elif cmd == "grade":
        cmd_grade(slugs, mix=mix)
    elif cmd == "gaps":
        cmd_gaps()
    elif cmd == "status":
        cmd_status()
    else:
        sys.exit("usage: emotion_corpus.py [fetch|analyze|grade|gaps|status] [slug…] [--mix]")


if __name__ == "__main__":
    main()
