"""실내 shared-model pose 가이드 — VLM provider (Wave5).

- judge_indoor_pose_guide: group-level default-deny route 판정 (Gemini text,
  structured). outdoor judge_shared_model_guide 패턴 차용·indoor 전용 분리.
- qc_indoor_pose_guide: 생성된 guide PNG 의 시각 QC (real VLM 이미지리드, litellm,
  fail-closed). floor_plan_vlm_provider 패턴 차용.

★시나리오 의존성 0: judge 입력은 구조 group payload(이름/문구는 데이터 label 로만
흘러들 뿐 템플릿은 generic), QC 는 이미지+generic rubric 만. system/schema 에 작품
고유명사·방/소품/캐릭터명·예시 토큰 0. judge/QC 모두 default-deny / fail-closed.
"""
import base64
import json
import os
from pathlib import Path
from typing import Any, Dict, Optional

# ── judge (text structured, default-deny route) ─────────────────────────

INDOOR_POSE_GUIDE_JUDGE_SCHEMA: Dict[str, Any] = {
    "type": "object",
    "properties": {
        "needs_indoor_pose_guide": {"type": "boolean"},
        "decision_type": {
            "type": "string",
            "enum": [
                "cross_shot_continuity", "single_shot_complexity", "both", "no_guide",
            ],
        },
        "confidence": {"type": "string", "enum": ["low", "medium", "high"]},
        "evidence": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "shot_key": {"type": "string"},
                    "source_field": {"type": "string"},
                    "quote": {
                        "type": "string",
                        "description": "verbatim words from that shot's structured "
                                       "signals/staging that support the route decision",
                    },
                },
                "required": ["shot_key", "source_field", "quote"],
                "additionalProperties": False,
            },
        },
        "reasons": {
            "type": "array",
            "items": {
                "type": "string",
                "enum": [
                    "multi_angle_same_room", "shared_surface_continuity",
                    "figure_placement_continuity", "depth_layering",
                    "support_contact_risk", "single_shot_pose_grounding",
                    "insufficient_evidence",
                ],
            },
        },
        "guide_scope": {"type": "string", "enum": ["group", "single", "none"]},
        "target_shot_keys": {
            "type": "array",
            "items": {"type": "string"},
            "description": "For single_shot_complexity ONLY: the exact shot_key(s) "
                           "(e.g. '14_5') that have a figure with a real pose/support/"
                           "contact grounding risk. Each MUST be one of the group's shots "
                           "and MUST contain at least one figure. Leave empty for "
                           "cross_shot_continuity/both/no_guide.",
        },
        "risk_notes": {"type": "string"},
        "deny_reason": {"type": "string"},
    },
    "required": [
        "needs_indoor_pose_guide", "decision_type", "confidence", "evidence",
        "reasons", "guide_scope", "target_shot_keys", "risk_notes",
    ],
    "additionalProperties": False,
}

INDOOR_POSE_GUIDE_JUDGE_SYSTEM = (
    "You ROUTE an optional shared-model pose guide for a GROUP of film shots the "
    "production has placed in the SAME interior space, sharing the SAME generated "
    "background plate. The guide is a faint copy of that plate with clean line-art "
    "artist's mannequins registered onto it; its ONLY purpose is to keep each figure's "
    "PLACEMENT, DEPTH and weight-bearing SUPPORT consistent and physically grounded "
    "across the group's shots, so an unguided render does not float a figure in the air, "
    "seat someone on nothing, or move the figures around between angles. You decide "
    "ROUTING ONLY — whether such a guide is warranted and at what scope. You do NOT "
    "decide identity, clothing, faces, materials, decor or any visual design, and you do "
    "NOT describe how anything should look.\n"
    "Set needs_indoor_pose_guide=true with decision_type=cross_shot_continuity when the "
    "group shows the same interior across two or more shots — especially from different "
    "framings or angles, with figures placed in TWO OR MORE shots at specific screen "
    "zones/depths — so an unguided render would risk inconsistent placement or an "
    "ungrounded (floating) figure across the shots.\n"
    "Set needs_indoor_pose_guide=true with decision_type=single_shot_complexity for the "
    "SINGLE-SHOT POSE/SUPPORT GROUNDING lane: when ONE shot places a figure in a way that "
    "risks an ungrounded or physically-implausible pose — a figure reaching toward or "
    "resting a hand/limb on a support surface, kneeling/sitting/leaning where weight-"
    "bearing contact must land on a real surface, or a figure at a specific depth/zone an "
    "unguided render could float or mis-place. When you choose single_shot_complexity you "
    "MUST list the exact shot_key(s) at risk in target_shot_keys, and each MUST be a shot "
    "that actually contains a figure (figure_count >= 1). NEVER target an empty "
    "establishing/insert shot with no figure. Use both when cross-shot continuity AND a "
    "single-shot grounding risk both hold. Use no_guide (needs=false) when the shots do "
    "not share a set in a way a guide helps AND no shot has a real pose/support/contact "
    "grounding risk — e.g. plain establishing/insert shots with no figure or no contact "
    "risk.\n"
    "Judge ONLY from the structured group signals and each shot's staging below; never "
    "decide by what the place, people or objects are CALLED — use the generic spatial "
    "classes (foreground/background figure, left/right/center, support surface) in "
    "reasons only. Put the deciding words in evidence with their shot_key and "
    "source_field. If evidence is empty or the case is unclear, answer "
    "needs_indoor_pose_guide=false with confidence=low and decision_type=no_guide."
)


