#!/usr/bin/env python3
"""REFINE ORANGE — refinagem automática de clips laranja já “quase” no lugar.

Depois de placements.py (CT/continuidade), cada clip orange está tipicamente a poucos
segundos do offset certo. Re-sincroniza com xcorr local vs espinha numa JANELA ESTREITA
(±1.5s pass1), com áudio NIVELADO por RMS — volume de mic vs master deixa de atrapalhar
o pico da correlação.

Pass1 (estrito): janela ±1.5s, min_ratio 2.3 — pega os “quase certos”.
Pass2 (clipes curtos / CT um pouco mais longe): janela ±3s, min_ratio 1.9, usa
duração real do clip. Ainda exige VERIFY residual ~0 — sem isso não vira verde.

Promote NUNCA é só por ratio: 2ª medida no ponto refinado com residual ≈ 0.
Se fraco: mantém orange e posição CT (não piora).

READ-ONLY nos originais: wavs só em scratch.

Uso:
  refine_orange.py <placements.json> <decupagem.json>
                   [--window-s 1.5] [--workdir <cwd do pipeline>]
                   [--min-ratio 2.3] [--pass2-window-s 3.0] [--pass2-min-ratio 1.9]
"""
from __future__ import annotations

import argparse
import json
import os
import subprocess
import sys
import tempfile

import numpy as np
from scipy.io import wavfile
from scipy.signal import correlate, stft

FPS = 30
FFMPEG = "/opt/homebrew/bin/ffmpeg"
FFPROBE = "/opt/homebrew/bin/ffprobe"
# Pass1: janela estreita em volta da posição CT (±). Laranja já está perto — 1.5s ~45f.
DEFAULT_WINDOW_S = 1.5
# Clip longo: só o começo pro xcorr (basta pra firmar o in-point).
CLIP_USE_S = 25.0
# Extract a 8 kHz; downsample 4× pra xcorr PCM (2 kHz) — barato e sample-accurate.
AR = 8000
XCORR_DOWNSAMPLE = 4
FLUX_FPS = 100
# ratio = |peak| / p95(|c|). PCM em janela curta costuma dar 2.5–7 quando casa.
DEFAULT_MIN_RATIO = 2.3
# Promote NÃO usa só ratio alto: exige VERIFY (2ª medida no ponto refinado com residual ~0).
DEFAULT_VERIFY_WINDOW_S = 0.5
DEFAULT_VERIFY_MAX_RESIDUAL_S = 0.08  # ~2.4 frames @30fps
# Ratio na janela de verify costuma cair (menos contexto); o que manda é residual ~0
# no ponto refinado (= re-lock). Ratio só precisa ficar acima de ruído puro.
DEFAULT_VERIFY_MIN_RATIO = 1.55
# Pass2: clipes que falharam no pass1 (curtos ou CT ~1.5–3s fora). Ainda local + verify.
DEFAULT_PASS2_WINDOW_S = 3.0
DEFAULT_PASS2_MIN_RATIO = 1.9
DEFAULT_PASS2_VERIFY_MIN_RATIO = 1.45
DEFAULT_PASS2_VERIFY_WINDOW_S = 0.6


def sh(cmd):
    return subprocess.run(cmd, capture_output=True, text=True)


def ffdur(path):
    r = sh([FFPROBE, "-v", "error", "-show_entries", "format=duration",
            "-of", "csv=p=0", path])
    try:
        return float(r.stdout.strip())
    except (ValueError, AttributeError):
        return 0.0


def extract_wav_segment(src, dst, start_s=0.0, dur_s=None, ar=AR):
    """Extrai mono PCM; start/dur em segundos. READ-ONLY no original."""
    os.makedirs(os.path.dirname(dst) or ".", exist_ok=True)
    cmd = [FFMPEG, "-y", "-ss", f"{max(0.0, start_s):.3f}", "-i", src]
    if dur_s is not None and dur_s > 0:
        cmd += ["-t", f"{dur_s:.3f}"]
    cmd += ["-vn", "-ac", "1", "-ar", str(ar), "-c:a", "pcm_s16le", dst]
    r = sh(cmd)
    if r.returncode != 0 or not os.path.exists(dst) or ffdur(dst) < 0.2:
        return None
    return dst


