"""투룸 옥탑방 도면 v3 + S5_Shot3 + S25_Shot13 두 케이스 검증.

시나리오 분석 결과:
- L05는 단칸이 아닌 투룸 옥탑방 (안방=민숙 방 + 수리영 방 + 거실/주방 + 욕실 + 현관 + 옥상 외부).
- 도면 v1, v2 모두 단칸으로 그려져 잘못. v3는 multi-room 정확히 표현.

테스트 두 케이스:
  Variant F: S5_Shot3 (낮 / 거실 / 민숙이 TV 보는 순간)
    - chain bg 노드 'interior_living_kitchen_day_normal' 재생성
    - 도면 v3 + 새 chain bg + C04 + P06 + 기존 prompt
    - 검증: TV 이중 합성 회피 + 정확한 거실 layout

  Variant G: S25_Shot13 (해질 / 거실 disturbed / 수리영이 옥상으로 나가는 순간)
    - chain bg 노드 'interior_living_room_dusk_disturbed' 재생성
    - 도면 v3 + 새 chain bg + C01 + P01 + P04 + 기존 prompt
    - 검증: front door 너머가 옥상(rooftop concrete)인지 vs 베란다인지
"""
from __future__ import annotations

import base64
import json
import os
import socket
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path

try:
    from dotenv import load_dotenv
    load_dotenv(Path(__file__).parent.parent / ".env")
except ImportError:
    pass

from openai import OpenAI


PID = "c00bbe19-a9b5-463f-acfc-806f2e820258"
EID = "fe165e3a-19c2-4a0f-9acb-e0c9bab0ee5a"
PROJECT_ROOT = Path("/Users/manta/Documents/Projects/TheRoad-I1")
OUT_DIR = Path(__file__).parent / "experiment_results"

# S5_Shot3 references
C04_FACE = PROJECT_ROOT / "projects" / PID / "images" / EID / "reference" / "008b5d4c-5a02-4233-853e-ffc99225cfe1.png"
P06_REF = PROJECT_ROOT / "projects" / PID / "images" / EID / "reference" / "48735792-33c0-415b-afdf-46146fe19ed4.png"

# S25_Shot13 references
C01_FACE = PROJECT_ROOT / "projects" / PID / "images" / EID / "reference" / "aa3ea6a8-2dd8-4e10-a520-4964146baa7f.png"
P01_REF = PROJECT_ROOT / "projects" / PID / "images" / EID / "reference" / "2add2a95-eb2e-4365-8613-0ee619d2cf8b.png"
P04_REF = PROJECT_ROOT / "projects" / PID / "images" / EID / "reference" / "d73172cc-2acb-40be-98f4-302de81060ad.png"

# ────────── Step 1: 투룸 도면 v3 prompt ──────────
FLOORPLAN_V3_PROMPT = (
    "Top-down architectural floor plan of a Korean two-room rooftop apartment "
    "(투룸 옥탑방). Multi-room layout viewed directly from above. The whole "
    "apartment sits on top of a building roof — the front entry door opens "
    "outward to ROOFTOP CONCRETE FLOOR (NOT a balcony, NOT a street level, "
    "NOT a hallway). "
    "Layout (the apartment occupies most of the canvas; rooftop area is below):"
    "(1) LIVING/KITCHEN ROOM (largest, center): TV on a low stand against the "
    "left wall (label 'TV'), kitchen counter with sink along the upper "
    "(back) wall (label 'SINK'), small window with curtain to the right of "
    "the sink along the same back wall (label 'WINDOW'), wooden dining "
    "table with chairs in the center floor (label 'TABLE'). "
    "(2) MAIN BEDROOM (안방 — mother's room, top-right corner): bed inside "
    "(label 'MAIN BEDROOM'). Accessed via interior door from the living room. "
    "(3) DAUGHTER'S BEDROOM (수리영 방, right side, below main bedroom): bed "
    "inside, curtain over the back window of this room (label 'DAUGHTER "
    "BEDROOM'). Accessed via interior door from the living room. "
    "(4) BATHROOM (bottom-right corner of the apartment): mirror over sink "
    "(label 'BATHROOM'). Accessed via interior door from the living room. "
    "(5) ENTRYWAY (현관, lower-left of the apartment): a small space between "
    "the front entry door and the living room (label 'ENTRYWAY'). "
    "(6) FRONT ENTRY DOOR (lower wall of the apartment): a door swinging "
    "outward toward the rooftop, with arc symbol showing swing direction "
    "(label 'FRONT DOOR'). "
    "OUTSIDE THE APARTMENT (drawn at the very bottom of the canvas, below "
    "the entry door): a horizontal strip labeled 'ROOFTOP' showing concrete "
    "floor texture (parallel lines representing concrete tiles) and a thin "
    "metal railing along the bottom edge of the canvas (label 'ROOFTOP "
    "RAILING' or 'PARAPET'). This rooftop strip indicates the apartment is "
    "ON TOP OF A BUILDING and the entry door opens to outdoor rooftop, NOT "
    "a hallway or balcony. "
    "Floor surface inside the apartment: wooden floorboards indicated by "
    "light parallel hatching. "
    "Style: clean schematic architectural diagram, plain black ink lines on "
    "off-white paper, English labels in small caps placed clearly next to "
    "each room/feature, no perspective, no shading, no people."
)

