#!/usr/bin/env python3
"""
Gemini vs GPT Image 1.5 비교 — 카메라 각도 중심 5개 씬.

실행: cd backend && .venv/bin/python tests/compare_t2i_models.py
결과: projects/t2i_comparison/ 에 이미지 + HTML
"""

import base64
import json
import os
import sys
import time
import urllib.request
import urllib.error
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
# 2026-04-27 hotfix: prod URL setdefault 제거. 단독 실행 시 prod DB가 의도하지 않은
# 변경에 노출될 위험이 있어 sqlite default로 변경. prod DB가 필요하면 명시적으로
# export DATABASE_URL=postgresql://... 후 실행할 것.
import tempfile as _tmp_default
os.environ.setdefault("DATABASE_URL", f"sqlite:///{_tmp_default.gettempdir()}/theroad_standalone.sqlite")

from app.core.config import settings

# ── 5개 씬 데이터 (DB에서 추출한 것) ──

SCENES = [
    {
        "id": "scene_1",
        "beat": "자기 몸을 수술하는 한재호",
        "camera": {"angle": "약한 하이앵글", "shot_size": "미디엄 와이드", "lens": "35mm", "composition": "수술대 중앙 배치"},
        "prompt": "수술대 위에는 한재호의 몸이 누워 있고, 은성의 몸에 라이딩된 한재호가 수술복 차림으로 직접 메스를 들고 자신의 몸을 집도한다. 주변 대원들은 장비를 건네며 긴장한 채 보조한다.",
        "gemini_image": "/Users/manta/Documents/Projects/TheRoad-I1/projects/677e5f0c-d7cb-4082-9723-98280fe899ca/images/3b310c12-dba4-4768-9849-10d2108d62ac/scene/230b0db2-c773-457b-9b39-7e4edb53cd85.png",
    },
    {
        "id": "scene_2",
        "beat": "캡슐 속 동녘과 떨어지는 멘탈 수치",
        "camera": {"angle": "아이레벨", "shot_size": "미디엄 와이드", "lens": "28mm", "composition": "캡슐과 모니터 동시 프레임"},
        "prompt": "전략실 한편 캡슐 안에 다시 누운 동녘 옆으로 모니터에 서현 멘탈 레이트 수치가 떠 있고, 은성과 치호가 심각한 얼굴로 계기판을 본다. 차가운 숫자와 불안한 표정이 같은 프레임에 묶인다.",
        "gemini_image": "/Users/manta/Documents/Projects/TheRoad-I1/projects/677e5f0c-d7cb-4082-9723-98280fe899ca/images/3b310c12-dba4-4768-9849-10d2108d62ac/scene/a6c559f0-833f-48c2-8e30-1183792aaf26.png",
    },
    {
        "id": "scene_3",
        "beat": "나물 보따리와 넘어짐",
        "camera": {"angle": "약간 하이앵글", "shot_size": "미디엄샷", "lens": "50mm", "composition": "넘어진 인물 중심"},
        "prompt": "바닥에 내팽개쳐진 나물 보따리 위를 밟은 광훈이 뒤로 꽈당 넘어지고, 순례는 미안한 얼굴로 두 손을 모은다. 옆의 동녘과 덕현은 웃음을 참느라 입을 막고 있다.",
        "gemini_image": "/Users/manta/Documents/Projects/TheRoad-I1/projects/677e5f0c-d7cb-4082-9723-98280fe899ca/images/93ac1d8d-b926-4a4a-a032-76148803f904/scene/de02a776-41e9-494b-9328-eb007ab176dd.png",
    },
    {
        "id": "scene_4",
        "beat": "서창의 점프 탈출",
        "camera": {"angle": "측면", "shot_size": "중간 와이드", "lens": "35mm", "composition": "점프 동작 측면 포착"},
        "prompt": "열린 수송차량 측면에서 서창이 몸을 던져 코카가 모는 바이크나 차량 쪽으로 점프한다. 뒤편에서는 서현이 실신한 채 늘어져 있고, 동녘이 그녀를 흔들어 깨우려 한다.",
        "gemini_image": "/Users/manta/Documents/Projects/TheRoad-I1/projects/677e5f0c-d7cb-4082-9723-98280fe899ca/images/1a22fd86-b6ba-4b66-9bab-ba120f2ca66e/scene/58e94e26-6220-405a-bbef-f1b8dc997a62.png",
    },
    {
        "id": "scene_5",
        "beat": "드롭을 노린 난사",
        "camera": {"angle": "망원 아이레벨", "shot_size": "와이드샷", "lens": "85mm compressed", "composition": "산 전경 깊이감"},
        "prompt": "산 정상 쪽 드롭 상자를 향해 여러 참가자들이 몰려 뛰는 순간, 산중턱의 49번 남자가 기관단총을 난사한다. 달리던 몸들이 비탈에서 픽픽 쓰러지고, 동녘은 낮은 곳에서 그 광경을 지켜본다.",
        "gemini_image": "/Users/manta/Documents/Projects/TheRoad-I1/projects/677e5f0c-d7cb-4082-9723-98280fe899ca/images/93ac1d8d-b926-4a4a-a032-76148803f904/scene/adfbcf24-649a-4574-8f60-82c6055df059.png",
    },
]

