"""gpt-image-2 롤 생성 gen_fn — `make_nb2_gen_fn` 과 같은 시그니처.

2026-08-03 사용자 결정: 배경 씨드의 그림 모델을 gpt-image-2 로 전면 전환.

근거(6그룹 실측). 같은 저작 프롬프트·같은 형태 참조 사진으로 그림 모델만
바꿔 그리고, 참조 사진 앞에서 좌우를 바꿔 두 번 물어 **두 번 다 이긴 쪽만**
승자로 셌다 — gpt-image-2 4승 · 무승부 2 · 기존 모델 0승. 사람이 표면에
얹은 시각물(여러 위치·크기의 한 체계, 부속 표시, 진열)이 특히 크게 갈렸다.

`multiroll_select` 계약을 그대로 지킨다: `(tag, prompt, labeled_refs,
out_path) -> Path`. 참조가 있으면 edit, 없으면 generate 로 간다.
"""
from __future__ import annotations

import logging
from pathlib import Path
from typing import Any, Callable, Dict, Optional

logger = logging.getLogger(__name__)

# 비율 → 크기. ★**한 자리**에서만 정한다 (2026-09-20) — 종전에 여기
# `16:9 → 1536x1024` 라고 적혀 있었는데 그것은 **3:2** 다. 그 값으로
# 스틸을 그리면 전 샷의 비율이 어긋난다. 사용자 지시로 16:9 는
# **1536x864**(=`BGFIRST_BG_SIZE`, 수동 수리 8장이 이 값으로 맞았다).
from app.modules.llm.gpt_image_client import (           # noqa: E402
    DEFAULT_SIZE as _DEFAULT_SIZE,
    SIZE_BY_ASPECT as _SIZE_BY_ASPECT,
)


def make_gpt_image_gen_fn(
    *,
    project_id: str,
    episode_id: Optional[str] = None,
    operation_type: str = "multiroll_roll",
    aspect_ratio: str = "16:9",
    quality: str = "high",
    model: Optional[str] = None,
    sanitizer: Any = None,
    openai_client: Any = None,
    context_extra: Optional[Dict[str, Any]] = None,
) -> Callable:
    """gpt-image-2 gen_fn. moderation 이면 sanitizer 로 1회 재시도.

    ★`project_id`/`episode_id` 는 받되 쓰지 않는다. 교체 대상인 nb2 gen_fn 은
    클라이언트에 컨텍스트를 직접 심어야 했지만, 이 경로의 포착은 호출자가
    열어 둔 스코프(`generation_context`)를 따라간다 — 여기서 다시 심으면
    두 곳이 같은 것을 정하게 된다. 시그니처를 맞춰 두는 것은 호출부가
    한 줄 교체로 오갈 수 있게 하기 위함이다.
    """
    from app.core.config import settings
    from app.core.steps.shot_conti_light_step import _resolve_openai_client
    from app.modules.llm.gpt_image_primitive import call_gpt_image_bytes
    from app.core.image_call_budget import reserve_current_call
    from app.modules.pipeline.multiroll_gemini import atomic_write_bytes

    client = openai_client or _resolve_openai_client()
    resolved = model or getattr(settings, "openai_image_model", "gpt-image-2.5-sunburst")
    size = _SIZE_BY_ASPECT.get(aspect_ratio, _DEFAULT_SIZE)

    def gen_fn(tag, prompt, labeled_refs, out_path: Path) -> Path:
        # ★없는 참조를 버리고 그리지 않는다. 참조는 무엇이 형태를 정하고
        #  무엇이 표면을 정하는지 나누는 계약이라, 하나가 사라지면 아예 다른
        #  그림이 된다. 조용히 빼면 그 그림이 원래 계약의 산출인 것처럼
        #  기록되고 승패까지 매겨진다(2026-08-03 실측: 도해가 지워진 채 사진
        #  한 장으로 그려진 것이 "스케치 경로"로 판정에 올라갔다).
        refs: list[str] = []
        for lab, p in (labeled_refs or []):
            if p is None:
                raise FileNotFoundError(
                    f"gpt-image[{tag}]: 참조 '{lab}' 이 비었다")
            if not Path(str(p)).exists():
                raise FileNotFoundError(
                    f"gpt-image[{tag}]: 참조 '{lab}' 파일이 없다 — {p}")
            refs.append(str(p))
        current = prompt
        for attempt in (1, 2):
            try:
                # ★이미지 문 — run-wide cap 을 **모든** gpt-image 자리가 지난다
                reserve_current_call(source=f"gpt_image_gen.{tag}")
                png = call_gpt_image_bytes(
                    client,
                    mode="edit" if refs else "generate",
                    prompt=current,
                    ref_paths=refs or None,
                    call_kwargs={"model": resolved, "size": size,
                                 "quality": quality, "n": 1},
                    capture_role=operation_type,
                    capture_metadata={"multiroll_tag": tag,
                                      **(context_extra or {})},
                )
                if not png:
                    raise RuntimeError("gpt-image: 빈 응답")
                return atomic_write_bytes(out_path, png)
            except Exception as exc:  # noqa: BLE001
                msg = str(exc).lower()
                moderated = ("moderation" in msg or "safety" in msg
                             or "blocked" in msg or "content_policy" in msg)
                if attempt == 1 and moderated and sanitizer is not None:
                    # ★sanitizer 는 호출 가능한 객체가 아니라 `sanitize(...)`
                    #  를 가진 객체다(차단 사유·범주·회차를 받아 dict 로
                    #  돌려준다). 함수처럼 부르면 재시도 경로가 통째로
                    #  TypeError 로 죽는다 — 정화가 필요한 바로 그 순간에.
                    sr = sanitizer.sanitize(current, str(exc), [], attempt)
                    sanitized = sr.get("sanitized_prompt") or ""
                    if sanitized:
                        logger.warning(
                            "gpt-image[%s]: moderation — sanitize 재시도", tag)
                        current = sanitized
                        continue
                raise
        raise RuntimeError("gpt-image: 재시도 소진")   # 도달 불가

    return gen_fn
