#!/usr/bin/env python3
"""emotion_drift — cross-model agreement on the ground truth itself.

The corpus GT is one model's training-knowledge read of each track's feeling. That's a
prior, not gospel. So we ask a DIFFERENT model (Sonnet) to label the same tracks
independently, and measure where the two disagree. High inter-model drift on a track =
its GT is UNCERTAIN — either the track is genuinely ambiguous, or our 12-emotion
ontology can't pin it. Those tracks should be down-weighted in calibration and surfaced
for review, instead of trusted like the rest. (Spirit of PLN's "prompt sonnet, check
drift" — single-model GT is a blind spot; a second pair of ears finds it.)

    python3 emotion_drift.py <sonnet_labels.json>   # report + write emotion_drift.json

sonnet_labels.json: [{slug|artist|title, emotions[], valence, arousal}, …] — matched to
the opus GT by normalized artist|title. See [[project_hexa_emotion_convergence]]."""
import json
import re
import sys
import unicodedata
from pathlib import Path

import numpy as np

HERE = Path(__file__).resolve().parent
GT = HERE / "emotion_corpus_gt.json"
OUT = HERE / "emotion_drift.json"


def _norm(s):
    s = unicodedata.normalize("NFKD", s or "").encode("ascii", "ignore").decode().lower()
    return re.sub(r"[^a-z0-9]+", "", s)


def _key(t):
    return _norm(t.get("artist")) + "|" + _norm(t.get("title"))


def _quad(v, a):
    return ("+" if v >= 0 else "-") + ("+" if a >= 0 else "-")


def main():
    args = [a for a in sys.argv[1:] if not a.startswith("--")]
    if not args:
        sys.exit("usage: emotion_drift.py <sonnet_labels.json>")
    opus = {(_key(t)): t for t in json.loads(GT.read_text())["tracks"]}
    sondata = json.loads(Path(args[0]).read_text())
    if isinstance(sondata, dict) and "tracks" in sondata:
        sondata = sondata["tracks"]
    son = {_key(t): t for t in sondata}

    rows = []
    for k, o in opus.items():
        s = son.get(k)
        if not s:
            continue
        try:
            sv, sa = float(s["valence"]), float(s["arousal"])
        except (KeyError, TypeError, ValueError):
            continue
        va_drift = float(np.hypot(o["valence"] - sv, o["arousal"] - sa))
        oe, se = set(o.get("emotions", [])), set(s.get("emotions", []))
        jac = len(oe & se) / len(oe | se) if (oe | se) else 0.0
        rows.append({
            "slug": o["slug"],
            "artist": o["artist"], "title": o["title"],
            "va_drift": round(va_drift, 3),
            "quad_agree": _quad(o["valence"], o["arousal"]) == _quad(sv, sa),
            "emo_jaccard": round(jac, 2),
            "opus": f"{'/'.join(o.get('emotions', []))} {o['valence']:+.2f}/{o['arousal']:+.2f}",
            "sonnet": f"{'/'.join(s.get('emotions', []))} {sv:+.2f}/{sa:+.2f}",
        })
    if not rows:
        sys.exit("no tracks matched between opus GT and sonnet labels (check keys)")

    n = len(rows)
    mean_drift = float(np.mean([r["va_drift"] for r in rows]))
    quad_agree = float(np.mean([r["quad_agree"] for r in rows]))
    mean_jac = float(np.mean([r["emo_jaccard"] for r in rows]))
    # uncertain GT: top-quartile V/A drift OR quadrant disagreement
    thresh = float(np.quantile([r["va_drift"] for r in rows], 0.75))
    uncertain = sorted([r for r in rows if r["va_drift"] >= thresh or not r["quad_agree"]],
                       key=lambda r: -r["va_drift"])

    print(f"⛵ cross-model GT drift — {n} tracks matched (opus vs sonnet)\n")
    print(f"  mean V/A drift     : {mean_drift:.3f}")
    print(f"  quadrant agreement : {quad_agree*100:.0f}%")
    print(f"  emotion Jaccard    : {mean_jac:.2f}")
    print(f"  uncertain-GT (drift ≥ p75={thresh:.2f} or quad mismatch): {len(uncertain)}\n")
    for r in uncertain[:25]:
        flag = "" if r["quad_agree"] else "  ⚠ quad"
        print(f"    {r['va_drift']:.2f}  {r['artist'][:18]:<18} {r['title'][:22]:<22} "
              f"O:{r['opus']:<24} S:{r['sonnet']}{flag}")

    OUT.write_text(json.dumps({
        "schema": "cross-model GT drift (opus vs sonnet) — uncertain-GT detector",
        "n_matched": n,
        "mean_va_drift": mean_drift,
        "quad_agreement": quad_agree,
        "mean_emo_jaccard": mean_jac,
        "uncertain_threshold_p75": thresh,
        "uncertain_slugs": [r["slug"] for r in uncertain],
        "rows": rows,
    }, ensure_ascii=False, indent=1))
    print(f"\n  ✓ {OUT.name}")


if __name__ == "__main__":
    main()
