"""Ganho por trecho medido, ANTES do render_cut: a voz chega perto do alvo e o loudnorm fica linear.

Escrito pelo PrimoCode em cima do EDIT IA PRO (SHK Soluções Digitais, com autorização por escrito, 19/10/2026).

O problema que resolve. Uma gravação de celular com a voz em -38 LUFS precisa de +24 dB para chegar em -14. O
loudnorm do render_cut (duas passadas, `linear=true`) só fica linear quando o pico real medido mais o ganho que ele
aplica não passa de -1 dBTP; com a voz baixa e os picos soltos, ele cai sozinho no modo DINÂMICO (um compressor que
bombeia a voz) sem avisar. Antes isso era um `gain_db` posto à mão em cada trecho do EDL.

O que faz:
1. mede cada trecho do EDL (ffmpeg ebur128 com passa-alta de 80 Hz: o ronco da sala não conta como voz), com o
   `gain_db` que o trecho já tem (o relativo que o voice_levels/montar_corte pôs continua valendo);
2. mede o corte inteiro do jeito que o render_cut vai juntar (os trechos em sequência, com o ganho atual) e acha a
   referência integrada;
3. soma a todos os trechos o mesmo ganho base = ALVO_PRE - referência. Assim a voz chega uns 1,5 dB ACIMA de -14 e
   o loudnorm só abaixa (abaixar é sempre linear). O limitador por trecho do render_cut (alimiter em -1 dBFS, ataque
   5 ms, liga quando gain_db > 0) segura os picos que o ganho empurra;
4. trecho que ficou 3 dB ou mais fora da referência ganha a diferença (até 9 dB), para a voz não pular de volume
   entre tomadas; trechos que continuam a mesma fala (mesma fonte, encostados) ficam com o MESMO ganho (senão o
   render_cut põe um fade de 30 ms no meio da palavra);
5. grava o `gain_db` em cada trecho (com `ganho_auto` e `nivel_lufs`, só para ler) e um relatório em
   <edit>/verify/ganho_por_trecho.json.

Uso:
    python helpers/ganho_por_trecho.py <edit>/edl_manual.json [--alvo -12.5] [--dry-run] [--json]

Rodar de novo é seguro: o ganho é recalculado a partir do nível medido com o ganho que já está lá.
Saída 0 quando gravou (ou nada a mudar), 2 em erro de entrada.
"""
from __future__ import annotations

import argparse
import json
import re
import subprocess
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
from common import probe_video  # noqa: E402
import render_cut as rc  # noqa: E402

ALVO_PRE = -12.5      # LUFS integrado do corte antes do loudnorm (o loudnorm de -14 só abaixa)
DESVIO_MIN = 3.0      # dB: trecho mais longe que isso da referência é corrigido sozinho
DESVIO_MAX = 9.0      # dB: teto da correção relativa
GANHO_MAX = 32.0
GANHO_MIN = -12.0
CURTO_S = 1.0         # trecho mais curto que isso não tem leitura integrada confiável: fica com o ganho base
GRUDADO_S = 0.02      # dois trechos da mesma fonte a menos disso são a mesma fala


def erro(msg: str, code: int = 2):
    print(f"erro: {msg}", file=sys.stderr)
    sys.exit(code)


def resolver_fonte(edl_path: Path, edl: dict, chave: str) -> Path:
    bruto = (edl.get("sources") or {}).get(chave)
    if not bruto:
        erro(f"a fonte '{chave}' não está em sources")
    p = Path(bruto)
    for c in (p, edl_path.parent / p, edl_path.parent.parent / p):
        if c.exists():
            return c.resolve()
    erro(f"não achei o arquivo da fonte '{chave}': {bruto}")


def ebur128(args: list[str]) -> tuple[float | None, float | None]:
    r = subprocess.run(["ffmpeg", "-hide_banner", "-nostats", "-nostdin", *args, "-f", "null", "-"],
                       capture_output=True, text=True)
    txt = r.stderr
    resumo = txt[txt.rfind("Summary:"):] if "Summary:" in txt else txt
    mi = re.search(r"I:\s+(-?[\d.]+|-inf)\s+LUFS", resumo)
    mp = re.search(r"Peak:\s+(-?[\d.]+|-inf)\s+dBFS", resumo)
    def num(m):
        if not m or "inf" in m.group(1):
            return None
        v = float(m.group(1))
        return None if v <= -69.9 else v
    return num(mi), num(mp)




_CANAIS: dict[str, str] = {}


def filtro(src: Path) -> str:
    """Os canais viram estéreo do mesmo jeito que o render_cut faz (mono vai cheio nos dois lados: +3 LU sobre o
    mono), e o passa-alta de 80 Hz tira o ronco da sala da leitura."""
    k = str(src)
    if k not in _CANAIS:
        _CANAIS[k] = rc.channel_filter(rc.channel_mode(src, probe_video(src)))
    return f"aresample=48000,{_CANAIS[k]},highpass=f=80"


def medir_trecho(src: Path, ini: float, fim: float, ganho: float) -> tuple[float | None, float | None]:
    af = f"{filtro(src)},volume={ganho:.2f}dB,ebur128=peak=true"
    return ebur128(["-ss", f"{ini:.3f}", "-t", f"{max(0.05, fim - ini):.3f}", "-i", str(src), "-vn", "-af", af])


