"""실험: 씬 12의 두 샷을 라인 + 점 기반 구도 이미지로 생성

목적:
- 실사 대신 선/점으로 구도와 엔티티 배치를 표현하는 T2I 프롬프트를 GPT-5.4와 Gemini Pro에 각각 생성시킴
- 같은 씬의 두 샷이 비슷한 카메라 흐름을 공유하는지 검증
- 배경: 흑백 / 각 엔티티: 고유 색상 선과 점

출력:
- output/line_art_s12/prompts.json — 두 모델 x 두 샷 = 4개 프롬프트
- output/line_art_s12/*.png — 각 프롬프트로 Gemini Image 생성 이미지 (선택)

사용법:
    cd backend && source .venv/bin/activate
    python scripts/experiment_line_art_composition.py
    # 이미지 생성까지:
    python scripts/experiment_line_art_composition.py --generate-images
"""
import argparse
import json
import logging
import sys
from pathlib import Path

# backend 루트를 sys.path에 추가
BACKEND = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(BACKEND))

from app.modules.llm.llm_client import call_structured  # noqa: E402

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)

# 테스트 대상
PROJECT_ID = "47cf90e9-2101-45a3-aa5d-cf2ae99ceebf"
EPISODE_ID = "483a7759-4be4-42e5-8d36-5b343c13a9d1"
SCENE_INDEX = 12
OUTPUT_DIR = BACKEND / "scripts" / "output" / "line_art_s12"


def _load_cp(step_id: str) -> dict:
    """체크포인트 로드 — 현재 manifest.json 없으면 최신 archive."""
    cp_dir = (
        BACKEND.parent / "projects" / PROJECT_ID
        / "checkpoints" / "episodes" / EPISODE_ID / step_id
    )
    current = cp_dir / "manifest.json"
    if current.exists():
        return json.loads(current.read_text(encoding="utf-8"))
    # 최신 archive 탐색
    archives = sorted(cp_dir.glob("manifest_*.json"), reverse=True)
    if archives:
        logger.info("step=%s: using archive %s", step_id, archives[0].name)
        return json.loads(archives[0].read_text(encoding="utf-8"))
    raise FileNotFoundError(f"No checkpoint for {step_id}")


def collect_s12_context() -> dict:
    """S12 관련 모든 컨텍스트 수집."""
    ctx = {"scene_index": SCENE_INDEX}

    # 씬 텍스트
    save_cp = _load_cp("scene_save")
    for seg in save_cp.get("data", {}).get("segments", []):
        if seg.get("scene_index") == SCENE_INDEX:
            ctx["scene_text"] = seg.get("text", "")
            ctx["scene_heading"] = seg.get("heading", "")
            break

    # 샷 설명
    shot_cp = _load_cp("shot_extract")
    for sc in shot_cp.get("data", {}).get("scenes", []):
        if sc.get("scene_index") == SCENE_INDEX:
            ctx["shots"] = sc.get("shots", [])[:2]  # 첫 2개만
            break

    # 샷 스테이징 (카메라/조명/인물배치)
    staging_cp = _load_cp("shot_staging")
    ctx["staging"] = []
    for st in staging_cp.get("data", {}).get("shots", []):
        if st.get("scene_index") == SCENE_INDEX and st.get("shot_index") in [1, 2]:
            ctx["staging"].append(st)

    # scene_consistency 고정 요소
    try:
        cons_cp = _load_cp("scene_consistency")
        for sc in cons_cp.get("data", {}).get("scenes", []):
            if sc.get("scene_index") == SCENE_INDEX:
                ctx["fixed_elements"] = sc.get("fixed_elements", [])
                break
    except FileNotFoundError:
        ctx["fixed_elements"] = []

    # 엔티티 이름 매핑
    merge_cp = _load_cp("entity_merge")
    ctx["entities"] = {}
    for etype in ["characters", "locations", "props"]:
        for e in merge_cp.get("data", {}).get(etype, []):
            sid = e.get("short_id", "")
            if sid:
                ctx["entities"][sid] = {
                    "name": e.get("name", ""),
                    "type": etype[:-1],
                    "description": e.get("description", "")[:200],
                }

    # 샷에서 등장하는 엔티티만 필터
    ve_sids = set()
    for shot in ctx["shots"]:
        for c in shot.get("characters", []):
            if isinstance(c, dict):
                sid = c.get("short_id") or c.get("id", "")
            else:
                sid = c
            if sid:
                ve_sids.add(sid)
    # fixed_elements에서도 수집
    for fe in ctx.get("fixed_elements", []):
        if fe.get("character_name"):
            for sid, info in ctx["entities"].items():
                if info["name"] == fe["character_name"]:
                    ve_sids.add(sid)

    ctx["visible_entity_ids"] = sorted(ve_sids)
    return ctx


# ── 프롬프트 템플릿 ──

