#!/usr/bin/env python3
"""최종 변환이 무엇을 바꿨는가 — 완주 판 7쌍을 축별로 관찰한다.

무엇이 문제였나 — `still_cine_transform` 산출은 **아무 판정 없이** 최종본이
된다(`still_recipe_service.py:4536-4538`: `applied` 면 그대로 `final_src`).
그 앞 단계들은 판정을 겹겹이 거치는데(judge·critique·fix_rejudge) 마지막
픽셀만 무검증이다. 실제로 앞 단계까지 멀쩡하던 구도가 최종본에서 다른
장소로 바뀐 샷이 있었다.

이 도구는 **이미 있는 파일 두 장**(`<tag>_sel.png` / `<tag>_cine.png`)만
읽는다 — 그림을 새로 만들지 않으므로 생성 비용이 0 이다.

★판정을 「어느 쪽이 좋은가」로 묻지 않는다. 그 물음은 못 믿는다는 실측이
 있다(Qwen 판정 실험: 관찰·셈은 좋고 `matches_brief` 는 못 믿는다). 대신
 **축마다 「같은가 · 무엇이 달라졌는가」만** 묻는다 — 관찰이다.

★좌우를 바꿔 2회 돌린다(위치 편향 방어). 두 순서가 엇갈리는 축은
 `불일치` 로 따로 센다 — 한 번만 돌리면 그 흔들림이 결론으로 굳는다.

★코드에 사물 이름을 두지 않는다. 축은 **데이터 계약**이고 무엇이 보이는지는
 VLM 이 말한다.

usage:
  ab_cine_drift.py                      # 7쌍 × 2순서
  ab_cine_drift.py --shots S2sh1
  ab_cine_drift.py --model gemini-pro   # 판정 모델 교체
  ab_cine_drift.py --json OUT.json
"""
import argparse
import json
import sys
from pathlib import Path

sys.path.insert(0, "/Users/manta/Documents/Projects/TheRoad-I1/scratchpad")
import _opik_env  # noqa: E402,F401  ★cwd 를 backend 로 고정

ROOT = Path("/Users/manta/Documents/Projects/TheRoad-I1")
RECIPE = (ROOT / "projects/da049582-2c6d-492c-979d-f468d61bab6e/images"
          "/fb7a883f-baac-4145-9131-732ce628d474/scene/recipe")

# ★축·스키마·문안은 **프로덕션 모듈이 소유한다** — 재는 자리와 도는 자리가
#  갈리면 무엇을 쟀는지 알 수 없다(2026-08-10 카나리아 실측: 측정 도구에만
#  없던 sanitizer 때문에 첫 실측이 통째로 죽었다). 이 도구는 그 계약을
#  그대로 불러 쓴다.
sys.path.insert(0, "/Users/manta/Documents/Projects/TheRoad-I1/backend")
from app.modules.pipeline.cine_verify import (  # noqa: E402
    AXES as _AXES, build_axis_schema, build_axis_user_head,
    load_prompt as _lp, resolve_verify_pack,
)

AXES = list(_AXES)


def build_schema():
    return build_axis_schema()