# ────────── Step 2: chain bg 노드 1 (S5용 — 거실 day normal) ──────────
CHAIN_BG_LIVING_DAY_PROMPT = (
    "Photorealistic cinematic interior background of a Korean two-room "
    "rooftop apartment (투룸 옥탑방), pale daylight, no people, "
    "single static frame, 35mm film grain, eye-level wide angle. Match the "
    "architectural floor plan reference (top-down) EXACTLY for furniture "
    "and door positions. "
    "Camera is positioned in the LIVING/KITCHEN room near the front entry "
    "side, looking diagonally toward the back where the kitchen counter "
    "and the two interior bedroom doors are visible. "
    "Visible in this view: ONE television on a low wooden stand against "
    "the left wall (single TV — do NOT add a second TV); kitchen counter "
    "with stainless sink in the middle of the back wall; small window with "
    "thin curtain to the right of the sink; wooden dining table with two or "
    "three chairs in the center floor; the closed interior door to the main "
    "bedroom (안방) at the upper-right; the closed interior door to the "
    "daughter's bedroom (수리영 방) on the right wall further forward. "
    "The front entry door is offscreen behind the camera, ignore it. "
    "DO NOT show interior of bedrooms or bathroom — only the living/kitchen "
    "room is in this frame. The apartment is one rooftop apartment, NOT a "
    "multi-floor or multi-unit building. "
    "Material/lighting: dirty plaster walls in muted yellow-gray, worn "
    "wooden floorboards, low ceiling with a single bare bulb fixture, pale "
    "daylight from the small back window, soft warm tungsten ambient, dust "
    "in the air, damp domestic shadows, uneasy stillness, muted blue-gray "
    "and washed yellow palette."
)

# ────────── Step 3: chain bg 노드 2 (S25용 — 거실 dusk disturbed + 옥상 외부) ──────────
CHAIN_BG_LIVING_DUSK_DISTURBED_PROMPT = (
    "Photorealistic cinematic interior background of the same Korean "
    "two-room rooftop apartment (투룸 옥탑방), now at DUSK and in a "
    "disturbed/ransacked state. No people. Single static frame, 35mm film "
    "grain, eye-level wide angle. Match the architectural floor plan "
    "reference EXACTLY for furniture and door positions. "
    "Camera is positioned in the LIVING/KITCHEN room facing the FRONT "
    "ENTRY DOOR (which is now wide open). The view looks across the "
    "ransacked living room and OUT through the open front door to the "
    "ROOFTOP CONCRETE FLOOR with a thin metal railing in the far "
    "background — the door does NOT open to a balcony, hallway, or "
    "city view; it opens to a flat building rooftop with concrete tiles. "
    "Visible in this view: the SAME television on a low wooden stand "
    "against the left wall (single TV); the kitchen counter with sink "
    "in the back; the dining table now slightly askew with overturned "
    "chairs; scattered cushions, papers, and debris on the wooden "
    "floorboards; the open front entry door framing a rectangle of "
    "fading dusk sky over the rooftop concrete; the interior bedroom "
    "doors slightly visible in the background. "
    "DO NOT depict any balcony, balcony railing, distant cityscape with "
    "buildings far below, or street-level scenery. The exterior view "
    "through the door must be: flat rooftop concrete + simple parapet/"
    "railing + dusk sky. "
    "Material/lighting: muted plaster walls in cold blue-gray, worn "
    "wooden floorboards, fading dusk light leaking in through the open "
    "front door and a partially open back window, hollow underlit feel, "
    "uneasy stillness, muted blue-gray and washed yellow palette."
)

