"""이미지 품질 검증 모듈 — GPT-5.4 비전으로 생성된 이미지 품질 판정.

prompt_loader 통합 (problems.md #14): hard-coded ``v1`` 디렉토리 직접 read 제거
→ ``prompt_loader.load_prompt`` 경유 (DB 우선 + numeric version 정렬 + #6 #13).
"""

import base64
import json
import socket
import time
import urllib.error
import urllib.request
from typing import Any, Dict, List

from app.core.config import settings
from app.modules.llm.image_tracer import traced_call

_MODULE = "image_validation"

OPENAI_API_URL = "https://api.openai.com/v1/responses"

VALIDATION_SCHEMA: Dict[str, Any] = {
    "type": "object",
    "additionalProperties": False,
    "properties": {
        "description": {"type": "string"},
        "score": {"type": "integer"},
        "passed": {"type": "boolean"},
        "issues": {
            "type": "array",
            "items": {"type": "string"},
        },
    },
    "required": ["description", "score", "passed", "issues"],
}

PASS_THRESHOLD = 60


def _load_prompt(filename: str) -> str:
    """``prompts/_base/image_validation`` 에서 stem 로드 (DB 우선).

    파일 모듈 이름은 ``image_validator`` (단수) 이지만 prompts 디렉토리는
    ``image_validation`` (다른 형태) — 흡수 시 매핑 confusion 회피 (review I4).

    Raises FileNotFoundError 또는 RuntimeError (#6 strict mode) — caller 처리.
    """
    from app.modules.prompt_loader import load_prompt

    stem = filename[:-3] if filename.endswith(".md") else filename
    return load_prompt(_MODULE, stem)


@traced_call(operation="vision_validation", provider="openai",
             model_of=lambda api_key, model, *a, **k: model,
             prompt_of=lambda api_key, model, image_bytes, text_prompt,
             *a, **k: text_prompt,
             output_text="[vision judged]")
def _call_openai_vision(
    api_key: str,
    model: str,
    image_bytes: bytes,
    text_prompt: str,
) -> Dict[str, Any]:
    """Call OpenAI Responses API with image input for vision analysis.

    ★기록: 이 경로는 urllib 로 Responses API 를 직접 쳐서 litellm 콜백이 안
    닿는다 — 감싸지 않으면 VLM 판정이 Opik 에 한 줄도 안 남는다(2026-08-07).
    """
    b64_data = base64.b64encode(image_bytes).decode("ascii")

    body: Dict[str, Any] = {
        "model": model,
        "input": [
            {
                "type": "message",
                "role": "user",
                "content": [
                    {"type": "input_text", "text": text_prompt},
                    {
                        "type": "input_image",
                        "image_url": f"data:image/png;base64,{b64_data}",
                    },
                ],
            },
        ],
        "text": {
            "format": {
                "type": "json_schema",
                "name": "image_validation",
                "strict": True,
                "schema": VALIDATION_SCHEMA,
            }
        },
        "temperature": 0.1,
        "store": False,
    }

    req = urllib.request.Request(
        OPENAI_API_URL,
        data=json.dumps(body).encode("utf-8"),
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        },
        method="POST",
    )

    timeout = settings.llm_timeout_validation
    max_retries = settings.llm_max_retries
    last_error: Exception | None = None
    for attempt in range(1, max_retries + 2):  # 첫 시도 + max_retries 재시도
        try:
            with urllib.request.urlopen(req, timeout=timeout) as resp:
                payload = json.loads(resp.read().decode("utf-8"))
            break
        except urllib.error.HTTPError as exc:
            error_text = exc.read().decode("utf-8", errors="replace")
            last_error = RuntimeError(f"OpenAI Vision API error {exc.code}: {error_text}")
            # urllib 은 provider 를 안 실어 브로커가 OpenAI 호출인지 모른다 —
            # 올라가는 예외에 표시를 붙여야 기존 판정 계약이 적용된다.
            from app.core.openai_keys import mark_openai_failure
            mark_openai_failure(
                last_error, status=exc.code, body=error_text)
            # ★키 수준 실패는 같은 키로 재시도해도 풀리지 않는다 — quota 소진이
            # 429 로 올 때 죽은 키를 max_retries 번 더 태우고 backoff 까지
            # 걸었다(실측 13.8초). 전환은 브로커가 판단하게 넘긴다.
            from app.core.openai_keys import is_key_level_failure
            if (exc.code in {429, 500, 502, 503, 504}
                    and attempt <= max_retries
                    and not is_key_level_failure(last_error)):
                time.sleep(2 * attempt)
                continue
            raise last_error from exc
        except (urllib.error.URLError, socket.timeout) as exc:
            last_error = exc
            if attempt <= max_retries:
                time.sleep(2 * attempt)
                continue
            raise RuntimeError(f"OpenAI Vision API failed after {max_retries} retries: {exc}") from exc
    else:
        raise RuntimeError(f"OpenAI Vision API failed: {last_error}")

    # Extract text from Responses API payload
    output_text = payload.get("output_text")
    if isinstance(output_text, str) and output_text.strip():
        return json.loads(output_text)

    output = payload.get("output")
    if isinstance(output, list):
        for item in output:
            if not isinstance(item, dict):
                continue
            content = item.get("content")
            if not isinstance(content, list):
                continue
            for part in content:
                if isinstance(part, dict) and part.get("type") == "output_text":
                    text = part.get("text")
                    if isinstance(text, str) and text.strip():
                        return json.loads(text)

    raise RuntimeError("OpenAI Vision response did not include output_text.")


