"""space_set_bg provider — LLM/VLM/이미지 호출 래퍼 (fail-closed).

pure core(space_set_bg)가 만든 프롬프트를 실제 모델에 전달한다.
dwelling_zone_map_provider 패턴: 키 preflight → 지연 import → 단발 호출 → 빈 응답 fail.
★입력 텍스트는 절대 자르지 않는다 (CLAUDE.md). 출력 한도만 인자.
"""
from __future__ import annotations

import base64
import json
import os
from pathlib import Path
from typing import Any, Dict, List

from app.modules.llm.gpt_image_primitive import call_gpt_image_bytes

TEXT_MODEL_DEFAULT = "openai/gpt-5.6-sol"
VISION_MODEL_DEFAULT = "openai/gpt-5.6-sol"
IMAGE_MODEL_DEFAULT = "gpt-image-2"
IMAGE_SIZE_DEFAULT = "1536x1024"
TEXT_TIMEOUT_DEFAULT = 300
TEXT_DEADLINE_DEFAULT = 360
VISION_TIMEOUT_DEFAULT = 480
# analyze 출력(JSON, life_baseline 포함)이 커서 8000 이면 empty/truncate (실험 12~13차 실측)
ANALYZE_MAX_TOKENS = 16000


class SpaceSetBgProviderError(Exception):
    """Fail-closed signal for the space-set-bg provider path."""


def _require_api_key() -> None:
    """호출 전 키 존재 확인 — 슬롯 브로커 기준 (2026-07-30).

    이전에는 `os.environ["OPENAI_API_KEY"]` 만 봤다. 그러면 1차 키가 죽어
    보조 슬롯으로 전환된 상태에서도 환경변수의 옛 키를 보고 통과시키거나,
    보조 슬롯에만 키가 있을 때 없다고 세운다.
    """
    from app.core.openai_keys import has_openai_key

    if has_openai_key():
        return
    raise SpaceSetBgProviderError(
        "OPENAI_API_KEY missing or empty; refusing to call the model"
    )


def text_completion(
    *,
    system: str,
    user: str,
    model: str = TEXT_MODEL_DEFAULT,
    max_tokens: int = 1800,
    timeout: int = TEXT_TIMEOUT_DEFAULT,
    retries: int = 3,
) -> str:
    """텍스트 LLM 단발 호출 (+empty 재시도). llm_deadline 으로 slow-stream 행 차단."""
    _require_api_key()
    try:
        import litellm  # type: ignore
        from app.modules.pipeline.llm_deadline import call_with_deadline
    except Exception as exc:  # pragma: no cover — env-dependent
        raise SpaceSetBgProviderError(
            f"litellm import failed: {type(exc).__name__}: {exc}"
        ) from exc
    last = "empty"
    for _ in range(max(1, retries)):
        try:
            resp = call_with_deadline(
                litellm.completion, model=model,
                messages=[
                    {"role": "system", "content": system},
                    {"role": "user", "content": user},
                ],
                timeout=timeout, max_completion_tokens=max_tokens,
                num_retries=1, deadline_seconds=timeout + 60,
                reasoning_effort="low",
            )
        except Exception as exc:
            raise SpaceSetBgProviderError(
                f"text completion raised: {type(exc).__name__}: {exc}"
            ) from exc
        content = (resp.choices[0].message.content if resp.choices else None) or ""
        if content.strip():
            return content.strip()
    raise SpaceSetBgProviderError(f"text completion returned empty content ({last})")


