"""Area D-next-min + C7 — keep_elements shape SOT (environment / static_prop only).

producer (shot_dependency_t2i, schema v8) 가 LLM 으로 emit 하는
keep_elements 의 shape 검증. memory `feedback_llm_based_judgment` 정합
— code 가 kind / subject_kind 의미 판단 안 함, enum check 만.

KEEP_ELEMENT_KINDS 는 2종 (environment / static_prop) 만 — character /
person / body / dead / unconscious 묘사는 keep_elements 가 다루지 않음.
C7 (2026-05-20) — KEEP_ELEMENT_SUBJECT_KINDS 는 인물/비인물 enum 2종
(non_human_visual_element / human_or_character_reference). producer 가
각 entry 의 인물 여부를 explicit emit, human_or_character_reference 는
fail-fast 거부. character state 보존은 별도 layer
(scene_consistency.character_state / character_state_variant ref /
semantic_contract_router) 가 deterministic 처리.

이 module 은 leaf — 외부 모듈 import 안 함 (`AppError` 만). 다른
모든 영역 (producer step / loader / scene_reference / detail_steps)
이 import 해서 사용.
"""
from __future__ import annotations

from typing import Any, Iterable

from app.core.errors import AppError


KEEP_ELEMENT_KINDS: frozenset[str] = frozenset({
    "environment", "static_prop",
})

# C7 (2026-05-20) — 인물/비인물 axis. producer 가 각 keep_elements entry 의
# 인물 여부를 explicit enum 으로 emit. code 는 enum membership 만 검사
# (open-world label 판단 X — feedback_llm_based_judgment Semantic Regex Ban).
KEEP_ELEMENT_SUBJECT_KINDS: frozenset[str] = frozenset({
    "non_human_visual_element", "human_or_character_reference",
})


def validate_keep_elements_entry(
    entry: Any,
    *,
    label_source: str,
    error_code: str,
) -> None:
    """단일 keep_elements entry 검증 — {label, kind, subject_kind} dict + kind ∈ KEEP_ELEMENT_KINDS (2종) + subject_kind ∈ KEEP_ELEMENT_SUBJECT_KINDS + human_or_character_reference fail-fast.

    Args:
        entry: keep_elements list 의 한 entry.
        label_source: error message 에 포함할 위치 (예: "S1_Shot2 loc_ref[0]").
        error_code: AppError code (layer 별 다름 —
                    "step.shot_dependency_t2i.keep_elements_shape_invalid" /
                    "step.scene_checkpoint_loaders.keep_elements_entry_invalid" /
                    "step.scene_reference.keep_elements_kind_invalid").

    Raises: AppError.
    """
    if not isinstance(entry, dict):
        raise AppError(
            code=error_code,
            message=(
                f"{label_source}: keep_elements entry must be dict, "
                f"got {type(entry).__name__}: {entry!r}. "
                f"Producer schema v8: list[{{label, kind∈{{environment, static_prop}}, subject_kind}}]."
            ),
            status_code=400,
        )
    if "label" not in entry or "kind" not in entry or "subject_kind" not in entry:
        raise AppError(
            code=error_code,
            message=(
                f"{label_source}: keep_elements entry missing required keys — "
                f"got keys {sorted(entry.keys())!r}, value={entry!r}"
            ),
            status_code=400,
        )
    # Area D-next v3 (Codex I-4 흡수) — label type check. silent fallback 0.
    if not isinstance(entry["label"], str):
        raise AppError(
            code=error_code,
            message=(
                f"{label_source}: keep_elements entry.label must be str, "
                f"got {type(entry['label']).__name__}: {entry['label']!r}"
            ),
            status_code=400,
        )
    if entry["kind"] not in KEEP_ELEMENT_KINDS:
        raise AppError(
            code=error_code,
            message=(
                f"{label_source}: keep_elements entry.kind must be one of "
                f"{sorted(KEEP_ELEMENT_KINDS)}, got {entry['kind']!r}"
            ),
            status_code=400,
        )
    # C7 — subject_kind enum check. open-world label 판단 X, enum membership 만.
    if entry["subject_kind"] not in KEEP_ELEMENT_SUBJECT_KINDS:
        raise AppError(
            code=error_code,
            message=(
                f"{label_source}: keep_elements entry.subject_kind must be one of "
                f"{sorted(KEEP_ELEMENT_SUBJECT_KINDS)}, got {entry['subject_kind']!r}"
            ),
            status_code=400,
        )
    # C7 — producer 가 인물/캐릭터 ref 로 self-classify 한 entry 는 fail-fast.
    # silent pass 0 — 인물 상태 보존은 routing layer 책임.
    if entry["subject_kind"] == "human_or_character_reference":
        raise AppError(
            code=error_code,
            message=(
                f"{label_source}: keep_elements entry has subject_kind="
                f"'human_or_character_reference' (label={entry['label']!r}) — "
                f"keep_elements carries only non-human visual elements "
                f"(environment / static_prop). Person/character references are "
                f"routed to scene_consistency.character_state, "
                f"character_state_variant, and semantic_contract_router."
            ),
            status_code=400,
        )


def validate_keep_elements(
    keep_elements: Iterable[Any],
    *,
    label_source: str,
    error_code_entry: str,
    error_code_legacy_str: str | None = None,
) -> None:
    """List 단위 검증 — 각 entry 를 validate_keep_elements_entry 로 검증.

    Args:
        keep_elements: list (or iterable).
        label_source: error message 에 포함할 위치 base.
        error_code_entry: dict shape/key/enum 위반 시 AppError code.
        error_code_legacy_str: 본 list 에 str entry 가 있을 때 (legacy v5)
            사용할 AppError code. None 이면 일반 error_code_entry 사용.

    Raises: AppError.
    """
    if not isinstance(keep_elements, list):
        raise AppError(
            code=error_code_entry,
            message=(
                f"{label_source}: keep_elements must be list, "
                f"got {type(keep_elements).__name__}: {keep_elements!r}"
            ),
            status_code=400,
        )
    for idx, entry in enumerate(keep_elements):
        entry_source = f"{label_source}[{idx}]"
        if isinstance(entry, str):
            raise AppError(
                code=error_code_legacy_str or error_code_entry,
                message=(
                    f"{entry_source}: keep_elements entry is str ({entry!r}) "
                    f"— legacy v5 shape. shot_dependency_t2i schema_version 가 "
                    f"v8 (1→4) 로 bump 되었고 entry 는 list[{{label, kind∈"
                    f"{{environment, static_prop}}, subject_kind}}] 이어야 "
                    f"합니다. cp 를 force re-run 하세요."
                ),
                status_code=400,
            )
        validate_keep_elements_entry(
            entry, label_source=entry_source, error_code=error_code_entry,
        )
