#!/usr/bin/env python3
"""조사한 참조가 배경에 반영되게 하는 수리안 A/B — 「목록 밖 금지」 절만 바꾼다.

무엇이 문제였나 — era 조사는 제대로 돌아 그 지역·시대의 실물 사진을 확보하고
(`records.json` 의 `era_research`), 프롬프트에도 역할문이 붙는다:

    PERIOD REFERENCE — <대상>: a researched photograph of the real thing.
    Copy its era-accurate form, proportions, materials, fittings and styling
    exactly.

그런데 같은 프롬프트의 `bg_fill_tail` 이 더 강하게 막는다:

    build only what the sketch already has lines for, and do not invent
    facilities that are not listed.

그리고 `THINGS AT THIS PLACE` 목록은 배치용이라 `intersection / approach /
streetlight / alley` 같은 **시설 이름**뿐이다. 그 지역 장소를 그 지역답게 만드는
것(노면 표시·보도 마감·전주와 가공선·상점 얼굴과 간판·벽과 창 재질)은 전부
「목록 밖」이 되어 지워진다. **재료는 줬는데 쓰지 말라는 규칙이 이겼다.**

수리 = 「무엇이 있나(시설·배치)」와 「그것이 어떻게 생겼나(외형·재질·마감)」를
가른다. 배치는 스케치·목록 권위 그대로, 외형은 조사 사진 권위.

    arm A  기록된 프롬프트 그대로
    arm B  `bg_fill_tail` 절만 수리안으로 교체 (나머지 전부 동일)

★참조는 기록된 그대로 붙인다 — 콘티(+플레이트) + era 참조.
★판정은 육안이다. 「달라졌다」와 「그 지역답다」는 다른 물음이다.

usage: ab_bg_fabric.py [--dry]
"""
import json
import re
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")
PROJ = "da049582-2c6d-492c-979d-f468d61bab6e"
EPI = "fb7a883f-baac-4145-9131-732ce628d474"
IMG = ROOT / "projects" / PROJ / "images" / EPI
RECIPE = IMG / "scene" / "recipe"
OUT = ROOT / "artifact" / "20260825_bg_fabric_ab"
DRAFT = Path("/private/tmp/claude-501/-Users-manta-Documents-Projects-TheRoad-I1"
             "/e9fa4bb4-eeb7-49ef-819c-f7e09fc1224f/scratchpad/bg_fill_tail_v21_draft.md")

# 기록된 프롬프트 안에서 바꿀 구간 — 현행 tail 의 첫 문단
OLD_HEAD = "THINGS THAT LIVE AT THIS PLACE"


def _dsn() -> str:
    """DB 접속 문자열은 `backend/.env` 에서 읽는다.

    ★코드에 적어 두면 저장소에 자격 증명이 남는다(GitGuardian).
     `_opik_env` 가 cwd 를 backend 로 옮겨 두므로 설정이 채워진다.
    """
    from app.core.config import settings

    url = getattr(settings, "database_url", "") or ""
    if not url:
        raise SystemExit("[설정 미로드] database_url 이 비었다 — "
                         "backend/.env 를 못 읽었다")
    return url


def swap_tail(prompt: str, new_tail: str) -> tuple[str, int]:
    """`THINGS THAT LIVE AT THIS PLACE` 부터 끝까지를 새 tail 로 갈아 끼운다.

    ★그 절이 프롬프트 **맨 끝**이 아닐 수 있다 — era 역할문이 뒤에 붙는다.
      그래서 era 역할문(`PERIOD REFERENCE`)은 떼어 두고 tail 만 바꾼 뒤 다시 붙인다.
    """
    i = prompt.find(OLD_HEAD)
    if i < 0:
        return prompt, 0
    rest = prompt[i:]
    j = rest.find("PERIOD REFERENCE")
    era_part = rest[j:] if j >= 0 else ""
    replaced = prompt[:i] + new_tail.strip() + (("\n\n" + era_part) if era_part else "\n")
    return replaced, len(rest) - len(era_part)


def refs_for(shot: str, rec: dict) -> list[tuple[str, Path]]:
    """기록된 입력 자산 + era 참조를 실제 파일로. 라벨은 프롬프트 지칭과 맞춘다."""
    import psycopg2
    cn = psycopg2.connect(_dsn())
    cur = cn.cursor()
    out: list[tuple[str, Path]] = []
    for aid in (rec.get("input_asset_ids") or []):
        cur.execute("SELECT pipeline_role, file_path FROM image_asset WHERE id=%s", (aid,))
        row = cur.fetchone()
        if not row:
            continue
        role, path = row
        p = ROOT / path if not str(path).startswith("/") else Path(path)
        if not p.exists():
            p2 = IMG / path
            p = p2 if p2.exists() else p
        if p.exists():
            label = ("STORYBOARD SKETCH" if "sketch" in role or "conti" in role
                     else "LOCATION PHOTOGRAPH")
            out.append((label, p))
    # era 참조 — 파일 이름이 `eraref_<hash>.png`
    er = rec.get("era_research") or {}
    if er:
        sha = er.get("cache_key") or ""
        cands = sorted(RECIPE.glob("eraref_*.png"))
        pick = None
        for c in cands:
            if sha and sha[:16] in c.name:
                pick = c
                break
        if pick is None:
            # subject 로 records 의 era_ref:: 항목을 되짚는다
            recs = json.loads((RECIPE / "records.json").read_text(encoding="utf-8"))
            for k, v in recs.items():
                if k.startswith("era_ref::") and isinstance(v, dict) \
                        and v.get("subject") == er.get("subject"):
                    h = k.split("::", 1)[1]
                    f = RECIPE / f"eraref_{h}.png"
                    if f.exists():
                        pick = f
                    break
        if pick:
            out.append((f"PERIOD REFERENCE — {er.get('subject','')}", pick))
    cn.close()
    return out


