#!/usr/bin/env python3
"""
Gerador de Reels — O Oeirense (v2 dark editorial — abril/2026)
Camadas:
  1. Base navy #0d1f2d (1080×1920)
  2. Foto Ken Burns na zona superior (1080×1100 = 57%)
  3. Overlay estático: radial spot + divisor 4px + chapéu fundido + título + rodapé full-width
  4. Legenda sincronizada na zona da foto, centralizada
  5. Áudio ElevenLabs Daniel
"""

import json, base64, os, sys, subprocess, tempfile, shutil, ssl, urllib.request, urllib.parse
sys.stdout.reconfigure(line_buffering=True)
from PIL import Image, ImageDraw, ImageFont, ImageFilter

# SSL context com CA bundle do certifi — Python 3.14 no macOS não usa CAs do sistema
try:
    import certifi
    SSL_CTX = ssl.create_default_context(cafile=certifi.where())
except ImportError:
    SSL_CTX = ssl.create_default_context()

# ── Configurações ──────────────────────────────────────────
FFMPEG   = shutil.which("ffmpeg") or "/usr/local/bin/ffmpeg"  # Linux: /usr/bin/ffmpeg · macOS: Homebrew
FPS      = 25
# Velocidade da locução. 1.0 = como o ElevenLabs entregou.
# Aceleração é feita aqui com atempo do ffmpeg, e NÃO pelo campo `speed` do
# voice_settings: o eleven_multilingual_v2 trata esse campo como sugestão e a
# duração varia de geração pra geração — atempo é determinístico e não mexe no tom.
# Quem acelera aqui tem que reescalar os timestamps junto (ver acelerar_locucao).
VELOCIDADE = 1.08
W, H     = 1080, 1920
FOOTER_H = 130
PHOTO_H  = H - FOOTER_H   # foto ocupa tudo menos o rodapé (0 → 1790)

# Fontes — Raleway
FONTE_BLACK     = "/Users/mac/Library/Fonts/Raleway-Black.ttf"
FONTE_BOLD      = "/Users/mac/Library/Fonts/Raleway-Bold.ttf"
FONTE_EXTRABOLD = "/Users/mac/Library/Fonts/Raleway-ExtraBold.ttf"
FONTE_REGULAR   = "/Users/mac/Library/Fonts/Raleway-Regular.ttf"

# Legenda (zona da foto)
LEGENDA_MAX_CHARS = 22
LEGENDA_FONT_SIZE = 54
LEGENDA_Y_CENTER  = 1180  # centro da legenda — torso/ombros (abaixo do rosto, acima do chapéu ~1331)
LEGENDA_PADDING_X = 32
LEGENDA_PADDING_Y = 16
LEGENDA_BG        = (13, 31, 45, 215)      # navy semi-transparente (alinhado à paleta)
LEGENDA_COR       = (255, 255, 255, 255)
LEGENDA_ACCENT    = (22, 150, 190, 255)    # #1696be — barra lateral

# Paleta v2
NAVY    = (13, 31, 45)       # #0d1f2d — fundo bloco texto
AMARELO = (255, 222, 0)      # #FFDE00 — dot chapéu
BRANCO  = (255, 255, 255)
GRAD_C1 = (18, 123, 157)     # #127b9d
GRAD_C2 = (62, 160, 129)     # #3ea081
GRAD_C3 = (98, 192, 105)     # #62c069


# ══════════════════════════════════════════════════════════
#  LEGENDA — agrupamento e renderização
# ══════════════════════════════════════════════════════════

def agrupar_palavras(chars, starts, ends):
    palavras = []
    p, ini = "", None
    for c, s, e in zip(chars, starts, ends):
        if c in (" ", "\n"):
            if p:
                palavras.append((p, ini, e))
                p, ini = "", None
        else:
            if ini is None: ini = s
            p += c
    if p:
        palavras.append((p, ini, ends[-1]))
    return palavras


