"""실험 — v3 도면 결과(JSON 메타 + 도면 PNG)를 입력으로 받아 **실사 배경 사진** 생성.

흐름:
  1. v3 run 디렉토리 로드 (step1_spatial.json + step2_base_plans.json + step3_shot_*.json)
  2. GPT-5.5 → 사진 T2I 프롬프트 JSON 생성
     - base 도면별 wide 사진 1장씩 (인물 없음)
     - 선택된 샷별 카메라 시점 사진 (인물 없음, 환경 디테일만)
  3. gpt-image-2 generate

인물·시체·혈흔 직접 표현 금지. 환경 디테일(쓰러진 의자/창문/벽 마크)만 묘사.
출력: <run-dir>/photos/ 또는 --out-dir 지정.
"""
from __future__ import annotations

import argparse
import base64
import json
import logging
import os
import sys
from pathlib import Path
from typing import Dict, Any, List

BACKEND = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(BACKEND))

from dotenv import load_dotenv  # noqa: E402
load_dotenv(BACKEND / ".env")

from openai import OpenAI  # noqa: E402

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger("plan_to_photo")


SYSTEM_PROMPT = """당신은 영화 미술·시네마토그래피 전문가입니다. 건축 도면 분석 데이터(공간 구조 + 가구/소품 메타 + 카메라 정보)를 받아 그 공간의 **실사 사진 스타일 T2I 프롬프트**를 작성합니다.

## 사진 스타일
- Photorealistic cinematic still, 35mm film aesthetic
- 자연 조명 (씬 데이터에 시간 단서가 있으면 반영: night/dusk/morning 등)
- 환경 디테일 풍부 (가구·소품·텍스처·먼지·생활감)
- **No people, no characters, no figures, empty space** — 무인 공간만

## 데이터 의존
- spatial/elements_meta/fixed_elements에 명시된 요소만 사용
- 임의 가구·인물·소품 추가 금지

## ⚠ T2I 프롬프트 — 고유명사 절대 금지 (CLAUDE.md 절대 규칙)
- 어떤 인명·공간 고유명·지역명·작품 명칭도 t2i_prompt 본문에 넣지 않는다.
  - ❌ 캐릭터 이름 (어떤 언어든, 한국어/영문/표기 어떻든)
  - ❌ 도시·국가·지역명, 작품 제목
  - ✅ 일반 명사: "a small bedroom", "a small rooftop residential apartment"
- 입력 메타에 인명·고유 공간명이 포함된 라벨이 보이면 (예: "<character>'s room", "<character> 방") 일반 명사로 변환:
  - 인명이 붙은 방 → "the small bedroom" / "the adjacent small bedroom"
  - 거실/주방 등 일반 명사 + 인명 보조 라벨 → 인명 부분만 제거
- 사진은 도면이 아니므로 그래픽 라벨도 그릴 필요 없다 (clean photo, no overlaid text).

## 안전 어휘 (이미지 generation moderation 회피)
- 인물 묘사 절대 금지: "no people", "empty room", "uninhabited"
- 폭력/사망/외상 직접 단어 금지: blood / body / corpse / victim / wound / gore / weapon / knife
- 환경 변경 표현은 허용 (단, 위험 조합 회피):
  - 쓰러진 의자/난장판: "overturned chair", "scattered household items", "ransacked appearance"
  - 벽 마크: "weathered red mark on wall", "circular stain", "faded ring shape"
  - 바닥 자국: "dark dried floor stain" (단어 minimal, 광택·번짐 묘사 금지)
  - 깨진 유리/창문: "broken window glass", "open window with curtain blown"
- 시간/조명: "dim interior at night", "fading dusk light", "first morning light" 등

## ⚠ 위험 조합 회피 (검열 deterministic 트리거)
다음 조합은 한 프롬프트에 동시에 등장 금지 — sexual/violence 분류 강하게 유발:
- "low angle through doorway/door gap" + "bedroom" + "floor stain"
- "doll" + "backpack" + "dim bedroom" + "stain/footprint"
- "open door" + "dark bedroom interior" + "reddish/rust stain"
대안: 카메라를 eye-level 또는 high angle로, 시점을 방 내부 또는 외부 와이드로, 인형/백팩 같은 미성년 연관 소품은 언급 최소화.

## 카메라 (샷 사진의 경우)
- 도면 step3의 camera position/heading/height/fov/lens_note를 사진 카메라 어휘로 변환
  - WS=wide shot, MS=medium shot, MCU=medium close-up, CU=close-up
  - low angle / eye level / high angle / overhead
  - handheld / static / dolly-in / static frame

## 출력 JSON (엄격)
```
{
  "base_photos": [
    {
      "id": "photo_<plan_id>",
      "source_plan_id": "string (step2 base_plans의 id)",
      "label": "string",
      "lighting": "string",
      "camera_note": "string (wide environment shot 등)",
      "t2i_prompt": "string (영문, gpt-image-2)"
    }
  ],
  "shot_photos": [
    {
      "id": "photo_S##_Shot#",
      "source_shot": {"scene_index": int, "shot_index": int},
      "source_plan_id": "string",
      "label": "string",
      "lighting": "string",
      "camera_note": "string (도면 camera 정보 사진 변환)",
      "t2i_prompt": "string (영문)"
    }
  ]
}
```

JSON만 출력. 추가 설명 금지.
"""