# ────────── Step 4: scene image prompts (DB에서 가져온 그대로) ──────────
S5_SHOT3_PROMPT = (
    "Photorealistic cinematic still. "
    "[L05: A cramped modern Korean 옥탑방 (rooftop room) interior, dusty small "
    "window, worn sink, low table, pale daylight, damp muted domestic shadows.] "
    "C04O06 in a plain apron occupies the right-center of the frame, at "
    "three-quarter angle from the left, one hand gripping the apron hem, eyes "
    "fixed on the television at the far left. The lens emphasizes her serious "
    "expression, tightened jaw, and the tense hand at the apron hem through a "
    "thin veil of white kitchen steam in the left foreground. A blurred "
    "television edge, screen angled away, sits at the far left, its cold "
    "blue-gray glow brushing her cheek; pale daylight from the closed dusty "
    "window adds washed yellow highlights. A steaming pot with a slightly "
    "lifted lid sits in the foreground, P06 rests rinsed beside the sink, and "
    "a thin stream from the faucet falls into the basin in the background. "
    "Muted blue-gray and washed yellow palette, damp domestic surfaces, uneasy "
    "stillness."
)

S25_SHOT13_PROMPT = (
    "Photorealistic cinematic still. From just inside the Oktapbang threshold "
    "at shoulder height, a young Korean woman in active casual clothes stands "
    "centered with back to camera, one foot beyond the open front door and "
    "the other still inside, P04 strapped to her shoulders. Her head is "
    "angled toward the doorway, gaze directed outside. Focus on the small "
    "creased P01 clenched low at her side near her thigh, its bloodstained "
    "partial front and edge visible, smaller than her hand. The front door is "
    "open away from the camera toward the exterior, slightly wind-tugged, "
    "forming a dark boundary around her silhouette. [L05: ransacked Oktapbang "
    "interior in fading dusk, open window, overturned chairs, murky blue-gray "
    "shadows, cramped domestic clutter receding toward the doorway] Cold "
    "evening light from the open window and doorway outlines her shoulders "
    "and backpack, preserving body volume against heavy dim shadows; muted "
    "blue-gray tones dominate the abandoned living area in the lower frame."
)

GEMINI_API_URL_TEMPLATE = (
    "https://generativelanguage.googleapis.com/v1beta/models/{model}:"
    "generateContent?key={api_key}"
)
GEMINI_MODEL = "gemini-3.1-flash-image-preview"
ASPECT_RATIO = "16:9"
TIMEOUT = 240
MAX_RETRIES = 2


