#!/usr/bin/env python3
"""최종 변환 문안의 두 절을 A/B — 카메라 자유와 광원 자유.

무엇이 문제였나 — 상류는 구도를 「이 스틸의 구도 권위」로 못박고
(`CAMERA & FRAME (follow exactly — this is the composition authority)`)
judge·critique·fix_rejudge 로 지켜내는데, **마지막 변환 문안이 그것을
명시로 풀어준다**:

    prompts/_base/still_recipe/20.202608141300/cine_transform.md:3
      "you may change the camera angle, height, distance or foreground
       layering — the source still does not fix where the camera stands."

그리고 「motivated practical light」를 쓰라면서 **무엇이 그 빛을 동기
짓는지 안 준다** — 이 단계가 받는 것은 문안 한 단락과 소스 스틸 한 장뿐이다
(`still_recipe_service.py:4536-4548`). 그러니 지어낸다.

    arm A  현행 (팩에서 그대로 로드 — 프로덕션 경로)
    arm B  카메라를 소스에 고정
    arm C  광원을 소스에 고정 (대비·falloff·공기감은 그대로 재량)
    arm D  B+C

★arm A 도 **다시 돌린다.** 이미 `<tag>_cine.png` 가 있지만 그것 한 장만
 기준선으로 쓰면 모델의 판마다 흔들림이 「문안 때문에 달라졌다」로 읽힌다
 (2026-08-25 실측 사고: 기준선 3회만 잡아 가짜 퇴행 2건을 보고했다).

★금지형으로 쓰지 않는다 — 「바꾸지 마라」만 남기면 그릴 재료가 없다.
 소스 이미지가 곧 재료라는 것을 **값으로** 적는다.

★판정은 `ab_cine_drift.py` 와 같은 축 계약으로 따로 돌린다 — 이 도구는
 그림만 만든다(만드는 자리와 재는 자리를 섞지 않는다).

usage:
  ab_cine_clause.py --shots S2sh1,S1sh1 --rounds 2
  ab_cine_clause.py --arms A,B --dry        # 나갈 문안만 찍어 본다
"""
import argparse
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")
OUT = ROOT / "artifact" / "20260825_cine_clause_ab"

# 현행 문안에서 갈아 끼울 두 문장 — 원문 그대로여야 치환이 성립한다.
CAM_OLD = ("Reframe and relight like a film director choosing a stronger "
           "setup: you may change the camera angle, height, distance or "
           "foreground layering — the source still does not fix where the "
           "camera stands.")
CAM_NEW = ("Relight and finish like a film director working from where this "
           "camera already stands: keep its angle, height and distance and "
           "the layering of what is nearer and farther — the source still "
           "fixes the camera.")
LIGHT_OLD = "Use motivated practical light with soft falloff;"
LIGHT_NEW = ("The lights visible in the source ARE this scene's lights: keep "
             "each one's place, kind and colour, and whether it is switched "
             "on, and shape only their contrast, falloff and the air between "
             "them;")
# arm E — 같은 문단이 「사물을 더하지 마라」와 「전경 배치는 바꿔도 된다」를
# 동시에 적는다. 실측 4/7 에서 없던 전경 물체가 생겼으니 허가 쪽이 이겼다.
# 카메라 자유(설계상 재량)는 그대로 두고 **전경 배치 허가만** 뺀다 —
# 그래야 「사물이 는 것」과 「카메라가 움직인 것」이 갈린다.
FG_OLD = ("the camera angle, height, distance or foreground layering — the "
          "source still does not fix where the camera stands.")
FG_NEW = ("the camera angle, height and distance — the source still does not "
          "fix where the camera stands, but whatever stands in its foreground "
          "is what the source already has there.")


def arms(base: str, which: str) -> str:
    def sub(text: str, old: str, new: str) -> str:
        if old not in text:
            raise SystemExit(
                f"★치환 실패 — 현행 문안에 이 문장이 없다:\n  {old[:80]}…\n"
                f"팩이 개정됐으면 이 도구의 상수를 먼저 맞춰라.")
        return text.replace(old, new)

    if which == "A":
        return base
    if which == "B":
        return sub(base, CAM_OLD, CAM_NEW)
    if which == "C":
        return sub(base, LIGHT_OLD, LIGHT_NEW)
    if which == "E":
        return sub(base, FG_OLD, FG_NEW)
    if which == "D":   # C+E — 세계 사실 두 축만 되찾고 카메라는 재량 유지
        return sub(sub(base, LIGHT_OLD, LIGHT_NEW), FG_OLD, FG_NEW)
    raise SystemExit(f"모르는 arm {which!r}")


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("--shots", default="S2sh1")
    ap.add_argument("--arms", default="A,B,C")
    ap.add_argument("--rounds", type=int, default=2)
    ap.add_argument("--dry", action="store_true",
                    help="나갈 문안만 찍는다 — 그림 안 만든다(무료)")
    args = ap.parse_args()

    from app.core.config import settings
    from app.modules.pipeline.cine_transform import CINE_SOURCE_LABEL
    from app.modules.pipeline.still_recipe import build_cine_transform_prompt

    base = build_cine_transform_prompt()
    want_arms = [a.strip() for a in args.arms.split(",") if a.strip()]
    shots = [s.strip() for s in args.shots.split(",") if s.strip()]

    if args.dry:
        for a in want_arms:
            t = arms(base, a)
            print(f"══ arm {a} ({len(t)}자)\n{t}\n")
        return 0

    from app.modules.llm.grok_image_client import GrokImageClient
    from app.modules.pipeline.multiroll_gemini import atomic_write_bytes

    OUT.mkdir(parents=True, exist_ok=True)
    client = GrokImageClient()
    model = settings.grok_image_model
    total = len(shots) * len(want_arms) * args.rounds
    print(f"{len(shots)}샷 × {len(want_arms)}arm × {args.rounds}회 "
          f"= {total}장 (grok ≈${total * 0.03:.2f}) · 모델 {model}\n")

    for tag in shots:
        sel = RECIPE / f"{tag}_sel.png"
        if not sel.is_file():
            print(f"  {tag} ✘ 원본 없음")
            continue
        sel_bytes = sel.read_bytes()
        for a in want_arms:
            text = arms(base, a)
            for r in range(1, args.rounds + 1):
                dest = OUT / f"{tag}_{a}_r{r}.png"
                if dest.is_file() and dest.stat().st_size > 0:
                    print(f"  {tag} arm{a} r{r} — 이미 있다, 건너뜀")
                    continue
                client.set_context(
                    operation_type="cine_clause_ab",
                    multiroll_tag=f"{tag}_{a}_r{r}")
                try:
                    png, ms = client.generate_image(
                        text, labeled_references=[(CINE_SOURCE_LABEL,
                                                   sel_bytes)])
                except Exception as exc:  # noqa: BLE001
                    print(f"  {tag} arm{a} r{r} ✘ "
                          f"{type(exc).__name__}: {str(exc)[:90]}")
                    continue
                atomic_write_bytes(dest, png)
                print(f"  {tag} arm{a} r{r} → {dest.name} "
                      f"({len(png) // 1024}KB · {int(ms)}ms)")
    print(f"\n→ {OUT}")
    return 0


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