"""Spike Test 1 — 공통 옥탑방 base floor plan spec.json LLM 출력 검증.

목적: GPT-5.5 + Gemini-3.1-pro에게 옥탑방 씬 텍스트 + selected shot description 전달
      → spec.json (rooms polygon + doors + furniture) 출력
      → schema validation + 좌표 합리성 검증

비교 대상: 실험 v3 수동 도면 (FLOORPLAN_V3_PROMPT) — 사람이 시나리오 분석 + 3차례 정제한 결과.
        LLM이 그 수준의 정확도로 spec.json을 한 번에 만들어내는지가 핵심 검증 항목.

실행: backend cwd에서
    .venv/bin/python scripts/spike_floor_plan_svg/test_01_base_floor_plan.py
"""
from __future__ import annotations

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

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


PID = "c00bbe19-a9b5-463f-acfc-806f2e820258"
EID = "fe165e3a-19c2-4a0f-9acb-e0c9bab0ee5a"
ROOT = Path("/Users/manta/Documents/Projects/TheRoad-I1")
CKPT = ROOT / "projects" / PID / "checkpoints" / "episodes" / EID
OUT = Path(__file__).parent / "results"
OUT.mkdir(parents=True, exist_ok=True)

ROOFTOP_SCENES = (5, 12, 14, 18, 25, 27)


# ───────── spec.json schema ─────────

SPEC_SCHEMA: Dict[str, Any] = {
    "type": "object",
    "properties": {
        "schema_version": {"type": "integer"},
        "location_id": {"type": "string"},
        "location_type": {
            "type": "string",
            "description": "Generic (no scenario words). e.g., 'rooftop_apartment_two_room'",
        },
        "canvas": {
            "type": "object",
            "description": "Logical coordinate canvas. Top-down view. Origin (0,0) at bottom-left.",
            "properties": {
                "width": {"type": "number"},
                "height": {"type": "number"},
                "unit": {"type": "string", "description": "e.g., 'meters' or 'logical'"},
            },
            "required": ["width", "height", "unit"],
            "additionalProperties": False,
        },
        "rooms": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "id": {"type": "string", "description": "snake_case, e.g., 'living'"},
                    "label_ko": {"type": "string"},
                    "label_en": {"type": "string"},
                    "polygon": {
                        "type": "array",
                        "description": "ordered vertices [[x,y],...] forming closed polygon",
                        "items": {
                            "type": "array",
                            "items": {"type": "number"},
                            "minItems": 2,
                            "maxItems": 2,
                        },
                        "minItems": 3,
                    },
                },
                "required": ["id", "label_ko", "label_en", "polygon"],
                "additionalProperties": False,
            },
        },
        "doors": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "id": {"type": "string"},
                    "from_room": {"type": "string", "description": "room.id or 'exterior'"},
                    "to_room": {"type": "string", "description": "room.id or 'exterior'"},
                    "position": {
                        "type": "array",
                        "items": {"type": "number"},
                        "minItems": 2,
                        "maxItems": 2,
                    },
                    "kind": {
                        "type": "string",
                        "enum": ["interior", "front_entry", "window"],
                    },
                },
                "required": ["id", "from_room", "to_room", "position", "kind"],
                "additionalProperties": False,
            },
        },
        "furniture": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "id": {"type": "string"},
                    "type": {
                        "type": "string",
                        "description": "generic: tv, sofa, table, sink, bed, mirror, toilet, curtain, etc.",
                    },
                    "room": {"type": "string"},
                    "wall": {
                        "type": "string",
                        "enum": ["left", "right", "top", "bottom", "interior", ""],
                    },
                    "position": {
                        "type": "array",
                        "items": {"type": "number"},
                        "minItems": 2,
                        "maxItems": 2,
                    },
                    "size": {
                        "type": "array",
                        "items": {"type": "number"},
                        "minItems": 2,
                        "maxItems": 2,
                    },
                    "label_en": {"type": "string"},
                },
                "required": ["id", "type", "room", "wall", "position", "size", "label_en"],
                "additionalProperties": False,
            },
        },
        "exterior_adjacency_zones": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "id": {"type": "string"},
                    "type": {
                        "type": "string",
                        "description": "e.g., 'rooftop_concrete', 'street', 'balcony'",
                    },
                    "polygon": {
                        "type": "array",
                        "items": {
                            "type": "array",
                            "items": {"type": "number"},
                            "minItems": 2,
                            "maxItems": 2,
                        },
                        "minItems": 3,
                    },
                    "connection_door": {"type": "string", "description": "door.id"},
                    "label_en": {"type": "string"},
                },
                "required": ["id", "type", "polygon", "connection_door", "label_en"],
                "additionalProperties": False,
            },
        },
        "rejected_layouts": {
            "type": "array",
            "items": {"type": "string"},
            "description": "Layout patterns that contradict the scenario. Avoid generating these.",
        },
    },
    "required": [
        "schema_version",
        "location_id",
        "location_type",
        "canvas",
        "rooms",
        "doors",
        "furniture",
        "exterior_adjacency_zones",
        "rejected_layouts",
    ],
    "additionalProperties": False,
}