def load_run_dir(run_dir: Path) -> Dict[str, Any]:
    spatial = json.loads((run_dir / "step1_spatial.json").read_text(encoding="utf-8"))
    plans = json.loads((run_dir / "step2_base_plans.json").read_text(encoding="utf-8"))
    shots = []
    for p in sorted(run_dir.glob("step3_shot_*.json")):
        shots.append(json.loads(p.read_text(encoding="utf-8")))
    context = {}
    ctx_path = run_dir / "context.json"
    if ctx_path.exists():
        ctx = json.loads(ctx_path.read_text(encoding="utf-8"))
        context = {
            "scenes": ctx.get("scenes", []),
            "fixed_elements": ctx.get("fixed_elements", []),
            "locations": ctx.get("locations", []),
            "visual_world": ctx.get("visual_world", {}),
        }
    return {
        "spatial": spatial,
        "plans": plans,
        "shots": shots,
        "context": context,
    }


def build_user_message(payload: Dict[str, Any], shot_filter: List[str]) -> str:
    """LLM에게 전달할 메시지. 자르지 않음."""
    spatial = payload["spatial"]
    plans = payload["plans"]
    shots = payload["shots"]
    context = payload["context"]

    # 샷 필터 (예: ["S12:4","S25:2"]) — 빈 리스트면 모든 step3 사용
    if shot_filter:
        wanted = set()
        for s in shot_filter:
            sc, sh = s.split(":")
            wanted.add((int(sc), int(sh)))
        shots = [
            sh for sh in shots
            if (sh.get("scene_index"), sh.get("shot_index")) in wanted
        ]

    scenes = context.get("scenes", [])
    fixed = context.get("fixed_elements", [])

    return (
        "## v3 도면 결과 (자르지 않음)\n\n"
        "### Step 1 spatial_analysis\n```json\n"
        + json.dumps(spatial, ensure_ascii=False, indent=2)
        + "\n```\n\n"
        "### Step 2 base_plans (이미지가 이미 생성되어 있음 — 동일 공간을 사진으로 변환)\n```json\n"
        + json.dumps(plans, ensure_ascii=False, indent=2)
        + "\n```\n\n"
        f"### Step 3 shot overlays ({len(shots)}개)\n```json\n"
        + json.dumps(shots, ensure_ascii=False, indent=2)
        + "\n```\n\n"
        f"### 관련 씬 원문 ({len(scenes)}개, 시간/조명 단서)\n"
        + "\n\n".join(f"【{s['heading']}】\n{s['text']}" for s in scenes)
        + "\n\n"
        f"### fixed_elements ({len(fixed)}개)\n"
        + "\n".join(
            f"S{fe['scene_index']}: {fe.get('analysis_summary','')}\n"
            + "\n".join(
                f"  - [{e['element_type']}] {e.get('element_id')}: {e.get('description','')}"
                for e in fe.get("fixed_elements", [])
            )
            for fe in fixed
        )
        + "\n\n"
        "## 작업\n"
        "1) **base_photos**: Step 2의 base_plans 각각에 대해 wide 환경 사진 T2I 프롬프트 1개씩.\n"
        "2) **shot_photos**: 위 step3 shots 각각에 대해 그 샷 카메라 시점의 환경 사진 T2I 프롬프트.\n"
        "**모두 인물 없음**. 환경(가구·소품·벽 마크·창문·바닥 자국)만 묘사. 안전 어휘 엄격 준수.\n"
        "JSON만 출력."
    )