def agrupar_em_blocos(palavras, max_chars=LEGENDA_MAX_CHARS, max_linhas=2):
    linhas = []
    lp, li, lf, comp = [], None, None, 0
    for palavra, ini, fim in palavras:
        nc = comp + len(palavra) + (1 if lp else 0)
        if nc > max_chars and lp:
            linhas.append((" ".join(lp), li, lf))
            lp, li, lf, comp = [palavra], ini, fim, len(palavra)
        else:
            if not lp: li = ini
            lp.append(palavra); lf = fim; comp = nc
    if lp:
        linhas.append((" ".join(lp), li, lf))

    blocos = []
    for i in range(0, len(linhas), max_linhas):
        g = linhas[i:i + max_linhas]
        texto = "\n".join(l[0] for l in g)
        blocos.append((texto, g[0][1], g[-1][2]))
    return blocos


def calcular_chapeu_top(chapeu, titulo):
    """Retorna a coord Y do topo do chapéu — mesma lógica de criar_overlay, só medindo.
    Usado pra posicionar a legenda dinamicamente acima do bloco chapéu+título."""
    draw = ImageDraw.Draw(Image.new("RGBA", (W, H)))
    try:
        f_chapeu = ImageFont.truetype(FONTE_BOLD, 26)
        f_titulo = ImageFont.truetype(FONTE_EXTRABOLD, 64)
    except:
        f_chapeu = f_titulo = ImageFont.load_default()

    titulo_max_w = W - 136
    titulo_linhas = quebrar_texto(titulo, f_titulo, titulo_max_w, draw)
    bb_tl = draw.textbbox((0, 0), "Ag", font=f_titulo)
    linha_h = int((bb_tl[3] - bb_tl[1]) * 1.18)
    total_titulo_h = len(titulo_linhas) * linha_h
    titulo_y_end = 1550
    titulo_y = titulo_y_end - total_titulo_h

    bb_ch = draw.textbbox((0, 0), chapeu.upper(), font=f_chapeu)
    ch_th = bb_ch[3] - bb_ch[1]
    ch_pad_y = 14
    ch_block_h = ch_th + ch_pad_y * 2
    return titulo_y - 14 - ch_block_h


def renderizar_legenda(texto, y_center=None):
    """PNG 1080×1920 transparente com legenda centralizada (navy bg + accent lateral teal).
    Se y_center não for passado, usa LEGENDA_Y_CENTER fixo (880 → 1180)."""
    if y_center is None:
        y_center = LEGENDA_Y_CENTER

    img = Image.new("RGBA", (W, H), (0, 0, 0, 0))
    draw = ImageDraw.Draw(img)

    try:
        fonte = ImageFont.truetype(FONTE_EXTRABOLD, LEGENDA_FONT_SIZE)
    except:
        fonte = ImageFont.load_default()

    linhas = texto.split("\n")
    # Medir advance width real (inclui side-bearings corretamente) — evita texto
    # estourar a caixa em fontes com tracking pronunciado (Raleway-ExtraBold)
    larguras = [int(round(draw.textlength(l, font=fonte))) for l in linhas]
    ascent, descent = fonte.getmetrics()
    linha_h = ascent + descent
    espaco = 10
    total_h = linha_h * len(linhas) + espaco * (len(linhas) - 1)
    max_w = max(larguras)

    cx = W // 2
    bx0 = cx - max_w // 2 - LEGENDA_PADDING_X
    by0 = y_center - total_h // 2 - LEGENDA_PADDING_Y
    bx1 = cx + max_w // 2 + LEGENDA_PADDING_X
    by1 = y_center + total_h // 2 + LEGENDA_PADDING_Y
    draw.rounded_rectangle([bx0, by0, bx1, by1], radius=10, fill=LEGENDA_BG)
    # Accent lateral esquerda (teal) — coerente com divisor
    draw.rectangle([bx0, by0, bx0 + 4, by1], fill=LEGENDA_ACCENT)

    # Desenhar cada linha ancorada no topo-centro — Pillow posiciona glifos usando
    # a mesma métrica do textlength, garantindo alinhamento perfeito com o box
    y = y_center - total_h // 2
    for linha in linhas:
        draw.text((cx, y), linha, font=fonte, fill=LEGENDA_COR, anchor="mt")
        y += linha_h + espaco

    return img


