"""Area B — entity_canon.metadata_json shape helper (전 entity_type closed shape).

shape: {"location": <D6 obj | null>, "visual_identity": <Area B obj | null>}
- prop: location=None, visual_identity={"reference_required": bool}
- location: location={<space_profile>}, visual_identity=None
- character: location=None, visual_identity=None

sync (DB write 전) + consumer (render_contracts producer) 양쪽 공용 — drift 차단.

Spec: docs/superpowers/specs/2026-05-13-area-b-render-contracts-design.md
"""

from __future__ import annotations
from typing import Any

from app.core.errors import AppError

#: ★★이 helper 가 받는 갈래. **EntitySync 가 넘기는 것과 같아야** 한다 —
#:  다르면 그 갈래 loop 를 여는 순간 `shape_violation` 으로 즉사한다
#:  (Codex BLOCK 2026-09-01). `outlook` 은 별도 path 라 여기 없다.
#:  ★`location_part` 는 **중립 모양**이다: `{location: None,
#:   visual_identity: None}` — `character` 와 같다. 그 갈래는 「장소의 고정
#:   설비」라 제 좌표를 갖지 않고(부모 location 이 갖는다), 사람 얼굴 같은
#:   시각 신원도 없다.
_NEUTRAL_SHAPE_TYPES: frozenset[str] = frozenset({"character", "location_part"})
_ALLOWED_ENTITY_TYPES: frozenset[str] = frozenset(
    {"location", "prop"} | _NEUTRAL_SHAPE_TYPES)


#: ★`character` 는 **실패 marker 패턴**을 쓴다 — 실패해도 `t2i_prompt=""` 로
#:  내려보내고 이미지 preflight 가 잡는다. 그것을 fail-fast 로 바꾸면 인물
#:  하나가 실패할 때 **에피소드 전체가 무너진다**(그 자리 주석이 그렇게 적혀
#:  있다). 그래서 이 목록에서 뺀다 — 검증 자체는 두 shape 다 받는다.
_MARKER_ON_FAILURE_TYPES: frozenset[str] = frozenset({"character"})


def neutral_shape_types() -> frozenset:
    """metadata 에 **실을 값이 없는** 갈래 — `{location: None, visual_identity: None}`."""
    return _NEUTRAL_SHAPE_TYPES


def normalize_metadata_for_type(entity_type: str, metadata_json):
    """검증 **앞**에서 중립 모양 갈래의 metadata 를 계약 모양으로 접는다.

    ★실측 (2026-09-02 canary ①): `location_part` 는 계약이 중립 모양인데 프롬프트가
    그 갈래에 아무 지시도 안 해 모델이 `visual_identity` 에 dict 를 지어냈다 →
    검증 실패 → 3회 재시도 → `partial`(LP01·LP02·LP03). 결정적이라 다시 열어도 또
    실패한다. 그 갈래엔 실을 값이 없으므로(좌표는 부모 location 이 갖고 시각
    신원은 없다) 모델이 낸 것을 **버리는 것이 계약**이다 — 우회가 아니다.
    `character` 는 이미 같은 closed shape 로 내려간다.

    ★prop·location 은 **손대지 않는다** — 그 갈래는 값이 곧 SOT 다.
    """
    if entity_type in _NEUTRAL_SHAPE_TYPES:
        return {"location": None, "visual_identity": None}
    return metadata_json


def validated_entity_types() -> frozenset:
    """산출 모양을 검증하고 **틀리면 안 보내는** 갈래. ★공개 끝점.

    ★★2026-09-01: `entity_t2i` 가 `("location","prop")` 를 손으로 적어 두어
    `location_part` 만 **검증 없이** sync 까지 갔다(Codex 재현). 이 함수가
    그 목록의 한 곳이다 — 갈래가 늘면 여기만 늘어난다.
    ★인물은 뺀다(위 주석) — 막으려는 실패가 서로 다르다.
    """
    return _ALLOWED_ENTITY_TYPES - _MARKER_ON_FAILURE_TYPES