def main() -> int:
    dry = "--dry" in sys.argv
    recs = json.loads((RECIPE / "records.json").read_text(encoding="utf-8"))
    new_tail = DRAFT.read_text(encoding="utf-8")

    # ★그 절이 실제로 들어 있는 샷만 — 없는 샷은 A 와 B 가 같은 프롬프트라
    #  돈만 쓰고 아무것도 못 가른다(실측: 3샷 중 2샷이 tail 0자 교체였다).
    shots = [k.split("::")[0] for k in recs if k.endswith("::bgfirst_bg")
             and OLD_HEAD in (recs[k].get("prompt") or "")]
    shots.sort()
    rounds = 3
    for a in sys.argv[1:]:
        if a.startswith("--rounds="):
            rounds = int(a.split("=", 1)[1])
    OUT.mkdir(parents=True, exist_ok=True)
    plan = []
    for sh in shots:
        rec = recs[f"{sh}::bgfirst_bg"]
        pa = rec.get("prompt") or ""
        pb, removed = swap_tail(pa, new_tail)
        rf = refs_for(sh, rec)
        er = (rec.get("era_research") or {}).get("subject", "")
        plan.append((sh, pa, pb, rf, er))
        print(f"{sh}  {len(pa):>6,}자 → {len(pb):>6,}자  (tail {removed}자 교체)  "
              f"참조 {len(rf)}장: {[l.split('—')[0].strip()[:18] for l, _ in rf]}")
        if er:
            print(f"      era 조사 대상: {er}")

    if dry:
        (OUT / "_plan.json").write_text(json.dumps(
            [{"shot": s, "era": e, "refs": [str(p) for _, p in r],
              "labels": [l for l, _ in r]} for s, _, _, r, e in plan],
            ensure_ascii=False, indent=2), encoding="utf-8")
        (OUT / "_prompt_B_sample.txt").write_text(plan[1][2] if len(plan) > 1
                                                  else plan[0][2], encoding="utf-8")
        print(f"\n--dry — 계획 {OUT}/_plan.json · B 프롬프트 표본 _prompt_B_sample.txt")
        return 0

    # ★프로덕션과 같은 엔진·같은 만드는 법으로 — `_bgfirst_gpt_client` 는
    #  서비스 함수 안 지역 변수라 import 가 안 된다(`still_recipe_service.py:1525`).
    #  같은 자리에서 쓰는 팩토리를 그대로 부른다.
    from app.core.config import settings
    from app.modules.llm.gpt_image_primitive import call_gpt_image_bytes
    from app.core.openai_keys import openai_client
    from app.modules.pipeline.still_recipe import (
        BGFIRST_BG_IMAGE_MODEL, BGFIRST_BG_SIZE,
    )

    _bgfirst_gpt_client = openai_client(
        timeout=float(settings.llm_timeout_image_gen))

    # ★1회로는 못 잰다 — 같은 프롬프트로도 그림이 흔들린다. ABBA 로 회차를
    #  섞어 시간대 편향도 지운다.
    order = []
    for r in range(1, rounds + 1):
        arms = ("A", "B") if r % 2 else ("B", "A")
        order.extend((r, a) for a in arms)

    for sh, pa, pb, rf, _ in plan:
        paths = [p for _, p in rf]
        for rnd, arm in order:
            text = pa if arm == "A" else pb
            dst = OUT / f"{sh}_{arm}{rnd}.png"
            if dst.exists():
                print(f"  {sh}_{arm}{rnd} 이미 있음 — 건너뜀")
                continue
            try:
                # ★프로덕션과 같은 인자 (still_recipe_service.py:1653-1658)
                png = call_gpt_image_bytes(
                    _bgfirst_gpt_client, mode="edit", prompt=text,
                    ref_paths=paths,
                    call_kwargs={"model": BGFIRST_BG_IMAGE_MODEL,
                                 "size": BGFIRST_BG_SIZE,
                                 "quality": "high", "n": 1},
                )
                dst.write_bytes(png)
                print(f"  {sh}_{arm}{rnd} ✔ {len(png):,}B")
            except Exception as e:
                print(f"  {sh}_{arm}{rnd} ✘ {e}")
    print(f"\n→ {OUT}")
    return 0


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