"""최종 i2i 변환 provider 선택 (2026-08-25 사용자 지시 "grok, reve 등으로
교환 가능하게").

호출부는 **어느 제공자인지 몰라도 된다** — `build_cine_client()` 가 주는
것은 `generate_image(prompt, labeled_references=[(label, png)])` 슬롯 하나뿐
이고, 두 client 가 같은 모양을 낸다.

★신원을 값으로 돌려주는 자리가 여기다(`cine_provider_identity`). 지문이
 provider·endpoint·model 을 봐야 하기 때문이다 — **같은 문안·같은 원본이라도
 다른 제공자가 만든 그림은 다른 산출**이다. 신원을 지문에 안 넣으면 제공자를
 바꿔도 옛 산출이 「지금 계약의 것」으로 통한다(cine_verify 가 판정 문안을
 지문에 안 넣어 같은 자리에서 걸렸던 것과 같은 결함).

★endpoint 는 **변동하는 host URL 이 아니라 논리 ID** 로 적는다. host 는
 제공자 사정으로 바뀌는데 그때마다 지문이 흔들리면 그림을 다시 사게 된다.

값(2026-08-25 공식 안내): Grok 약 $0.03/장 · Reve $0.25/장 = **8.3배**.
"""
from __future__ import annotations

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

logger = logging.getLogger(__name__)

# 기본 제공자 — 이 값이 종전 동작이다(byte-identical).
LEGACY_PROVIDER = "grok"
# Grok 의 논리 endpoint. OpenRouter chat/completions 를 거치는 한 갈래뿐이다.
GROK_ENDPOINT = "openrouter/chat-completions"

#: MAI 는 Grok 과 **같은 운반층**(OpenRouter chat/completions ·
#: `modalities:["image"]`)이라 client 를 그대로 쓰고 모델만 갈아 끼운다.
MAI_ENDPOINT = GROK_ENDPOINT

#: Seedream 은 **다른 운반층**이다 — OpenRouter 전용 이미지 엔드포인트
#: (`/api/v1/images`, 참조는 `input_references`). chat/completions 로 보내면
#: **500** 이 온다.
#:
#: ★[2026-09-09 실측] 그 500 을 「모델이 못 그린다」로 오독했다. 이 저장소
#:  주석이 이미 적어 뒀는데(`marker_map_engine.py:97~104`) 안 찾아봤다 —
#:  grok-imagine 이 chat 으로도 받아 줘서 그 방식만 알고 있었던 것이다.
#:  전용 경로로 보내니 **두 컷 다 114초에 성공**했다.
#: ★요청에 화면 비율을 싣게 되며 값을 바꿨다 (2026-09-19). 요청 모양이
#  바뀌면 **같은 입력에서 다른 그림**이 나온다(정사각형 → 16:9). 신원이 그대로면
#  비율 없이 만든 옛 변환이 「지금 계약의 것」으로 재사용된다. 이 값은 변환
#  지문(`cine_fingerprint_v2`)에만 접히고 단계 config hash 는 주 제공자
#  신원만 접으므로, 움직이는 것은 seedream 슬롯 기록뿐이다.
SEEDREAM_ENDPOINT = "openrouter/images+aspect_ratio"

#: xAI 직접 편집 — OpenRouter 를 안 거친다 (2026-09-18 실측: 그날 14:18 부터
#:  OpenRouter 의 grok 이미지 슬러그가 404. xAI 직접 편집만 원본을 유지한다).
XAI_ENDPOINT = "xai/images/edits"

KNOWN_PROVIDERS = ("grok", "reve", "mai", "seedream", "xai")


def resolve_cine_provider() -> str:
    """설정이 고른 제공자 — 모르는 값은 **되돌리지 않고 크게 실패**한다.

    설정 검증(`config._still_cine_provider_known`)이 startup 에서 이미
    막지만, 실행 중 설정 API 로 바뀌는 갈래가 있어 여기서도 본다. 조용히
    기본값으로 되돌리면 운영자는 reve 로 도는 줄 아는데 지문·기록은 grok
    으로 남는다.
    """
    from app.core.config import settings

    v = str(getattr(settings, "still_cine_provider", LEGACY_PROVIDER) or "")
    if v not in KNOWN_PROVIDERS:
        raise ValueError(
            f"still_cine_provider unknown: {v!r} "
            f"(known: {list(KNOWN_PROVIDERS)})")
    return v


def cine_provider_identity(provider: str = "") -> Dict[str, str]:
    """지문에 접을 신원 — (provider, endpoint, model).

    model 은 alias 가 아니라 **물리 모델 문자열**이다. alias 만 넣으면 판
    교체가 캐시를 못 뚫는다(cine_verify 의 `resolve_verify_model_physical`
    과 같은 이유).
    """
    from app.core.config import settings

    p = provider or resolve_cine_provider()
    if p == "reve":
        model = str(getattr(settings, "reve_image_model", "") or "")
        return {"provider": "reve", "endpoint": model, "model": model}
    if p == "seedream":
        return {
            "provider": "seedream",
            "endpoint": SEEDREAM_ENDPOINT,
            "model": str(getattr(settings, "seedream_image_model", "") or ""),
        }
    if p == "mai":
        return {
            "provider": "mai",
            "endpoint": MAI_ENDPOINT,
            "model": str(getattr(settings, "mai_image_model", "") or ""),
        }
    if p == "xai":
        return {
            "provider": "xai",
            "endpoint": XAI_ENDPOINT,
            "model": str(getattr(settings, "xai_image_model", "") or ""),
        }
    return {
        "provider": "grok",
        "endpoint": GROK_ENDPOINT,
        "model": str(getattr(settings, "grok_image_model", "") or ""),
    }


