#!/usr/bin/env python3
"""Compose REEL dinámico v3 (AI4M #5): SIN franja superior + MIX de captions
(caja sans negra-sobre-blanca ALTERNANDO con serif Playfair blanco grande, centro-bajo)
+ gráficas DETRÁS del sujeto (avatar_behind.mp4) + b-rolls full-screen snappy."""
import subprocess, pathlib, sys, re
sys.path.insert(0, "/home/clawd/bin/agentsquad-shorts")
from lib import captions

BASE = "/home/clawd/playgrounds/ai4m-negocio"
AV = f"{BASE}/avatar_behind.mp4"
BROLL = f"{BASE}/brolls"
WORK = pathlib.Path("/tmp/ai4m-neg-dyn2"); WORK.mkdir(exist_ok=True)
OUTF = f"{BASE}/final_dyn2.mp4"

BEATS = [
    {"clip": f"{BROLL}/sora.mp4",     "start": 8.6,  "end": 10.8},  # cortado: libera el caption "Y esa es la señal..." (antes 12.6)
    {"clip": f"{BROLL}/contexto.mp4", "start": 22.0, "end": 30.0},
    {"clip": f"{BROLL}/equipos.mp4",  "start": 46.5, "end": 55.5},
    {"clip": f"{BROLL}/pasos.mp4",    "start": 59.1, "end": 68.5},
    {"clip": f"{BROLL}/pregunta.mp4", "start": 76.6, "end": 83.0},
]
DATA = []
# padding: el caption cede pantalla un pelo ANTES de que aparezca el b-roll y reaparece
# 0.35s DESPUES de que termina, cubriendo su fade-out (evita solape caption-vs-texto-horneado-del-broll)
sup = [(b["start"]-0.10, b["end"]+0.35) for b in BEATS]