# ───────── Prompt ─────────

SYSTEM_PROMPT = """You are an architectural layout planner for cinematic scene rendering.

Given the screenplay scenes that occur in a single shooting LOCATION, output a structured `floor_plan_spec` JSON describing the location's top-down geometry.

CRITICAL RULES:
1. Coordinate system: top-down view. Origin (0,0) at bottom-left. X increases rightward, Y increases upward. Use meters as units.
2. Output ONLY structured JSON conforming to the schema. No prose.
3. Do NOT use any scenario-specific proper nouns (character names, place names, work titles) in id/label_en/type/rejected_layouts. Korean labels (label_ko) MAY use Korean common nouns (e.g., '거실', '안방') but never proper nouns.
4. Every furniture item must lie INSIDE its assigned room polygon.
5. Every door must lie ON a room boundary edge or wall.
6. exterior_adjacency_zones describe what is OUTSIDE the building near a front_entry door (e.g., rooftop concrete floor, street, balcony).
7. rejected_layouts: list 2~5 layout patterns that the scenario explicitly contradicts. Examples: "single-room studio (the scenario shows separate bedrooms)", "balcony connection (the scenario shows rooftop access)".
8. Read every scene/shot carefully. Identify zone markers (e.g., '/거실', '/안방', '/욕실', '/현관'). The presence of those markers means the location has those zones.
9. Identify external connection from front entry: is it a rooftop? hallway? street? — match the scenario.

OUTPUT REQUIREMENTS:
- 5~7 rooms typically (interior zones)
- 1 front_entry door + N interior doors
- 6~12 furniture items minimum (the visible/important ones from the scenes/shots)
- 1+ exterior_adjacency_zones if front entry leads outside
"""


def _build_user_prompt() -> str:
    """옥탑방 씬 텍스트 + selected shot description 빌드."""
    scene_save = json.loads((CKPT / "scene_save" / "manifest.json").read_text())
    sv = json.loads((CKPT / "shot_validator" / "manifest.json").read_text())["data"]
    ss = json.loads((CKPT / "shot_selection" / "manifest.json").read_text())["data"]

    segs = scene_save["data"]["segments"]
    sel_map = {s["scene_index"]: set(s.get("selected_shot_indices", []))
               for s in ss.get("scenes", [])}

    parts: List[str] = [
        "LOCATION ID: L05",
        "LOCATION DESCRIPTION (from scenario): a Korean rooftop apartment that the family lives in.",
        "",
        "── ALL SCENES OCCURRING IN THIS LOCATION ──",
        "",
    ]
    for seg in segs:
        if seg["scene_index"] in ROOFTOP_SCENES:
            parts.append(f"### Scene {seg['scene_index']} — {seg.get('heading', '')}")
            parts.append(seg.get("text", ""))
            parts.append("")

    parts.append("── SELECTED SHOTS IN THIS LOCATION (visible elements) ──")
    parts.append("")
    for s in sv.get("scenes", []):
        si = s["scene_index"]
        if si not in ROOFTOP_SCENES:
            continue
        sel = sel_map.get(si, set())
        parts.append(f"### Scene {si} selected shots:")
        for sh in s.get("shots", []):
            if sh["shot_index"] in sel:
                parts.append(f"- Shot {sh['shot_index']}: {sh.get('description','')}")
        parts.append("")

    parts.append("")
    parts.append("Now output the floor_plan_spec JSON for this location.")
    return "\n".join(parts)


# ───────── LLM clients ─────────

def call_gpt(user_prompt: str) -> Dict[str, Any]:
    from openai import OpenAI
    client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
    resp = client.chat.completions.create(
        model="gpt-5.5",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user_prompt},
        ],
        response_format={
            "type": "json_schema",
            "json_schema": {
                "name": "floor_plan_spec",
                "schema": SPEC_SCHEMA,
                "strict": True,
            },
        },
    )
    return json.loads(resp.choices[0].message.content)


