"""실험 — GPT-5.x가 씬 내용으로 4 view 순서/prompt를 직접 설계 → gpt-image-2 chain (도면 ref 없이).

흐름:
  Step A. run_dir + base_plan_id로 도면 + base_photo + 관련 shots 수집
  Step B. GPT vision: 도면 PNG + scene → JSON {reasoning, atmosphere_canon, views[1..4]}
            views[i]: { order, label, description, t2i_prompt, uses_prev }
  Step C. gpt-image-2:
            - view 1: images.generate (텍스트 전용, reference 없음)
            - view 2,3,4: images.edit with [previous view image] as the ONLY reference (도면 ref 미사용)

가설: GPT가 prompt를 자기-완결형(self-contained)으로 만들어주면, 도면 없이도 chain만으로
       방의 일관성을 유지할 수 있는지 검증.
"""
from __future__ import annotations

import argparse
import base64
import glob
import json
import logging
import os
import sys
from pathlib import Path

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("gpt_planned_4view")


SYSTEM_PROMPT = """You are an architectural visualization specialist designing a 4-view photorealistic chain of an interior room.

Pipeline constraints (READ CAREFULLY):
- Image 1 will be generated TEXT-ONLY by gpt-image-2 (NO reference image at all).
- Images 2, 3, and 4 will be generated by gpt-image-2 image-edit with ONLY the immediately previous image as reference (NO floor plan reference, NO atmosphere image — just the previous photo).
- Therefore each t2i_prompt must be self-contained: describe enough of the room's atmosphere, materials, and lighting that the model can produce a coherent photo without ever seeing the floor plan.

Your job:
1. Decide the optimal ORDER of the 4 views to maximize cumulative consistency.
   Tips:
   - Image 1 should be the most distinctive/anchor-worthy direction (rich furniture, defining wall) so it pins the room identity.
   - Images 2-4 should rotate in a way that each view is spatially adjacent to the previous (shares a wall corner), so the model can reuse texture/lighting from the prev image naturally.
   - Avoid jumps that share no visible context (e.g., kitchen wall → opposite blank wall) — that breaks the chain.

2. For each view, output:
   - order: 1, 2, 3, or 4
   - label: short snake_case name (e.g., "kitchen_wall", "bed_corner", "entry_door_wall")
   - description: 2-3 sentences in English describing the camera direction, what walls/doors/furniture are in frame
   - t2i_prompt: full single-string prompt for gpt-image-2

Prompt rules:
- view 1 prompt: thoroughly describe the room atmosphere (wall finish, floor, ceiling, lighting, palette, lived-in details), plus the specific contents of this view (which doors/windows/furniture). 35mm cinematic still aesthetic, soft natural daylight + dim domestic practicals, realistic shadows, NO people, empty room.
- view 2/3/4 prompt: must include
   * a brief atmospheric anchor ("same room as the previous photo — match its wall finish, floor, ceiling, lighting tone, color palette, and overall material aging")
   * a clear rotation cue ("camera rotated NN degrees clockwise from the previous view; now facing X")
   * the specific contents of THIS view (doors/windows/furniture), DIFFERENT from the previous image
   * 35mm cinematic still + same atmospheric details + NO people

3. Provide:
   - reasoning: ONE paragraph explaining why this order maximizes consistency
   - atmosphere_canon: ONE paragraph distilling the wall/floor/ceiling/lighting/palette canon, used internally and visible in every view's t2i_prompt

STRICT RULES:
- ENGLISH ONLY in all prompts and any rendered text.
- COMMON NOUNS only — NEVER use proper names of characters, places, or productions from the scenes.
- Reflect scene atmosphere (lived-in, worn) but NO narrative events: no people, no blood, no broken glass, no action.
- Each view must show DIFFERENT content; no two views may look the same.
- Mention the rotation step explicitly in views 2-4.

Return strict JSON. No markdown fence, no commentary outside JSON.
"""


RESPONSE_SCHEMA = {
    "type": "object",
    "properties": {
        "reasoning": {"type": "string"},
        "atmosphere_canon": {"type": "string"},
        "views": {
            "type": "array",
            "minItems": 4,
            "maxItems": 4,
            "items": {
                "type": "object",
                "properties": {
                    "order": {"type": "integer"},
                    "label": {"type": "string"},
                    "description": {"type": "string"},
                    "t2i_prompt": {"type": "string"},
                    "uses_prev": {"type": "boolean"},
                },
                "required": ["order", "label", "description", "t2i_prompt", "uses_prev"],
                "additionalProperties": False,
            },
        },
    },
    "required": ["reasoning", "atmosphere_canon", "views"],
    "additionalProperties": False,
}