def is_legacy_identity(identity: Dict[str, str]) -> bool:
    """이 신원이 **지문 v1 이 뜻하던 바로 그것**인가.

    지문 v1 은 provider·endpoint 칸이 없어 model 만 적었다. 그 시절 변환은
    전부 Grok 이었으므로, 지금 선택이 **정확히 그 Grok** 일 때만 옛 산출을
    지금 것으로 읽을 수 있다. 조금이라도 다르면 재생성이 맞다 — 제공자
    교체는 산출 계약이 실제로 다르다(소급 동등 처리 금지).
    """
    from app.core.config import settings

    return (
        identity.get("provider") == LEGACY_PROVIDER
        and identity.get("endpoint") == GROK_ENDPOINT
        and identity.get("model") == str(
            getattr(settings, "grok_image_model", "") or "")
    )


def build_cine_client(
    provider: str = "",
    *,
    on_submit: Optional[Callable[[Dict[str, Any]], None]] = None,
) -> Any:
    """제공자에 맞는 client — 호출부는 슬롯만 안다.

    `on_submit` 은 **접수 기반 제공자에만** 뜻이 있다(fal queue). 동기
    왕복인 Grok 에는 접수라는 단계가 없어 넘겨도 쓰이지 않는다 — 넘긴
    쪽이 「기록이 남는다」고 오해하지 않도록 여기 적어 둔다.
    """
    p = provider or resolve_cine_provider()
    if p == "xai":
        from app.modules.llm.xai_image_client import XaiImageClient

        return XaiImageClient()
    if p == "reve":
        from app.modules.llm.reve_image_client import ReveImageClient

        return ReveImageClient(on_submit=on_submit)
    from app.modules.llm.grok_image_client import GrokImageClient

    if p == "seedream":
        from app.core.config import settings

        return _SeedreamCineClient(str(
            getattr(settings, "seedream_image_model", "") or ""))
    if p == "mai":
        # ★운반층이 같으므로 client 를 새로 쓰지 않는다 — 모델만 갈아 끼운다.
        #  (실측 2026-09-05: 같은 프롬프트·같은 입력으로 24샷 전부 성공)
        from app.core.config import settings

        return GrokImageClient(model=str(
            getattr(settings, "mai_image_model", "") or ""))
    return GrokImageClient()


class _SeedreamCineClient:
    """Seedream 을 cine 슬롯 모양(`generate_image`)으로 감싼다.

    ★운반은 **이미 있는 것**을 쓴다 — `marker_map_engine` 의 OpenRouter 전용
     이미지 경로다. 새로 짜면 같은 실수를 또 한다(chat/completions 로 보내
     500 을 받고 「모델이 못 그린다」로 읽는 것).
    """

    def __init__(self, model: str) -> None:
        if not model:
            raise ValueError("seedream_image_model 이 비었다")
        self._model = model
        self._tls_ctx: Dict[str, Any] = {}

    def set_context(self, **kwargs: Any) -> "_SeedreamCineClient":
        """다른 cine client 와 **같은 슬롯**. 호출부는 제공자를 몰라야 한다.

        ★없으면 `AttributeError` 로 변환이 통째로 죽는다 — 실측으로 잡았다.
         새 제공자를 더할 때 이 계약을 안 맞추면 조용히 fallback 으로 떨어진다.
        """
        self._tls_ctx.update({k: v for k, v in kwargs.items() if v is not None})
        return self

    def generate_image(self, prompt, reference_images=None,
                       aspect_ratio="16:9", labeled_references=None):
        import tempfile
        import time
        from pathlib import Path as _P

        from app.modules.pipeline.marker_map_engine import (
            draw_marker_map_via_model)

        refs = [b for _lbl, b in (labeled_references or [])] or list(
            reference_images or [])
        t0 = time.monotonic()
        with tempfile.TemporaryDirectory() as td:
            paths = []
            for i, b in enumerate(refs):
                f = _P(td) / f"ref_{i}.png"
                f.write_bytes(b)
                paths.append(f)
            # ★비율을 **싣는다** (2026-09-19). 받기만 하고 안 실어서
            #  seedream 기본값(정사각형)이 최종본이 됐다(S72sh43).
            png = draw_marker_map_via_model(
                model=self._model, prompt=prompt, ref_paths=paths,
                role="still_cine_transform",
                capture_context=dict(self._tls_ctx),
                aspect_ratio=aspect_ratio)
        return png, int((time.monotonic() - t0) * 1000)
