#!/usr/bin/env python3
"""Final mix: VO + procedural SFX + ducked music bed -> the delivered master.

Timeline comes from the ENGINE (single source of truth). VO clip i lands at
BEAT_STARTS[i] + 0.3s; the last clip belongs to the outro.

The VO fit gate lives HERE and is the reason this script exists: every clip is
ffprobed (reported TTS durations lie — one clip reported 6.0s and was really
10.03s), overruns are time-compressed with atempo (<=1.2x is inaudible), and
anything needing more HARD-FAILS — that case is a re-record, never a stretch.

ffmpeg traps handled: `sidechaincompress` ends at the SHORTER input, so the VO
key chain is apad-ed to full length; `alimiter` uses level=disabled; output is
forced to stereo because mono VO wavs otherwise collapse the whole master.

Env: BRAND_PACK, BRAND_PROJECT. Optional --check exits after the fit table
(preflight mode, no mix).
"""
import importlib.util
import os
import subprocess
import sys

PROJ = os.environ.get("BRAND_PROJECT", "")
if not PROJ:
    sys.exit("mix: set BRAND_PROJECT")
MAX_ATEMPO = 1.2
CHECK_ONLY = "--check" in sys.argv

_here = os.path.dirname(os.path.abspath(__file__))
sys.argv = ["mix", "16x9"]
_spec = importlib.util.spec_from_file_location("_engine", os.path.join(_here, "engine.py"))
E = importlib.util.module_from_spec(_spec)
sys.modules[_spec.name] = E
_spec.loader.exec_module(E)

TOTAL = E.TOTAL
STARTS = list(E.BEAT_STARTS) + [E.TOTAL - E.OUTRO_D]
DURS = list(E.BEAT_DURS) + [E.OUTRO_D]
LEAD = 0.3

SLUG = os.path.basename(PROJ.rstrip("/"))
VIDEO = f"{PROJ}/final/videos/{SLUG}-video-only.mp4"
SFX = f"{PROJ}/artifacts/audio/sfx.wav"
MUSIC = os.path.join(PROJ, E.P.get("MUSIC_FILE", "artifacts/audio/soundtrack-suno.mp3"))
OUT = f"{PROJ}/final/videos/{SLUG}.mp4"


def probe(path):
    r = subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration",
                        "-of", "csv=p=0", path], capture_output=True, text=True)
    return float(r.stdout.strip())


vo = []
for i in range(len(STARTS)):
    p = f"{PROJ}/artifacts/audio/dialogue-{i}-narrator.wav"
    if not os.path.exists(p):
        sys.exit(f"mix: missing VO clip {i}: {p}")
    vo.append((i, p, probe(p)))

parts = []
vo_labels = []
failed = False
print(f"{'#':>2} {'start':>6} {'budget':>7} {'clip':>6}  action")
for n, (i, _p, d) in enumerate(vo):
    idx = 3 + n
    budget = DURS[i] - 0.5
    chain = f"[{idx}:a]"
    if d > budget:
        tempo = d / budget
        if tempo > MAX_ATEMPO:
            note = f"FAIL atempo {tempo:.3f} > {MAX_ATEMPO} — re-record shorter"
            failed = True
        else:
            chain += f"atempo={tempo:.5f},"
            note = f"atempo {tempo:.3f}"
    else:
        note = "fits"
    delay = int((STARTS[i] + LEAD) * 1000)
    parts.append(f"{chain}adelay={delay}|{delay}[v{n}]")
    vo_labels.append(f"[v{n}]")
    print(f"{i:>2} {STARTS[i]:>6.1f} {budget:>7.2f} {d:>6.2f}  {note}")

if failed:
    sys.exit("mix: a VO line exceeds its beat beyond atempo range — fix the script, re-record")
if CHECK_ONLY:
    print("mix: --check OK (all VO clips fit)")
    sys.exit(0)
for req, what in ((VIDEO, "video-only render"), (SFX, "sfx.wav"), (MUSIC, "music bed")):
    if not os.path.exists(req):
        sys.exit(f"mix: missing {what}: {req}")

inputs = ["-i", VIDEO, "-i", SFX, "-i", MUSIC]
for _, p, _d in vo:
    inputs += ["-i", p]

# VO bus, padded to full length so sidechaincompress does not truncate the bed
parts.append("".join(vo_labels) + f"amix=inputs={len(vo_labels)}:normalize=0,"
             f"volume=1.25,apad,atrim=0:{TOTAL}[vo]")
parts.append("[vo]asplit=2[vomix][vokey]")
parts.append(f"[2:a]atrim=0:{TOTAL},volume=0.20[mus]")
parts.append("[mus][vokey]sidechaincompress=threshold=0.05:ratio=12:attack=8:release=340[musd]")
parts.append(f"[1:a]atrim=0:{TOTAL},volume=0.42[sfx]")
parts.append("[vomix][musd][sfx]amix=inputs=3:normalize=0,"
             "alimiter=level=disabled:limit=0.94,"
             "aformat=sample_fmts=fltp:channel_layouts=stereo,aresample=48000[a]")

cmd = ["ffmpeg", "-y", "-v", "error", *inputs,
       "-filter_complex", ";".join(parts),
       "-map", "0:v", "-map", "[a]",
       "-c:v", "copy", "-c:a", "aac", "-b:a", "192k", "-ac", "2",
       "-t", f"{TOTAL}", OUT]
print("\nmixing…")
r = subprocess.run(cmd)
if r.returncode != 0:
    sys.exit(r.returncode)
print("master:", OUT, f"{probe(OUT):.2f}s")