def collect_scene_context(run_dir: Path, base_plan_id: str) -> dict:
    ctx: dict = {"base_plan_id": base_plan_id, "base_plan": None,
                 "base_photo": None, "shots": [], "spatial": None}
    sp2 = run_dir / "step2_plan_specs.json"
    if sp2.exists():
        d = json.loads(sp2.read_text(encoding="utf-8"))
        for p in d.get("base_plans", []):
            if p.get("id") == base_plan_id:
                ctx["base_plan"] = p; break
    sp7 = run_dir / "step7_photo_specs.json"
    if sp7.exists():
        d = json.loads(sp7.read_text(encoding="utf-8"))
        for ph in d.get("base_photos", []):
            if ph.get("source_plan_id") == base_plan_id:
                ctx["base_photo"] = ph; break
    for f in sorted(glob.glob(str(run_dir / "step3_shot_*.json"))):
        s = json.loads(Path(f).read_text(encoding="utf-8"))
        if s.get("base_plan_id") == base_plan_id:
            ctx["shots"].append(s)
    sp1 = run_dir / "step1_spatial.json"
    if sp1.exists():
        ctx["spatial"] = json.loads(sp1.read_text(encoding="utf-8"))
    return ctx


def build_user_prompt(ctx: dict) -> str:
    parts: list[str] = []
    parts.append("== BASE PLAN SPEC ==")
    bp = ctx.get("base_plan") or {}
    parts.append(f"id: {bp.get('id')}")
    parts.append(f"label: {bp.get('label')}")
    parts.append(f"visual_domain: {bp.get('visual_domain')}")
    if bp.get("t2i_prompt"):
        parts.append(f"\nplan_drawing_prompt:\n{bp['t2i_prompt']}")
    if bp.get("legend"):
        parts.append(f"\nlegend: {json.dumps(bp['legend'], ensure_ascii=False)}")
    if bp.get("elements_meta"):
        parts.append(f"\nelements_meta: {json.dumps(bp['elements_meta'], ensure_ascii=False)}")

    parts.append("\n== BASE PHOTO SPEC (existing wall-by-wall annotation) ==")
    bph = ctx.get("base_photo") or {}
    if bph.get("camera_note"):
        parts.append(f"camera_note: {bph['camera_note']}")
    if bph.get("lighting"):
        parts.append(f"lighting: {bph['lighting']}")
    if bph.get("t2i_prompt"):
        parts.append(f"\nbase_photo_prompt:\n{bph['t2i_prompt']}")

    parts.append("\n== RELATED SHOTS ==")
    for s in ctx.get("shots", []):
        si = s.get("scene_index"); sx = s.get("shot_index")
        parts.append(f"\n--- Shot S{si:02d}_Shot{sx} ---")
        cam = s.get("camera") or {}
        parts.append(f"camera position: {cam.get('position', '(none)')}")
        parts.append(f"camera heading: {cam.get('heading', '(none)')}")
        parts.append(f"height/fov/lens: {cam.get('height')} / {cam.get('fov')} / {cam.get('lens_note')}")
        for a in s.get("additions", []):
            parts.append(f"  + [{a.get('type')}] @ {a.get('position', '?')}: {a.get('note', '')}")

    parts.append("\n== INTERIOR CANON ==")
    sp = ctx.get("spatial") or {}
    interior = (sp.get("environment_canon") or {}).get("interior") or {}
    if interior:
        parts.append(json.dumps(interior, ensure_ascii=False, indent=2))

    parts.append("\n== TASK ==")
    parts.append("Use the floor plan IMAGE (attached, for spatial reasoning ONLY) and ALL information above to plan the 4-view chain.")
    parts.append("Return JSON with: reasoning, atmosphere_canon, and 4 views (order 1..4 each with label/description/t2i_prompt/uses_prev).")
    return "\n".join(parts)


def call_gpt_vision(client: OpenAI, model: str, plan_png: Path,
                    system_prompt: str, user_prompt: str) -> dict:
    with open(plan_png, "rb") as f:
        b64 = base64.b64encode(f.read()).decode("ascii")
    resp = client.chat.completions.create(
        model=model,
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": [
                {"type": "text", "text": user_prompt},
                {"type": "image_url",
                 "image_url": {"url": f"data:image/png;base64,{b64}"}},
            ]},
        ],
        response_format={
            "type": "json_schema",
            "json_schema": {
                "name": "FourViewChainPlan",
                "schema": RESPONSE_SCHEMA,
                "strict": True,
            },
        },
    )
    txt = resp.choices[0].message.content or ""
    return json.loads(txt)


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