# ══════════════════════════════════════════════════════════
#  OVERLAY ESTÁTICO — v2 dark editorial
# ══════════════════════════════════════════════════════════

def grad_cor(c1, c2, c3, frac):
    """Interpola 3 stops (0 → 0.5 → 1)."""
    if frac < 0.5:
        t = frac / 0.5
        return tuple(int(c1[i] + (c2[i] - c1[i]) * t) for i in range(3))
    t = (frac - 0.5) / 0.5
    return tuple(int(c2[i] + (c3[i] - c2[i]) * t) for i in range(3))


def quebrar_texto(texto, fonte, max_w, draw):
    palavras = texto.split()
    linhas, linha_atual = [], []
    for p in palavras:
        teste = " ".join(linha_atual + [p])
        bb = draw.textbbox((0, 0), teste, font=fonte)
        if bb[2] - bb[0] > max_w and linha_atual:
            linhas.append(" ".join(linha_atual))
            linha_atual = [p]
        else:
            linha_atual.append(p)
    if linha_atual:
        linhas.append(" ".join(linha_atual))
    return linhas


def criar_overlay(chapeu, titulo):
    """
    Overlay 1080×1920 (v2 dark editorial — foto full, sem bloco navy):
      - Gradient dissolve sutil no terço inferior (contraste para o título)
      - Chapéu com gradient teal→verde colado ao título (14px de gap)
      - Título branco com sombra sutil
      - Rodapé full-width 130px com gradient
    """
    overlay = Image.new("RGBA", (W, H), (0, 0, 0, 0))
    draw = ImageDraw.Draw(overlay)

    photo_bottom = H - FOOTER_H   # 1790 — onde a foto termina

    # ── Gradient dissolve — leve, só para contraste do título ──
    # Começa em y=900 (alpha 0) → y=1790 (alpha 215, navy). Curva 1.5 dá mais densidade no meio.
    grad_start = 900
    for y in range(grad_start, photo_bottom):
        frac = (y - grad_start) / (photo_bottom - grad_start)
        alpha = int((frac ** 1.5) * 215)
        draw.rectangle([(0, y), (W, y + 1)], fill=(13, 31, 45, alpha))

    # ── Fontes ──
    try:
        f_chapeu       = ImageFont.truetype(FONTE_BOLD,      26)
        f_titulo       = ImageFont.truetype(FONTE_EXTRABOLD, 64)
        f_footer_left  = ImageFont.truetype(FONTE_BOLD,      24)
        f_footer_right = ImageFont.truetype(FONTE_BOLD,      20)
    except:
        f_chapeu = f_titulo = f_footer_left = f_footer_right = ImageFont.load_default()

    # ── Título — ancorado de baixo (40px acima do rodapé) ──
    titulo_max_w = W - 136
    titulo_linhas = quebrar_texto(titulo, f_titulo, titulo_max_w, draw)
    bb_tl = draw.textbbox((0, 0), "Ag", font=f_titulo)
    linha_h = int((bb_tl[3] - bb_tl[1]) * 1.18)
    total_titulo_h = len(titulo_linhas) * linha_h

    # Ancorar o bloco (chapéu+título) na metade do meio inferior (~y=1375)
    # Bottom do título ~1550 → dá respiro generoso pro rodapé e facilita leitura
    titulo_y_end = 1550
    titulo_y = titulo_y_end - total_titulo_h

    # ── Chapéu — 14px acima do título, gradient local ──
    chapeu_txt = chapeu.upper()
    bb_ch = draw.textbbox((0, 0), chapeu_txt, font=f_chapeu)
    ch_tw, ch_th = bb_ch[2] - bb_ch[0], bb_ch[3] - bb_ch[1]
    dot_d, dot_gap = 12, 14
    ch_pad_x, ch_pad_y = 26, 14
    ch_block_w = ch_pad_x * 2 + dot_d + dot_gap + ch_tw
    ch_block_h = ch_th + ch_pad_y * 2
    ch_x = 68
    ch_y = titulo_y - 14 - ch_block_h

    # Chapéu com gradient (3 stops teal→verde)
    chapeu_bg = Image.new("RGBA", (ch_block_w, ch_block_h), (0, 0, 0, 0))
    cbg_draw = ImageDraw.Draw(chapeu_bg)
    for i in range(ch_block_w):
        frac = i / max(ch_block_w - 1, 1)
        cor = grad_cor(GRAD_C1, GRAD_C2, GRAD_C3, frac)
        cbg_draw.line([(i, 0), (i, ch_block_h)], fill=(*cor, 255))
    mask = Image.new("L", (ch_block_w, ch_block_h), 0)
    ImageDraw.Draw(mask).rounded_rectangle(
        [0, 0, ch_block_w - 1, ch_block_h - 1], radius=4, fill=255
    )
    chapeu_bg.putalpha(mask)

    # Sombra
    shadow = Image.new("RGBA", (W, H), (0, 0, 0, 0))
    ImageDraw.Draw(shadow).rounded_rectangle(
        [ch_x + 2, ch_y + 6, ch_x + ch_block_w + 2, ch_y + ch_block_h + 10],
        radius=4, fill=(0, 0, 0, 130)
    )
    shadow = shadow.filter(ImageFilter.GaussianBlur(radius=14))
    overlay = Image.alpha_composite(overlay, shadow)

    overlay.paste(chapeu_bg, (ch_x, ch_y), chapeu_bg)
    draw = ImageDraw.Draw(overlay)

    # Dot amarelo + texto chapéu
    dot_cx = ch_x + ch_pad_x + dot_d // 2
    dot_cy = ch_y + ch_block_h // 2
    draw.ellipse(
        [dot_cx - dot_d // 2, dot_cy - dot_d // 2,
         dot_cx + dot_d // 2, dot_cy + dot_d // 2],
        fill=(*AMARELO, 255)
    )
    draw.text(
        (dot_cx + dot_d // 2 + dot_gap,
         ch_y + (ch_block_h - ch_th) // 2 - bb_ch[1]),
        chapeu_txt, font=f_chapeu, fill=(*BRANCO, 255)
    )

    # ── Título (com sombra sutil para reforço sobre foto) ──
    ty = titulo_y
    for linha in titulo_linhas:
        draw.text((70, ty + 2), linha, font=f_titulo, fill=(0, 0, 0, 120))
        draw.text((68, ty), linha, font=f_titulo, fill=(*BRANCO, 255))
        ty += linha_h

    # ── Rodapé full-width gradient ──
    footer_y = photo_bottom
    for x in range(W):
        frac = x / W
        cor = grad_cor(GRAD_C1, GRAD_C2, GRAD_C3, frac)
        draw.rectangle([(x, footer_y), (x + 1, H)], fill=(*cor, 255))

    left_txt = "@OOEIRENSE"
    bb_fl = draw.textbbox((0, 0), left_txt, font=f_footer_left)
    fl_h = bb_fl[3] - bb_fl[1]
    draw.text(
        (68, footer_y + (FOOTER_H - fl_h) // 2 - bb_fl[1]),
        left_txt, font=f_footer_left, fill=(*BRANCO, 255)
    )

    right_txt = "OOEIRENSE.COM.BR"
    spacing = 3
    char_bbs = []
    total_w = 0
    for c in right_txt:
        b = draw.textbbox((0, 0), c, font=f_footer_right)
        cw = b[2] - b[0]
        char_bbs.append((c, cw))
        total_w += cw + spacing
    total_w -= spacing
    bb_fr = draw.textbbox((0, 0), right_txt, font=f_footer_right)
    fr_h = bb_fr[3] - bb_fr[1]
    cx = W - 68 - total_w
    cy = footer_y + (FOOTER_H - fr_h) // 2 - bb_fr[1]
    for c, cw in char_bbs:
        draw.text((cx, cy), c, font=f_footer_right, fill=(255, 255, 255, 235))
        cx += cw + spacing

    return overlay


# ══════════════════════════════════════════════════════════
#  KEN BURNS — só na zona da foto (1080×1100)
# ══════════════════════════════════════════════════════════

def aplicar_ken_burns(foto_base, frame_idx, total_frames, eh_landscape=False):
    """
    Saída: PIL Image RGBA 1080×PHOTO_H (1100px).

    Landscape (pan centrado — v3 abril/2026):
      - Zoom 1.08 → 1.13 (começa já enquadrado no centro, termina levemente mais fechado)
      - Pan lateral cobre APENAS o terço central do range total — nunca toca as bordas
      - Mantém ponto de interesse típico (centro) em quadro durante toda a locução

    Portrait: zoom central (1.0 → 1.30).
    """
    prog = frame_idx / max(total_frames - 1, 1)
    fw, fh = foto_base.size

    if eh_landscape:
        zoom = 1.08 + 0.05 * prog
        crop_w = int(W / zoom)
        crop_h = int(PHOTO_H / zoom)
        x0_max = fw - crop_w
        # Pan só no terço central: amplitude = 1/3 do range lateral máximo
        x0_center = x0_max // 2
        pan_amp = x0_max // 3
        x0 = x0_center - pan_amp // 2 + int(pan_amp * prog)
        y0 = (fh - crop_h) // 2
        x0 = max(0, min(x0, x0_max))
        y0 = max(0, min(y0, fh - crop_h))
    else:
        zoom = 1.0 + 0.30 * prog
        crop_w = int(W / zoom)
        crop_h = int(PHOTO_H / zoom)
        x0 = (fw - crop_w) // 2
        y0 = (fh - crop_h) // 2

    cropped = foto_base.crop((x0, y0, x0 + crop_w, y0 + crop_h))
    return cropped.resize((W, PHOTO_H), Image.LANCZOS)


# ══════════════════════════════════════════════════════════
#  PRINCIPAL
# ══════════════════════════════════════════════════════════

def acelerar_locucao(audio_path, timestamps_json, fator):
    """Acelera o MP3 e reescala os timestamps pelo mesmo fator.

    Os dois andam juntos por obrigação: a legenda sincronizada nasce do alignment
    da fala original. Áudio acelerado com tempo antigo faz a legenda atrasar, e o
    erro cresce até o fim do vídeo.
    """
    if abs(fator - 1.0) < 0.001:
        return audio_path, timestamps_json

    raiz, ext = os.path.splitext(audio_path)
    acelerado = f"{raiz}_x{fator:.2f}{ext}"
    subprocess.run([FFMPEG, "-v", "error", "-y", "-i", audio_path,
                    "-filter:a", f"atempo={fator}", "-b:a", "192k", acelerado], check=True)

    ts = dict(timestamps_json)
    for campo in ("character_start_times_seconds", "character_end_times_seconds"):
        ts[campo] = [round(t / fator, 3) for t in timestamps_json[campo]]

    print(f"Locução acelerada {fator:.2f}x")
    return acelerado, ts


def gerar_reels(foto_path, audio_path, timestamps_json, chapeu, titulo, output_path,
                velocidade=VELOCIDADE):
    audio_path, timestamps_json = acelerar_locucao(audio_path, timestamps_json, velocidade)

    chars  = timestamps_json["characters"]
    starts = timestamps_json["character_start_times_seconds"]
    ends   = timestamps_json["character_end_times_seconds"]

    palavras = agrupar_palavras(chars, starts, ends)
    blocos   = agrupar_em_blocos(palavras)

    print(f"Blocos de legenda: {len(blocos)}")
    for b in blocos:
        print(f"  [{b[1]:.2f}s → {b[2]:.2f}s]\n    {b[0].replace(chr(10), ' | ')}")

    # Duração do áudio
    res = subprocess.run([FFMPEG, "-i", audio_path, "-f", "null", "-"],
                         capture_output=True, text=True)
    dur_str = [l for l in res.stderr.split("\n") if "Duration" in l][0]
    dur_str = dur_str.split("Duration: ")[1].split(",")[0].strip()
    h, m, s = dur_str.split(":")
    duracao = int(h) * 3600 + int(m) * 60 + float(s)
    total_frames = int(duracao * FPS) + 10
    print(f"Duração: {duracao:.2f}s | Frames: {total_frames}")

    # Carregar foto — escalar para preencher a zona da foto (1080×PHOTO_H)
    foto_raw = Image.open(foto_path).convert("RGBA")
    fw, fh = foto_raw.size
    eh_landscape = fw > fh

    if eh_landscape:
        escala = PHOTO_H / fh
        novo_w = int(fw * escala)
        novo_h = PHOTO_H
    else:
        escala = max(W / fw, PHOTO_H / fh)
        novo_w = int(fw * escala)
        novo_h = int(fh * escala)

    foto_base = foto_raw.resize((novo_w, novo_h), Image.LANCZOS)
    print(f"Foto: {fw}×{fh} → escalada: {novo_w}×{novo_h} | "
          f"{'landscape (pan)' if eh_landscape else 'portrait (zoom)'}")

    overlay = criar_overlay(chapeu, titulo)
    print("Criando overlay v2... OK")

    # Posição dinâmica da legenda: sempre ~30px acima do chapéu, respeitando
    # o tamanho real do título (títulos longos empurram chapéu pra cima)
    chapeu_top = calcular_chapeu_top(chapeu, titulo)
    # Box da legenda ≈ 160px (2 linhas × 64 + padding). Centro = top - 30 - box_h/2
    legenda_y_center = chapeu_top - 30 - 80
    print(f"Chapéu top: {chapeu_top} | Legenda centro: {legenda_y_center}")

    print("Gerando frames (Ken Burns só na zona da foto)...")

    tmpdir = tempfile.mkdtemp()

    for i in range(total_frames):
        t = i / FPS

        # 1. Base navy 1080×1920
        frame = Image.new("RGBA", (W, H), (*NAVY, 255))

        # 2. Foto Ken Burns na zona superior (1080×1100)
        foto_zoom = aplicar_ken_burns(foto_base, i, total_frames, eh_landscape)
        frame.paste(foto_zoom, (0, 0))

        # 3. Overlay estático (divisor + chapéu + título + rodapé + radial)
        frame = Image.alpha_composite(frame, overlay)

        # 4. Legenda sincronizada
        bloco_ativo = next(
            (texto for texto, ini, fim in blocos if ini <= t <= fim + 0.08),
            None
        )
        if bloco_ativo:
            legenda = renderizar_legenda(bloco_ativo, y_center=legenda_y_center)
            frame = Image.alpha_composite(frame, legenda)

        frame.convert("RGB").save(f"{tmpdir}/frame_{i:05d}.jpg", quality=92)

    print("Montando vídeo com ffmpeg...")
    cmd = [
        FFMPEG, "-y",
        "-framerate", str(FPS),
        "-i", f"{tmpdir}/frame_%05d.jpg",
        "-i", audio_path,
        "-c:v", "libx264", "-preset", "fast", "-crf", "22",
        "-c:a", "aac", "-b:a", "128k",
        "-shortest", "-pix_fmt", "yuv420p",
        output_path
    ]
    subprocess.run(cmd, check=True, capture_output=True)
    shutil.rmtree(tmpdir)
    print(f"✅ Vídeo salvo: {output_path}")

    # Sinalizar pro n8n que o vídeo está pronto.
    # Não precisa de upload externo: o n8n usa Resumable Upload do Meta (binário direto).
    with open(output_path + ".done", "w") as f:
        f.write("ok")


# ── CLI ────────────────────────────────────────────────────
if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description="Gerador de Reels O Oeirense v2")
    parser.add_argument("--foto",       required=True)
    parser.add_argument("--audio",      required=True)
    parser.add_argument("--timestamps", required=True)
    parser.add_argument("--chapeu",     required=True)
    parser.add_argument("--titulo",     required=True)
    parser.add_argument("--output",     required=True)
    parser.add_argument("--velocidade", type=float, default=VELOCIDADE,
                        help=f"fator de velocidade da locução (padrão {VELOCIDADE})")
    args = parser.parse_args()

    gerar_reels(
        foto_path       = args.foto,
        audio_path      = args.audio,
        timestamps_json = json.load(open(args.timestamps)),
        chapeu          = args.chapeu,
        titulo          = args.titulo,
        output_path     = args.output,
        velocidade      = args.velocidade
    )