SYSTEM_PROMPT = """당신은 영화 연출 분석가입니다. 하나의 씬에서 연속된 두 샷을 분석하여, 각 샷의 구도와 엔티티 배치를 선과 점으로 표현할 T2I 프롬프트를 작성합니다.

## 목적
실사 이미지가 아닌 **구도 다이어그램**을 만듭니다. 배경은 흑백 선화, 각 엔티티는 고유 색상의 선과 점으로 표시됩니다. 인물의 정확한 외모나 표정은 중요하지 않고, **화면 속 위치, 크기, 자세, 방향, 시선**을 표현하는 것이 목적입니다.

## 두 샷의 관계
두 샷은 같은 씬 안에 있고, 카메라가 비슷한 공간을 다른 각도/거리로 흐르는 관계입니다.
**두 샷의 구도가 자연스럽게 이어지도록** 작성하세요. 같은 환경의 같은 요소들이 두 이미지에 일관되게 유지되어야 합니다.

## 스타일 (두 프롬프트 공통)
- Pure line drawing on black background
- Foreground entities drawn with colored vector lines only (no fill, no shading, no gradient)
- Background architecture and furniture drawn with thin white lines
- Each character represented as a colored outline silhouette + joint dots (like motion capture skeleton)
- Each prop represented as a colored geometric outline
- NO photorealism, NO texture, NO color fill inside shapes
- Flat 2D diagram style, like architectural blueprint fused with storyboard sketch

## 엔티티별 색상 (고정)
- 인물 1 (주 인물): bright cyan lines (#00E5FF)
- 인물 2 (보조): bright magenta lines (#FF00FF)
- 인물 3+: yellow, green 등 다른 형광색
- 핵심 소품: orange lines (#FFA500)
- 혈흔/액체 패턴: red dots (#FF0000)
- 배경 가구: thin white lines (#FFFFFF)
- 벽/바닥 구조: thin gray lines (#888888)

## 각 T2I 프롬프트에 반드시 포함
1. 배경 공간의 구도 묘사 (벽 위치, 가구 배치, 원근감)
2. 각 인물의 정확한 위치, 자세, 바라보는 방향
3. 각 인물을 특정 색상으로 명시 (C##은 사용 금지 — "character 1 in cyan", "character 2 in magenta" 식)
4. 핵심 소품과 혈흔/환경 상태의 색상과 위치
5. 카메라 앵글 (top-down, eye-level 등) + 원근
6. 두 샷의 카메라 이동 관계 설명 (첫 번째는 wider, 두 번째는 closer 등)

## 출력 형식
JSON. 각 프롬프트는 영어로, 한 단락(8-15문장).
"""


RESPONSE_SCHEMA = {
    "type": "object",
    "properties": {
        "analysis": {
            "type": "string",
            "description": "두 샷의 카메라 흐름과 구도 연속성에 대한 한국어 분석 (2-3문장)"
        },
        "shot_1_line_art_t2i": {
            "type": "string",
            "description": "Shot 1의 라인아트 T2I 프롬프트 (영어, 8-15문장)"
        },
        "shot_2_line_art_t2i": {
            "type": "string",
            "description": "Shot 2의 라인아트 T2I 프롬프트 (영어, 8-15문장)"
        },
        "color_legend": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "label": {"type": "string"},
                    "color": {"type": "string"},
                    "description": {"type": "string"}
                },
                "required": ["label", "color", "description"],
                "additionalProperties": False
            },
            "description": "엔티티 → 색상 매핑 범례"
        }
    },
    "required": ["analysis", "shot_1_line_art_t2i", "shot_2_line_art_t2i", "color_legend"],
    "additionalProperties": False
}


def build_user_prompt(ctx: dict) -> str:
    lines = [
        f"씬 {ctx['scene_index']} 분석: {ctx.get('scene_heading', '')}",
        "",
        "[씬 텍스트]",
        ctx.get("scene_text", ""),
        "",
        "[사용 가능한 엔티티]",
    ]
    for sid in ctx["visible_entity_ids"]:
        info = ctx["entities"].get(sid, {})
        lines.append(f"  {sid} [{info.get('type', '?')}] {info.get('name', '')}: {info.get('description', '')}")
    lines.append("")

    for i, shot in enumerate(ctx["shots"], 1):
        lines.append(f"[Shot {i}]")
        lines.append(f"  설명: {shot.get('description', '')}")
        # staging 매칭
        stg = next((s for s in ctx["staging"] if s.get("shot_index") == shot.get("shot_index")), None)
        if stg:
            lines.append(f"  카메라: {stg.get('camera_direction', '')}")
            lines.append(f"  조명: {stg.get('lighting_mood', '')}")
            for ca in stg.get("character_angles", []):
                lines.append(
                    f"    인물 배치: {ca.get('character', '')} — "
                    f"{ca.get('angle', '')}, {ca.get('body_pose', '')}, "
                    f"eyes→{ca.get('gaze_target', '')}"
                )
        lines.append("")

    if ctx.get("fixed_elements"):
        lines.append("[교차 샷 고정 요소 — 두 샷에 동일하게 존재]")
        for fe in ctx["fixed_elements"]:
            lines.append(f"  [{fe.get('element_type', '')}] {fe.get('element_id', '')}")
            if fe.get("character_name"):
                lines.append(f"    character: {fe['character_name']}")
            lines.append(f"    {fe.get('description', '')}")
        lines.append("")

    lines.append("위 정보를 바탕으로 두 샷에 대해 선/점 기반 구도 다이어그램을 생성하는 T2I 프롬프트를 작성하세요.")
    lines.append("두 샷의 구도는 같은 공간을 다른 각도/거리로 담으면서 자연스럽게 이어져야 합니다.")
    return "\n".join(lines)