def gpt_image_edit_with_prev(client: OpenAI, model: str, prev_path: Path,
                             prompt: str, size: str, quality: str,
                             out_path: Path) -> Path:
    logger.info("[image edit] %s + prev[%s] → %s (%s, %s)",
                model, prev_path.name, out_path.name, size, quality)
    with open(prev_path, "rb") as f:
        resp = client.images.edit(
            model=model, image=[f], prompt=prompt,
            size=size, quality=quality, n=1,
        )
    b64 = resp.data[0].b64_json
    if not b64:
        raise RuntimeError(f"empty b64 for {out_path.name}")
    out_path.write_bytes(base64.b64decode(b64))
    logger.info("  saved %d KB", out_path.stat().st_size // 1024)
    return out_path


def main() -> int:
    p = argparse.ArgumentParser()
    p.add_argument("--plan-png", required=True)
    p.add_argument("--run-dir", required=True)
    p.add_argument("--base-plan-id", required=True)
    p.add_argument("--out-dir", required=True)
    p.add_argument("--text-model", default=os.getenv("OPENAI_MODEL", "gpt-5.5"))
    p.add_argument("--image-model", default="gpt-image-2")
    p.add_argument("--size", default="1024x1024")
    p.add_argument("--quality", default="high")
    p.add_argument("--skip-text", action="store_true")
    p.add_argument("--text-only", action="store_true")
    args = p.parse_args()

    plan_png = Path(args.plan_png).resolve()
    run_dir = Path(args.run_dir).resolve()
    out_dir = Path(args.out_dir).resolve()
    out_dir.mkdir(parents=True, exist_ok=True)

    if not os.getenv("OPENAI_API_KEY"):
        logger.error("OPENAI_API_KEY not set"); return 1
    client = OpenAI()
    prompts_p = out_dir / "prompts.json"

    # Step A + B
    if args.skip_text and prompts_p.exists():
        plan = json.loads(prompts_p.read_text(encoding="utf-8"))
        logger.info("[skip text] reusing prompts.json")
    else:
        ctx = collect_scene_context(run_dir, args.base_plan_id)
        if not ctx["base_plan"]:
            logger.error("base_plan_id 매칭 실패: %s", args.base_plan_id); return 1
        logger.info("collected: base_plan=ok, base_photo=%s, %d shots",
                    bool(ctx["base_photo"]), len(ctx["shots"]))
        user_prompt = build_user_prompt(ctx)
        logger.info("user prompt: %d chars; calling %s", len(user_prompt), args.text_model)
        plan = call_gpt_vision(client, args.text_model, plan_png,
                               SYSTEM_PROMPT, user_prompt)
        plan["_meta"] = {
            "base_plan_id": args.base_plan_id,
            "shot_count": len(ctx["shots"]),
            "text_model": args.text_model,
            "image_model": args.image_model,
            "size": args.size,
            "quality": args.quality,
        }
        prompts_p.write_text(json.dumps(plan, ensure_ascii=False, indent=2),
                             encoding="utf-8")
        logger.info("saved prompts.json")

    # Print for review
    print("\n" + "=" * 60)
    print("=== REASONING ===")
    print(plan.get("reasoning", "(missing)"))
    print("\n=== ATMOSPHERE CANON ===")
    print(plan.get("atmosphere_canon", "(missing)"))
    views = sorted(plan.get("views", []), key=lambda v: v.get("order", 99))
    for v in views:
        print(f"\n--- order={v.get('order')} label={v.get('label')} uses_prev={v.get('uses_prev')} ---")
        print(f"description: {v.get('description', '')}")
        print(f"t2i_prompt:\n{v.get('t2i_prompt', '')}")
    print()

    if args.text_only:
        logger.info("text-only mode → skip image generation"); return 0

    # Step C: chain image generation
    prev_path: Path | None = None
    for v in views:
        i = v.get("order")
        label = v.get("label", f"view_{i}")
        prompt = v.get("t2i_prompt", "")
        if not prompt:
            logger.error("missing t2i_prompt for order=%s — skip", i); continue
        out_p = out_dir / f"{i:02d}_{label}.png"

        if prev_path is None:
            gpt_image_generate(client, args.image_model, prompt,
                               args.size, args.quality, out_p)
        else:
            gpt_image_edit_with_prev(client, args.image_model, prev_path,
                                     prompt, args.size, args.quality, out_p)
        prev_path = out_p

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


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