def gemini_generate(api_key: str, prompt: str, input_images: list) -> bytes:
    parts: list = [{"text": prompt}]
    for label, img_bytes in input_images:
        if label:
            parts.append({"text": label})
        parts.append({
            "inline_data": {
                "mime_type": "image/png",
                "data": base64.b64encode(img_bytes).decode("ascii"),
            }
        })
    body = {
        "contents": [{"parts": parts}],
        "generationConfig": {
            "responseModalities": ["TEXT", "IMAGE"],
            "imageConfig": {"aspectRatio": ASPECT_RATIO, "imageSize": "2K"},
        },
    }
    url = GEMINI_API_URL_TEMPLATE.format(model=GEMINI_MODEL, api_key=api_key)
    req = urllib.request.Request(
        url, data=json.dumps(body).encode("utf-8"),
        headers={"Content-Type": "application/json"}, method="POST",
    )
    last = None
    for attempt in range(1, MAX_RETRIES + 2):
        try:
            with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
                payload = json.loads(resp.read().decode("utf-8"))
            break
        except urllib.error.HTTPError as exc:
            text = exc.read().decode("utf-8", errors="replace")
            last = RuntimeError(f"HTTP {exc.code}: {text}")
            if exc.code in {429, 500, 502, 503, 504} and attempt <= MAX_RETRIES:
                time.sleep(2 * attempt); continue
            raise last from exc
        except (urllib.error.URLError, socket.timeout) as exc:
            last = exc
            if attempt <= MAX_RETRIES:
                time.sleep(2 * attempt); continue
            raise RuntimeError(f"URL error: {exc}") from exc
    else:
        raise RuntimeError(f"Failed: {last}")
    pf = payload.get("promptFeedback", {})
    if pf.get("blockReason"):
        raise RuntimeError(f"Moderation: {pf['blockReason']}")
    cands = payload.get("candidates", [])
    if cands and cands[0].get("finishReason") == "SAFETY":
        raise RuntimeError("Safety filter")
    for cand in cands:
        for part in cand.get("content", {}).get("parts", []):
            inline = part.get("inlineData") or part.get("inline_data")
            if isinstance(inline, dict) and inline.get("data"):
                return base64.b64decode(inline["data"])
    raise RuntimeError(f"No image: {json.dumps(payload)[:300]}")


def gpt_generate(client: OpenAI, prompt: str, ref_path: Path = None) -> bytes:
    if ref_path:
        with open(ref_path, "rb") as f:
            resp = client.images.edit(model="gpt-image-2", image=f, prompt=prompt,
                                      size="1536x1024", quality="high", n=1)
    else:
        resp = client.images.generate(model="gpt-image-2", prompt=prompt,
                                      size="1024x1024", quality="high", n=1)
    return base64.b64decode(resp.data[0].b64_json)