def read_mono(path):
    sr, x = wavfile.read(path)
    if x.ndim > 1:
        x = x.mean(axis=1)
    x = x.astype(np.float64)
    return sr, x


def rms_normalize(x, target=0.1):
    """Nivela energia RMS — mic alto vs master baixo deixa de dominar o xcorr."""
    x = np.asarray(x, dtype=np.float64)
    r = np.sqrt(np.mean(x * x) + 1e-20)
    if r < 1e-12:
        return x
    return x * (target / r)


def spectral_flux(x, sr, fps=FLUX_FPS):
    """Envelope de ataque espectral (mesmo espírito de sync_spec) — robusto a nível/timbre."""
    nseg = 1024
    hop = max(1, int(sr // fps))
    if len(x) < nseg + hop:
        # fallback: energia em janelas curtas
        w = max(1, hop)
        n = len(x) // w
        if n < 4:
            return np.abs(x[:: max(1, len(x) // 50)]), float(sr)
        e = np.array([np.sqrt(np.mean(x[i * w:(i + 1) * w] ** 2)) for i in range(n)])
        return e, float(sr) / w
    _, _, Z = stft(x, nperseg=nseg, noverlap=nseg - hop, boundary=None)
    mag = np.abs(Z)
    d = np.diff(mag, axis=1)
    fl = np.maximum(d, 0.0).sum(axis=0)
    return fl, float(sr) / hop


def zscore(v):
    v = np.asarray(v, dtype=np.float64)
    v = v - v.mean()
    s = v.std()
    return v / (s if s > 1e-12 else 1.0)


def local_xcorr_lag(spine_sig, clip_sig, sample_rate, left_pad_s):
    """
    Xcorr do clip (curto) dentro do trecho de espinha. Retorna (lag_s, ratio).

    Convenção: spine_seg = [est−W, est+use+W]; left_pad_s = est − spine_start (≈ W).
    lag_s = onde o clip casa no spine_seg. Ideal: lag_s ≈ left_pad_s.
    delta_s = lag_s − left_pad_s → soma em segundos no startFrame da TL.
    """
    A = zscore(rms_normalize(spine_sig))
    B = zscore(rms_normalize(clip_sig))
    if len(B) < 32 or len(A) < len(B) + 8:
        return None, 0.0
    c = correlate(A, B, mode="valid", method="fft") / max(len(B), 1)
    if c.size == 0:
        return None, 0.0
    # Pico absoluto (fase pode inverter mic vs master)
    i = int(np.argmax(np.abs(c)))
    peak = float(c[i])
    p95 = float(np.percentile(np.abs(c), 95)) or 1e-9
    med = float(np.median(np.abs(c))) or 1e-9
    ratio = abs(peak) / max(p95, med * 2, 1e-9)
    # Sub-sample parabolic no |c|
    fi = float(i)
    if 0 < i < len(c) - 1:
        y0, y1, y2 = abs(c[i - 1]), abs(c[i]), abs(c[i + 1])
        denom = (y0 - 2 * y1 + y2) or 1e-9
        fi = i + 0.5 * (y0 - y2) / denom
    lag_s = fi / float(sample_rate)
    return lag_s, ratio


def pcm_pair_for_xcorr(x_clip, x_spine, sr):
    """Downsample + já vem RMS-ready no caller; retorna (spine, clip, rate)."""
    ds = XCORR_DOWNSAMPLE
    if ds > 1 and len(x_clip) > ds * 64:
        x_clip = x_clip[::ds]
        x_spine = x_spine[::ds]
        sr = sr / ds
    return x_spine, x_clip, sr


def resolve_media(decup, cam, basename):
    """basename sem ext (C8196) → path absoluto via find_videos_multi."""
    sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
    import sync_ingest as si
    base = decup["base"]
    ci = decup.get("cameras", {}).get(cam)
    if not ci:
        return None
    by = {os.path.splitext(os.path.basename(v))[0]: v
          for v in si.find_videos_multi(base, ci["pasta"])}
    return by.get(basename)


def spine_source_paths(decup):
    """Lista de arquivos da espinha (chunks) absolutos."""
    base = decup["base"]
    ac = decup.get("audio_cerimonia") or {}
    files = ac.get("arquivos") or []
    out = []
    for f in files:
        p = f if os.path.isabs(f) else os.path.join(base, f)
        if os.path.exists(p):
            out.append(p)
    if out:
        return out
    # fallback: 1º master espinha
    for m in (decup.get("masters_cerimonia") or []):
        if m.get("espinha"):
            for f in (m.get("arquivos") or []):
                p = f if os.path.isabs(f) else os.path.join(base, f)
                if os.path.exists(p):
                    out.append(p)
            break
    return out


def ensure_spine_wav(decup, workdir):
    """WAV mono da espinha: 1) cache refine 2) spine do sync_ingest 3) master_ingest 4) concat."""
    cache = os.path.join(workdir, "_refine_spine.wav")
    if os.path.exists(cache) and ffdur(cache) > 30:
        return cache

    # sync_ingest grava em ~/Library/Caches/VideoTask/pipeline/sync/<slug>_cerimonia_spine.wav
    try:
        import wedslug
        slug = wedslug.slug(decup.get("casamento", "wedding"))
    except Exception:
        slug = "wedding"
    sync_dir = os.path.join(
        os.environ.get("VIDEOTASK_SCRATCH", os.path.expanduser("~/Library/Caches/VideoTask/pipeline")),
        "sync",
    )
    for momento in ("cerimonia", "ceremony"):
        p = os.path.join(sync_dir, f"{slug}_{momento}_spine.wav")
        if os.path.exists(p) and ffdur(p) > 30:
            return p

    # WAV de master_ingest no workdir do pipeline
    if os.path.isdir(workdir):
        cands = []
        for name in os.listdir(workdir):
            if not name.endswith(".wav") or "fp" in name:
                continue
            p = os.path.join(workdir, name)
            d = ffdur(p)
            if 60 < d < 200 * 60:
                cands.append((d, p))
        if cands:
            cands.sort(key=lambda x: x[0])  # preferir espinha de cerimônia (~1–2h) à festa
            return cands[0][1]

    srcs = spine_source_paths(decup)
    if not srcs:
        return None
    if len(srcs) == 1:
        return extract_wav_segment(srcs[0], cache, 0, None)
    lst = cache + ".txt"
    with open(lst, "w") as f:
        for s in srcs:
            f.write(f"file '{s}'\n")
    sh([FFMPEG, "-y", "-f", "concat", "-safe", "0", "-i", lst,
        "-ac", "1", "-ar", str(AR), "-c:a", "pcm_s16le", cache])
    return cache if os.path.exists(cache) and ffdur(cache) > 30 else None


def basename_from_placement_name(name, cam):
    """cam1_marcelo_C8196 → C8196"""
    prefix = cam + "_"
    if name.startswith(prefix):
        return name[len(prefix):]
    if "_" in name:
        return name.rsplit("_", 1)[-1]
    return name


def measure_offset(src, spine_wav, est_s, window_s, use_s, scratch, tag):
    """Mede delta_s (ajuste sugerido) e ratio no est_s atual. None se falhar extract/xcorr."""
    spine_start = max(0.0, est_s - window_s)
    spine_dur = use_s + 2 * window_s
    left_pad = est_s - spine_start
    clip_wav = os.path.join(scratch, f"{tag}_clip.wav")
    spine_seg = os.path.join(scratch, f"{tag}_sp.wav")
    if not extract_wav_segment(src, clip_wav, 0.0, use_s):
        return None
    if not extract_wav_segment(spine_wav, spine_seg, spine_start, spine_dur):
        return None
    try:
        sr_c, x_c = read_mono(clip_wav)
        _, x_s = read_mono(spine_seg)
    except Exception:
        return None
    x_c_n = rms_normalize(x_c)
    x_s_n = rms_normalize(x_s)
    sp_ds, cl_ds, rate = pcm_pair_for_xcorr(x_c_n, x_s_n, sr_c)
    lag_s, ratio = local_xcorr_lag(sp_ds, cl_ds, rate, left_pad)
    method = "pcm"
    if lag_s is None or ratio < 1.5:
        # Ambos os WAVs saem a AR; sr_c cobre spine e clip.
        fl_c, rate_c = spectral_flux(x_c_n, sr_c)
        fl_s, rate_s = spectral_flux(x_s_n, sr_c)
        lag2, ratio2 = local_xcorr_lag(fl_s, fl_c, min(rate_c, rate_s), left_pad)
        if lag2 is not None and ratio2 > (ratio or 0):
            lag_s, ratio, method = lag2, ratio2, "flux"
    if lag_s is None:
        return None
    delta_s = lag_s - left_pad
    return {
        "delta_s": delta_s,
        "ratio": ratio,
        "method": method,
        "left_pad": left_pad,
        "window_s": window_s,
    }


def clip_use_s(dur_s, full_clip=False):
    """Quanto de áudio do clip usar no xcorr.

    Pass1: mínimo 3s (estável em clipes médios).
    Pass2 (full_clip): usa a duração real — clipes de 1–2s não inventam 3s de silêncio.
    """
    if full_clip:
        return max(0.6, min(float(dur_s), CLIP_USE_S))
    return min(CLIP_USE_S, max(3.0, float(dur_s)))


def try_refine_one(c, src, spine_wav, master_sf, scratch, tag,
                   window_s, min_ratio, use_s,
                   verify_window_s, verify_max_residual_s, verify_min_ratio,
                   pass_name="p1"):
    """
    Tenta refinar um clip orange. Retorna dict:
      status: "promoted" | "refined_orange" | "skip"
      + logs em .msg
    Mutates c se refined/promoted.
    """
    name = c.get("name") or tag
    sf = int(c.get("startFrame") or 0)
    est_s = (sf - master_sf) / float(FPS)

    m1 = measure_offset(src, spine_wav, est_s, window_s, use_s, scratch, tag + "_m1")
    if not m1 or m1["ratio"] < min_ratio:
        r = m1["ratio"] if m1 else 0.0
        return {
            "status": "skip",
            "msg": f"  · {name} [{pass_name}]: keep orange (ratio={r:.2f} < {min_ratio})",
        }
    if abs(m1["delta_s"]) > window_s + 0.08:
        return {
            "status": "skip",
            "msg": (f"  · {name} [{pass_name}]: delta {m1['delta_s']:+.3f}s fora da janela "
                    f"— keep (ratio={m1['ratio']:.2f})"),
        }

    delta_f = int(round(m1["delta_s"] * FPS))
    new_sf = max(0, sf + delta_f)
    old_sf = sf
    new_est = (new_sf - master_sf) / float(FPS)

    # VERIFY no ponto refinado. Residual ~0 + ratio ok = confirmação (sem falso verde).
    m2 = measure_offset(
        src, spine_wav, new_est, verify_window_s, use_s, scratch, tag + "_m2"
    )
    verified = False
    verify_info = None
    if m2:
        verify_info = {
            "residual_s": round(m2["delta_s"], 4),
            "residual_frames": int(round(m2["delta_s"] * FPS)),
            "ratio": round(m2["ratio"], 3),
            "method": m2["method"],
        }
        if (abs(m2["delta_s"]) <= verify_max_residual_s
                and m2["ratio"] >= verify_min_ratio):
            verified = True
        elif abs(m2["delta_s"]) <= verify_max_residual_s * 2 and m2["ratio"] >= min_ratio:
            d2f = int(round(m2["delta_s"] * FPS))
            if d2f != 0:
                new_sf = max(0, new_sf + d2f)
                new_est = (new_sf - master_sf) / float(FPS)
                m3 = measure_offset(
                    src, spine_wav, new_est, verify_window_s, use_s, scratch, tag + "_m3"
                )
                if (m3 and abs(m3["delta_s"]) <= verify_max_residual_s
                        and m3["ratio"] >= verify_min_ratio):
                    verified = True
                    verify_info = {
                        "residual_s": round(m3["delta_s"], 4),
                        "residual_frames": int(round(m3["delta_s"] * FPS)),
                        "ratio": round(m3["ratio"], 3),
                        "method": m3["method"],
                        "second_nudge_frames": d2f,
                    }

    c["startFrame"] = new_sf
    c["refine"] = {
        "delta_frames": new_sf - old_sf,
        "delta_s": round((new_sf - old_sf) / float(FPS), 4),
        "ratio": round(m1["ratio"], 3),
        "window_s": window_s,
        "method": m1["method"],
        "leveled": True,
        "pass": pass_name,
        "verified": verified,
        "verify": verify_info,
    }

    if verified:
        c["orange"] = False
        c["confidence"] = 0.88 if pass_name == "p1" else 0.86
        res = verify_info["residual_frames"] if verify_info else 0
        vr = verify_info["ratio"] if verify_info else 0
        return {
            "status": "promoted",
            "msg": (f"  ✓ {name} [{pass_name}]: {old_sf}→{new_sf} (Δ{new_sf-old_sf:+d}f) "
                    f"ratio={m1['ratio']:.2f} verify_res={res}f vr={vr:.2f} PROMOTE"),
        }

    c["confidence"] = max(
        float(c.get("confidence") or 0),
        min(0.68, 0.4 + m1["ratio"] / 10),
    )
    vr = verify_info["ratio"] if verify_info else 0
    rsf = verify_info["residual_frames"] if verify_info else "?"
    return {
        "status": "refined_orange",
        "msg": (f"  ~ {name} [{pass_name}]: {old_sf}→{new_sf} (Δ{new_sf-old_sf:+d}f) "
                f"ratio={m1['ratio']:.2f} verify_res={rsf}f vr={vr:.2f} "
                f"(refinado, ainda orange — verify falhou)"),
    }


def refine_placements(placements_path, decup_path, window_s=DEFAULT_WINDOW_S,
                      min_ratio=DEFAULT_MIN_RATIO,
                      verify_window_s=DEFAULT_VERIFY_WINDOW_S,
                      verify_max_residual_s=DEFAULT_VERIFY_MAX_RESIDUAL_S,
                      verify_min_ratio=DEFAULT_VERIFY_MIN_RATIO,
                      pass2_window_s=DEFAULT_PASS2_WINDOW_S,
                      pass2_min_ratio=DEFAULT_PASS2_MIN_RATIO,
                      pass2_verify_min_ratio=DEFAULT_PASS2_VERIFY_MIN_RATIO,
                      pass2_verify_window_s=DEFAULT_PASS2_VERIFY_WINDOW_S,
                      workdir=None):
    pl = json.load(open(placements_path))
    decup = json.load(open(decup_path))
    workdir = workdir or os.path.dirname(os.path.abspath(placements_path))
    # sobe 2 níveis se estamos em weddings/slug/
    if os.path.basename(os.path.dirname(workdir)) == "weddings":
        workdir = os.path.dirname(os.path.dirname(workdir))
    pipeline_cwd = workdir
    if not os.path.isdir(pipeline_cwd):
        pipeline_cwd = os.getcwd()

    spine_wav = ensure_spine_wav(decup, pipeline_cwd)
    if not spine_wav:
        print("! refine_orange: sem WAV de espinha — nada a refinar")
        return pl

    master_sf = int(pl.get("masterStartFrame") or 0)
    clips = pl.get("clips") or []
    oranges = [c for c in clips if c.get("orange")]
    print(f"refine_orange: {len(oranges)} orange / {len(clips)} clips | "
          f"p1 ±{window_s}s r≥{min_ratio} | "
          f"p2 ±{pass2_window_s}s r≥{pass2_min_ratio} | "
          f"verify residual≤{verify_max_residual_s}s | "
          f"spine={os.path.basename(spine_wav)} ({ffdur(spine_wav)/60:.0f}min)")

    scratch = tempfile.mkdtemp(prefix="vt-refine-")
    n_ok = n_prom = n_skip = n_verify_fail = 0
    n_p2_prom = n_p2_try = 0

    # ── Pass1: estrito ──────────────────────────────────────────────
    for c in clips:
        if not c.get("orange"):
            continue
        cam = c.get("cam") or ""
        name = c.get("name") or ""
        bn = basename_from_placement_name(name, cam)
        src = resolve_media(decup, cam, bn)
        if not src or not os.path.exists(src):
            print(f"  ! {name}: mídia não achada")
            n_skip += 1
            continue

        dur_s = int(c.get("durationFrames") or 0) / float(FPS)
        use_s = clip_use_s(dur_s, full_clip=False)
        tag = f"{cam}_{bn}".replace("/", "_")
        res = try_refine_one(
            c, src, spine_wav, master_sf, scratch, tag,
            window_s, min_ratio, use_s,
            verify_window_s, verify_max_residual_s, verify_min_ratio,
            pass_name="p1",
        )
        print(res["msg"])
        if res["status"] == "promoted":
            n_ok += 1
            n_prom += 1
        elif res["status"] == "refined_orange":
            n_ok += 1
            n_verify_fail += 1
        else:
            n_skip += 1

    # ── Pass2: restantes (curtos / CT um pouco mais longe) ───────────
    # Ainda exige residual ~0. Janela ±3s + min_ratio um pouco mais baixo.
    # Só roda em quem AINDA é orange (não rebaixa verde).
    still_orange = [c for c in clips if c.get("orange")]
    if still_orange and pass2_window_s > 0 and pass2_min_ratio > 0:
        print(f"refine_orange pass2: {len(still_orange)} still orange | "
              f"window=±{pass2_window_s}s min_ratio={pass2_min_ratio} "
              f"verify_r≥{pass2_verify_min_ratio}")
        for c in still_orange:
            cam = c.get("cam") or ""
            name = c.get("name") or ""
            bn = basename_from_placement_name(name, cam)
            src = resolve_media(decup, cam, bn)
            if not src or not os.path.exists(src):
                continue
            n_p2_try += 1
            dur_s = int(c.get("durationFrames") or 0) / float(FPS)
            use_s = clip_use_s(dur_s, full_clip=True)
            tag = f"p2_{cam}_{bn}".replace("/", "_")
            res = try_refine_one(
                c, src, spine_wav, master_sf, scratch, tag,
                pass2_window_s, pass2_min_ratio, use_s,
                pass2_verify_window_s, verify_max_residual_s, pass2_verify_min_ratio,
                pass_name="p2",
            )
            print(res["msg"])
            if res["status"] == "promoted":
                n_ok += 1
                n_prom += 1
                n_p2_prom += 1
                n_skip = max(0, n_skip - 1)  # era skip no p1
            elif res["status"] == "refined_orange":
                n_ok += 1
                n_verify_fail += 1
                n_skip = max(0, n_skip - 1)

    # Re-enforce order per cam after shifts (não invadir vizinho confirmed/não-orange)
    by_cam = {}
    for c in clips:
        by_cam.setdefault(c["cam"], []).append(c)
    for cam, cs in by_cam.items():
        def num_key(name):
            import re
            m = re.search(r"(\d+)", name or "")
            return int(m.group(1)) if m else 0
        cs.sort(key=lambda x: num_key(x.get("name")))
        prev_end = None
        for c in cs:
            sf = int(c["startFrame"])
            dur = int(c.get("durationFrames") or 0)
            if prev_end is not None and c.get("orange") and sf < prev_end:
                c["startFrame"] = prev_end
                sf = prev_end
            prev_end = sf + dur

    pl["clips"] = clips
    remaining_orange = sum(1 for c in clips if c.get("orange"))
    pl["refine_orange"] = {
        "refined": n_ok, "promoted": n_prom, "skipped": n_skip,
        "verify_failed": n_verify_fail,
        "pass2_tried": n_p2_try, "pass2_promoted": n_p2_prom,
        "remaining_orange": remaining_orange,
        "window_s": window_s, "min_ratio": min_ratio,
        "pass2_window_s": pass2_window_s, "pass2_min_ratio": pass2_min_ratio,
        "verify_window_s": verify_window_s,
        "verify_max_residual_s": verify_max_residual_s,
        "verify_min_ratio": verify_min_ratio,
        "pass2_verify_min_ratio": pass2_verify_min_ratio,
    }
    tmp = placements_path + ".refine.tmp"
    with open(tmp, "w") as f:
        json.dump(pl, f, indent=0)
        f.write("\n")
    os.replace(tmp, placements_path)
    print(f"→ {placements_path}  refined={n_ok} promoted={n_prom} "
          f"(p2={n_p2_prom}) verify_fail={n_verify_fail} skipped={n_skip} "
          f"orange_left={remaining_orange}")
    return pl


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("placements")
    ap.add_argument("decupagem")
    ap.add_argument("--window-s", type=float, default=DEFAULT_WINDOW_S)
    ap.add_argument("--workdir", default=None)
    ap.add_argument("--min-ratio", type=float, default=DEFAULT_MIN_RATIO)
    ap.add_argument("--verify-window-s", type=float, default=DEFAULT_VERIFY_WINDOW_S)
    ap.add_argument("--verify-max-residual-s", type=float, default=DEFAULT_VERIFY_MAX_RESIDUAL_S)
    ap.add_argument("--verify-min-ratio", type=float, default=DEFAULT_VERIFY_MIN_RATIO)
    ap.add_argument("--pass2-window-s", type=float, default=DEFAULT_PASS2_WINDOW_S)
    ap.add_argument("--pass2-min-ratio", type=float, default=DEFAULT_PASS2_MIN_RATIO)
    ap.add_argument("--pass2-verify-min-ratio", type=float, default=DEFAULT_PASS2_VERIFY_MIN_RATIO)
    ap.add_argument("--pass2-verify-window-s", type=float, default=DEFAULT_PASS2_VERIFY_WINDOW_S)
    ap.add_argument("--no-pass2", action="store_true",
                    help="Desliga pass2 (só refinagem estrita pass1)")
    args = ap.parse_args()
    p2w = 0.0 if args.no_pass2 else args.pass2_window_s
    p2r = 0.0 if args.no_pass2 else args.pass2_min_ratio
    refine_placements(
        args.placements, args.decupagem,
        window_s=args.window_s, min_ratio=args.min_ratio,
        verify_window_s=args.verify_window_s,
        verify_max_residual_s=args.verify_max_residual_s,
        verify_min_ratio=args.verify_min_ratio,
        pass2_window_s=p2w, pass2_min_ratio=p2r,
        pass2_verify_min_ratio=args.pass2_verify_min_ratio,
        pass2_verify_window_s=args.pass2_verify_window_s,
        workdir=args.workdir,
    )


if __name__ == "__main__":
    main()
