#!/usr/bin/env python3
"""Fine-cut critic VLM — só candidatos EMPTY (mff≈0) do UNI.

Nativo sozinho confunde: mãos/alianças, wide/corredor (rosto pequeno) e chão real
todos dão mff=0. Este judge VÊ o frame e decide KEEP|SWAP com assimetria KEEP na dúvida.

Entrada: versions/UNI.json + facecache/placements (via judgelib/vlib)
Saída:   finecut_swaps.json  (só os SWAP) + finecut_decisions.json (audit completo)

Uso (no workdir do cut):
  ./venv/bin/python finecut_judge.py
Depois re-rodar unified.py (item6e aplica finecut_swaps.json).
"""
from __future__ import annotations

import json
import os
import sys

import numpy as np
from vlm import ask_images, par_map
import judgelib as J
import vlib as V
import cache_manifest as cm

OUT_SWAPS = "finecut_swaps.json"
OUT_DECISIONS = "finecut_decisions.json"
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))

EMPTY_MFF = 0.02
EMPTY_FRAC = 0.15
ALT_MFF = 0.08
ALT_P80_MAX = 1.35
MIN_DUR = int(1.5 * 30)
MAX_DUR = int(12 * 30)  # empty auto era trechos curtos; wide longo não é “chão”


def empty_candidates():
    """Candidatos nativos empty com alt utilizável — mesma definição do 6e, sem auto."""
    face = V.load_face()
    jerk = V.load_jerk()
    COV = V.coverage()

    def face_pack(cam, w0, w1):
        w = V.face_window(face, cam, w0, w1)
        if not w or not len(w.get("mff", [])):
            return 0.0, 0.0
        mf = w["mff"]
        med = float(np.median(mf))
        frac = float(np.mean([1.0 if (x and x > 0.05) else 0.0 for x in mf]))
        return med, frac

    def p80(cam, w0, w1):
        ts, js = V.jerk_series(jerk, cam, w0, w1)
        return float(np.percentile(js, 80)) if js is not None and len(js) else 9.0

    cps = sorted(json.load(open("versions/UNI.json"))["cutpoints"], key=lambda c: c["frame"])
    P = json.load(open("placements.json"))["clips"]
    end = max(c["startFrame"] + c["durationFrames"] for c in P)
    out = []
    for i, c in enumerate(cps):
        f0 = c["frame"]
        f1 = cps[i + 1]["frame"] if i + 1 < len(cps) else end
        span = f1 - f0
        if span < MIN_DUR or span > MAX_DUR:
            continue
        a = c["angle"]
        cam = V.ANGLE_CAM[a]
        m, fr = face_pack(cam, f0, f1)
        if not (m < EMPTY_MFF and fr < EMPTY_FRAC):
            continue
        best = None
        for cc in V.CAMS:
            an = V.CAM_ANGLE[cc]
            if an == a or not V.covers(COV, an, f0, f1):
                continue
            m2, fr2 = face_pack(cc, f0, f1)
            jc = p80(cc, f0, f1)
            if m2 < ALT_MFF or fr2 < 0.35 or jc >= ALT_P80_MAX:
                continue
            sc = m2 * 2.0 - jc
            if best is None or sc > best[0]:
                best = (sc, an, m2, jc)
        if best is None:
            continue
        out.append({
            "f0": f0, "f1": f1, "from": a, "to": best[1],
            "mff_from": round(m, 3), "mff_to": round(best[2], 3),
            "p80_to": round(best[3], 2),
        })
    return out


def main():
    if not os.path.exists("versions/UNI.json"):
        print("precisa versions/UNI.json (rode unified antes)", file=sys.stderr)
        sys.exit(1)

    _sig = {
        "content": cm.content_signature(["versions/UNI.json", "placements.json"]),
        "mtime": cm.input_signature([
            os.path.join(_SCRIPT_DIR, "prompts", "judge_finecut_v1.md"),
            os.path.join(_SCRIPT_DIR, "judgelib.py"),
            __file__,
        ]),
    }
    if cm.is_fresh(".", "finecut_judge", __file__, _sig) and os.path.exists(OUT_SWAPS):
        print(f"[cache] finecut_judge: reusando {OUT_SWAPS}")
        return

    cands = empty_candidates()
    print(f"finecut: {len(cands)} candidatos empty nativos")
    if not cands:
        json.dump([], open(OUT_SWAPS, "w"), indent=1)
        json.dump([], open(OUT_DECISIONS, "w"), indent=1)
        cm.mark_done(".", "finecut_judge", __file__, _sig)
        return

    prompt = J.prompt("judge_finecut_v1.md")

    def work(ic):
        i, c = ic
        f0, f1, a_from, a_to = c["f0"], c["f1"], c["from"], c["to"]
        mid = (f0 + f1) // 2
        # 2 frames na janela (início/meio) × ativa|alt — mais evidência sem flood
        imgs = []
        for ff in (f0 + (f1 - f0) // 4, mid):
            fa = J.frame_of(a_from, ff)
            fb = J.frame_of(a_to, ff)
            if fa:
                imgs.append(fa)
            if fb:
                imgs.append(fb)
        if len(imgs) < 2:
            print(f"  [{i+1}/{len(cands)}] {J.tc(f0)} sem frames -> KEEP")
            return {**c, "action": "KEEP", "why": "sem frame", "source": "fallback"}
        txt, _ = ask_images(imgs[:4], prompt)
        v = J.jparse(txt)
        action = str(v.get("action", "KEEP")).upper().strip()
        if action not in ("KEEP", "SWAP"):
            action = "KEEP"
        why = str(v.get("why", ""))[:120]
        what = str(v.get("what_active", ""))[:80]
        print(f"  [{i+1}/{len(cands)}] {J.tc(f0)} ang{a_from}→{a_to} -> {action} | {what} | {why}")
        return {
            **c, "action": action, "why": why, "what_active": what, "source": "vlm",
        }

    decisions = list(par_map(work, list(enumerate(cands))))
    swaps = [
        {
            "f0": d["f0"], "f1": d["f1"], "from": d["from"], "to": d["to"],
            "why": d.get("why", ""), "what_active": d.get("what_active", ""),
        }
        for d in decisions if d and d.get("action") == "SWAP"
    ]
    json.dump(decisions, open(OUT_DECISIONS, "w"), indent=1)
    json.dump(swaps, open(OUT_SWAPS, "w"), indent=1)
    cm.mark_done(".", "finecut_judge", __file__, _sig)
    print(f"finecut: {len(swaps)} SWAP / {len(decisions)} decididos → {OUT_SWAPS}")


if __name__ == "__main__":
    main()
