"""Spike Test 5 (Variant F) — 좌표 array (10×10 grid bbox) in prompt.

가설: 가장 단순한 좌표 array 형식. 모델이 학습한 코드/JSON 형식.
      예: TV: [1, 2, 3, 1] = bbox cols 1-3 row 1.

대상: chain bg 노드 interior_living_kitchen_day_normal.
모델: gpt-image-2 + Gemini nano-banana-2 (text-to-image).
"""
from __future__ import annotations

import base64
import concurrent.futures
import json
import os
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any, Dict, List, Tuple

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

from openai import OpenAI


HERE = Path(__file__).parent
RESULTS = HERE / "results"
RESULTS.mkdir(parents=True, exist_ok=True)

GRID_COLS = 10
GRID_ROWS = 10


# ───────── chain bg prompt (variant F와 동일) ─────────

CHAIN_BG_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. "
    "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. "
    "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."
)


# ───────── spec.json → 좌표 array DSL ─────────

def _bbox(polygon: List[List[float]]) -> Tuple[float, float, float, float]:
    xs = [p[0] for p in polygon]
    ys = [p[1] for p in polygon]
    return min(xs), min(ys), max(xs), max(ys)


def spec_to_coord_array(
    spec: Dict[str, Any],
    grid_cols: int = GRID_COLS,
    grid_rows: int = GRID_ROWS,
) -> str:
    """spec.json → 10×10 grid 좌표 array 텍스트."""
    canvas = spec.get("canvas", {"width": 10, "height": 10})
    cw = float(canvas["width"])
    ch = float(canvas["height"])

    # extend bbox for exterior
    xmin, ymin, xmax, ymax = 0.0, 0.0, cw, ch
    for ez in spec.get("exterior_adjacency_zones", []):
        for x, y in ez.get("polygon", []):
            xmin = min(xmin, x)
            ymin = min(ymin, y)
            xmax = max(xmax, x)
            ymax = max(ymax, y)

    span_x = xmax - xmin
    span_y = ymax - ymin

    def to_grid(x: float, y: float) -> Tuple[int, int]:
        c = int((x - xmin) / span_x * grid_cols)
        r = int((y - ymin) / span_y * grid_rows)
        c = max(0, min(grid_cols - 1, c))
        r = max(0, min(grid_rows - 1, r))
        return c, r

    def bbox_to_grid(x1: float, y1: float, x2: float, y2: float) -> Tuple[int, int, int, int]:
        c1, r1 = to_grid(x1, y1)
        c2, r2 = to_grid(x2, y2)
        return min(c1, c2), min(r1, r2), max(c1, c2), max(r1, r2)

    parts: List[str] = []
    parts.append(
        f"# Grid: {grid_cols} cols × {grid_rows} rows (top-down view, north up).\n"
        f"# Cell coords: [col_start, row_start, col_end, row_end] (bbox, inclusive).\n"
        f"# Each cell = {span_x/grid_cols:.2f}m × {span_y/grid_rows:.2f}m.\n"
        f"# Grid covers x=[{xmin},{xmax}], y=[{ymin},{ymax}] in meters.\n"
    )

    # ROOMS
    parts.append("ROOMS:")
    for room in spec.get("rooms", []):
        x1, y1, x2, y2 = _bbox(room["polygon"])
        gb = bbox_to_grid(x1, y1, x2, y2)
        label_en = room.get("label_en", room["id"])
        label_ko = room.get("label_ko", "")
        parts.append(f"  {room['id']:24s}: {list(gb)}    # {label_en} ({label_ko})")

    # EXTERIOR
    if spec.get("exterior_adjacency_zones"):
        parts.append("\nEXTERIOR (outside the apartment, beyond front entry):")
        for ez in spec["exterior_adjacency_zones"]:
            poly = ez.get("polygon", [])
            if poly:
                x1, y1, x2, y2 = _bbox(poly)
                gb = bbox_to_grid(x1, y1, x2, y2)
                parts.append(f"  {ez['id']:24s}: {list(gb)}    # {ez.get('type','')}")

    # FURNITURE
    parts.append("\nFURNITURE:")
    for f in spec.get("furniture", []):
        fx, fy = f["position"]
        sw, sh = f["size"]
        gb = bbox_to_grid(fx - sw / 2, fy - sh / 2, fx + sw / 2, fy + sh / 2)
        ftype = f.get("type", "")
        room = f.get("room", "")
        wall = f.get("wall", "")
        parts.append(f"  {ftype:14s}: {list(gb)}    # in {room}, on {wall} wall" if wall else
                     f"  {ftype:14s}: {list(gb)}    # in {room}")

    # DOORS
    parts.append("\nDOORS (point coords [col, row]):")
    for d in spec.get("doors", []):
        c, r = to_grid(*d["position"])
        kind = d.get("kind", "interior")
        from_r = d.get("from_room", "")
        to_r = d.get("to_room", "")
        parts.append(f"  {kind:14s}: [{c}, {r}]      # {from_r} ↔ {to_r}")

    return "\n".join(parts)


# ───────── Build variant F prompt ─────────