def main():
    ok = os.environ.get("OPENAI_API_KEY", "")
    gk = os.environ.get("GEMINI_API_KEY", "")
    if not ok or not gk:
        print("ERROR: OPENAI_API_KEY + GEMINI_API_KEY required", file=sys.stderr)
        sys.exit(1)

    OUT_DIR.mkdir(exist_ok=True)
    client = OpenAI(api_key=ok)

    # Step 1: 도면 v3
    print("="*60); print("Step 1: 투룸 도면 v3"); print("="*60)
    fp_path = OUT_DIR / "30_floorplan_v3_tworoom.png"
    if fp_path.exists():
        print(f"reuse: {fp_path}")
    else:
        print(f"prompt: {len(FLOORPLAN_V3_PROMPT)} chars")
        fp_path.write_bytes(gpt_generate(client, FLOORPLAN_V3_PROMPT))
        print(f"OK: {fp_path} ({fp_path.stat().st_size:,} bytes)")
    print()

    # Step 2: chain bg 노드 1 (거실 day normal)
    print("="*60); print("Step 2: chain bg #1 (거실 day normal)"); print("="*60)
    bg1_path = OUT_DIR / "31_chain_bg_v3_living_day.png"
    if bg1_path.exists():
        print(f"reuse: {bg1_path}")
    else:
        print(f"prompt: {len(CHAIN_BG_LIVING_DAY_PROMPT)} chars")
        bg1_path.write_bytes(gpt_generate(client, CHAIN_BG_LIVING_DAY_PROMPT, fp_path))
        print(f"OK: {bg1_path} ({bg1_path.stat().st_size:,} bytes)")
    print()

    # Step 3: chain bg 노드 2 (거실 dusk disturbed + 옥상 외부)
    print("="*60); print("Step 3: chain bg #2 (거실 dusk disturbed + 옥상 외부)"); print("="*60)
    bg2_path = OUT_DIR / "32_chain_bg_v3_living_dusk_disturbed.png"
    if bg2_path.exists():
        print(f"reuse: {bg2_path}")
    else:
        print(f"prompt: {len(CHAIN_BG_LIVING_DUSK_DISTURBED_PROMPT)} chars")
        bg2_path.write_bytes(gpt_generate(client, CHAIN_BG_LIVING_DUSK_DISTURBED_PROMPT, fp_path))
        print(f"OK: {bg2_path} ({bg2_path.stat().st_size:,} bytes)")
    print()

    # Variant F: S5_Shot3
    print("="*60); print("Variant F: S5_Shot3 (낮 / 거실 / TV 시청)"); print("="*60)
    f_input = [
        ("spatial layout reference (floor plan, top-down) — two-room rooftop apartment",
         fp_path.read_bytes()),
        ("background reference (rebuilt from floor plan) — match all furniture and door positions",
         bg1_path.read_bytes()),
        ("character C04 identity", C04_FACE.read_bytes()),
        ("object P06", P06_REF.read_bytes()),
    ]
    f_path = OUT_DIR / "s5shot3_F_tworoom_v3.png"
    print(f"refs: {len(f_input)}, prompt: {len(S5_SHOT3_PROMPT)} chars")
    try:
        f_path.write_bytes(gemini_generate(gk, S5_SHOT3_PROMPT, f_input))
        print(f"OK: {f_path} ({f_path.stat().st_size:,} bytes)")
    except Exception as exc:
        print(f"FAIL: {exc}")
        f_path = None
    print()

    # Variant G: S25_Shot13
    print("="*60); print("Variant G: S25_Shot13 (해질 / 옥상 외부 / 수리영 나가는 순간)"); print("="*60)
    g_input = [
        ("spatial layout reference (floor plan, top-down) — two-room rooftop apartment "
         "with rooftop exterior beyond front door",
         fp_path.read_bytes()),
        ("background reference (rebuilt for dusk disturbed state) — front door opens to "
         "ROOFTOP CONCRETE FLOOR (not balcony, not street); reuse all interior furniture",
         bg2_path.read_bytes()),
        ("character C01 identity", C01_FACE.read_bytes()),
        ("object P01 (small bloodstained photograph)", P01_REF.read_bytes()),
        ("object P04 (backpack with doll)", P04_REF.read_bytes()),
    ]
    g_path = OUT_DIR / "s25shot13_G_tworoom_v3.png"
    print(f"refs: {len(g_input)}, prompt: {len(S25_SHOT13_PROMPT)} chars")
    try:
        g_path.write_bytes(gemini_generate(gk, S25_SHOT13_PROMPT, g_input))
        print(f"OK: {g_path} ({g_path.stat().st_size:,} bytes)")
    except Exception as exc:
        print(f"FAIL: {exc}")
        g_path = None
    print()

    # metadata
    meta = {
        "test": "투룸 옥탑방 도면 v3 + S5_Shot3 + S25_Shot13 검증",
        "step1_floorplan_v3": {"path": str(fp_path), "prompt_chars": len(FLOORPLAN_V3_PROMPT)},
        "step2_chain_bg_living_day": {"path": str(bg1_path), "prompt_chars": len(CHAIN_BG_LIVING_DAY_PROMPT)},
        "step3_chain_bg_living_dusk_disturbed": {"path": str(bg2_path), "prompt_chars": len(CHAIN_BG_LIVING_DUSK_DISTURBED_PROMPT)},
        "variant_F_S5_Shot3": {
            "result": str(f_path) if f_path else None,
            "refs": ["floor plan v3", "chain bg living day", "C04", "P06"],
            "prompt": S5_SHOT3_PROMPT,
        },
        "variant_G_S25_Shot13": {
            "result": str(g_path) if g_path else None,
            "refs": ["floor plan v3", "chain bg dusk disturbed", "C01", "P01", "P04"],
            "prompt": S25_SHOT13_PROMPT,
        },
    }
    (OUT_DIR / "tworoom_v3_metadata.json").write_text(
        json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8"
    )
    print(f"metadata: {OUT_DIR / 'tworoom_v3_metadata.json'}")


if __name__ == "__main__":
    main()
