#!/usr/bin/env python3
"""EVAL de sync do-zero vs GROUND-TRUTH (Paula synctable2.tsv). Casa clips por NÚMERO (chave única:
cams têm ranges disjuntos), compara offset. Reporta % de acerto <tol entre os clips CONFIÁVEIS (fp
rank alto) — é aí que o modelo tem que estar certo; os fracos viram laranja de propósito.
Uso: validate_sync.py placements_paula_cerimonia.json synctable2.tsv [--tol 2]"""
import sys, os, json, re, argparse


def num(s):
    m = re.search(r'(\d{2,5})', s)
    return int(m.group(1)) if m else None


def internal(placements, conf_min=40, gate=False, gate_min=95.0, save=None):
    """MÉTRICA REAL (sem GT externo): consistência interna do sync — offsets fortes MONOTÔNICOS por
    câmera + incrementos ~duração. Sync correto = clips sequenciais em posições crescentes na espinha.
    (synctable2 não serve de GT: método/convenção diferentes — ver licao-synctable2-gt-nao-confiavel.)
    --gate: sai com código 1 se a consistência ficar abaixo de gate_min% — usado no run_dozero pra
    NUNCA buildar timeline com sync fisicamente impossível (verde errado é o pior erro; ver
    SYNC_CONSENSUS_PLAN.md etapa 0). --save: grava histórico de métricas (eval permanente, etapa 5) —
    tendência entre runs, não só o snapshot deste."""
    from collections import defaultdict
    P = json.load(open(placements))["clips"]
    byc = defaultdict(list)
    items = P.items() if isinstance(P, dict) else [(c.get("name",""), c) for c in P]
    n_orange = sum(1 for _, c in items if "orange" in c and c.get("orange"))
    per_cam = {}
    for fn, c in items:
        m = re.search(r'(\d{2,5})', fn)
        if not m:
            continue
        # sync json: via=fp+rank. placements final (lista): não-orange = confirmado; pos=startFrame.
        strong = (c.get("via")=="fp" and (c.get("rank") or 0)>=conf_min) if "via" in c else (not c.get("orange"))
        if strong:
            byc[c["cam"]].append((int(m.group(1)), c.get("offset_s", c.get("startFrame"))))
    tot_mono = tot_pairs = 0
    for cam in sorted(byc):
        cl = sorted(byc[cam])
        mono = sum(1 for i in range(1,len(cl)) if cl[i][1]>=cl[i-1][1])
        tot_mono += mono; tot_pairs += max(0,len(cl)-1)
        per_cam[cam] = {"fortes": len(cl), "mono": mono, "pares": max(0,len(cl)-1)}
        print(f"  {cam}: {len(cl)} fortes | monotônicos {mono}/{max(1,len(cl)-1)}")
    pct = 100*tot_mono/max(tot_pairs,1)
    print(f"\nCONSISTÊNCIA INTERNA: {tot_mono}/{tot_pairs} pares monotônicos "
          f"({pct:.0f}%) — sync correto ~= 100%")
    if save:
        import time
        hist = json.load(open(save)) if os.path.exists(save) else []
        hist.append({"ts": time.strftime("%Y-%m-%dT%H:%M:%S"), "pct": round(pct, 1),
                     "tot_pairs": tot_pairs, "n_orange": n_orange, "per_cam": per_cam})
        json.dump(hist, open(save, "w"), indent=1, ensure_ascii=False)
        print(f"  [eval] histórico salvo em {save} ({len(hist)} runs)")
        if len(hist) >= 2 and hist[-2]["pct"] > pct:
            print(f"  ⚠️ REGRESSÃO: consistência caiu {hist[-2]['pct']:.0f}% -> {pct:.0f}% vs run anterior")
    if gate and pct < gate_min:
        print(f"\n✗ GATE: consistência {pct:.0f}% < {gate_min:.0f}% — sync tem posição fisicamente "
              f"impossível (não-monotônica) entre clips CONFIÁVEIS. ABORT (evita buildar verde errado).")
        sys.exit(1)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("placements"); ap.add_argument("gt", nargs="?")
    ap.add_argument("--tol", type=float, default=2.0)
    ap.add_argument("--conf-min", type=int, default=40)
    ap.add_argument("--internal", action="store_true", help="valida consistência interna (métrica real)")
    ap.add_argument("--gate", action="store_true", help="sai 1 se consistência < --gate-min (uso no run_dozero)")
    ap.add_argument("--gate-min", type=float, default=95.0)
    ap.add_argument("--save", help="grava histórico de métricas (json) — eval permanente entre runs")
    a = ap.parse_args()
    if a.internal or not a.gt:
        internal(a.placements, a.conf_min, gate=a.gate, gate_min=a.gate_min, save=a.save); return
    P = json.load(open(a.placements))["clips"]
    mine = {}
    for fn, c in P.items():
        n = num(fn)
        if n is not None:
            mine[n] = c
    gt = {}
    for l in open(a.gt):
        p = l.rstrip().split("\t")
        if len(p) < 3: continue
        n = num(p[0])
        try: gt[n] = {"off": float(p[1]), "rank": float(p[2])}
        except ValueError: pass

    common = sorted(set(mine) & set(gt))
    print(f"clips GT={len(gt)} meu={len(mine)} comuns={len(common)}\n")
    # SHIFT: alinha por 1 âncora forte comum (espinhas podem diferir por constante)
    strong = [n for n in common if mine[n].get("via")=="fp" and (mine[n].get("rank") or 0)>=a.conf_min
              and gt[n]["rank"]>=8]
    shift = 0.0
    if strong:
        anchor = max(strong, key=lambda n: mine[n]["rank"])
        shift = gt[anchor]["off"] - (mine[anchor].get("offset_s") or 0)
        print(f"âncora shift = clip {anchor}: GT {gt[anchor]['off']:.1f} - meu {mine[anchor].get('offset_s'):.1f} = {shift:.1f}s")
    ok = bad = weak = 0
    for n in common:
        c = mine[n]; conf = (c.get("rank") or 0); myoff = (c.get("offset_s") or 0) + shift
        d = abs(myoff - gt[n]["off"])
        if c.get("via")=="fp" and conf>=a.conf_min:
            if d <= a.tol: ok += 1
            else: bad += 1; print(f"  ✗ clip {n}: meu {myoff:.1f} vs GT {gt[n]['off']:.1f} (Δ{d:.0f}s) rank{conf}")
        else:
            weak += 1
    tot = ok+bad
    print(f"\nCONFIÁVEIS (fp rank>={a.conf_min}): {tot} | batem <{a.tol}s: {ok} ({100*ok/max(tot,1):.0f}%) | erram: {bad}")
    print(f"fracos (viram laranja): {weak}")


if __name__ == "__main__":
    main()
