"""C1 — perception_mode enum SOT v1 (helper module).

Spec ref: docs/superpowers/specs/2026-05-20-c1-perception-mode-enum-sot-v1-design.md
Plan ref: docs/superpowers/plans/2026-05-20-c1-perception-mode-enum-sot-v1-implementation.md
Codex APPROVED_FOR_EXECUTION (plan narrow re-review #4, N-1~N-13 absorbed).

Field separation (Codex Guard 1): the directionality_class domain (Area C closed
boundary) and the perception_mode domain are separate. This module imports/uses no
directionality_class constant or render_prompt_card symbol.
"""

from typing import Mapping

from app.core.errors import AppError


PERCEPTION_MODES: frozenset[str] = frozenset({
    "direct", "hallucination", "dream", "memory",
    "reflection", "mirror", "through_device", "projection",
})

REPRODUCTION_PERCEPTION_MODES: frozenset[str] = frozenset({
    "reflection", "mirror", "through_device", "projection",
})
# Invariant 4.1: REPRODUCTION_PERCEPTION_MODES is a strict subset of PERCEPTION_MODES.
assert REPRODUCTION_PERCEPTION_MODES < PERCEPTION_MODES


_PERCEPTION_GUIDES: Mapping[str, str] = {
    # Spec N-5: helper-owned guide mapping (not a detail_steps function-local var).
    # Non-direct 7-key full coverage. "direct" is not a key (direct gets no guide, N-7).
    "through_device": "이 장면은 화면/렌즈를 통해 보이는 것이지만, t2i_prompt에서 '디바이스 프레임/화면 테두리'를 묘사하지 마세요. 화면 속 내용물만 직접 촬영한 것처럼 묘사하세요. 약간의 디지털 질감이나 해상도 차이만 반영.",
    "hallucination": "이 장면은 환각/왜곡된 시각이므로 현실과 다른 색감/왜곡 효과를 반영하세요. 하지만 장면 자체는 직접 촬영한 것처럼 묘사.",
    "dream": "이 장면은 꿈/몽환적 상태이므로 부드러운 포커스, 비현실적 색감을 반영하세요.",
    "memory": "이 장면은 회상이므로 탈색된 색감, 부드러운 빛을 반영하세요.",
    "reflection": "이 장면은 거울/수면 반사이므로 좌우 반전된 구도를 고려하세요.",
    "mirror": "이 장면은 거울(mirror) surface 안에 보이는 것이므로, 거울면의 좌우 반전 + 반사면 quality (광택/표면 상태/변형 정도) + 거울 frame 가시 여부를 t2i_prompt 에 반영하세요. reflection (일반 반사면) 과 다른 mirror specific 행동: 거울 가장자리/거울 frame/mirror image 의 별도 surface 명시.",
    "projection": "이 장면은 프로젝터/스크린에 투사된 것이지만, 프로젝터 장비나 스크린 프레임을 묘사하지 마세요. 투사된 내용물만 직접 촬영한 것처럼 묘사.",
}
# Invariant 4.3: helper-owned guide keys equal PERCEPTION_MODES minus {direct} (7-key).
assert set(_PERCEPTION_GUIDES) == PERCEPTION_MODES - {"direct"}


def _validate_perception_mode(pm: str) -> None:
    """Validation gate: a value outside PERCEPTION_MODES raises AppError.

    None / empty / "direct" default coercion is the caller's responsibility
    (no silent coercion inside this helper).
    """
    if pm not in PERCEPTION_MODES:
        raise AppError(
            code="perception_mode.unknown",
            message=(
                f"perception_mode={pm!r} not in PERCEPTION_MODES enum "
                f"{sorted(PERCEPTION_MODES)}"
            ),
        )


def is_reproduction_perception(pm: str) -> bool:
    """Validate first, then subset check (no silent false on an unknown value).

    Caller convention: legacy default coercion is the caller's responsibility
    (e.g. ``pm = perception_mode or "direct"``). An empty string reaches
    _validate_perception_mode and raises AppError.
    """
    _validate_perception_mode(pm)
    return pm in REPRODUCTION_PERCEPTION_MODES


def get_perception_guide(pm: str) -> str:
    """Return the guide string for a non-direct perception mode.

    Two-stage check before any dict lookup (no raw KeyError surfaces):
      1. _validate_perception_mode(pm) — enum membership. An unknown value
         raises AppError("perception_mode.unknown").
      2. Membership in _PERCEPTION_GUIDES (the non-direct 7-key set). A value
         that is enum-valid but has no guide entry (i.e. "direct") raises
         AppError("perception_mode.guide_not_applicable") before the lookup.
      3. Dict lookup is then always safe.
    """
    _validate_perception_mode(pm)
    if pm not in _PERCEPTION_GUIDES:
        raise AppError(
            code="perception_mode.guide_not_applicable",
            message=(
                f"perception_mode={pm!r} is enum-valid but has no guide entry. "
                f"get_perception_guide must be called only inside a non-direct branch."
            ),
        )
    return _PERCEPTION_GUIDES[pm]