def medir_corte(trechos: list[dict]) -> tuple[float | None, float | None]:
    """O corte como o render_cut junta: cada trecho com o seu ganho, em sequência."""
    entradas, cadeias, rotulos = [], [], []
    fontes = {}
    for t in trechos:
        if str(t["src"]) not in fontes:
            fontes[str(t["src"])] = len(fontes)
            entradas += ["-i", str(t["src"])]
    for i, t in enumerate(trechos):
        k = fontes[str(t["src"])]
        cadeias.append(f"[{k}:a]atrim=start={t['start']:.3f}:end={t['end']:.3f},asetpts=PTS-STARTPTS,"
                       f"{filtro(t['src'])},volume={t['ganho']:.2f}dB[a{i}]")
        rotulos.append(f"[a{i}]")
    grafo = ";".join(cadeias) + ";" + "".join(rotulos) + f"concat=n={len(trechos)}:v=0:a=1,ebur128=peak=true[o]"
    return ebur128([*entradas, "-filter_complex", grafo, "-map", "[o]"])


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
    ap.add_argument("edl", type=Path)
    ap.add_argument("--alvo", type=float, default=ALVO_PRE, help="LUFS do corte antes do loudnorm (padrão -12.5)")
    ap.add_argument("--dry-run", action="store_true")
    ap.add_argument("--json", action="store_true")
    a = ap.parse_args()
    if not a.edl.exists():
        erro(f"não achei {a.edl}")
    edl = json.loads(a.edl.read_text(encoding="utf-8"))
    ranges = edl.get("ranges") or []
    if not ranges:
        erro("o EDL não tem trechos")

    trechos = []
    for r in ranges:
        src = resolver_fonte(a.edl, edl, r["source"])
        ganho = float(r.get("gain_db") or 0.0)
        if r.get("ganho_auto") is not None:     # rodada anterior: volta ao ganho relativo que havia antes
            ganho -= float(r["ganho_auto"])
        nivel, pico = medir_trecho(src, float(r["start"]), float(r["end"]), ganho)
        trechos.append(dict(id=r["id"], src=src, start=float(r["start"]), end=float(r["end"]), ganho=ganho,
                            nivel=nivel, pico=pico, dur=float(r["end"]) - float(r["start"])))

    ref, pico_ref = medir_corte(trechos)
    if ref is None:
        validos = sorted((t["nivel"], t["dur"]) for t in trechos if t["nivel"] is not None)
        if not validos:
            erro("não consegui medir a voz de nenhum trecho (áudio mudo?)")
        meio, acc = sum(d for _, d in validos) / 2, 0.0
        for n, d in validos:
            acc += d
            if acc >= meio:
                ref = n
                break
    base = a.alvo - ref

    for t in trechos:
        rel = 0.0
        if t["nivel"] is not None and t["dur"] >= CURTO_S:
            d = ref - t["nivel"]
            if abs(d) >= DESVIO_MIN:
                rel = max(-DESVIO_MAX, min(DESVIO_MAX, d))
        t["rel"] = rel
        t["novo"] = max(GANHO_MIN, min(GANHO_MAX, t["ganho"] + base + rel))

    # a mesma fala (mesma fonte, encostada) leva o mesmo ganho: a média ponderada pela duração
    grupos, atual = [], [trechos[0]]
    for t in trechos[1:]:
        ant = atual[-1]
        if t["src"] == ant["src"] and abs(t["start"] - ant["end"]) <= GRUDADO_S:
            atual.append(t)
        else:
            grupos.append(atual)
            atual = [t]
    grupos.append(atual)
    for g in grupos:
        if len(g) > 1:
            m = sum(t["novo"] * t["dur"] for t in g) / max(1e-6, sum(t["dur"] for t in g))
            for t in g:
                t["novo"] = m

    por_id = {t["id"]: t for t in trechos}
    for r in ranges:
        t = por_id[r["id"]]
        r["gain_db"] = round(t["novo"], 1)
        r["ganho_auto"] = round(t["novo"] - t["ganho"], 1)
        r["nivel_lufs"] = None if t["nivel"] is None else round(t["nivel"], 1)

    rel = {"alvo_pre_lufs": a.alvo, "referencia_lufs": round(ref, 2),
           "pico_ref_dbfs": None if pico_ref is None else round(pico_ref, 2),
           "ganho_base_db": round(base, 2),
           "trechos": [{"id": t["id"], "nivel_lufs": None if t["nivel"] is None else round(t["nivel"], 1),
                        "pico_dbfs": None if t["pico"] is None else round(t["pico"], 1),
                        "ganho_antes": round(t["ganho"], 1), "correcao_relativa": round(t["rel"], 1),
                        "gain_db": round(t["novo"], 1)} for t in trechos]}
    if not a.dry_run:
        a.edl.write_text(json.dumps(edl, ensure_ascii=False, indent=1), encoding="utf-8")
        ver = a.edl.parent / "verify"
        ver.mkdir(exist_ok=True)
        (ver / "ganho_por_trecho.json").write_text(json.dumps(rel, ensure_ascii=False, indent=1), encoding="utf-8")
    if a.json:
        print(json.dumps(rel, ensure_ascii=False))
    else:
        print(f"voz do corte em {ref:.1f} LUFS; ganho base {base:+.1f} dB para chegar em {a.alvo:.1f} antes do loudnorm")
        for t in trechos:
            extra = f" (fora da referência: {t['rel']:+.1f} dB)" if t["rel"] else ""
            nv = "curto" if t["nivel"] is None else f"{t['nivel']:.1f} LUFS"
            print(f"  {t['id']}: {nv} -> gain_db {t['novo']:+.1f}{extra}")
        if not a.dry_run:
            print(f"gravado: {a.edl}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