def build_variant_f_prompt(coord_text: str) -> str:
    return (
        "[ARCHITECTURAL FLOOR PLAN — coordinate array, top-down view, "
        "use as authoritative spatial layout]\n"
        f"{coord_text}\n\n"
        "How to read these coordinates:\n"
        f"  - The apartment is mapped onto a {GRID_COLS}×{GRID_ROWS} grid.\n"
        "  - Origin (0,0) at bottom-left. col increases rightward (east), row increases upward (north).\n"
        "  - For ROOMS and FURNITURE, [c1, r1, c2, r2] = bbox in grid cells (inclusive).\n"
        "  - For DOORS, [c, r] = single cell where the door is located.\n"
        "  - 'LEFT wall' of a room = column c1 (west edge), 'RIGHT wall' = column c2 (east edge),\n"
        "    'BACK wall' = row r2 (north edge, far from camera in this view), \n"
        "    'FRONT wall' = row r1 (south edge, closer to camera).\n\n"
        "[RENDER INSTRUCTIONS]\n"
        + CHAIN_BG_PROMPT
        + "\n\nMatch the coordinate array EXACTLY. The TV must occupy the cells "
        "specified as 'tv' bbox (which is on the LEFT wall of the living room). "
        "The kitchen sink must occupy the 'sink' bbox (BACK wall of living). "
        "The dining table must occupy the 'table' bbox (CENTER of living). "
        "Bedroom doors at the cells specified.\n\n"
        "Render as a single photorealistic eye-level cinematic frame from the "
        "camera position described above (NOT a top-down view, NOT a grid)."
    )


# ───────── gpt-image-2 ─────────

def call_gpt_image_2(prompt: str) -> bytes:
    client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
    resp = client.images.generate(
        model="gpt-image-2",
        prompt=prompt,
        size="1536x1024",
        quality="high",
        n=1,
    )
    return base64.b64decode(resp.data[0].b64_json)


# ───────── Gemini nano-banana-2 ─────────

GEMINI_API_URL = (
    "https://generativelanguage.googleapis.com/v1beta/models/"
    "gemini-3.1-flash-image-preview:generateContent?key={key}"
)


def call_gemini_nano_banana(prompt: str) -> bytes:
    api_key = os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY")
    body = {
        "contents": [{"parts": [{"text": prompt}]}],
        "generationConfig": {
            "responseModalities": ["TEXT", "IMAGE"],
            "imageConfig": {"aspectRatio": "16:9", "imageSize": "2K"},
        },
    }
    url = GEMINI_API_URL.format(key=api_key)
    last_err: Exception | None = None
    for attempt in range(1, 4):
        try:
            req = urllib.request.Request(
                url,
                data=json.dumps(body).encode("utf-8"),
                headers={"Content-Type": "application/json"},
                method="POST",
            )
            with urllib.request.urlopen(req, timeout=600) as resp:
                payload = json.loads(resp.read().decode("utf-8"))
            for part in payload["candidates"][0]["content"]["parts"]:
                if "inline_data" in part:
                    return base64.b64decode(part["inline_data"]["data"])
                if "inlineData" in part:
                    return base64.b64decode(part["inlineData"]["data"])
            raise RuntimeError("no image part in response")
        except urllib.error.HTTPError as exc:
            last_err = RuntimeError(f"{exc.code} {exc.read().decode('utf-8', errors='replace')[:300]}")
            if exc.code in {429, 500, 502, 503, 504} and attempt < 3:
                time.sleep(2 * attempt)
                continue
            raise last_err from exc
        except Exception as exc:
            last_err = exc
            if attempt < 3:
                time.sleep(2 * attempt)
                continue
            raise
    raise RuntimeError(f"Gemini failed after retries: {last_err}")


# ───────── Main ─────────

def main() -> int:
    spec_path = RESULTS / "spec_gpt.json"
    if not spec_path.exists():
        print(f"[ERROR] {spec_path} not found.")
        return 1
    spec = json.loads(spec_path.read_text())

    coord_text = spec_to_coord_array(spec)
    (RESULTS / "spec_gpt_coord_array.txt").write_text(coord_text, encoding="utf-8")
    print(f"[INFO] coord array text: {len(coord_text)}자")
    print(f"\n--- COORD ARRAY ---")
    print(coord_text)

    prompt = build_variant_f_prompt(coord_text)
    (RESULTS / "variant_f_prompt.txt").write_text(prompt, encoding="utf-8")
    print(f"\n[INFO] variant F prompt: {len(prompt)}자")

    print("\n[INFO] gpt-image-2 + Gemini nano-banana-2 병렬 호출...")
    with concurrent.futures.ThreadPoolExecutor(max_workers=2) as ex:
        f_gpt = ex.submit(call_gpt_image_2, prompt)
        f_gem = ex.submit(call_gemini_nano_banana, prompt)
        results: Dict[str, Tuple[str, bytes | str]] = {}
        for name, fut in (("gpt-image-2", f_gpt), ("nano-banana-2", f_gem)):
            try:
                img_bytes = fut.result(timeout=600)
                results[name] = ("ok", img_bytes)
            except Exception as exc:
                results[name] = ("error", f"{type(exc).__name__}: {exc}")

    for name, (status, payload) in results.items():
        slug = name.replace("-", "_").replace(".", "")
        if status == "ok":
            out = RESULTS / f"variant_f_{slug}.png"
            out.write_bytes(payload)
            print(f"[OK] {name} → {out} ({len(payload):,} bytes)")
        else:
            err_path = RESULTS / f"variant_f_{slug}_error.txt"
            err_path.write_text(str(payload), encoding="utf-8")
            print(f"[ERROR] {name}: {payload}")

    return 0


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