"""episode_reference_policy — scene_detail 직전 reference necessity manifest.

설계: docs/reference-necessity/index.html (v2 decisions locked).

deterministic 계산 (LLM 없음). 이번 에피소드 selected-shot usage 로
각 catalog 엔티티의 reference necessity 를 1회 확정한다. 이 manifest 가
render_prompt_card producer 와 verify recompute 의 단일 immutable SOT 가 되어
card hash drift 를 막는다.

규칙 (§5.1 / §10):
- character / nonhuman (C##): visible_shot_count >= 2 또는 variant pole →
  reference_required, 아니면 text_only.
- prop (P## 등): provisional — mode 는 reference_candidate / text_only.
  reference_candidate 는 "생성 확정" 이 아니라 후보 표시일 뿐이고, 최종
  게이트는 materialization 단계의 required_ref_count >= 1 이다 (§10.3).

required_ref_count 미사용 사유: 이 step 은 scene_detail 전이라 required_ref_
count 를 알 수 없다 (circularity). §10.1 의 `required_ref_count >= 1` 절은
downstream 에서 집행된다 — scene_detail 이 required_ref 를 만들면 Phase 1
narrow 보호가 materialization 시점에 그 엔티티를 보호한다.
"""
from __future__ import annotations

from typing import Any, Dict, Optional, Set

EPISODE_REFERENCE_POLICY_SCHEMA_VERSION = 1

# character / nonhuman recurring threshold (§10.1).
_RECURRING_VISIBLE_SHOT_THRESHOLD = 2


def _is_character_subject(short_id: str) -> bool:
    """C## prefix = identity 를 가진 depicted subject (동물/비인간 포함)."""
    return short_id.startswith("C")


def compute_episode_reference_policy(
    *,
    visible_shot_count: Dict[str, int],
    entity_types: Dict[str, str],
    variant_pole_short_ids: Set[str],
) -> Dict[str, Any]:
    """reference necessity manifest 계산.

    visible_shot_count = {short_id: selected-shot 등장 횟수}.
    entity_types       = {short_id: entity_type}.
    variant_pole_short_ids = identity/transformation variant pole short_id 집합.
    """
    policy: Dict[str, Dict[str, Any]] = {}
    all_ids = set(visible_shot_count) | set(entity_types)
    for sid in sorted(all_ids):
        etype = entity_types.get(sid, "")
        vsc = visible_shot_count.get(sid, 0)
        is_variant = sid in variant_pole_short_ids

        if _is_character_subject(sid):
            if is_variant:
                mode, reason = "reference_required", (
                    "identity/transformation variant pole — variant 보호 유지"
                )
            elif vsc >= _RECURRING_VISIBLE_SHOT_THRESHOLD:
                mode, reason = "reference_required", (
                    f"visible_shot_count={vsc} >= "
                    f"{_RECURRING_VISIBLE_SHOT_THRESHOLD}"
                )
            else:
                mode, reason = "text_only", (
                    f"visible_shot_count={vsc} — selected-shot 저빈도 "
                    f"주변 subject"
                )
            policy[sid] = {
                "mode": mode,
                "reason": reason,
                "entity_type": etype or "character",
                "visible_shot_count": vsc,
                "provisional": False,
            }
        else:
            # prop / 기타 — provisional. reference_candidate 는 후보 표시일
            # 뿐, 최종 게이트는 materialization 의 required_ref_count >= 1.
            if vsc >= _RECURRING_VISIBLE_SHOT_THRESHOLD:
                mode, reason = "reference_candidate", (
                    f"provisional candidate: visible_shot_count={vsc} — "
                    f"최종 확정은 materialization 의 required_ref_count"
                )
            else:
                mode, reason = "text_only", (
                    f"provisional: visible_shot_count={vsc}"
                )
            policy[sid] = {
                "mode": mode,
                "reason": reason,
                "entity_type": etype or "prop",
                "visible_shot_count": vsc,
                "provisional": True,
            }
    return {
        "schema_version": EPISODE_REFERENCE_POLICY_SCHEMA_VERSION,
        "policy": policy,
    }


def extract_text_only_subjects(
    manifest: Optional[Dict[str, Any]],
) -> Dict[str, str]:
    """manifest 에서 text_only character subject 만 {C##: reason} 으로 추출.

    render_prompt_card 의 downgrade overlay 입력. prop / 기타는 제외 —
    prop 은 render_contracts SOT + materialization 게이트가 담당.
    """
    if not manifest or not isinstance(manifest, dict):
        return {}
    out: Dict[str, str] = {}
    for sid, p in (manifest.get("policy") or {}).items():
        if not isinstance(p, dict):
            continue
        if p.get("mode") != "text_only":
            continue
        if not str(sid).startswith("C"):
            continue
        out[sid] = p.get("reason") or "episode_reference_policy: text_only"
    return out