def assert_matches_sync_owners() -> None:
    """★EntitySync 가 넘기는 갈래를 **여기가 다 받는지**. 안 받으면 즉사한다."""
    from app.modules.pipeline.grounding_entity_contract import (
        ENTITY_SYNC_OWNER_TYPES)

    missing = set(ENTITY_SYNC_OWNER_TYPES) - _ALLOWED_ENTITY_TYPES
    if missing:
        raise AssertionError(
            f"EntitySync 가 {sorted(missing)} 를 넘기는데 metadata validator 가 "
            "안 받는다 — 그 loop 를 여는 순간 shape_violation 이다")

_FORCE_RE_RUN_NOTE = (
    " Rerun entity_extract / entity_t2i for this episode with Area B schema "
    "(force re-run)."
)


def validate_entity_metadata_shape(
    entity_type: str,
    metadata_json: Any,
    short_id: str = "",
) -> None:
    """non-conforming shape 시 AppError raise.

    spec §3.1 contract:
    - metadata_json is dict; keys == {"location", "visual_identity"}; both present.
    - prop:      location is None; visual_identity is dict; reference_required is bool.
    - location:  location is dict; visual_identity is None. (D6 space_profile 내부 검증 통합)
    - character:      location is None; visual_identity is None.
    - location_part:  location is None; visual_identity is None. (중립 모양 —
      제 좌표는 부모 location 이 갖고, 시각 신원은 없다)
    - 그 외 entity_type (outlook 등) → raise (별도 path).
    """
    sid_hint = f" (short_id={short_id!r})" if short_id else ""

    if entity_type not in _ALLOWED_ENTITY_TYPES:
        raise AppError(
            code="entity_metadata.shape_violation",
            message=(
                f"validate_entity_metadata_shape: unsupported entity_type "
                f"{entity_type!r}{sid_hint}. Allowed: {sorted(_ALLOWED_ENTITY_TYPES)}. "
                "outlook 등 다른 entity_type 은 별도 path 라 본 helper 도달 자체가 invariant 위반."
            ),
        )

    if not isinstance(metadata_json, dict):
        raise AppError(
            code="entity_metadata.shape_violation",
            message=(
                f"validate_entity_metadata_shape: metadata_json must be dict, "
                f"got {type(metadata_json).__name__}{sid_hint}."
            ),
        )

    keys = set(metadata_json.keys())
    if keys != {"location", "visual_identity"}:
        raise AppError(
            code="entity_metadata.shape_violation",
            message=(
                f"validate_entity_metadata_shape: metadata_json keys must be "
                f"exactly {{'location', 'visual_identity'}}, got {sorted(keys)}"
                f"{sid_hint}."
            ),
        )

    loc = metadata_json["location"]
    vi = metadata_json["visual_identity"]

    if entity_type == "prop":
        if loc is not None:
            raise AppError(
                code="entity_metadata.shape_violation",
                message=(
                    f"validate_entity_metadata_shape: prop entity must have "
                    f"location=None, got {type(loc).__name__}{sid_hint}."
                ),
            )
        if not isinstance(vi, dict):
            raise AppError(
                code="entity_metadata.shape_violation",
                message=(
                    f"validate_entity_metadata_shape: prop entity must have "
                    f"visual_identity as dict, got {type(vi).__name__}{sid_hint}."
                ),
            )
        if "reference_required" not in vi:
            raise AppError(
                code="entity_metadata.shape_violation",
                message=(
                    f"validate_entity_metadata_shape: prop visual_identity missing "
                    f"reference_required field{sid_hint}."
                ),
            )
        rbr = vi["reference_required"]
        # bool subclass check — int (1/0) 거부 (silent disarm 차단, Area C carry).
        if type(rbr) is not bool:
            raise AppError(
                code="entity_metadata.shape_violation",
                message=(
                    f"validate_entity_metadata_shape: prop reference_required "
                    f"must be bool, got {type(rbr).__name__}{sid_hint}."
                ),
            )

    elif entity_type == "location":
        if not isinstance(loc, dict):
            raise AppError(
                code="entity_metadata.shape_violation",
                message=(
                    f"validate_entity_metadata_shape: location entity must have "
                    f"location as dict, got {type(loc).__name__}{sid_hint}."
                ),
            )
        if vi is not None:
            raise AppError(
                code="entity_metadata.shape_violation",
                message=(
                    f"validate_entity_metadata_shape: location entity must have "
                    f"visual_identity=None, got {type(vi).__name__}{sid_hint}."
                ),
            )
        # Area B (review fix): space_profile 내부 검증 통합 — D6 helper 호출.
        # EntitySyncService 가 top-level 만 보고 invalid space_profile 을 DB write
        # 하지 않도록 single point of validation. D6 helper 의 SpaceProfileError 는
        # 그대로 propagate (D6 error code 보존).
        # Signature 주의: validate_location_space_profile 은 outer metadata_json
        # (= {"location": {...}, ...}) 을 받음. loc 만 넘기면 .location lookup 실패.
        from app.core.bg_state_vocab import validate_location_space_profile
        validate_location_space_profile(metadata_json, short_id=short_id)

    elif entity_type in _NEUTRAL_SHAPE_TYPES:
        # 분리 message — 어느 field 위반인지 호출자가 즉시 식별 (prop/location branch
        # 와 일관). 동시 위반 시 location 먼저 raise → 호출자는 fix 후 vi 재시도.
        if loc is not None:
            raise AppError(
                code="entity_metadata.shape_violation",
                message=(
                    f"validate_entity_metadata_shape: {entity_type} entity must "
                    f"have location=None, got {type(loc).__name__}{sid_hint}."
                ),
            )
        if vi is not None:
            raise AppError(
                code="entity_metadata.shape_violation",
                message=(
                    f"validate_entity_metadata_shape: {entity_type} entity must "
                    f"have visual_identity=None, "
                    f"got {type(vi).__name__}{sid_hint}."
                ),
            )


