"""s21 remake 1차 — 배경 참조만 교체한 재생성 비교 (프롬프트 불변).

  - 대상: 사용자 지정 샷 키(CLI 인자, 콤마 구분)의 **최신** 생성 스틸.
  - 계약: 원본 생성 콜(llm_call_log)의 user_prompt 재사용, labeled refs
    순서/라벨 그대로 — 단 ① pipeline_role='background_render' 참조의
    이미지 파일만 새 배경(--bg)으로 치환, ② pipeline_role=
    'outdoor_camera_sketch' 참조는 **제외**(구배경 파생 구도 가이드라
    새 배경 실험을 오염 — 사용자 지시). 둘 다 구조 필드 판단(라벨 의미
    파싱 0). 제외된 참조의 슬롯 번호("image N")를 가리키는 프롬프트
    헤더/불릿 줄만 결정론 제거(잔재 0 assert, 장면/엔티티 본문 불변).
    모델=nb2(원본과 동일), 16:9.
  - 산출: out/shots_outdoor/remake/<샷키>_bgswap.png
    + plans/outdoor_remake_bg.json + remake_bg.html (8897 비교 갤러리).
  - DB write 0(capture 미개방), 커밋 금지(scratchpad).
사용: backend/.venv/bin/python s21_remake_bg.py \
        --shots S4_Shot1,S10_Shot5,S11_Shot4,S13_Shot3,S17_Shot1 \
        --bg out/blockset/stairfix_nb2.png
"""
import argparse
import html as html_mod
import json
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))
import forest_lib as F  # noqa: E402

import psycopg2  # noqa: E402

ROOT = Path("/Users/manta/Documents/Projects/TheRoad-I1")
EXP = Path(__file__).parent
OUT = F.OUT / "shots_outdoor" / "remake"
PAGE = EXP / "remake_bg.html"


def load_latest_calls(shot_keys):
    collect = json.loads(
        (EXP / "plans" / "outdoor_shots_collect.json").read_text("utf-8"))
    stills_meta = json.loads(
        (EXP / "plans" / "outdoor_shots_refs.json").read_text("utf-8"))["stills"]
    conn = psycopg2.connect(host="localhost", user="theroad",
                            password="theroad_dev_2026", dbname="theroad")
    cur = conn.cursor()
    jobs = []
    for k in shot_keys:
        imgs = collect["shots"][k]["images"]
        latest = max(imgs,
                     key=lambda im: stills_meta[im["asset_id"]]["created_at"])
        aid = latest["asset_id"]
        cur.execute(
            """
            SELECT cl.user_prompt, ia.input_image_ids, cl.reference_image_ids
            FROM image_asset ia
            JOIN llm_call_log cl ON cl.id = ia.generation_call_id
            WHERE ia.id = %s
            """, (aid,))
        prompt, iids, labels = cur.fetchone()
        ii = json.loads(iids or "[]")
        lb = json.loads(labels or "[]")
        assert len(ii) == len(lb), f"{k}: 라벨/UUID 길이 불일치"
        cur.execute(
            "SELECT id, file_path, pipeline_role FROM image_asset"
            " WHERE id = ANY(%s)", (ii,))
        rmap = {i: (fp, role) for i, fp, role in cur.fetchall()}
        refs = [{"id": r, "label": lb[i], "file_path": rmap[r][0],
                 "pipeline_role": rmap[r][1]} for i, r in enumerate(ii)]
        jobs.append({"shot_key": k, "asset_id": aid,
                     "orig_file": latest["file"], "prompt": prompt,
                     "refs": refs})
    conn.close()
    return jobs


def strip_ref_slot(prompt, slot_no):
    """제외된 참조 슬롯 번호를 가리키는 줄만 제거 (본문 불변).

    대상: 'Reference image N:' 헤더 줄 + 'image N' 토큰을 포함한 불릿 줄.
    슬롯 번호는 콜 로그 라벨 인덱스(구조적 확정)에서 옴 — 의미 파싱 아님.
    """
    token = f"image {slot_no}"
    kept, removed = [], []
    for ln in prompt.splitlines():
        low = ln.lower()
        if (low.startswith(f"reference image {slot_no}:")
                or (ln.lstrip().startswith("-") and token in low)):
            removed.append(ln)
            continue
        kept.append(ln)
    out = "\n".join(kept)
    assert token not in out.lower(), f"슬롯 {slot_no} 잔재 남음"
    return out, removed


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--shots", required=True)
    ap.add_argument("--bg", required=True,
                    help="새 배경 이미지 (EXP 상대 or 절대 경로)")
    args = ap.parse_args()
    shot_keys = [s.strip() for s in args.shots.split(",") if s.strip()]
    bg_path = (EXP / args.bg) if not Path(args.bg).is_absolute() \
        else Path(args.bg)
    assert bg_path.exists(), f"새 배경 없음: {bg_path}"

    jobs = load_latest_calls(shot_keys)
    OUT.mkdir(parents=True, exist_ok=True)
    results = []
    for j in jobs:
        labeled = []
        swapped = 0
        dropped_slots = []
        for i, r in enumerate(j["refs"]):
            if r["pipeline_role"] == "background_render":
                labeled.append((r["label"], bg_path))
                swapped += 1
            elif r["pipeline_role"] == "outdoor_camera_sketch":
                dropped_slots.append(i + 1)  # 구배경 파생 가이드 → 제외
            else:
                labeled.append((r["label"], ROOT / r["file_path"]))
        assert swapped == 1, (f"{j['shot_key']}: background_render 참조 "
                              f"{swapped}건 (1건이어야 함)")
        assert all(s > len(labeled) for s in dropped_slots), (
            f"{j['shot_key']}: 제외 슬롯이 마지막이 아님 — 남은 참조 번호"
            f" 재정렬 필요 (미구현이므로 중단)")
        prompt = j["prompt"]
        removed_lines = []
        for s in dropped_slots:
            prompt, rm = strip_ref_slot(prompt, s)
            removed_lines.extend(rm)
        out = OUT / f"{j['shot_key']}_bgswap.png"
        F.img_nb2(f"remake_bgswap_{j['shot_key']}", prompt, labeled,
                  out_path=out)
        results.append({**{k: j[k] for k in
                           ("shot_key", "asset_id", "orig_file")},
                        "refs": [{**r, "swapped":
                                  r["pipeline_role"] == "background_render",
                                  "dropped": r["pipeline_role"]
                                  == "outdoor_camera_sketch"}
                                 for r in j["refs"]],
                        "removed_prompt_lines": removed_lines,
                        "new_bg": str(bg_path), "out": out.name,
                        "prompt": prompt})
    F.save_plan("outdoor_remake_bg", {"new_bg": str(bg_path),
                                      "jobs": results})
    build_page(results, bg_path)
    print(f"remake {len(results)}장 -> {OUT}")
    print(f"page -> {PAGE}")


