"""OpenRouter VLM 판정 클라이언트 — QK 판정 체계 경로 (2026-08-12).

`backend/build_qk_openrouter_eval.py` 재평가에서 검증된 부품의 프로덕션
이관이다. Router(`call_structured`)를 타지 않는 이유는 qwen_vlm_client 와
같다 — 이 저장소의 구조화 호출은 `json_schema`/`strict` 를 보내는데
OpenRouter 경유 모델(qwen/kimi)은 `json_object` 까지만 신뢰할 수 있고,
모델·배포본에 따라 `response_format` 자체를 거부(400)하기도 한다. 그래서
스키마 전문을 시스템에 동봉하고 여기서 `jsonschema` 로 검증한 뒤, 실패하면
오류를 되먹여 한 번 다시 묻는다 (Qwen 클라이언트와 같은 계약).

★litellm 우회 직접 호출이므로 **모든 API 왕복을 `record_provider_call` 로
남긴다** (llm_call_log+Opik — 2026-08-07 전수 기록 지시). 우회 호출 AST
스캐너(`test_all_provider_calls_are_traced`)가 이 배선을 검사한다.
"""
from __future__ import annotations

import json
import logging
import time
from typing import Any, Dict, List, Optional

from app.modules.llm.qwen_vlm_client import JSON_CLAUSE, extract_json

logger = logging.getLogger(__name__)


class OpenRouterNotConfigured(RuntimeError):
    pass


def openrouter_configured() -> bool:
    from app.core.config import settings

    return bool((settings.openrouter_api_key or "").strip())


def _client():
    from app.core.config import settings

    key = (settings.openrouter_api_key or "").strip()
    if not key:
        raise OpenRouterNotConfigured(
            "OPENROUTER_API_KEY 가 비어 있다 — backend/.env 에 키를 넣어라."
        )
    from openai import OpenAI

    return OpenAI(
        api_key=key,
        base_url=(settings.openrouter_base_url or "").strip(),
        timeout=300.0,
    )


def _prompt_text_of(parts: List[Dict[str, Any]]) -> str:
    """기록용 프롬프트 — 텍스트 파트만 잇는다 (base64 이미지는 DB 에 안 싣는다)."""
    return "\n".join(
        str(p.get("text") or "") for p in parts if p.get("type") == "text"
    )