# ---------- MIX de captions ----------
CAP_Y = 1075            # centro vertical del bloque (an5), mid-frame bajo la cara, libre de behind-text
# --- serif dinamico (match modelo IG: texto grande que llena el ancho) ---
# Safe zone YouTube Shorts: la columna de botones ocupa x>936 -> el texto NO debe pasar de ahi.
CHARF = 0.362           # ancho medio por glifo / fontsize (calibrado por PIL contra el render real)
SERIF_TARGET_W = 760    # ancho objetivo de la linea mas larga (~70% del frame, dentro de safe zone: xmax<936)
SERIF_MAXFS = 236       # techo (lineas cortas no exceden el ancho safe)
SERIF_RATIO = 0.72      # leading/fontsize (negativo, interbloqueo)
HOOK_END = 6.2          # #5: el bloque hook (0-6.2s) va TODO en serif grande (= momento mas grande)
STYLES = {  # (fontsize_default, max_chars_por_linea, line_height) — serif se recalcula dinamico
    "Box":   (94, 17, 132),
    "Serif": (200, 10, 144),
}
def _ts(t):
    h=int(t//3600); m=int((t%3600)//60); s=t%60; return f"{h:d}:{m:02d}:{s:05.2f}"
def _wrap(text, maxc, cap=3):
    words=text.split(); lines=[]; cur=""
    for w in words:
        if cur and len(cur)+1+len(w) > maxc: lines.append(cur); cur=w
        else: cur=(cur+" "+w).strip()
    if cur: lines.append(cur)
    return lines[:cap]
def _wrap_serif(text):
    """Balancea el texto en <=3 lineas SIN cortar ninguna palabra: busca el menor
    ancho-de-linea que mete TODO en <=3 lineas (asi la fuente queda lo mas grande posible)."""
    words=text.split()
    if not words: return [text]
    maxword=max(len(w) for w in words)
    for cand in range(maxword, len(text)+2):
        lines=_wrap(text, cand, cap=99)
        if len(lines)<=3: return lines
    return _wrap(text, len(text)+2, cap=99)[:3]
def _subtract_sup(t0,t1):
    """devuelve sub-intervalos de [t0,t1] que NO caen bajo un b-roll (sup)."""
    segs=[(t0,t1)]
    for ws,we in sup:
        out=[]
        for a,b in segs:
            if we<=a or ws>=b: out.append((a,b)); continue
            if a<ws: out.append((a,ws))
            if we<b: out.append((we,b))
        segs=out
    return [(a,b) for a,b in segs if b-a>0.30]

def build_mixed_ass(words, out_path):
    box_fs = STYLES["Box"][0]; serif_fs = STYLES["Serif"][0]
    head=("[Script Info]\nScriptType: v4.00+\nPlayResX: 1080\nPlayResY: 1920\n"
          "ScaledBorderAndShadow: yes\nWrapStyle: 2\n\n"
          "[V4+ Styles]\n"
          "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n"
          # BOX: texto negro en caja blanca (BorderStyle=3, Outline=caja blanca). Sans.
          f"Style: Box,DejaVu Sans,{box_fs},&H00141414,&H00141414,&H00FFFFFF,&H00FFFFFF,1,0,0,0,100,100,1,0,3,9,0,5,60,60,0,1\n"
          # SERIF: PP Editorial New blanco grande, trazos LIMPIOS (sin contorno) + sombra mínima
          # para nitidez como el modelo. Shadow=2.
          f"Style: Serif,PP Editorial New,{serif_fs},&H00FFFFFF,&H00FFFFFF,&H00000000,&H80000000,0,0,0,0,100,100,0,0,1,0,2,5,60,60,0,1\n\n"
          "[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n")
    chunks=captions._phrase_chunks(words)
    # pre-computar starts para clamp (sin solapamiento entre captions consecutivos)
    starts=[float(ch[0]["start"]) for ch in chunks]
    events=[]
    for i,ch in enumerate(chunks):
        txt=" ".join(str(w["word"]).strip() for w in ch).strip()
        txt=re.sub(r"\s+"," ",txt)
        if not txt: continue
        t0=float(ch[0]["start"]); t1=float(ch[-1].get("end",t0+0.4))+0.12
        # clamp: el caption N termina ANTES de que arranque el N+1 (hard-cut, sin overlap garble)
        if i+1 < len(starts): t1=min(t1, starts[i+1]-0.02)
        if t1 <= t0: t1=t0+0.20
        style = "Box" if (i%2==0 or txt.rstrip().endswith("?")) else "Serif"
        if t0 < HOOK_END: style = "Serif"   # #5: hook completo en serif grande
        fs,maxc,lh = STYLES[style]
        if style=="Serif":
            # balancea en <=3 lineas SIN cortar (fix: antes [:3] descartaba palabras),
            # luego TAMANO DINAMICO: agranda hasta llenar el ancho safe segun la linea mas larga.
            wl=_wrap_serif(txt)
            longest=max(len(l) for l in wl) if wl else 1
            fs=min(SERIF_MAXFS, int(SERIF_TARGET_W/(longest*CHARF)))
            lh=int(round(SERIF_RATIO*fs))   # leading negativo (interbloqueo como el modelo)
        else:
            wl=_wrap(txt, maxc)
        N=len(wl)
        segs=_subtract_sup(t0,t1)
        if not segs: continue
        # una línea = un evento centrado (caja propia por línea, como la referencia)
        # SIN \fad: estilo IG hard-cut, evita superposicion de 2 captions en transicion
        for k,line in enumerate(wl):
            y = int(CAP_Y + (k - (N-1)/2.0)*lh)
            eff=("\\fs%d\\pos(540,%d)"%(fs,y)) if style=="Serif" else ("\\pos(540,%d)"%y)
            for a,b in segs:
                events.append(f"Dialogue: 0,{_ts(a)},{_ts(b)},{style},,0,0,0,,{{{eff}}}{line}")
    open(out_path,"w").write(head+"\n".join(events)+"\n")
    return len(events)

def run(cmd):
    r = subprocess.run(cmd, capture_output=True, text=True)
    if r.returncode != 0:
        raise RuntimeError(f"FAILED: {' '.join(map(str,cmd))}\n{r.stderr[-1800:]}")

print("captions MIX (caja sans / serif Playfair)...")
words = captions.transcribe_words(AV)
SCRIPT_TXT = open(f"{BASE}/script.txt").read().strip()
words = captions.align_to_script(words, SCRIPT_TXT)
def fix_brand(ws):
    out=[]; i=0
    while i < len(ws):
        w=ws[i]; wl=str(w.get("word","")).strip().lower().strip(".,!?")
        if wl=="ai" and i+2<len(ws):
            w1=str(ws[i+1].get("word","")).strip().lower().strip(".,!?")
            w2raw=str(ws[i+2].get("word","")).strip(); w2=w2raw.lower().strip(".,!?")
            if w1 in ("for","form","four","fore") and w2.startswith("manager"):
                trail="." if w2raw.endswith(".") else ""
                out.append({"word":"AI4Managers"+trail,"start":float(w.get("start",0)),"end":float(ws[i+2].get("end",0))}); i+=3; continue
        out.append(w); i+=1
    return out
words=fix_brand(words)
ass = WORK/"mix.ass"
n=build_mixed_ass(words, ass)
print(f"  {n} eventos de caption")

print("audio...")
audio_norm = WORK/"audio.m4a"
run(["ffmpeg","-y","-i",AV,"-vn","-af","loudnorm=I=-16:TP=-1.5","-ar","44100","-c:a","aac","-b:a","192k",str(audio_norm)])

print("b-rolls...")
PPEN = "/home/clawd/.fonts/ppen/PPEditorialNew-Regular.otf"
for i,b in enumerate(BEATS):
    vf = "scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,fps=30,setsar=1"
    if i == 0:  # caption hablado sobre la toma del "cuello de botella" (persona sola entre pantallas)
        a = "if(lt(t,0.3),t/0.3,if(gt(t,1.9),max(0,1-(t-1.9)/0.3),1))"  # fade in/out (clip mostrado ~2.2s)
        l1 = (f"drawtext=fontfile={PPEN}:text='Yo era el cuello':fontcolor=white:fontsize=131:"
              f"x=(w-tw)/2:y=965:alpha='{a}':shadowcolor=black@0.6:shadowx=3:shadowy=3")
        l2 = (f"drawtext=fontfile={PPEN}:text='de botella.':fontcolor=white:fontsize=131:"
              f"x=(w-tw)/2:y=1059:alpha='{a}':shadowcolor=black@0.6:shadowx=3:shadowy=3")
        vf = vf + "," + l1 + "," + l2
    run(["ffmpeg","-y","-i",b["clip"],"-vf",vf,"-an",str(WORK/f"fill{i}.mp4")])

print("componiendo (full-frame + mix captions + b-rolls)...")
inputs=["-i",AV]
for i in range(len(BEATS)): inputs+=["-i",str(WORK/f"fill{i}.mp4")]
inputs+=["-i",str(audio_norm)]
audio_in = 1+len(BEATS)
# #1: push-in lento sobre el avatar en la apertura 0-8s (frena el scroll), captions fijos
# (zoompan ANTES de subtitles: el avatar empuja, el texto queda clavado por \pos)
pushin = "zoompan=z='if(lte(on,240),1+0.0003*on,1.072)':d=1:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=1080x1920:fps=30"
parts=[
    f"[0:v]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,fps=30,{pushin},setsar=1,subtitles={ass}[base]",
]
base="[base]"
for i,b in enumerate(BEATS):
    dur=b["end"]-b["start"]
    parts.append(f"[{i+1}:v]fade=t=out:st={dur-0.12:.2f}:d=0.12:alpha=1,setpts=PTS+{b['start']:.2f}/TB[ov{i}]")
for i,b in enumerate(BEATS):
    lbl="[v]" if i==len(BEATS)-1 else f"[c{i}]"
    parts.append(f"{base}[ov{i}]overlay=0:0:enable='between(t,{b['start']:.2f},{b['end']:.2f})'{lbl}")
    base=lbl
fc=";".join(parts)
run(["ffmpeg","-y",*inputs,"-filter_complex",fc,"-map","[v]","-map",f"{audio_in}:a",
     "-c:v","libx264","-preset","veryfast","-crf","19","-pix_fmt","yuv420p","-c:a","copy","-movflags","+faststart",OUTF])
dur=subprocess.check_output(["ffprobe","-v","error","-show_entries","format=duration","-of","csv=p=0",OUTF]).decode().strip()
print(f"OK -> {OUTF} ({float(dur):.1f}s)")