def call_gemini(user_prompt: str) -> Dict[str, Any]:
    """Gemini REST API 직접 호출 — production 패턴 (gemini_text_client.py)과 동일."""
    api_key = os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY")
    if not api_key:
        raise RuntimeError("GEMINI_API_KEY (or GOOGLE_API_KEY) not set")
    model = os.environ.get("GEMINI_TEXT_MODEL", "gemini-3.1-pro-preview")
    url = (
        f"https://generativelanguage.googleapis.com/v1beta/models/{model}"
        f":generateContent?key={api_key}"
    )
    body: Dict[str, Any] = {
        "systemInstruction": {"parts": [{"text": SYSTEM_PROMPT}]},
        "contents": [{"role": "user", "parts": [{"text": user_prompt}]}],
        "generationConfig": {
            "temperature": 0.2,
            "responseMimeType": "application/json",
            "responseJsonSchema": SPEC_SCHEMA,
            "maxOutputTokens": 16384,
        },
    }
    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"))
    text = payload["candidates"][0]["content"]["parts"][0]["text"]
    return json.loads(text)


# ───────── Validation ─────────

def _point_in_polygon(point, polygon) -> bool:
    """Ray casting algorithm."""
    x, y = point
    inside = False
    n = len(polygon)
    j = n - 1
    for i in range(n):
        xi, yi = polygon[i]
        xj, yj = polygon[j]
        if ((yi > y) != (yj > y)) and (x < (xj - xi) * (y - yi) / (yj - yi + 1e-12) + xi):
            inside = not inside
        j = i
    return inside


def validate_spec(spec: Dict[str, Any]) -> List[str]:
    """좌표 합리성 검증. 위반 시 경고 list 반환 (빈 list = 통과)."""
    warnings: List[str] = []
    rooms = {r["id"]: r for r in spec.get("rooms", [])}

    # 1) 가구가 room 안에 있는지
    for f in spec.get("furniture", []):
        room_id = f.get("room")
        if room_id not in rooms:
            warnings.append(f"furniture '{f['id']}' room='{room_id}' not in rooms")
            continue
        polygon = rooms[room_id]["polygon"]
        if not _point_in_polygon(f["position"], polygon):
            warnings.append(
                f"furniture '{f['id']}' at {f['position']} OUTSIDE room '{room_id}'"
            )

    # 2) 문이 room polygon edge 위 또는 근처(<0.5m)에 있는지
    for d in spec.get("doors", []):
        for room_label in (d.get("from_room"), d.get("to_room")):
            if room_label == "exterior":
                continue
            if room_label not in rooms:
                warnings.append(f"door '{d['id']}' room='{room_label}' not in rooms")

    # 3) room polygon 자기 교차 검사 (간단히 vertex 수 ≥ 3)
    for r in spec.get("rooms", []):
        if len(r["polygon"]) < 3:
            warnings.append(f"room '{r['id']}' polygon has <3 vertices")

    # 4) front_entry door 1개 이상
    n_front = sum(1 for d in spec.get("doors", []) if d.get("kind") == "front_entry")
    if n_front == 0:
        warnings.append("no front_entry door")
    elif n_front > 1:
        warnings.append(f"multiple ({n_front}) front_entry doors")

    return warnings


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

def main() -> int:
    user_prompt = _build_user_prompt()
    (OUT / "user_prompt.txt").write_text(user_prompt, encoding="utf-8")
    print(f"[INFO] user_prompt 길이: {len(user_prompt)}자 → {OUT/'user_prompt.txt'}")

    print("[INFO] LLM 병렬 호출 (GPT-5.5 + Gemini-3.1-pro)...")
    with concurrent.futures.ThreadPoolExecutor(max_workers=2) as ex:
        f_gpt = ex.submit(call_gpt, user_prompt)
        f_gem = ex.submit(call_gemini, user_prompt)
        results: Dict[str, Dict[str, Any] | str] = {}
        for name, fut in (("gpt", f_gpt), ("gemini", f_gem)):
            try:
                results[name] = fut.result(timeout=600)
            except Exception as exc:
                results[name] = f"ERROR: {type(exc).__name__}: {exc}"

    for name, spec in results.items():
        out_path = OUT / f"spec_{name}.json"
        if isinstance(spec, dict):
            out_path.write_text(json.dumps(spec, ensure_ascii=False, indent=2), encoding="utf-8")
            warns = validate_spec(spec)
            n_rooms = len(spec.get("rooms", []))
            n_doors = len(spec.get("doors", []))
            n_furn = len(spec.get("furniture", []))
            n_ext = len(spec.get("exterior_adjacency_zones", []))
            n_rej = len(spec.get("rejected_layouts", []))
            print(f"\n[{name.upper()}] OK → {out_path}")
            print(f"  rooms={n_rooms}  doors={n_doors}  furniture={n_furn}  exterior={n_ext}  rejected={n_rej}")
            print(f"  validation warnings: {len(warns)}")
            for w in warns:
                print(f"    - {w}")
        else:
            print(f"\n[{name.upper()}] {spec}")
            (OUT / f"spec_{name}_error.txt").write_text(str(spec), encoding="utf-8")

    return 0


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