OUTPUT_DIR = Path(settings.projects_dir) / "t2i_comparison"


def build_cinematic_prompt(scene: dict) -> str:
    """카메라 정보 포함 영어 프롬프트 생성."""
    cam = scene["camera"]
    return (
        f"A cinematic still frame from a Korean sci-fi thriller film. "
        f"Pre-visualization concept art for director's shot planning.\n\n"
        f"Scene: {scene['prompt']}\n\n"
        f"Camera: {cam.get('shot_size', 'medium')} shot, {cam.get('angle', 'eye level')} angle, "
        f"{cam.get('lens', '35mm')} lens, {cam.get('composition', 'centered')}.\n"
        f"Dramatic cinematic lighting, near-future Korean setting, photorealistic."
    )


def generate_gpt_image(prompt: str, reference_image_path: str = None) -> bytes:
    """GPT Image 1.5 (high) 로 이미지 생성."""
    api_key = settings.openai_api_key

    # Prepend square instruction per docs
    full_prompt = f"Create the following as a 16:9 widescreen cinematic image: {prompt}"

    body = {
        "model": "gpt-image-1.5",
        "prompt": full_prompt,
        "n": 1,
        "size": "1024x1024",
        "quality": "high",
    }

    req = urllib.request.Request(
        "https://api.openai.com/v1/images/generations",
        data=json.dumps(body).encode(),
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        },
    )

    with urllib.request.urlopen(req, timeout=120) as resp:
        data = json.loads(resp.read())

    b64 = data["data"][0].get("b64_json")
    if b64:
        return base64.b64decode(b64)

    # If URL returned instead of b64
    url = data["data"][0].get("url")
    if url:
        with urllib.request.urlopen(url, timeout=60) as img_resp:
            return img_resp.read()

    raise RuntimeError("No image data in response")


def generate_gemini_image(prompt: str) -> bytes:
    """Gemini로 이미지 생성 (새로 생성)."""
    api_key = settings.gemini_api_key
    model = settings.gemini_image_model
    url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}"

    body = {
        "contents": [{"parts": [{"text": prompt}]}],
        "generationConfig": {
            "responseModalities": ["TEXT", "IMAGE"],
            "imageConfig": {"aspectRatio": "16:9", "imageSize": "2K"},
        },
    }

    req = urllib.request.Request(url, data=json.dumps(body).encode(), headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=120) as resp:
        data = json.loads(resp.read())

    for candidate in data.get("candidates", []):
        for part in candidate.get("content", {}).get("parts", []):
            inline = part.get("inlineData") or part.get("inline_data")
            if inline and inline.get("data"):
                return base64.b64decode(inline["data"])

    raise RuntimeError("No image in Gemini response")


def img_to_data_uri(path_or_bytes) -> str:
    """이미지를 HTML data URI로 변환."""
    if isinstance(path_or_bytes, bytes):
        b64 = base64.b64encode(path_or_bytes).decode()
    else:
        b64 = base64.b64encode(Path(path_or_bytes).read_bytes()).decode()
    return f"data:image/png;base64,{b64}"


def run():
    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    results = []

    for i, scene in enumerate(SCENES):
        print(f"\n[{i+1}/5] {scene['beat']}")
        prompt = build_cinematic_prompt(scene)
        print(f"  프롬프트: {prompt[:80]}...")

        result = {
            "id": scene["id"],
            "beat": scene["beat"],
            "camera": scene["camera"],
            "prompt": prompt,
            "original_prompt_ko": scene["prompt"],
        }

        # 기존 Gemini 이미지 (이미 생성됨)
        gemini_existing = Path(scene["gemini_image"])
        if gemini_existing.exists():
            result["gemini_existing"] = str(gemini_existing)
            print(f"  Gemini 기존: ✅")
        else:
            result["gemini_existing"] = None
            print(f"  Gemini 기존: ❌ 파일 없음")

        # Gemini 새로 생성 (같은 프롬프트로)
        try:
            gemini_bytes = generate_gemini_image(prompt)
            gemini_path = OUTPUT_DIR / f"{scene['id']}_gemini_new.png"
            gemini_path.write_bytes(gemini_bytes)
            result["gemini_new"] = str(gemini_path)
            print(f"  Gemini 새로: ✅ ({len(gemini_bytes)//1024}KB)")
        except Exception as e:
            result["gemini_new"] = None
            print(f"  Gemini 새로: ❌ {str(e)[:80]}")
        time.sleep(2)

        # GPT Image 1.5
        try:
            gpt_bytes = generate_gpt_image(prompt)
            gpt_path = OUTPUT_DIR / f"{scene['id']}_gpt.png"
            gpt_path.write_bytes(gpt_bytes)
            result["gpt_image"] = str(gpt_path)
            print(f"  GPT Image:  ✅ ({len(gpt_bytes)//1024}KB)")
        except Exception as e:
            result["gpt_image"] = None
            print(f"  GPT Image:  ❌ {str(e)[:80]}")
        time.sleep(2)

        results.append(result)

    # HTML 생성
    html = build_comparison_html(results)
    html_path = OUTPUT_DIR / "comparison.html"
    html_path.write_text(html, encoding="utf-8")
    print(f"\n✅ 비교 페이지: {html_path}")
    print(f"   열기: open {html_path}")


