#!/usr/bin/env python3
"""scene_detail 프롬프트 판을 A/B 로 잰다 — **실제 나간 payload 를 그대로 쓴다.**

왜 이렇게 하나
--------------
`scene_detail` 의 user 메시지는 카드 JSON·엔티티·아웃룩·고정 요소·촬영 감독
블록이 겹겹이 조립된 것이다. 그걸 시험용으로 다시 짜면 **프로덕션보다
단순해져서 있는 문제를 숨긴다.** 그래서 Opik 에 남은 **그 payload 그대로**를
꺼내 쓰고, **system 메시지만** 판별로 갈아 끼운다. 바뀌는 것은 한 가지뿐이다.

★source-positive / source-negative 를 나눠 잰다
  - source-positive : 입력(`camera_direction`)에 **등급 낱말이 있는** 컷.
                      여기서 줄어야 「소비 쪽 수리가 통했다」가 된다.
  - source-negative : 입력에 없는 컷. 여기 남는 것이 **진짜 발명**이다.
둘을 합쳐서 「R1 몇 건」이라고 하면 무엇이 고쳐졌는지 알 수 없다.

★ABBA 로 섞는다 — 판별 순서가 결과에 섞이지 않게.

usage:
  ab_scene_detail.py <A판디렉토리> <B판디렉토리> [회차]
  예) ab_scene_detail.py 46.202608250329 _staged/47.202608250422 6
"""
import json
import re
import sys
from collections import defaultdict

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

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

PROMPTS = "/Users/manta/Documents/Projects/TheRoad-I1/prompts/_base/scene_detail"

# 프레임 크기를 등급 이름으로 부르는 말 (primary outcome)
LABEL = re.compile(r"\b(close-?up|wide shot|medium shot|long shot|wide view|"
                   r"establishing shot|full shot)\b", re.I)
# safety — camera_direction 이 담는 의미가 살아 있는가 (Codex 4슬롯)
SLOT = {
    "자리(높이·각도)": re.compile(
        r"\b(eye[- ]level|knee|waist|hip|overhead|from above|from below|"
        r"high|low|tilted?|angle|corner|oblique)\b", re.I),
    "거리·잘림": re.compile(
        r"\b(fills? the frame|from the waist|edge of the frame|frame edge|"
        r"top edge|bottom|cropped?|only .{0,12}visible|partially)\b", re.I),
    "화면 배치": re.compile(
        r"\b(left|right|center|centre|foreground|midground|background|"
        r"behind|in front of|beside|between)\b", re.I),
    "시각 관계": re.compile(
        r"\b(past the .{0,20}shoulder|over the shoulder|reflect\w*|mirror|"
        r"through the|blur\w*|out of focus|silhouett\w*|point of view)\b", re.I),
}


def load_system(rel):
    return open(f"{PROMPTS}/{rel}/system.md").read()


def harvest(since, until="2026-12-31T00:00:00", want=None):
    """실제 나간 payload 를 (shot, user_prompt, source_positive) 로 거둔다."""
    base, ws, proj = opik_target()
    out, seen = [], set()
    for x in sorted((s for s in fetch_spans(base, ws, proj, since, until)
                     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 = ""
        for m in msgs:
            c = m.get("content")
            if isinstance(c, list):
                c = "\n".join(p.get("text", "") for p in c if isinstance(p, dict))
            if m.get("role") == "user":
                user = c or ""
        mm = re.search(r'"shot_key":\{"scene_index":(\d+),"shot_index":(\d+)\}', user)
        if not mm or not user:
            continue
        shot = f"S{mm.group(1)}sh{mm.group(2)}"
        cd = re.search(r'"camera_direction":"(.*?)","constraints"', user, re.S)
        pos = bool(LABEL.search(cd.group(1) if cd else ""))
        if want is not None and pos is not want:
            continue
        if shot in seen:
            continue
        seen.add(shot)
        out.append((shot, user, pos))
    return out


def run_one(system, user, schema):
    r = call_structured(
        step="scene_detail", system_prompt=system, user_prompt=user,
        response_schema=schema, project_config={},
        schema_name="ab_probe")
    return [v.get("t2i_prompt") or "" for v in (r or {}).get("t2i_variations") or []]


def main():
    a_rel, b_rel = sys.argv[1], sys.argv[2]
    n = int(sys.argv[3]) if len(sys.argv) > 3 else 6
    sysA, sysB = load_system(a_rel), load_system(b_rel)
    schema = load_schema("scene_detail", "detail_schema")

    # source-positive 는 귀하다 — 두 에피소드 전체에서 거둔다.
    pos = harvest("2026-08-01T00:00:00", want=True)
    neg = harvest("2026-08-24T19:20:00", want=False)[:4]
    print(f"A={a_rel} ({len(sysA):,}자)  B={b_rel} ({len(sysB):,}자)")
    print(f"source-positive {len(pos)}컷 · source-negative {len(neg)}컷 · "
          f"컷당 {n}회 (ABBA)\n")

    tally = defaultdict(lambda: {"n": 0, "label": 0, "slots": defaultdict(int)})
    for ci, (shot, user, pos_flag) in enumerate(pos + neg):
        kind = "positive" if pos_flag else "negative"
        # ABBA — 순서가 결과에 섞이지 않게. ★컷마다 시작 arm 을 바꿔야
        #   n 이 작을 때 한쪽만 도는 일이 없다(시운전에서 실제로 그랬다).
        order = [("A", sysA), ("B", sysB), ("B", sysB), ("A", sysA)]
        for i in range(n):
            arm, sysmd = order[(i + ci) % 4]
            try:
                prompts = run_one(sysmd, user, schema)
            except Exception as exc:
                print(f"  {shot} {arm} 호출 실패: {type(exc).__name__}: {exc}")
                continue
            for p in prompts:
                t = tally[(arm, kind)]
                t["n"] += 1
                t["label"] += bool(LABEL.search(p))
                for name, pat in SLOT.items():
                    t["slots"][name] += bool(pat.search(p))
            print(f"  {shot:8} {kind:8} {arm} · 컷 {len(prompts)} · "
                  f"등급낱말 {sum(bool(LABEL.search(p)) for p in prompts)}",
                  flush=True)

    print(f"\n{'':<10}{'컷':>5}{'★등급낱말':>10}  " +
          "  ".join(f"{k:>12}" for k in SLOT))
    for kind in ("positive", "negative"):
        for arm in ("A", "B"):
            t = tally.get((arm, kind))
            if not t or not t["n"]:
                continue
            print(f"{kind[:3]}-{arm:<6}{t['n']:>5}{t['label']:>10}  " +
                  "  ".join(f"{t['slots'][k]:>12}" for k in SLOT))
    print("\n★source-positive 행에서 등급낱말이 줄고 네 슬롯이 유지돼야 초록.")
    print("★source-negative 행에 남는 것이 진짜 발명이다.")


if __name__ == "__main__":
    main()