def _format_traits_block(entity_info: Dict[str, Any]) -> str:
    """Format stable traits into a readable block."""
    stable_traits = entity_info.get("stable_traits", "{}")
    if isinstance(stable_traits, str):
        try:
            traits_data = json.loads(stable_traits)
        except json.JSONDecodeError:
            traits_data = {}
    else:
        traits_data = stable_traits

    visual_traits = traits_data.get("visual_anchor_traits", [])
    if not visual_traits and isinstance(traits_data, dict):
        for _k, v in traits_data.items():
            if isinstance(v, str):
                visual_traits.append(v)

    if not visual_traits:
        return "- (no specific traits listed)"
    return "\n".join(f"- {trait}" for trait in visual_traits)


class ImageValidator:
    """GPT-5.4 비전으로 생성된 이미지 품질 판정."""

    def __init__(
        self,
        api_key: str | None = None,
        model: str | None = None,
    ) -> None:
        # ★생성 시점에 복사하면 이후 슬롯 전환이 이 인스턴스에 닿지 않는다.
        # 명시 키만 보관하고 실제 키는 호출 시점에 브로커가 정한다.
        self._explicit_key = api_key
        self._model = model or settings.openai_model

    def validate_reference_image(
        self,
        image_bytes: bytes,
        entity_info: Dict[str, Any],
    ) -> Dict[str, Any]:
        """참조 이미지가 엔티티 설명과 일치하는지 검증.

        Args:
            image_bytes: PNG image bytes to validate.
            entity_info: Dict with keys: name, entity_type, description, stable_traits.

        Returns:
            {"score": 0-100, "passed": bool, "issues": [...], "description": str}
        """
        if not _has_openai_key():
            raise RuntimeError("OpenAI API key is not configured for image validation.")

        template = _load_prompt("reference_validation.md")
        prompt = template.format(
            entity_name=entity_info.get("name", ""),
            entity_type=entity_info.get("entity_type", ""),
            description=entity_info.get("description", ""),
            traits_block=_format_traits_block(entity_info),
            world_context=entity_info.get("world_context", "Not provided"),
        )

        result = _with_key_failover(
            lambda _k: _call_openai_vision(
                api_key=_k,
                model=self._model,
                image_bytes=image_bytes,
                text_prompt=prompt,
            ),
            fixed_key=self._explicit_key,
        )

        # Enforce threshold
        score = result.get("score", 0)
        result["passed"] = score >= PASS_THRESHOLD
        return result

    def validate_scene_image(
        self,
        image_bytes: bytes,
        scene_info: Dict[str, Any],
        entity_names: List[str],
    ) -> Dict[str, Any]:
        """씬 이미지가 프롬프트 및 엔티티와 일치하는지 검증.

        Args:
            image_bytes: PNG image bytes to validate.
            scene_info: Dict with keys: scene_heading, beat_title, still_frame_prompt.
            entity_names: List of entity names expected to be visible.

        Returns:
            {"score": 0-100, "passed": bool, "issues": [...], "description": str}
        """
        if not _has_openai_key():
            raise RuntimeError("OpenAI API key is not configured for image validation.")

        template = _load_prompt("scene_validation.md")
        prompt = template.format(
            scene_heading=scene_info.get("scene_heading", ""),
            beat_title=scene_info.get("beat_title", ""),
            scene_prompt=scene_info.get("still_frame_prompt", ""),
            entity_names=", ".join(entity_names) if entity_names else "(none)",
            world_context=scene_info.get("world_context", "Not provided"),
        )

        result = _with_key_failover(
            lambda _k: _call_openai_vision(
                api_key=_k,
                model=self._model,
                image_bytes=image_bytes,
                text_prompt=prompt,
            ),
            fixed_key=self._explicit_key,
        )

        score = result.get("score", 0)
        result["passed"] = score >= PASS_THRESHOLD
        return result

    def validate_pdf_page(
        self,
        pdf_page_image: bytes,
        expected_content: Dict[str, Any],
    ) -> Dict[str, Any]:
        """PDF 페이지가 올바르게 렌더링되었는지 검증.

        Args:
            pdf_page_image: PNG image bytes of the rendered PDF page.
            expected_content: Dict with keys: page_number, has_image, has_text, section_title.

        Returns:
            {"score": 0-100, "passed": bool, "issues": [...], "description": str}
        """
        if not _has_openai_key():
            raise RuntimeError("OpenAI API key is not configured for image validation.")

        template = _load_prompt("pdf_validation.md")
        prompt = template.format(
            page_number=expected_content.get("page_number", "unknown"),
            has_image=str(expected_content.get("has_image", False)),
            has_text=str(expected_content.get("has_text", True)),
            section_title=expected_content.get("section_title", ""),
        )

        result = _with_key_failover(
            lambda _k: _call_openai_vision(
                api_key=_k,
                model=self._model,
                image_bytes=pdf_page_image,
                text_prompt=prompt,
            ),
            fixed_key=self._explicit_key,
        )

        score = result.get("score", 0)
        result["passed"] = score >= PASS_THRESHOLD
        return result

def _has_openai_key() -> bool:
    from app.core.openai_keys import has_openai_key

    return has_openai_key()


def _with_key_failover(fn, *, fixed_key=None):
    """활성 슬롯 키로 부르고 키 수준 실패면 다음 슬롯으로 (2026-08-01)."""
    from app.core.openai_keys import call_with_key_failover

    return call_with_key_failover(
        fn, where="image_validator.vision", fixed_key=fixed_key)


def _active_openai_key() -> str:
    """활성 키 슬롯 — 1차 필드를 직접 보면 보조 키가 안 쓰인다."""
    from app.core.openai_keys import active_key

    return active_key()
