"""실험 — 도면을 line elevation 4분할로 거쳐 photorealistic 4분할까지 단계화.

가설: 도면(top-down) → 사진(eye-level) 직접 점프가 어려우니,
중간에 "선으로만 그린 4면 elevation"을 한 번 거치면 공간 토폴로지가 보존된다.

Step 1: 도면 → 4분할 line elevation (N/E/W/S, black on white, no shading)
Step 2: line elevation + 도면 → 4분할 photorealistic

사용:
  python scripts/experiment_line_elevation_quad.py \
    --plan-png <path> --out-dir <dir> [--canon "..."] [--run-dir <v4 run>]
"""
from __future__ import annotations

import argparse
import base64
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("line_quad")


def compact_canon_text(canon: dict) -> str:
    parts = []
    b = canon.get("building") or {}
    if b:
        bp = []
        for k in ("stories", "primary_material", "exterior_stairs", "rooftop_features",
                  "window_pattern", "weathering"):
            v = b.get(k)
            if v:
                if isinstance(v, list): v = ", ".join(map(str, v))
                bp.append(f"{k}={v}")
        if b.get("color_palette"):
            bp.append("colors=" + ", ".join(map(str, b["color_palette"])))
        if bp: parts.append("Building: " + "; ".join(bp))
    i = canon.get("interior") or {}
    if i:
        ip = []
        for k in ("wall_finish", "floor_finish", "ceiling",
                  "lighting_fixtures", "general_clutter_level"):
            v = i.get(k)
            if v:
                if isinstance(v, list): v = ", ".join(map(str, v))
                ip.append(f"{k}={v}")
        if ip: parts.append("Interior: " + "; ".join(ip))
    return ". ".join(parts).strip()


def line_quad_prompt(canon_text: str) -> str:
    return (
        "4-QUADRANT WALL ELEVATION line drawing of the interior room shown in the "
        "architectural floor plan reference image. Each quadrant shows ONE wall as if "
        "standing at the room's geometric center looking directly at that wall "
        "(orthographic frontal elevation, no perspective).\n\n"
        "Layout (2x2 grid, thin black grid dividers between quadrants):\n"
        "- TOP-LEFT quadrant: NORTH wall elevation, label 'N' in top-left corner.\n"
        "- TOP-RIGHT quadrant: EAST wall elevation, label 'E' in top-left corner.\n"
        "- BOTTOM-LEFT quadrant: WEST wall elevation, label 'W' in top-left corner.\n"
        "- BOTTOM-RIGHT quadrant: SOUTH wall elevation, label 'S' in top-left corner.\n\n"
        "Strict requirements:\n"
        "- PURE BLACK LINE on WHITE background. No shading, no hatching, no fill, no "
        "color, no texture, no gray tones. Architectural line drawing only.\n"
        "- Orthographic frontal projection — NO perspective, NO foreshortening, NO "
        "vanishing points. Each wall drawn flat as in elevation drawings.\n"
        "- Show as simple outlined contours: wall outline, floor line, ceiling line, "
        "door openings (with arc indicating swing if applicable), window frames with "
        "mullions, fixed built-ins (kitchen counter, bathroom sink, fixed shelving), "
        "and any furniture silhouettes that touch or sit against this wall (bed, "
        "table, chair, TV stand, etc.) viewed from the front.\n"
        "- Maintain EXACT wall length, door positions, window positions, and furniture "
        "placement consistent with the floor plan reference. Walls that are interior "
        "partitions in the plan should be drawn as the visible inner side from the room "
        "center.\n"
        "- Furniture in front of the wall: outline only, no internal detail. Show "
        "where it sits along the floor line.\n"
        "- Quadrant corner labels (N/E/W/S) in ENGLISH ONLY, small clean sans-serif.\n"
        "- No overlaid text other than the quadrant labels. No captions, no dimensions, "
        "no notes, no compass rose.\n\n"
        f"Canon (for furniture/fixture inventory only, not for style): "
        f"{canon_text or 'aged residential interior, modest domestic clutter'}"
    )


