"""Gaze direction enum SOT + target pairing + dispatch helper (Area #2 Q1 + Q3 + Q7).

closed-world enum only — LLM producer (shot_staging) emit gaze_direction_kind +
gaze_target_id, consumer = enum check via helper.
regex / substring / NL fallback 금지 (Gate 1).

validate_pairing = production runtime primary defense for kind↔target pairing.
shot_staging v15 부터 schema 의 oneOf 조건부가 제거됐다 (OpenAI strict structured-output
호환 — combinator 미지원). schema 는 gaze_direction_kind enum + gaze_target_id
required-nullable 만 강제하고, kind↔target 정합은 shot_staging 코드측에서 이
validate_pairing 으로 fail-fast 검증한다.
Q7 closure: gaze_target_id shape = registered short_id only.
"""
from __future__ import annotations

import re

from app.core.errors import AppError


GAZE_DIRECTION_KINDS: tuple[str, ...] = (
    "camera", "down", "up", "distant", "closed_eyes", "off_screen",
    "looks_at_character", "looks_at_object",
)

TARGET_REQUIRED_KINDS: frozenset[str] = frozenset({"looks_at_character", "looks_at_object"})

_CHAR_ID_RE = re.compile(r"^C\d{2,3}$")
_OBJECT_ID_RE = re.compile(r"^(P|B)\d{2,3}$")


def is_target_required(kind: str) -> bool:
    return kind in TARGET_REQUIRED_KINDS


def validate_pairing(kind: str, target_id: str | None, where: str) -> None:
    if kind not in GAZE_DIRECTION_KINDS:
        raise AppError(
            "step.contract_violation.gaze_direction_kind.enum",
            f"{where}: invalid kind={kind!r}, expected one of {GAZE_DIRECTION_KINDS}",
        )
    target_required = is_target_required(kind)
    if target_required and not target_id:
        raise AppError(
            "step.contract_violation.gaze_target_id.missing_for_target_kind",
            f"{where}: kind={kind!r} requires gaze_target_id",
        )
    if not target_required and target_id is not None:
        raise AppError(
            "step.contract_violation.gaze_target_id.present_for_non_target_kind",
            f"{where}: kind={kind!r} forbids gaze_target_id (got {target_id!r})",
        )
    # Q7 closure: shape validation (closed-world registered short_id only)
    if target_required:
        if kind == "looks_at_character" and not _CHAR_ID_RE.match(target_id or ""):
            raise AppError(
                "step.contract_violation.gaze_target_id.shape",
                f"{where}: kind=looks_at_character requires ^C\\d{{2,3}}$ (got {target_id!r})",
            )
        if kind == "looks_at_object" and not _OBJECT_ID_RE.match(target_id or ""):
            raise AppError(
                "step.contract_violation.gaze_target_id.shape",
                f"{where}: kind=looks_at_object requires ^(P|B)\\d{{2,3}}$ (got {target_id!r})",
            )


def resolve_visible_character_target(
    target_id: str,
    visible_character_ids: set[str],
    id_to_name: dict[str, str],
    where: str,
) -> tuple[str, str] | None:
    """target_id (C##) → (sid, canonical_name) lookup.

    structural validation only: C## shape + visible_set membership.
    None = visible 밖 → consumer 가 결정 (skip or warn).
    name matching / noun matching / character/object 재판정 금지 (Gate 1)."""
    if not _CHAR_ID_RE.match(target_id or ""):
        return None
    if target_id not in visible_character_ids:
        return None
    name = id_to_name.get(target_id)
    if not name:
        return None
    return (target_id, name)