def get_visual_identity_reference_required(
    metadata_json: Any,
    short_id: str = "",
) -> bool:
    """consumer 진입점 — prop entity 의 SOT bool 추출 + fail-fast.

    stale data / invalid shape 시 friendly raise + force re-run 안내.
    visible_entity_details 의 prop entry 에 대해 호출.
    """
    sid_hint = f" (short_id={short_id!r})" if short_id else ""

    if not isinstance(metadata_json, dict) or "visual_identity" not in metadata_json:
        raise AppError(
            code="entity_metadata.shape_violation",
            message=(
                f"Prop entity metadata_json missing or non-dict{sid_hint}." + _FORCE_RE_RUN_NOTE
            ),
        )

    vi = metadata_json["visual_identity"]
    if vi is None:
        raise AppError(
            code="entity_metadata.shape_violation",
            message=(
                f"Prop entity metadata_json.visual_identity is None{sid_hint}." + _FORCE_RE_RUN_NOTE
            ),
        )

    if not isinstance(vi, dict) or "reference_required" not in vi:
        raise AppError(
            code="entity_metadata.shape_violation",
            message=(
                f"Prop metadata_json.visual_identity missing reference_required field"
                f"{sid_hint}." + _FORCE_RE_RUN_NOTE
            ),
        )

    rbr = vi["reference_required"]
    if type(rbr) is not bool:
        raise AppError(
            code="entity_metadata.shape_violation",
            message=(
                f"Prop metadata_json.visual_identity.reference_required must be bool, "
                f"got {type(rbr).__name__}{sid_hint}." + _FORCE_RE_RUN_NOTE
            ),
        )

    return rbr
