"""SemanticContractRouter — moderation sanitize 시점의 polarity 보존 인터페이스.

LLM-produced structured SOT (shot_staging / render_prompt_card) 만 읽어
SemanticContract 를 deterministic 하게 산출. 호출자 (scene_image_pipeline 등) 가
contract.sanitizer_constraints 를 PromptSanitizer.sanitize() 에 전달하면 user
prompt 의 SEMANTIC CONSTRAINTS 섹션 + final sanitized_prompt 의 SEMANTIC
OVERRIDE block 으로 2-layer defense.

Patch B-min wiring + Area #2 Q5 closure (Rule 1 = subject_state, Rule 2 =
character_state). subject_state 는 shot_staging v13 schema required field —
character_angles[*].subject_state ∈ {alive, unconscious, dead, severely_injured}.
is_immobilized_state helper (app.core.subject_state SOT) 가 dispatch 결정.

regex / LLM judge / LVM gate 없음 — 다국어 + 정확도 한계 (spec §1.3).
"""
from __future__ import annotations

from dataclasses import dataclass
from typing import Any

from app.core.subject_state import is_immobilized_state


SEMANTIC_PRIMARY_MODES: tuple[str, ...] = ("none", "pose_locked", "immobilized")


@dataclass(frozen=True)
class SemanticContract:
    primary_mode: str                                # SEMANTIC_PRIMARY_MODES 중 하나
    tags: tuple[str, ...]                            # 미래 비배타 mode 누적 hook. B-min 빈 tuple.
    pose_locked_entity_ids: tuple[str, ...]          # 정렬된 C## 목록
    immobilized_entity_ids: tuple[str, ...]          # ⊆ pose_locked
    source_states: tuple[tuple[str, str], ...]       # (("C91", "dead"), ...) deterministic order
    evidence: tuple[dict[str, Any], ...]             # ({"source", "entity_id", "value"}, ...)
    sanitizer_constraints: dict[str, Any] | None     # primary_mode=="none" 면 None


def _build_sanitizer_constraints(
    primary_mode: str,
    pose_locked_entity_ids: tuple[str, ...],
    source_state_map: dict[str, str],
    evidence: tuple[dict[str, Any], ...],
) -> dict[str, Any] | None:
    """sanitizer_constraints 산출. SOT = dataclass field 하나만 (이중 SOT 차단)."""
    if primary_mode == "none":
        return None
    constraints: dict[str, Any] = {
        "semantic_mode": primary_mode,
        "entity_ids": list(pose_locked_entity_ids),
        "source_states": dict(source_state_map),
        "preserve_pose": True,
        "override_strategy_prefix": True,
        "evidence": [dict(e) for e in evidence],
    }
    if primary_mode == "immobilized":
        constraints.update(
            preserve_subject_state=True,
            forbid_state_polarity_rewrite=True,
            forbid_unharmed_rewrite=True,
            forbid_active_reaction=True,
        )
    else:                                            # pose_locked
        constraints.update(
            preserve_subject_state=False,
            forbid_state_polarity_rewrite=False,    # character_state 과탐 방지 (sleep/rest/injury/death 혼재)
            forbid_unharmed_rewrite=False,
            forbid_active_reaction=False,
        )
    return constraints


def build_semantic_contract(
    *,
    shot_staging: dict | None,
    render_prompt_card: dict | None,
    visible_entities: list[dict[str, Any]],
) -> SemanticContract:
    """LLM-produced structured SOT 만 읽어 SemanticContract 산출.

    Rule 1 — subject_state ∈ IMMOBILIZED_STATES → immobilized.
    Rule 2 — render_prompt_card.continuity_elements_used.fixed_elements 의
             element_type=='character_state' → pose_locked (단, Rule 1 이 잡지
             않은 entity 만).
    """
    name_to_sid = {
        e["name"]: e["short_id"]
        for e in (visible_entities or [])
        if e.get("entity_type") == "character" and e.get("name") and e.get("short_id")
    }

    immobilized: set[str] = set()
    source_state_map: dict[str, str] = {}
    evidence_list: list[dict[str, Any]] = []

    # Rule 1
    for entry in (shot_staging or {}).get("character_angles", []) or []:
        state = entry["subject_state"]            # required by v13 schema, KeyError = schema violation (Gate 4 fail-fast)
        if not is_immobilized_state(state):
            continue
        sid = name_to_sid.get(entry.get("character") or "")
        if not sid:
            continue
        immobilized.add(sid)
        source_state_map[sid] = state
        evidence_list.append({
            "source": "shot_staging.character_angles.subject_state",  # Area #2 Q5
            "entity_id": sid,
            "value": state,
        })

    # Rule 2 — character_state via render_prompt_card.continuity_elements_used.fixed_elements.
    # 주의: shot 별 card 이므로 fixed_elements 는 이미 이 shot 에 적용되는 것만 옴
    # (detail_steps:364/2346 가 applies_to_shots 로 pre-filter).
    fixed_elements = (
        (render_prompt_card or {})
        .get("continuity_elements_used", {})
        .get("fixed_elements", []) or []
    )
    pose_locked: set[str] = set()
    for element in fixed_elements:
        if element.get("element_type") != "character_state":
            continue
        char_name = element.get("character_name") or ""           # 단일 필드 (schema.json:25)
        sid = name_to_sid.get(char_name)
        if sid and sid not in immobilized:
            pose_locked.add(sid)
            evidence_list.append({
                "source": "render_prompt_card.continuity_elements_used.fixed_elements.character_name",
                "entity_id": sid,
                "value": element.get("element_id") or "character_state",
            })

    # 결합
    if immobilized:
        primary_mode = "immobilized"
    elif pose_locked:
        primary_mode = "pose_locked"
    else:
        primary_mode = "none"

    pose_locked.update(immobilized)                  # MUST run AFTER primary_mode decision (else flips immobilized→pose_locked); maintains immobilized ⊆ pose_locked invariant

    pose_locked_ids = tuple(sorted(pose_locked))
    return SemanticContract(
        primary_mode=primary_mode,
        tags=(),
        pose_locked_entity_ids=pose_locked_ids,
        immobilized_entity_ids=tuple(sorted(immobilized)),
        source_states=tuple(sorted(source_state_map.items())),
        evidence=tuple(evidence_list),
        sanitizer_constraints=_build_sanitizer_constraints(
            primary_mode, pose_locked_ids, source_state_map, tuple(evidence_list),
        ),
    )
