"""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

_ALLOWED_ENTITY_TYPES: frozenset[str] = frozenset({"character", "location", "prop"})

_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.
    - 그 외 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 == "character":
        # 분리 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: character entity must have "
                    f"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: character entity must have "
                    f"visual_identity=None, 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