def gen_image(client: OpenAI, model: str, prompt: str, size: str, quality: str, out_path: Path) -> Path:
    logger.info("[image] %s → %s", model, out_path.name)
    resp = client.images.generate(model=model, prompt=prompt, size=size, quality=quality, n=1)
    if not resp.data or not getattr(resp.data[0], "b64_json", None):
        raise RuntimeError(f"empty/missing b64_json for {out_path.name}")
    out_path.write_bytes(base64.b64decode(resp.data[0].b64_json))
    logger.info("  saved %d KB", out_path.stat().st_size // 1024)
    return out_path


def parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(description="도면 → 실사 배경 사진 생성 실험 (시나리오 무관)")
    p.add_argument("--run-dir", required=True,
                   help="v3 run 디렉토리 (step1/step2/step3 JSON + context.json 포함)")
    p.add_argument("--shot-filter", nargs="*", default=[],
                   help='샷 필터, 예: "12:4 25:2". 빈 값이면 step3 전체 사용')
    p.add_argument("--out-dir", default=None,
                   help="출력 디렉토리 (기본: <run-dir>/photos/)")
    p.add_argument("--text-model", default="gpt-5.5")
    p.add_argument("--image-model", default="gpt-image-2")
    p.add_argument("--image-size", default="1536x1024",
                   help="배경 사진은 가로 긴 비율 권장")
    p.add_argument("--image-quality", default="high")
    p.add_argument("--skip-base", action="store_true", help="base_photos 생성 건너뛰기")
    p.add_argument("--skip-shots", action="store_true", help="shot_photos 생성 건너뛰기")
    return p.parse_args()


def main() -> int:
    args = parse_args()
    if not os.getenv("OPENAI_API_KEY"):
        logger.error("OPENAI_API_KEY not set"); return 1

    run_dir = Path(args.run_dir).resolve()
    if not run_dir.is_dir():
        logger.error("run-dir 없음: %s", run_dir); return 1

    out_dir = Path(args.out_dir) if args.out_dir else (run_dir / "photos")
    out_dir.mkdir(parents=True, exist_ok=True)
    logger.info("run_dir=%s out=%s", run_dir, out_dir)

    payload = load_run_dir(run_dir)
    logger.info("loaded: spatial.space_groups=%d plans=%d shots=%d scenes=%d",
                len(payload["spatial"].get("space_groups", [])),
                len(payload["plans"].get("base_plans", [])),
                len(payload["shots"]),
                len(payload["context"].get("scenes", [])))

    client = OpenAI()

    # LLM — 사진 T2I 프롬프트 생성
    user_msg = build_user_message(payload, args.shot_filter)
    logger.info("=== LLM (%s) — 사진 T2I 프롬프트 생성 ===", args.text_model)
    resp = client.chat.completions.create(
        model=args.text_model,
        messages=[{"role": "system", "content": SYSTEM_PROMPT},
                  {"role": "user", "content": user_msg}],
        response_format={"type": "json_object"},
    )
    photos = json.loads(resp.choices[0].message.content)
    (out_dir / "photo_prompts.json").write_text(
        json.dumps(photos, ensure_ascii=False, indent=2), encoding="utf-8"
    )
    logger.info("base_photos=%d shot_photos=%d",
                len(photos.get("base_photos", [])), len(photos.get("shot_photos", [])))

    # base_photos
    if not args.skip_base:
        for ph in photos.get("base_photos", []):
            pid = ph.get("id") or f"photo_{ph.get('source_plan_id','unknown')}"
            prompt = ph.get("t2i_prompt")
            if not prompt:
                logger.error("base photo skip — t2i_prompt 누락: %s", pid); continue
            try:
                gen_image(client, args.image_model, prompt,
                          args.image_size, args.image_quality, out_dir / f"{pid}.png")
            except Exception as e:
                logger.error("base photo failed (%s): %s", pid, e)

    # shot_photos
    if not args.skip_shots:
        for ph in photos.get("shot_photos", []):
            sid = ph.get("id")
            if not sid:
                src = ph.get("source_shot") or {}
                sid = f"photo_S{src.get('scene_index','?'):02d}_Shot{src.get('shot_index','?')}"
            prompt = ph.get("t2i_prompt")
            if not prompt:
                logger.error("shot photo skip — t2i_prompt 누락: %s", sid); continue
            try:
                gen_image(client, args.image_model, prompt,
                          args.image_size, args.image_quality, out_dir / f"{sid}.png")
            except Exception as e:
                logger.error("shot photo failed (%s): %s", sid, e)

    logger.info("=== DONE === %s", out_dir)
    return 0


if __name__ == "__main__":
    sys.exit(main())
