#!/usr/bin/env python3
"""emotion_corpus_merge — fold model-authored GT batches into emotion_corpus_gt.json.

The GT-expansion workflow fans opus subagents over genre cells; each returns a batch
of training-knowledge labels. This merges them SAFELY: validate against the fixed
12-emotion vocabulary + V/A range, dedup by slug AND by normalized artist|title (so
"Daft Punk – One More Time" can't sneak in twice under two slugs), default the yt-dlp
query, and append. Idempotent — re-running with the same batch adds nothing.

    python3 emotion_corpus_merge.py <batch1.json> [batch2.json …]   # merge & report
    python3 emotion_corpus_merge.py --dry <batch.json>              # validate only

Batch shape: {"tracks": [ {slug, artist, title, query?, emotions[], valence, arousal,
genre[], structure?} ]}  (or a bare list). See [[project_hexa_emotion_convergence]]."""
import json
import re
import sys
import unicodedata
from pathlib import Path

import emotion_ontology as EMO

HERE = Path(__file__).resolve().parent
GT = HERE / "emotion_corpus_gt.json"
VALID = set(EMO.NAMES)


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


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


def _clean(t):
    """Return a validated track dict, or (None, reason)."""
    if not t.get("artist") or not t.get("title"):
        return None, "missing artist/title"
    emos = [e for e in (t.get("emotions") or []) if e in VALID]
    if not emos:
        return None, f"no valid emotion in {t.get('emotions')}"
    try:
        v, a = float(t["valence"]), float(t["arousal"])
    except (KeyError, TypeError, ValueError):
        return None, "bad/missing valence|arousal"
    if not (-1.0 <= v <= 1.0 and -1.0 <= a <= 1.0):
        return None, f"V/A out of range ({v},{a})"
    slug = _slug(t.get("slug") or f"{t['artist']}-{t['title']}")
    return {
        "slug": slug,
        "artist": t["artist"].strip(),
        "title": t["title"].strip(),
        "query": (t.get("query") or f"{t['artist']} {t['title']} official audio").strip(),
        "emotions": emos[:3],
        "valence": round(v, 2),
        "arousal": round(a, 2),
        "genre": [g for g in (t.get("genre") or []) if g][:4],
        **({"structure": t["structure"]} if t.get("structure") else {}),
    }, None


def _load_batch(path):
    d = json.loads(Path(path).read_text())
    return d["tracks"] if isinstance(d, dict) and "tracks" in d else d


def main():
    args = sys.argv[1:]
    dry = "--dry" in args
    paths = [a for a in args if not a.startswith("--")]
    if not paths:
        sys.exit("usage: emotion_corpus_merge.py [--dry] <batch.json> …")

    gt = json.loads(GT.read_text())
    have_slug = {t["slug"] for t in gt["tracks"]}
    have_name = {_norm(t["artist"]) + "|" + _norm(t["title"]) for t in gt["tracks"]}
    added, dup, bad = [], 0, []

    for path in paths:
        for raw in _load_batch(path):
            t, reason = _clean(raw)
            if not t:
                bad.append((raw.get("title", "?"), reason))
                continue
            key = _norm(t["artist"]) + "|" + _norm(t["title"])
            if t["slug"] in have_slug or key in have_name:
                dup += 1
                continue
            have_slug.add(t["slug"]); have_name.add(key)
            added.append(t)

    print(f"⛵ merge: +{len(added)} new · {dup} dups skipped · {len(bad)} invalid")
    for title, reason in bad[:20]:
        print(f"    ✗ {title}: {reason}")
    if dry:
        print("  (--dry: not written)")
        return
    gt["tracks"].extend(added)
    GT.write_text(json.dumps(gt, ensure_ascii=False, indent=1))
    print(f"  ✓ {GT.name} now {len(gt['tracks'])} tracks")


if __name__ == "__main__":
    main()
