"""G3.2 scene_detail owned-objects judge wrapper.

post-parse 단계가 호출하는 1-call (no retry — round 4 Q1=B / round 5 M1) judge.

Routing:
    `_PIPELINE_STEP_EXTENSIONS["scene_detail_owned_judge"]` 에 등록된 model
    (gpt-mini) 로 `call_structured_fn(step="scene_detail_owned_judge", ...)`
    호출. `default_model` 키는 `call_structured()` 인자가 아니다 (round 3 #1).

Contract:
    - owned 가 빈 list → LLM 호출 자체 skip → `[]` 반환 (불필요한 비용 절감).
    - owned 가 1+ entries → judge 1회 호출 → `violations` list 반환.
    - violations 발견 시 retry 하지 않음 — caller 가 `contract_violation`
      status 로 marking 한다.
    - C2 v1 (W3): judge v4 prompt-pack 은 redraw verb whitelist 대신 producer
      가 declare 한 `owned_object_usage` echo 를 t2i_prompt 와 cross-check 한다.
      `run_owned_judge` 가 해당 echo 를 judge prompt 로 전달한다.
    - LLM 응답에 ``violations`` 키가 없거나 list 가 아니면 schema contract 위반
      → AppError(step.contract_violation) raise (Wave 6 BLOCKING fix —
      silent ``[]`` fallback 금지 / feedback_no_silent_fallback.md 준수).

이 모듈은 helper (`_owned_helpers.py`) 와 분리 — helper 는 hash/sentinel 등
순수 함수만 담고, judge 는 LLM 호출 의존성 (prompt_loader / call_structured)
을 가진다. 분리 이유: helper test 가 LLM 호출 mock 없이 가능해야 함.
"""
from __future__ import annotations

import json
from typing import Any, Callable, Dict, List, Optional

from app.core.errors import AppError


def run_owned_judge(
    *,
    t2i_prompt: str,
    owned: List[str],
    owned_object_usage: List[Dict[str, str]],
    camera_direction: str,
    call_structured_fn: Callable[..., Dict[str, Any]],
    project_config: Optional[Dict[str, Any]] = None,
    opik_metadata: Optional[Dict[str, Any]] = None,
    shot_intent: str = "",
) -> List[Dict[str, str]]:
    """post-parse 가 호출하는 judge wrapper.

    `call_structured_fn(step="scene_detail_owned_judge", ...)` 만 호출 — model
    라우팅은 `_PIPELINE_STEP_EXTENSIONS` 가 처리. `default_model` 은
    `call_structured()` 인자가 아님 (round 3 #1 / round 4 MINOR 1).

    owned 가 빈 list 면 LLM 호출 skip → 빈 violations.

    1-call only (round 4 Q1=B / round 5 M1): violations 가 비어있지 않아도 retry
    하지 않는다. caller 가 sentinel 안에 violations 보존 + status =
    contract_violation 로 marking.

    Args:
        t2i_prompt: scene_detail 이 작성한 영어 또는 source-language T2I 프롬프트.
        owned: English canonical common nouns (round 4 Q2=B / round 5 BLOCKING 1
            — `_owned_helpers.normalize_owned_list` 가 ASCII 만 통과시켜
            영어 canonical 강제).
        owned_object_usage: C2 v1 (W3) — producer (scene_detail v30) 가 본
            variation 에 declare 한 per-token echo. 각 entry =
            `{owned_token, usage_kind, source_phrase}`. judge v4 prompt 가
            t2i_prompt 와 cross-check 하도록 그대로 전달한다 (coverage validation
            은 caller 의 `build_owned_sentinel` 책임 — 여기서 중복 검증하지 않음).
        camera_direction: shot 의 자연어 카메라 정보.
        shot_intent: judge v5 (2026-07-02) — shot 의 representative moment
            텍스트. narrow exception (1) allowed_visual_state_change 의
            유일한 증거 소스 (shot-intent 가 owned 객체의 시각 상태/내용
            변형을 명시 요구하는 경우만 위반 아님). 빈 문자열이면 v5 prompt
            가 exception (1) 을 적용할 근거가 없음 = 기존과 동일 판정.
        call_structured_fn: `app.modules.llm.llm_client.call_structured` (또는
            test 에서 주입한 fake).
        project_config: optional — 프로젝트별 model override.
        opik_metadata: optional — Opik trace metadata.

    Returns:
        violations list. 각 항목:
            - `owned_object` (str): 위반 owned name.
            - `violating_phrase` (str): 위반 구절 발췌.
            - `reason` (str): 위반 사유 1 문장.
        violations 없거나 owned 가 빈 list → 빈 list.
    """
    if not owned:
        return []

    # lazy import — circular import 방지 + helper 모듈과 격리.
    from app.modules.prompt_loader import load_prompt, load_schema

    system = load_prompt("scene_detail_owned_judge", "system")
    schema = load_schema("scene_detail_owned_judge", "schema")
    template = load_prompt("scene_detail_owned_judge", "user_template")

    owned_block = "\n".join(f"- {o}" for o in owned)
    # C2 v1 (W3): producer 가 declare 한 owned_object_usage echo 를 judge v4 가
    # t2i_prompt 와 cross-check 하도록 JSON 직렬화. ensure_ascii=False —
    # source_phrase 가 한국어일 수 있음. sort_keys — 안정적 직렬화 (test 재현성).
    owned_usage_block = json.dumps(
        owned_object_usage, ensure_ascii=False, indent=2, sort_keys=True
    )
    user_prompt = (
        template
        .replace("{t2i_prompt}", t2i_prompt or "")
        .replace("{owned_list_block}", owned_block)
        .replace("{owned_object_usage}", owned_usage_block)
        .replace("{camera_direction}", camera_direction or "")
        # judge v5: placeholder 는 v5 template 에만 존재 — v4 이하 pack 에서는
        # no-op replace (하위 호환).
        .replace("{shot_intent}", shot_intent or "")
    )

    result = call_structured_fn(
        step="scene_detail_owned_judge",
        system_prompt=system,
        user_prompt=user_prompt,
        response_schema=schema,
        project_config=project_config,
        schema_name="scene_detail_owned_judge",
        opik_metadata=opik_metadata,
    )
    # Wave 6 BLOCKING fix: schema 가 violations 를 required 로 선언 — 누락/타입
    # 불일치는 upstream contract 위반. silent ``[]`` fallback 차단
    # (feedback_no_silent_fallback.md).
    if not isinstance(result, dict) or "violations" not in result:
        raise AppError(
            code="step.contract_violation",
            message=(
                "scene_detail_owned_judge response missing 'violations' field: "
                f"{result!r}"
            ),
        )
    violations = result["violations"]
    if not isinstance(violations, list):
        raise AppError(
            code="step.contract_violation",
            message=(
                "scene_detail_owned_judge response 'violations' must be list "
                f"(got {type(violations).__name__}): {violations!r}"
            ),
        )
    return list(violations)
