#!/usr/bin/env python3
"""emotion_overnight — disk-safe, deadline-bounded corpus grind for the ~6h night run.

The volume is at 97%%, so we NEVER hoard audio: every track is fetched, analyzed, and
its mp3 + demucs stems deleted immediately — only the tiny analysis JSON survives. Two
phases so the cheap signal is banked first:

  phase 1  fetch → WHOLE-MIX analyze → delete mp3        (fast; covers the whole corpus)
  phase 2  re-fetch → demucs STEMS → stem-analyze → delete mp3+stems  (slow; as many as
           the deadline allows — grows the #92 whole-mix-vs-stem evidence base)

Robust by design: per-track try/except, yt-dlp throttle + consecutive-failure backoff
(YouTube rate-limits a residential IP at volume), and a hard wall-clock deadline after
which it stops cleanly (cleanup always runs). Re-runnable — skips anything already
analyzed.

    python3 emotion_overnight.py --hours 6 [--phase1-only] [--throttle 4]

See [[project_hexa_emotion_convergence]], [[feedback_freebox_ssot]] (disk discipline)."""
import shutil
import sys
import time
from pathlib import Path

import emotion_corpus as EC

FOUNDRY = EC.CACHE / "foundry"


def _purge(slug):
    """Delete the mp3 and any demucs workspace for this slug. Keep analysis JSON."""
    for p in EC.AUDIO.glob(f"{slug}.*"):
        p.unlink(missing_ok=True)
    ws = FOUNDRY / slug
    if ws.exists():
        shutil.rmtree(ws, ignore_errors=True)


def _throttled_fetch(t, throttle, state):
    """Fetch with throttle + exponential backoff on consecutive failures."""
    time.sleep(throttle + min(state["fails"], 8) * throttle)  # backoff grows with fails
    p = EC.fetch_one(t)
    state["fails"] = 0 if p else state["fails"] + 1
    return p


def run(hours, phase1_only, throttle):
    deadline = time.time() + hours * 3600
    tracks = EC._tracks()
    state = {"fails": 0}
    n1 = n2 = 0

    # ── phase 1: whole-mix over the whole corpus (cheap, bank it first) ──────────
    print(f"⛵ overnight grind — {len(tracks)} tracks, deadline {hours}h, throttle {throttle}s",
          flush=True)
    for t in tracks:
        if time.time() > deadline:
            print("  ⏰ deadline — stopping phase 1", flush=True); break
        slug = t["slug"]
        if (EC.ANALYSIS / f"{slug}_mix.json").exists():
            continue
        try:
            if not _throttled_fetch(t, throttle, state):
                print(f"  ✗ fetch {slug}", flush=True); continue
            r = EC.analyze_one(t, mix=True)
            if r:
                n1 += 1
                s = r["summary"]
                print(f"  mix ✓ {slug:<30} {s['dominant']:<11} {s['valence']:+.2f}/{s['arousal']:+.2f}",
                      flush=True)
        except Exception as e:
            print(f"  ! {slug}: {type(e).__name__} {e}", flush=True)
        finally:
            _purge(slug)
    print(f"  phase 1 done: +{n1} whole-mix\n", flush=True)

    if phase1_only:
        print(f"✓ overnight (phase1-only): +{n1} whole-mix", flush=True)
        return

    # ── phase 2: demucs stems for as many as the deadline allows ────────────────
    state["fails"] = 0
    for t in tracks:
        if time.time() > deadline:
            print("  ⏰ deadline — stopping phase 2", flush=True); break
        slug = t["slug"]
        if (EC.ANALYSIS / f"{slug}.json").exists():
            continue
        try:
            if not _throttled_fetch(t, throttle, state):
                print(f"  ✗ refetch {slug}", flush=True); continue
            r = EC.analyze_one(t, mix=False)        # demucs → stem-aware
            if r:
                n2 += 1
                s = r["summary"]
                print(f"  stem ✓ {slug:<30} {s['dominant']:<11} {s['valence']:+.2f}/{s['arousal']:+.2f}",
                      flush=True)
        except Exception as e:
            print(f"  ! stem {slug}: {type(e).__name__} {e}", flush=True)
        finally:
            _purge(slug)

    # final disk hygiene — nothing should remain in audio/ or foundry/
    for p in EC.AUDIO.glob("*"):
        p.unlink(missing_ok=True)
    if FOUNDRY.exists():
        shutil.rmtree(FOUNDRY, ignore_errors=True)
    print(f"\n✓ overnight done: +{n1} whole-mix, +{n2} stem. Audio purged.", flush=True)


def main():
    args = sys.argv[1:]
    hours = float(args[args.index("--hours") + 1]) if "--hours" in args else 6.0
    throttle = float(args[args.index("--throttle") + 1]) if "--throttle" in args else 4.0
    run(hours, "--phase1-only" in args, throttle)


if __name__ == "__main__":
    main()
