#!/usr/bin/env python3
"""`카메라/프레이밍: {cam_dir}` 중복 주입을 빼면 촬영 용어 누출이 줄어드나.

무엇이 문제였나 — `detail_steps.py:2841-2844` 가 카드에 이미 있는
`render_strategy.camera_direction` 을 user 메시지 **뒤쪽**에
`[촬영 감독(DP) 연출 지시 — **반드시 t2i_prompt에 반영하세요**]` 블록으로
**다시** 넣는다. 앞(카드 JSON) + 뒤(그 블록, recency) 두 번이고, 뒤엣것은
「반드시 t2i_prompt 에 반영하세요」를 달고 있다. **system.md 의 어떤 문구도
이 지시를 못 이긴다** — v47 이 프롬프트만 고쳤는데 잔여가 남은 이유다.

정보 손실 없음(확인): 카드의 camera_direction 은 **같은 `staging` dict** 에서
온다(`_g41_staging = locals()["staging"]`, `cam_dir = staging_cam or shot_cam`).
오히려 카드 쪽이 더 완전하다(staging 이 비고 shot_info 만 있을 때도 채운다).

★코드를 고치기 전에 **기록된 payload 에서 그 줄만 빼고** 같은 조건으로 돌린다.
★A = 있는 그대로(중복 있음) · B = 그 줄만 뺀 것.
★source-positive(입력 camera_direction 에 등급 낱말이 있는 컷)에서 재야
  의미가 있다 — 없으면 샐 것이 없다.

usage: ab_camdir_dedup.py [회차]   (기본 6, ABBA)
"""
import json
import re
import sys
from collections import defaultdict

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

from app.modules.llm.llm_client import call_structured           # noqa: E402
from app.modules.prompt_loader import load_prompt, load_schema   # noqa: E402
from tools.opik_prompt_audit.audit.fetch import fetch_spans      # noqa: E402

N = int(sys.argv[1]) if len(sys.argv) > 1 else 6

LABEL = re.compile(r"\b(close-?up|wide shot|medium shot|long shot|wide view|"
                   r"establishing shot|full shot)\b", re.I)
NUM = re.compile(r"\b\d+(?:\.\d+)?\s?(?:cm|mm|m|meters?|inch\w*|feet|ft)\b", re.I)
ID_ANY = re.compile(r"\b([CPL]\d{2})(O\d{2})?\b")
DUP = re.compile(r"^카메라/프레이밍: .*\n", re.M)

# safety — camera_direction 이 담는 의미가 살아 있는가 (v47 과 같은 네 축)
SLOT = {
    "자리": re.compile(r"\b(eye[- ]level|knee|waist|hip|overhead|from above|"
                       r"from below|high|low|tilted?|angle|corner|oblique)\b", re.I),
    "거리·잘림": re.compile(r"\b(fills? the frame|from the waist|edge of the frame|"
                          r"frame edge|top edge|bottom|cropped?|partially)\b", re.I),
    "화면 배치": re.compile(r"\b(left|right|cent(?:er|re)|foreground|midground|"
                          r"background|behind|in front of|beside|between)\b", re.I),
    "시각 관계": re.compile(r"\b(past the .{0,20}shoulder|over the shoulder|"
                          r"reflect\w*|mirror|through the|blur\w*|out of focus|"
                          r"silhouett\w*|point of view)\b", re.I),
}


def harvest():
    from _opik_env import opik_target
    base, ws, proj = opik_target()
    out, seen = [], set()
    for x in sorted((s for s in fetch_spans(base, ws, proj, "2026-08-01T00:00:00",
                                            "2026-12-31T00:00:00")
                     if "op:scene_detail" in (s.get("tags") or [])),
                    key=lambda s: s["start_time"]):
        inp = x.get("input")
        msgs = inp if isinstance(inp, list) else (inp or {}).get("messages") or []
        user = next((m.get("content") for m in msgs
                     if m.get("role") == "user" and isinstance(m.get("content"), str)), "")
        mm = re.search(r'"shot_key":\{"scene_index":(\d+),"shot_index":(\d+)\}', user)
        cd = re.search(r'"camera_direction":"(.*?)","constraints"', user, re.S)
        if not (mm and cd and DUP.search(user)):
            continue
        if not LABEL.search(cd.group(1)):      # source-positive 만
            continue
        shot = f"S{mm.group(1)}sh{mm.group(2)}"
        if shot in seen:
            continue
        seen.add(shot)
        out.append((shot, user, DUP.sub("", user)))
    return out


def measure(ps):
    r = {"n": 0, "label": 0, "num": 0, "len": 0}
    r.update({k: 0 for k in SLOT})
    for p in ps:
        r["n"] += 1
        r["label"] += bool(LABEL.search(p))
        r["num"] += bool(NUM.search(p))
        r["len"] += len(p)
        for k, pat in SLOT.items():
            r[k] += bool(pat.search(p))
    return r


def main():
    system = load_prompt("scene_detail", "system")
    schema = load_schema("scene_detail", "detail_schema")
    cuts = harvest()[:6]   # 컷 수 제한 — 6컷 × N 회
    if not cuts:
        raise SystemExit("source-positive payload 를 못 거뒀다")
    print(f"system {len(system):,}자 (현행 판) · source-positive {len(cuts)}컷 · "
          f"컷당 {N}회 (ABBA)")
    print(f"빼는 줄 = `카메라/프레이밍: …` 한 줄 "
          f"(평균 {sum(len(a)-len(b) for _,a,b in cuts)//len(cuts):,}자)\n")

    tally = defaultdict(lambda: defaultdict(int))
    for ci, (shot, ua, ub) in enumerate(cuts):
        order = [("A", ua), ("B", ub), ("B", ub), ("A", ua)]
        for i in range(N):
            arm, user = order[(i + ci) % 4]
            try:
                out = call_structured(
                    step="scene_detail", system_prompt=system, user_prompt=user,
                    response_schema=schema, project_config={},
                    schema_name="ab_camdir_dedup")
            except Exception as exc:
                print(f"  {shot} {arm} 호출 실패: {type(exc).__name__}: {exc}",
                      flush=True)
                continue
            ps = [v.get("t2i_prompt") or ""
                  for v in (out or {}).get("t2i_variations") or []]
            m = measure(ps)
            for k, v in m.items():
                tally[arm][k] += v
            print(f"  {shot:8} {arm} · 컷 {m['n']} · 등급낱말 {m['label']}",
                  flush=True)

    hdr = f"\n{'arm':<22}{'컷':>5}{'★등급낱말':>10}{'실수치':>8}" + \
          "".join(f"{k:>11}" for k in SLOT) + f"{'평균길이':>10}"
    print(hdr)
    for arm, name in (("A", "A 중복 있음(현행)"), ("B", "B 그 줄 뺌")):
        t = tally[arm]
        if not t["n"]:
            continue
        print(f"{name:<22}{t['n']:>5}{t['label']:>10}{t['num']:>8}" +
              "".join(f"{t[k]:>11}" for k in SLOT) +
              f"{t['len']//t['n']:>9,}자")
    print("\n★B 에서 등급낱말이 줄고 네 슬롯이 유지돼야 중복을 뺄 수 있다.")


if __name__ == "__main__":
    main()
