#!/usr/bin/env python3
"""카드에서 **설명 부분만 빼고** 돌려 본다 — 걷어도 되는지 코드 고치기 전에.

카드의 73%가 샷마다 바이트 동일하고 그중
  `rationale_summary` 류(왜 그런지)   1,865자
  `forbidden_*` 나열(하지 마라 목록)   1,421자
가 걷을 후보다. 그런데 **「크다」와 「해로우니 걷어야 한다」는 다르다.**
`render_prompt_card.py` 를 고치기 전에, 실제 나간 payload 에서 그 두 갈래만
빼고 같은 조건으로 돌려 **나빠지는 것이 있는지** 본다.

★비열등성 확인이다 — B(뺀 쪽)가 A 보다 **나빠지지 않아야** 걷을 수 있다.
★`forbidden_*` 는 나빠지지 않는 것에 더해 **줄어들 수도** 있다. 이번 판에서
  금지어 나열이 프라이밍이라는 것이 system.md 쪽에서 실증됐다.

usage: ab_card_strip.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")

# 걷을 후보 — 키 이름으로만 고른다(값의 뜻을 판단하지 않는다)
# ★키 이름으로만 고르되 **계약 필드를 빨아들이면 안 된다.**
#   - `_summary$` 를 통째로 잡으면 `id_use_summary`("얼굴이 식별될 때만 C##
#     허용")·`scope_summary` 같은 **계약**까지 빠진다 → `rationale_summary` 만.
#   - `forbidden_refs` 는 금지 「나열」이 아니라 **첨부하면 안 될 참조 목록**
#     이라는 값이다 → 제외.
DROP = re.compile(r"(rationale_summary|forbidden_(?!refs\b))")


def strip_card(node):
    """카드에서 설명 갈래만 재귀로 뺀다."""
    if isinstance(node, dict):
        return {k: strip_card(v) for k, v in node.items() if not DROP.search(k)}
    if isinstance(node, list):
        return [strip_card(v) for v in node]
    return node


def harvest():
    base, ws, proj = opik_env_target()
    out, seen = [], set()
    for x in sorted((s for s in fetch_spans(base, ws, proj,
                                            "2026-08-24T16: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)), "")
        m2 = re.search(r"(\[RenderPromptCard v1\]\n)(\{.*?\})(\n)", user, re.S)
        mm = re.search(r'"shot_key":\{"scene_index":(\d+),"shot_index":(\d+)\}', user)
        if not (m2 and mm):
            continue
        shot = f"S{mm.group(1)}sh{mm.group(2)}"
        if shot in seen:
            continue
        seen.add(shot)
        try:
            card = json.loads(m2.group(2))
        except Exception:
            continue
        stripped = json.dumps(strip_card(card), ensure_ascii=False,
                              separators=(",", ":"), sort_keys=True)
        user_b = user[:m2.start(2)] + stripped + user[m2.end(2):]
        out.append((shot, user, user_b, len(m2.group(2)), len(stripped)))
    return out


def opik_env_target():
    from _opik_env import opik_target
    return opik_target()


def measure(prompts, card_ids):
    r = {"n": 0, "label": 0, "num": 0, "len": 0, "idmiss": 0}
    for p in prompts:
        r["n"] += 1
        r["label"] += bool(LABEL.search(p))
        r["num"] += bool(NUM.search(p))
        r["len"] += len(p)
        found = {b for b, o in ID_ANY.findall(p) if b.startswith("C")}
        comp = {b + o for b, o in ID_ANY.findall(p) if o}
        # 기본값이 composite 인데 base 만 쓴 인물 = ID 계약 위반
        r["idmiss"] += sum(1 for c in found if not any(k.startswith(c) for k in comp))
    return r


def main():
    system = load_prompt("scene_detail", "system")
    schema = load_schema("scene_detail", "detail_schema")
    cuts = harvest()[:6]
    if not cuts:
        raise SystemExit("payload 를 못 거뒀다")
    a_sz = sum(c[3] for c in cuts) / len(cuts)
    b_sz = sum(c[4] for c in cuts) / len(cuts)
    print(f"system {len(system):,}자 · 컷 {len(cuts)} · 컷당 {N}회 (ABBA)")
    print(f"카드 평균 {a_sz:,.0f}자 → 뺀 뒤 {b_sz:,.0f}자 "
          f"({b_sz - a_sz:+,.0f}, {100*(b_sz-a_sz)/a_sz:+.1f}%)\n")

    tally = defaultdict(lambda: {"n": 0, "label": 0, "num": 0, "len": 0, "idmiss": 0})
    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_card_strip")
            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, None)
            t = tally[arm]
            for k in m:
                t[k] += m[k]
            print(f"  {shot:8} {arm} · 컷 {m['n']} · 등급낱말 {m['label']} · "
                  f"실수치 {m['num']} · ID누락 {m['idmiss']}", flush=True)

    print(f"\n{'arm':<6}{'컷':>5}{'등급낱말':>9}{'실수치':>8}{'ID누락':>8}{'평균길이':>10}")
    for arm in ("A", "B"):
        t = tally[arm]
        if not t["n"]:
            continue
        print(f"{arm:<6}{t['n']:>5}{t['label']:>9}{t['num']:>8}{t['idmiss']:>8}"
              f"{t['len']//t['n']:>9,}자")
    print("\n★A=카드 그대로 · B=설명 갈래(rationale·forbidden) 뺀 것.")
    print("★B 가 A 보다 **나빠지지 않아야** 걷을 수 있다(비열등성).")


if __name__ == "__main__":
    main()