def photo_from_line_prompt(canon_text: str) -> str:
    return (
        "Photorealistic 4-QUADRANT COMPOSITE showing 4 different eye-level camera "
        "views of the SAME interior room, arranged in a 2x2 grid with thin black "
        "dividers between quadrants.\n\n"
        "Two reference images are provided:\n"
        "- FIRST reference: a 4-quadrant LINE ELEVATION drawing (architectural black-on-"
        "white line art) showing the exact wall layout, doors, windows, and furniture "
        "silhouettes for each of the 4 walls (N=TL, E=TR, W=BL, S=BR).\n"
        "- SECOND reference: the original architectural top-down FLOOR PLAN.\n\n"
        "Quadrant assignment (MUST match the line elevation reference exactly):\n"
        "- TOP-LEFT quadrant: photorealistic interior view of the NORTH wall — "
        "reproduce the silhouettes of doors, windows, furniture from the line "
        "reference TOP-LEFT, but rendered with materials, lighting, and depth. "
        "Label 'N' in the top-left corner.\n"
        "- TOP-RIGHT quadrant: photorealistic view of the EAST wall, matching line "
        "reference TOP-RIGHT. Label 'E'.\n"
        "- BOTTOM-LEFT quadrant: photorealistic view of the WEST wall, matching line "
        "reference BOTTOM-LEFT. Label 'W'.\n"
        "- BOTTOM-RIGHT quadrant: photorealistic view of the SOUTH wall, matching "
        "line reference BOTTOM-RIGHT. Label 'S'.\n\n"
        "Strict requirements:\n"
        "- Each quadrant must REPRODUCE the wall, door, window, and furniture "
        "positioning from the corresponding line-elevation quadrant. Do not invent "
        "additional walls, doors, or furniture.\n"
        "- Camera at room center, eye-level approx 1.6m, slight wide angle ~28mm.\n"
        "- Visual identity (wall finish, floor finish, ceiling, lighting fixtures, "
        "color palette, weathering) consistent across all 4 quadrants — same room, "
        "same daylight, just facing 4 different walls.\n"
        "- Photorealistic 35mm cinematic still aesthetic. Soft natural daylight from "
        "window mixed with dim domestic practicals (weak floor lamp, faint old "
        "television glow). Realistic shadows, subtle film grain, lived-in details "
        "(dust motes, scuff marks, subtle wear).\n"
        "- No people, no characters, empty room, uninhabited.\n"
        "- No overlaid text inside quadrants other than the corner labels. No diagram "
        "lines other than the 2x2 grid divider. No architectural label letters from "
        "the floor plan.\n"
        "- Quadrant corner labels (N/E/W/S) in ENGLISH ONLY, small clean sans-serif.\n\n"
        f"Canon: {canon_text or 'aged residential interior, modest domestic clutter'}"
    )


def edit_call(client: OpenAI, model: str, refs: list[Path], prompt: str,
              size: str, quality: str, out_path: Path) -> Path:
    logger.info("[image edit] %s + %d ref(s) → %s (%s, %s)",
                model, len(refs), out_path.name, size, quality)
    files = [open(p, "rb") for p in refs]
    try:
        resp = client.images.edit(
            model=model, image=files, prompt=prompt,
            size=size, quality=quality, n=1,
        )
    finally:
        for f in files:
            f.close()
    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("--out-dir", required=True)
    p.add_argument("--run-dir", default=None,
                   help="(선택) v4 run dir에서 canon 자동 추출")
    p.add_argument("--canon", default="")
    p.add_argument("--model", default="gpt-image-2")
    p.add_argument("--line-size", default="1024x1024")
    p.add_argument("--photo-size", default="1024x1024")
    p.add_argument("--quality", default="high")
    p.add_argument("--skip-line", action="store_true",
                   help="step1 line quad 건너뛰고 기존 line PNG 사용")
    p.add_argument("--line-png", default=None,
                   help="--skip-line 시 사용할 기존 line elevation PNG")
    args = p.parse_args()

    plan_png = Path(args.plan_png).resolve()
    if not plan_png.exists():
        logger.error("plan-png 없음: %s", plan_png); return 1
    out_dir = Path(args.out_dir).resolve()
    out_dir.mkdir(parents=True, exist_ok=True)

    canon_text = args.canon
    if not canon_text and args.run_dir:
        sp = Path(args.run_dir) / "step1_spatial.json"
        if sp.exists():
            spatial = json.loads(sp.read_text(encoding="utf-8"))
            canon_text = compact_canon_text(spatial.get("environment_canon") or {})
            logger.info("canon: %d chars", len(canon_text))

    if not os.getenv("OPENAI_API_KEY"):
        logger.error("OPENAI_API_KEY not set"); return 1
    client = OpenAI()

    # Step 1: line elevation quad
    line_p = out_dir / "line_elevation_quad.png"
    if args.skip_line and args.line_png:
        line_p = Path(args.line_png).resolve()
        logger.info("[step1 skip] reuse: %s", line_p)
    else:
        try:
            edit_call(client, args.model, [plan_png],
                      line_quad_prompt(canon_text),
                      args.line_size, args.quality, line_p)
        except Exception as e:
            logger.error("line quad failed: %s", e); return 1

    # Step 2: photo quad from line + plan
    photo_p = out_dir / "photo_quad_from_line.png"
    try:
        edit_call(client, args.model, [line_p, plan_png],
                  photo_from_line_prompt(canon_text),
                  args.photo_size, args.quality, photo_p)
    except Exception as e:
        logger.error("photo quad failed: %s", e)

    # prompts 저장
    (out_dir / "prompts.json").write_text(
        json.dumps({
            "line_quad": line_quad_prompt(canon_text),
            "photo_from_line": photo_from_line_prompt(canon_text),
            "canon": canon_text,
            "plan_png": str(plan_png),
        }, ensure_ascii=False, indent=2), encoding="utf-8"
    )
    logger.info("=== DONE === %s", out_dir)
    return 0


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