def build_indoor_pose_guide_judge_prompt(group_payload: Dict[str, Any]) -> str:
    """group route judge 입력 — 구조화 group meta + per-shot signals/staging
    (시나리오 명사는 데이터 label 로만 흘러들 뿐, 템플릿은 generic)."""
    return json.dumps(group_payload, ensure_ascii=False, indent=1)


def judge_indoor_pose_guide(
    group_payload: Dict[str, Any],
    *,
    model: str,
    log_context: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """group-level indoor pose 가이드 route 판정 (Gemini text, structured). route only —
    반환 = INDOOR_POSE_GUIDE_JUDGE_SCHEMA dict (attach gate 결정론 평가는 step).

    실패/예외 → {} (호출측 evaluate_indoor_pose_guide_judge 가 judge_failed → deny).
    """
    from app.modules.llm.gemini_text_client import GeminiTextClient

    client = GeminiTextClient(model=model)
    if log_context:
        client.set_context(**log_context)
    out = client.send(
        build_indoor_pose_guide_judge_prompt(group_payload),
        response_schema=INDOOR_POSE_GUIDE_JUDGE_SCHEMA,
        schema_name="indoor_pose_guide_route",
        system_instruction=INDOOR_POSE_GUIDE_JUDGE_SYSTEM,
        temperature=0.1,
    )
    return out if isinstance(out, dict) else {}


# ── guide QC (real VLM 이미지리드, fail-closed) ──────────────────────────

# ★#92 (2026-08-27): VLM 은 **gemini 3.1 pro + grok 최신 둘만**.
#  사용자 지시를 네 번째로 받았다 — 「무조건 gemini 3.1 pro 와 grok
#  최신 모델 둘을 사용해야해 / 아홉 전부 바꿔」.
#
# ★**기본 OFF 라고 안 바꾸면 안 된다.** 앞 판에서 「지금 안 도니까
#  놔둔다」고 내가 판단했는데, 그건 내 판단으로 사용자 명시 지시를
#  뒤집은 것이다. 켜는 순간 지시를 어긴 상태로 돌아간다.
#
# ★물리 이름을 직접 쓴다 — 이 자리는 Router 를 안 거치고 litellm 을
#  직접 친다. 접두 `gemini/` 는 Router 등록부와 같다
#  (`llm_client.py:538`) — 지어낸 것이 아니다.
QC_MODEL_DEFAULT = "gemini/gemini-3.1-pro-preview"
QC_MAX_COMPLETION_TOKENS = 4000

INDOOR_POSE_GUIDE_QC_SCHEMA: Dict[str, Any] = {
    "type": "json_schema",
    "json_schema": {
        "name": "indoor_pose_guide_qc",
        "strict": True,
        "schema": {
            "type": "object",
            "properties": {
                "layout_preserved": {"type": "boolean"},
                "photoreal_person": {"type": "boolean"},
                "clothing_or_face": {"type": "boolean"},
                "text_or_marker_leakage": {"type": "boolean"},
                "environment_redraw": {"type": "boolean"},
                "mannequin_count": {"type": "integer"},
            },
            "required": [
                "layout_preserved", "photoreal_person", "clothing_or_face",
                "text_or_marker_leakage", "environment_redraw", "mannequin_count",
            ],
            "additionalProperties": False,
        },
    },
}

INDOOR_POSE_GUIDE_QC_SYSTEM = (
    "You are a strict visual QA gate for a POSE GUIDE image. The image should be a faint, "
    "pale, washed-out copy of a real interior photo (the registration underlay) with clean "
    "high-contrast black LINE-ART artist's MANNEQUINS drawn on top. Report ONLY what you "
    "literally see, as booleans, with no benefit of the doubt:\n"
    "- layout_preserved: true if the faint background room layout (floor, walls, surfaces, "
    "openings) is intact and NOT repainted/relit/redecorated/sharpened.\n"
    "- photoreal_person: true if any figure looks like a photo-realistic or 3D-rendered "
    "human rather than a flat line-art mannequin.\n"
    "- clothing_or_face: true if any figure has a face, hair, skin texture, or clothing "
    "(mannequins must be featureless).\n"
    "- text_or_marker_leakage: true ONLY if NEW high-contrast text or diagram markers were "
    "ADDED on top of the room as part of the sharp mannequin/annotation layer — e.g. labels "
    "or captions naming the figures, callout numbers or letters, arrows, circle/dot markers, "
    "a legend or a signature. Do NOT flag faint, pale text that belongs to the "
    "washed-out background underlay itself (posters, notices, screens, clocks, documents or "
    "signs that were already present in the original interior photo); incidental background "
    "text baked into the pale underlay is expected and is NOT leakage. Judge by contrast and "
    "layer: leakage is sharp and sits on the mannequin/annotation layer, while background "
    "text is faint and part of the underlay.\n"
    "- environment_redraw: true if the background was redrawn, photo-realised, brightened "
    "to full contrast, or had new objects/architecture invented.\n"
    "- mannequin_count: how many distinct line-art mannequin figures are drawn.\n"
    "Answer with the structured booleans only; never describe the scene or its meaning."
)


def qc_indoor_pose_guide(
    guide_png: bytes,
    *,
    expected_figures: int = 0,
    model: str = QC_MODEL_DEFAULT,
    timeout_seconds: int = 120,
) -> Dict[str, Any]:
    """guide PNG 의 시각 QC (real VLM 1콜, litellm, fail-closed).

    반환 = INDOOR_POSE_GUIDE_QC_SCHEMA 구조 dict. preflight/응답 가드 실패는 모두
    VlmQcError raise → 호출측이 fail-closed no-guide 처리(default-deny). expected_figures
    는 프롬프트 힌트로만 전달(판정은 evaluate_guide_qc 가 결정론으로 비교).
    """
    if not guide_png:
        raise VlmQcError("guide_png is empty")
    # ★#92 (2026-08-27): 관문도 호출도 **모델을 보고** 정한다.
    from app.modules.pipeline.vlm_auth import auth_kwargs
    _auth_kw = auth_kwargs(model, VlmQcError)  # ★호출당 한 번만
    try:
        from app.core.openai_keys import (  # type: ignore
            llm_completion as _llm_completion,
        )
    except Exception as exc:  # pragma: no cover — env-dependent
        raise VlmQcError(f"litellm import failed: {type(exc).__name__}: {exc}") from exc

    image_b64 = base64.b64encode(guide_png).decode("ascii")
    image_data_url = f"data:image/png;base64,{image_b64}"
    user_text = (
        "Run the QC rubric on the attached pose-guide image. For reference the guide was "
        f"intended to contain about {max(0, int(expected_figures))} mannequin figure(s), "
        "but report the count you actually see. Return the structured booleans only."
    )
    messages = [
        {"role": "system", "content": INDOOR_POSE_GUIDE_QC_SYSTEM},
        {"role": "user", "content": [
            {"type": "text", "text": user_text},
            {"type": "image_url", "image_url": {"url": image_data_url, "detail": "high"}},
        ]},
    ]
    try:
        response = _llm_completion(
            model=model, **_auth_kw, messages=messages,
            response_format=INDOOR_POSE_GUIDE_QC_SCHEMA,
            timeout=timeout_seconds, max_completion_tokens=QC_MAX_COMPLETION_TOKENS,
            num_retries=0,
        )
    except Exception as exc:
        raise VlmQcError(f"litellm.completion raised: {type(exc).__name__}: {exc}") from exc

    choices = getattr(response, "choices", None) or []
    if not choices:
        raise VlmQcError("response.choices is empty")
    choice = choices[0]
    msg = getattr(choice, "message", None)
    if msg is None:
        raise VlmQcError("response.choices[0].message is missing")
    if getattr(msg, "refusal", None):
        raise VlmQcError("VLM emitted a refusal")
    content = getattr(msg, "content", None)
    if not content:
        raise VlmQcError("response content is empty")
    if getattr(choice, "finish_reason", None) != "stop":
        raise VlmQcError(
            f"finish_reason must be 'stop' (got {getattr(choice, 'finish_reason', None)!r})")
    try:
        parsed = json.loads(content)
    except (TypeError, ValueError) as exc:
        raise VlmQcError(f"content is not valid JSON: {type(exc).__name__}: {exc}") from exc
    if not isinstance(parsed, dict):
        raise VlmQcError("parsed QC content is not an object")
    return parsed


class VlmQcError(Exception):
    """indoor guide QC VLM 호출/응답 실패 — fail-closed no-guide 신호."""


def make_indoor_qc_fn(*, model: str, log_context: Optional[Dict[str, Any]] = None):
    """build_indoor_pose_guide 의 qc_fn(png, *, expected_figures) 합성.

    VLM 이미지리드(qc_indoor_pose_guide) → evaluate_guide_qc 결정론 판정.
    VLM 실패(VlmQcError) = fail-closed (False, qc_vlm_error) → no-guide degrade.
    """
    from app.services.indoor_shared_pose_guide_service import evaluate_guide_qc

    def _qc(png: bytes, *, expected_figures: int = 0):
        try:
            verdict = qc_indoor_pose_guide(
                png, expected_figures=expected_figures, model=model)
        except VlmQcError:
            return False, "qc_vlm_error"
        return evaluate_guide_qc(verdict, expected_figures=expected_figures)

    return _qc