def ask_openrouter_structured(
    step_tag: str,
    system: str,
    parts: List[Dict[str, Any]],
    schema: Dict[str, Any],
    *,
    model: str,
    max_retry: int = 1,
    max_tokens: Optional[int] = None,
    opik_metadata: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """OpenRouter 모델에 멀티모달 판정을 묻고 스키마 검증 payload 를 돌려준다.

    parts 는 `call_structured` 멀티모달 parts 와 같은 형식(`png_part`/
    `ref_parts` 산출 — OpenAI chat `image_url` data URI). 계약은
    `ask_qwen_structured` 와 동형 — client·모델 슬롯만 다르다.

    `response_format={"type":"json_object"}` 를 우선 시도하고, 모델이 그
    파라미터 자체를 거부(400)하면 빼고 한 번 더 시도한다(재평가 실측 계약).
    스키마 검증 실패 = 오류를 되먹여 교정 재질의 `max_retry` 회.

    max_tokens (2026-08-13 G+G46): 긴 구조화 출력의 미달-잘림 방지용 출력
    예산 — 파일럿 실측에서 판정류 출력이 기본치에 잘려 따옴표 파손으로
    죽었다(qwen 4000 잘림 14건 동류). None(default)=미전송 byte-identical.
    """
    from jsonschema import ValidationError, validate as _js_validate

    from app.modules.llm.image_tracer import (
        ambient_call_meta, record_provider_call, resolve_step_name)

    client = _client()
    sys_prompt = system + JSON_CLAUSE.format(
        schema=json.dumps(schema, ensure_ascii=False, indent=2))
    messages: List[Dict[str, Any]] = [
        {"role": "system", "content": sys_prompt},
        {"role": "user", "content": parts},
    ]
    meta = dict(ambient_call_meta())
    for k, v in (opik_metadata or {}).items():
        if v is not None and k not in meta:
            meta[k] = v
    step = resolve_step_name(step_tag, meta)
    log_prompt = _prompt_text_of(parts)

    def _record(status: str, dur_ms: int, *, output: Optional[str] = None,
                error: Optional[str] = None) -> None:
        record_provider_call(
            step=step, model=model, prompt=log_prompt, status=status,
            duration_ms=dur_ms, meta=meta, provider="openrouter",
            operation=step_tag, output_text=output, error=error)

    last_err = ""
    for attempt in range(max_retry + 1):
        resp = None
        t0 = time.time()
        # json_object 미지원 모델·배포본이면 400 — 빼고 한 번 더 (재평가
        # 실측: kimi 일부 배포본이 response_format 을 거부했다).
        for rf in ({"type": "json_object"}, None):
            t0 = time.time()
            try:
                resp = client.chat.completions.create(
                    model=model,
                    messages=messages,
                    **({"response_format": rf} if rf else {}),
                    **({"max_tokens": max_tokens} if max_tokens else {}),
                )
                break
            except Exception as exc:  # noqa: BLE001
                dur = int((time.time() - t0) * 1000)
                if rf is None or "400" not in repr(exc):
                    _record("error", dur, error=repr(exc)[:500])
                    raise
                _record("error", dur,
                        error="response_format 미지원(400) — 빼고 재시도")
        if resp is None:  # 폴백 루프 계약상 도달 불가 — 방어
            raise RuntimeError("openrouter 호출이 응답 없이 폴백 루프를 벗어났다")
        dur = int((time.time() - t0) * 1000)
        # ★유료 왕복이 성공한 뒤의 모든 실패도 그 왕복의 기록을 정확히
        #  1건 남긴다 (Codex BLOCK-1): choices 결손·message/content shape
        #  이상은 스키마 재질의 대상이 아니라 provider 이상이다 — error
        #  기록 후 그대로 전파. 기록 없는 유료 왕복은 "이 판정을 산 적
        #  없다"로 읽혀 resume 이중 지출의 창이 된다.
        try:
            content = resp.choices[0].message.content or ""
            if not isinstance(content, str):
                raise TypeError(
                    "message.content 가 str 이 아니다: "
                    f"{type(content).__name__}")
        except Exception as exc:  # noqa: BLE001
            _record("error", dur,
                    error=f"unexpected response shape: {exc!r}"[:400])
            raise
        try:
            payload = extract_json(content)
            _js_validate(payload, schema)
            _record("success", dur, output=content)
            return payload
        except (ValueError, ValidationError, json.JSONDecodeError) as exc:
            last_err = f"{type(exc).__name__}: {exc}"[:400]
            _record("error", dur, error=f"schema violation: {last_err}")
            if attempt >= max_retry:
                raise
            # json_object 는 문법만 보장 — 스키마 위반은 오류를 되먹여
            # 한 번의 교정 기회를 준다 (qwen 클라이언트와 동일 계약).
            messages += [
                {"role": "assistant", "content": content[:4000]},
                {"role": "user", "content": (
                    "That reply did not satisfy the json schema: "
                    f"{last_err}\nReturn the corrected json object only."
                )},
            ]
        except Exception as exc:  # noqa: BLE001
            # ★모델 출력 문제가 아닌 응답 후 실패 — 대표례 SchemaError
            #  (로컬 스키마 자체 불량; ValidationError 의 subclass 가
            #  아니다, Codex 재확인 BLOCK). 재질의 대상이 아니므로 그
            #  왕복의 error 기록 1건 후 그대로 전파한다 (기록 없는 유료
            #  왕복 금지 계약). _record 는 자체 실패를 삼키므로 위 성공
            #  기록이 이 갈래로 떨어질 일은 없다.
            _record("error", dur,
                    error=f"unexpected post-response failure: {exc!r}"[:400])
            raise
    raise RuntimeError("unreachable")