def vision_completion(
    *,
    system: str,
    user: str,
    image_paths: List[str],
    model: str = VISION_MODEL_DEFAULT,
    max_tokens: int = 1500,
    timeout: int = VISION_TIMEOUT_DEFAULT,
    retries: int = 3,
) -> str:
    """멀티이미지 VLM 호출 (frame_check 2장 / 좌표읽기 1장)."""
    _require_api_key()
    try:
        import litellm  # type: ignore
        from app.modules.pipeline.llm_deadline import call_with_deadline
    except Exception as exc:  # pragma: no cover — env-dependent
        raise SpaceSetBgProviderError(
            f"litellm import failed: {type(exc).__name__}: {exc}"
        ) from exc
    content: List[Dict[str, Any]] = [{"type": "text", "text": user}]
    for p in image_paths:
        b64 = base64.b64encode(Path(p).read_bytes()).decode()
        content.append({"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}})
    msgs = [{"role": "system", "content": system}, {"role": "user", "content": content}]
    for _ in range(max(1, retries)):
        try:
            resp = call_with_deadline(
                litellm.completion, model=model, messages=msgs,
                timeout=timeout, max_completion_tokens=max_tokens,
                num_retries=1, deadline_seconds=timeout + 60,
                reasoning_effort="low",
            )
        except Exception as exc:
            raise SpaceSetBgProviderError(
                f"vision completion raised: {type(exc).__name__}: {exc}"
            ) from exc
        c = (resp.choices[0].message.content if resp.choices else None) or ""
        if c.strip():
            return c.strip()
    raise SpaceSetBgProviderError("vision completion returned empty content")


def _openai_client():
    _require_api_key()
    try:
        from app.core.openai_keys import openai_client  # type: ignore
    except Exception as exc:  # pragma: no cover — env-dependent
        raise SpaceSetBgProviderError(
            f"openai import failed: {type(exc).__name__}: {exc}"
        ) from exc
    # bare `OpenAI()` 는 os.environ 만 읽어 슬롯 전환을 놓친다 — 브로커 경유.
    return openai_client()


def _reserve_image_call(source: str) -> None:
    """W20E5 image-call budget 예약 — budget 미설치 thread 에선 no-op,
    설치된 경로(image phase/cap 운영)에선 cap 초과 시 fail-closed (Codex 리뷰 #3)."""
    try:
        from app.core.image_call_budget import reserve_current_call
    except Exception:  # pragma: no cover — env-dependent
        return
    reserve_current_call(source=source)


def image_generate(
    *,
    prompt: str,
    out_path: str,
    model: str = IMAGE_MODEL_DEFAULT,
    size: str = IMAGE_SIZE_DEFAULT,
) -> None:
    """T2I 생성 → png 저장 (frame 2D / 옥외 단일 BG)."""
    _reserve_image_call("space_set_bg.image_generate")
    cl = _openai_client()
    # gpt-image 호출 + b64 decode 는 primitive wrapper 로 위임(생성물 capture 동시,
    # scope 미배선이면 no-op). reserve/empty 검증/out_path write 는 여기 유지. ★quality/n
    # 미전달(현 동작) 보존 — call_kwargs 에 model/size 만.
    try:
        png = call_gpt_image_bytes(
            cl,
            mode="generate",
            prompt=prompt,
            ref_paths=None,
            call_kwargs={"model": model, "size": size},
            capture_role="space_set_bg_t2i",
            capture_metadata={"budget_source": "space_set_bg.image_generate"},
        )
    except Exception as exc:
        raise SpaceSetBgProviderError(
            f"image generate raised: {type(exc).__name__}: {exc}"
        ) from exc
    if not png:
        raise SpaceSetBgProviderError("image generate returned empty payload")
    Path(out_path).parent.mkdir(parents=True, exist_ok=True)
    Path(out_path).write_bytes(png)


def image_edit(
    *,
    base_image_path: str,
    prompt: str,
    out_path: str,
    model: str = IMAGE_MODEL_DEFAULT,
    size: str = IMAGE_SIZE_DEFAULT,
) -> None:
    """i2i 편집 → png 저장 (addon 심볼 추가 / 마킹 FP→BG / same_space 참조)."""
    _reserve_image_call("space_set_bg.image_edit")
    cl = _openai_client()
    # i2i edit — wrapper 가 base_image_path 단일 핸들(image=f)로 edit 재현(현 동작 보존).
    # quality/n 미전달. reserve/empty 검증/out_path write 는 여기 유지.
    try:
        png = call_gpt_image_bytes(
            cl,
            mode="edit",
            prompt=prompt,
            ref_paths=[base_image_path],
            call_kwargs={"model": model, "size": size},
            capture_role="space_set_bg_i2i",
            capture_metadata={"budget_source": "space_set_bg.image_edit"},
        )
    except Exception as exc:
        raise SpaceSetBgProviderError(
            f"image edit raised: {type(exc).__name__}: {exc}"
        ) from exc
    if not png:
        raise SpaceSetBgProviderError("image edit returned empty payload")
    Path(out_path).parent.mkdir(parents=True, exist_ok=True)
    Path(out_path).write_bytes(png)


def strip_fence(text: str) -> str:
    """```json ... ``` 펜스 제거."""
    t = (text or "").strip()
    if t.startswith("```"):
        t = t.split("\n", 1)[1] if "\n" in t else ""
        if t.rstrip().endswith("```"):
            t = t.rstrip()[: -3]
    return t.strip()


def parse_json(text: str, *, what: str) -> Any:
    try:
        return json.loads(strip_fence(text))
    except Exception as exc:
        raise SpaceSetBgProviderError(
            f"{what}: JSON parse failed: {type(exc).__name__}: {exc}"
        ) from exc