def build_page(results, bg_path):
    def _esc(t):
        return html_mod.escape(str(t or ""))

    bg_rel = bg_path.relative_to(EXP) if bg_path.is_relative_to(EXP) \
        else bg_path

    def _ref_src(x):
        # s21_shot_refs.py 가 복사해 둔 서빙 가능 사본 (8897 루트=EXP)
        return f"out/shots_outdoor/refs/{x['pipeline_role']}_{x['id'][:8]}.png"

    secs = []
    for r in results:
        orig_bg = next((x for x in r["refs"] if x["swapped"]), None)
        kept = [x for x in r["refs"]
                if not x["swapped"] and not x.get("dropped")]
        dropped = [x for x in r["refs"] if x.get("dropped")]
        kept_html = "".join(
            f"<div class='ref'><img src='{_esc(_ref_src(x))}'>"
            f"<p class='rlabel'>유지 · {_esc(x['pipeline_role'])}</p></div>"
            for x in kept) + "".join(
            f"<div class='ref dropped'><img src='{_esc(_ref_src(x))}'>"
            f"<p class='rlabel'>제외됨 · {_esc(x['pipeline_role'])}"
            f" (구배경 파생 가이드 — 프롬프트 지시 줄도 제거)</p></div>"
            for x in dropped)
        secs.append(f"""
<h2>{_esc(r['shot_key'])}</h2>
<div class='pair'>
  <div><img src='out/shots_outdoor/{_esc(r['orig_file'])}'>
    <p class='cap'>원본 최신 스틸 (기존 배경)</p></div>
  <div><img src='out/shots_outdoor/remake/{_esc(r['out'])}'>
    <p class='cap'>재생성 (배경만 교체, 프롬프트 동일)</p></div>
</div>
<div class='refrow'>
  <div class='ref'><img src='{_esc(_ref_src(orig_bg))}'>
    <p class='rlabel'>교체됨 · 원본 배경 ({_esc(orig_bg['label'])})</p></div>
  <div class='ref'><img src='{_esc(bg_rel)}'>
    <p class='rlabel'>새 배경 · {_esc(bg_rel)}</p></div>
  {kept_html}
</div>
<details><summary>전송 프롬프트 (원본 콜 기반{
    ', 제외 참조 지시 ' + str(len(r['removed_prompt_lines'])) + '줄 제거'
    if r.get('removed_prompt_lines') else ' 그대로'})</summary>
<pre>{_esc(r['prompt'])}</pre></details>
""")
    PAGE.write_text(f"""<!doctype html><meta charset='utf-8'>
<title>s21 remake — 배경만 교체 재생성 비교</title>
<style>
body{{font-family:system-ui,'Apple SD Gothic Neo',sans-serif;margin:24px;
background:#fafafa;color:#222;max-width:1500px}}
h1{{font-size:20px}} h2{{font-size:16px;margin-top:32px;border-bottom:2px
solid #ddd;padding-bottom:4px}}
img{{width:100%;display:block;background:#fff;border:1px solid #ddd;
padding:3px;box-sizing:border-box}}
.pair{{display:grid;grid-template-columns:1fr 1fr;gap:12px}}
.cap{{font-size:12px;color:#333;margin:4px 0 10px;font-weight:600}}
.refrow{{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;
margin:6px 0}}
.rlabel{{font-size:10.5px;color:#666;margin:2px 0 0;line-height:1.3}}
.dropped img{{opacity:.35;border:2px dashed #c62828}}
.dropped .rlabel{{color:#c62828}}
pre{{white-space:pre-wrap;font-size:11px;background:#fff;border:1px solid
#e5e5e5;padding:10px}}
details{{margin:8px 0}} summary{{font-size:12px;color:#557;cursor:pointer}}
</style>
<h1>s21 remake 1차 — s20 최종본(계단 수정 stairfix_nb2)을 새 배경으로,
프롬프트·나머지 참조 동일 재생성 비교 ({len(results)}샷)</h1>
<p style='font-size:12.5px;color:#556'>좌=원본 최신 스틸 / 우=배경 참조만
교체한 재생성(nb2, 원본 콜 user_prompt 그대로). 아래 행=교체 전후 배경과
유지된 참조. 매핑=plans/outdoor_remake_bg.json</p>
{''.join(secs)}
""", encoding="utf-8")


if __name__ == "__main__":
    main()