def call_model(model_alias: str, system_prompt: str, user_prompt: str) -> dict:
    """특정 모델로 라인아트 T2I 프롬프트 생성."""
    project_config = {"_line_art_experiment": {"model": model_alias}}
    return call_structured(
        step="_line_art_experiment",
        system_prompt=system_prompt,
        user_prompt=user_prompt,
        response_schema=RESPONSE_SCHEMA,
        project_config=project_config,
        schema_name="line_art_composition",
    )


def generate_image(prompt_text: str, output_path: Path) -> bool:
    """Gemini Image로 실제 이미지 생성."""
    try:
        from app.modules.gemini_image_client import GeminiImageClient
        client = GeminiImageClient()
        result = client.generate_image(
            prompt=prompt_text,
            reference_images=[],
            step_id="_line_art_experiment",
        )
        if result and result.get("image_bytes"):
            output_path.write_bytes(result["image_bytes"])
            logger.info("  → saved %s (%d bytes)", output_path.name, len(result["image_bytes"]))
            return True
    except Exception as exc:
        logger.error("  image gen failed: %s", exc)
    return False


def main(generate_images: bool = False) -> None:
    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

    logger.info("Collecting S12 context...")
    ctx = collect_s12_context()
    logger.info("  scene_text: %d chars", len(ctx.get("scene_text", "")))
    logger.info("  shots: %d", len(ctx["shots"]))
    logger.info("  staging entries: %d", len(ctx["staging"]))
    logger.info("  fixed elements: %d", len(ctx.get("fixed_elements", [])))
    logger.info("  visible entities: %s", ctx["visible_entity_ids"])

    # 컨텍스트 저장 (디버깅용)
    (OUTPUT_DIR / "context.json").write_text(
        json.dumps(ctx, ensure_ascii=False, indent=2), encoding="utf-8"
    )

    user_prompt = build_user_prompt(ctx)
    (OUTPUT_DIR / "user_prompt.txt").write_text(user_prompt, encoding="utf-8")
    logger.info("  user_prompt: %d chars", len(user_prompt))

    # ── GPT-5.4 실행 ──
    logger.info("\n=== Running GPT-5.4 ===")
    try:
        gpt_result = call_model("gpt", SYSTEM_PROMPT, user_prompt)
        logger.info("  GPT analysis: %s", gpt_result.get("analysis", "")[:120])
    except Exception as exc:
        logger.error("  GPT failed: %s", exc)
        gpt_result = {"error": str(exc)}

    # ── Gemini Pro 실행 ──
    logger.info("\n=== Running Gemini Pro ===")
    try:
        gemini_result = call_model("gemini-pro", SYSTEM_PROMPT, user_prompt)
        logger.info("  Gemini analysis: %s", gemini_result.get("analysis", "")[:120])
    except Exception as exc:
        logger.error("  Gemini failed: %s", exc)
        gemini_result = {"error": str(exc)}

    # 결과 저장
    all_results = {
        "gpt": gpt_result,
        "gemini_pro": gemini_result,
    }
    (OUTPUT_DIR / "prompts.json").write_text(
        json.dumps(all_results, ensure_ascii=False, indent=2), encoding="utf-8"
    )
    logger.info("\nSaved: %s/prompts.json", OUTPUT_DIR)

    # ── 이미지 생성 (선택) ──
    if generate_images:
        logger.info("\n=== Generating images ===")
        for model_name, result in all_results.items():
            if "error" in result:
                continue
            for shot_key in ["shot_1_line_art_t2i", "shot_2_line_art_t2i"]:
                prompt = result.get(shot_key, "")
                if not prompt:
                    continue
                shot_num = 1 if "shot_1" in shot_key else 2
                fname = f"{model_name}_shot{shot_num}.png"
                logger.info("  %s...", fname)
                generate_image(prompt, OUTPUT_DIR / fname)


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--generate-images", action="store_true")
    args = parser.parse_args()
    main(generate_images=args.generate_images)