def system_text() -> str:
    return _lp("cine_verify", "axis_drift_system",
               version=resolve_verify_pack()).strip()


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("--model", default="gpt",
                    help="판정 모델 (기본 gpt — 분업표 '이미지 검증=GPT LVM')")
    ap.add_argument("--shots", default="")
    ap.add_argument("--json", default="")
    ap.add_argument("--from-dir", default="",
                    help="완주 판 대신 이 디렉토리의 `<tag>_<arm>_r<n>.png` 를 "
                         "각자의 `<tag>_sel.png` 와 잰다 (문안 A/B 판정)")
    args = ap.parse_args()

    from app.modules.llm.llm_client import call_structured
    from app.modules.pipeline.multiroll_gemini import png_part

    want = {s.strip() for s in args.shots.split(",") if s.strip()}
    pairs = []
    if args.from_dir:
        # 문안 A/B 산출 — 파일명 `<tag>_<arm>_r<n>.png`, 원본은 완주 판의 sel.
        # ★glob 을 좁힌다 — 갤러리 빌더가 같은 디렉토리에 원본·완주판 복사본을
        #  넣으므로 `*.png` 로 잡으면 원본을 원본과 대는 칸이 섞인다.
        # ★상대 경로는 저장소 뿌리 기준으로 푼다 — `_opik_env` 가 cwd 를
        #  backend 로 옮기므로 그대로 쓰면 조용히 0쌍이 된다(실측으로 걸렸다).
        d = Path(args.from_dir)
        d = d if d.is_absolute() else (ROOT / d)
        for p in sorted(d.glob("*_?_r?.png")):
            tag = p.name.split("_", 1)[0]
            sel = RECIPE / f"{tag}_sel.png"
            if sel.is_file() and (not want or tag in want):
                pairs.append((p.stem, sel, p))
    else:
        for cine in sorted(RECIPE.glob("*_cine.png")):
            tag = cine.name[:-len("_cine.png")]
            sel = RECIPE / f"{tag}_sel.png"
            if sel.is_file() and (not want or tag in want):
                pairs.append((tag, sel, cine))
    if not pairs:
        print("★쌍이 없다")
        return 1
    print(f"{len(pairs)}쌍 · 판정 {args.model} · 좌우 바꿔 2회\n")

    axis_head = build_axis_user_head()
    sys_txt = system_text()
    schema = build_schema()
    rows = []
    for tag, sel, cine in pairs:
        verdicts = {}
        for order, (p1, p2) in (("sel→cine", (sel, cine)),
                                ("cine→sel", (cine, sel))):
            parts = [{"type": "text", "text": axis_head},
                     {"type": "text", "text": "PHOTOGRAPH A:"}, png_part(p1),
                     {"type": "text", "text": "PHOTOGRAPH B:"}, png_part(p2)]
            try:
                v = call_structured(
                    "cine_drift_probe", sys_txt, parts, schema,
                    project_config={"cine_drift_probe": {"model": args.model}},
                    schema_name="cine_drift_probe")
            except Exception as exc:  # noqa: BLE001
                print(f"  {tag} {order} ✘ {type(exc).__name__}: {exc}")
                continue
            verdicts[order] = {d["axis"]: d for d in (v.get("axes") or [])}
        print(f"══ {tag}")
        for a, _ in AXES:
            o1 = verdicts.get("sel→cine", {}).get(a)
            o2 = verdicts.get("cine→sel", {}).get(a)
            if not (o1 and o2):
                print(f"   {a:13s} ─ 결손")
                continue
            if o1["same"] != o2["same"]:
                mark, note = "불일치", (
                    f"{o1['what_changed_ko'] or '(없음)'} / "
                    f"{o2['what_changed_ko'] or '(없음)'}")
            elif o1["same"]:
                mark, note = "같음  ", ""
            else:
                mark, note = "★바뀜", o1["what_changed_ko"]
            print(f"   {a:13s} {mark} {note[:96]}")
            rows.append({"shot": tag, "axis": a, "verdict": mark.strip(),
                         "note": note,
                         "orders": {k: v.get(a) for k, v in verdicts.items()}})
        print()

    changed = [r for r in rows if r["verdict"] == "★바뀜"]
    split = [r for r in rows if r["verdict"] == "불일치"]
    print(f"── 합계 {len(rows)}칸 중 바뀜 {len(changed)} · 불일치 {len(split)}")
    from collections import Counter
    for a, n in Counter(r["axis"] for r in changed).most_common():
        print(f"   {a:13s} {n}/{len(pairs)}샷")
    if args.json:
        Path(args.json).write_text(
            json.dumps(rows, ensure_ascii=False, indent=1), encoding="utf-8")
        print(f"→ {args.json}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