def build_comparison_html(results: list) -> str:
    cards = ""
    for r in results:
        cam = r["camera"]
        cam_info = f"Angle: {cam.get('angle','?')} | Shot: {cam.get('shot_size','?')} | Lens: {cam.get('lens','?')} | Comp: {cam.get('composition','?')}"

        gemini_existing_img = ""
        if r.get("gemini_existing"):
            gemini_existing_img = f'<img src="{img_to_data_uri(r["gemini_existing"])}" alt="Gemini existing">'
        else:
            gemini_existing_img = '<div class="no-img">이미지 없음</div>'

        gemini_new_img = ""
        if r.get("gemini_new"):
            gemini_new_img = f'<img src="{img_to_data_uri(r["gemini_new"])}" alt="Gemini new">'
        else:
            gemini_new_img = '<div class="no-img">생성 실패</div>'

        gpt_img = ""
        if r.get("gpt_image"):
            gpt_img = f'<img src="{img_to_data_uri(r["gpt_image"])}" alt="GPT Image 1.5">'
        else:
            gpt_img = '<div class="no-img">생성 실패</div>'

        cards += f"""
        <div class="scene-card">
            <h2>{r['beat']}</h2>
            <div class="camera-info">{cam_info}</div>
            <div class="prompt-ko">{r['original_prompt_ko']}</div>
            <details><summary>영어 프롬프트</summary><pre>{r['prompt']}</pre></details>
            <div class="comparison-grid">
                <div class="model-col">
                    <h3>Gemini 3.1 Flash (기존)</h3>
                    {gemini_existing_img}
                </div>
                <div class="model-col">
                    <h3>Gemini 3.1 Flash (새로)</h3>
                    {gemini_new_img}
                </div>
                <div class="model-col">
                    <h3>GPT Image 1.5 (high)</h3>
                    {gpt_img}
                </div>
            </div>
        </div>
        """

    return f"""<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>T2I 모델 비교 — 카메라 각도 중심</title>
<style>
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
body {{ font-family: -apple-system, "Pretendard", sans-serif; background: #0f1117; color: #e4e6ed; padding: 24px; }}
h1 {{ font-size: 24px; margin-bottom: 8px; }}
.subtitle {{ color: #8b90a0; margin-bottom: 32px; }}
.scene-card {{
    background: #1e2230; border: 1px solid rgba(255,255,255,0.08);
    border-radius: 16px; padding: 24px; margin-bottom: 24px;
}}
.scene-card h2 {{ font-size: 18px; margin-bottom: 8px; }}
.camera-info {{
    font-family: monospace; font-size: 13px; color: #6386ff;
    background: rgba(99,134,255,0.1); padding: 8px 12px;
    border-radius: 8px; margin-bottom: 12px;
}}
.prompt-ko {{ color: #8b90a0; font-size: 14px; margin-bottom: 12px; line-height: 1.6; }}
details {{ margin-bottom: 16px; }}
summary {{ color: #6386ff; cursor: pointer; font-size: 13px; }}
pre {{ background: #0a0c10; padding: 12px; border-radius: 8px; font-size: 12px; white-space: pre-wrap; color: #8b90a0; margin-top: 8px; }}
.comparison-grid {{
    display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px;
}}
.model-col h3 {{
    font-size: 14px; text-align: center; margin-bottom: 8px;
    color: #8b90a0; font-weight: 600;
}}
.model-col img {{
    width: 100%; border-radius: 8px; border: 1px solid rgba(255,255,255,0.08);
}}
.no-img {{
    width: 100%; aspect-ratio: 16/9; display: flex; align-items: center;
    justify-content: center; background: #252938; border-radius: 8px;
    color: #5c6070; font-size: 14px;
}}
</style>
</head>
<body>
<h1>T2I 모델 비교 — 카메라 각도 중심</h1>
<p class="subtitle">Gemini 3.1 Flash Image Preview vs GPT Image 1.5 (high) | 5개 씬 비교</p>
{cards}
</body>
</html>"""


if __name__ == "__main__":
    run()
