"""체크포인트 셋 → **중앙 조사기 입력 하나**. ★순수 함수. 유료 0.

## 왜 따로 있나

Codex D-inert 2/3 (2026-09-01) — 「screen 장부 + phase3 뒤 outlook_binding +
obligation planner 를 **한 CP 로 합침**」.

스텝 안에 넣으면 시험이 **스텝을 통해서만** 닿는데, 그 스텝은 활성화되기 전엔
`resolve_grounding_mode` 가 mode 를 거절해서 못 돈다. 그러면 무료로 못 잰다.
그래서 **읽는 것과 합치는 것**을 여기 순수 함수로 두고, 스텝은 부르기만 한다.

## 무엇을 읽나

    grounding_screen   `data.rows`  — 정본 행(판별·증거·좌표)
    grounding_plan     `data.decided[]._short_id` — subject → 등록된 정본 ID
    outlook_phase3     `data.outlooks` · `data.scene_assignments`

★셋 다 **이미 있는 CP** 다. 새 CP 를 만들지 않는다.
"""
from __future__ import annotations

from typing import Any, Dict, List, Optional, Sequence

CENTRAL_INPUTS_CONTRACT_VERSION = "1.202609013000"


class CentralInputsError(RuntimeError):
    """입력을 못 만든다. ★조용히 빈손으로 지나가지 않는다."""


def final_id_by_subject(plan_cp: Optional[Dict[str, Any]],
                        screen_cp: Optional[Dict[str, Any]] = None
                        ) -> Dict[str, str]:
    """subject → **등록된 정본 ID**. ★이름으로 다시 짝짓지 않는다.

    ★★★C(c) 판에서는 `grounding_plan` 이 **안 돈다**(`if_grounding_v2`).
    그런데 여기가 그 CP 만 읽으면 표가 비어 인물·장소·소품이 통째로
    `unresolved` 가 되고, 옛 v2 CP 가 남아 있으면 **stale 좌표를 되쓴다**
    (Codex 재현 2026-09-01).

    그래서 **정본 행이 든 링크를 먼저** 본다 — `grounding_carry` 가 검증한
    `producer_link.final_id` 다(발급자·계약 판까지 확인된 것). 없을 때만
    옛 `grounding_plan.decided[]._short_id` 로 내려간다.
    """
    # ★판별 스텝이 **검증된 링크에서만** 만들어 실은 표다
    out: Dict[str, str] = {
        str(k): str(v) for k, v in
        (((screen_cp or {}).get("data") or {}).get("final_id_by_subject")
         or {}).items() if str(k) and str(v)}
    if out:
        # ★새 갈래가 답했으면 **옛 CP 를 안 본다** — 섞으면 stale 이 샌다
        return out
    for d in (((plan_cp or {}).get("data") or {}).get("decided") or ()):
        sid = str((d or {}).get("research_subject_id") or "")
        short = str((d or {}).get("_short_id") or "")
        if sid and short:
            out[sid] = short
    return out


def phase3_data(phase3_cp: Optional[Dict[str, Any]], *,
                needed: bool) -> Dict[str, Any]:
    """아웃룩 결속에 넘길 phase3 산출.

    Args:
        needed: **결속할 아웃룩 줄이 있나.** 없으면 phase3 를 요구하지 않는다 —
            아웃룩이 없는 에피소드에서 중앙 단계가 잘못 서면 안 된다
            (Codex 2026-09-01). ★있는데 없으면 **선다** — 조용히 빈 것으로
            접으면 그 갈래가 통째로 사라진다.
    """
    got = (phase3_cp or {}).get("data") or {}
    ok = isinstance(got, dict) and "outlooks" in got
    if ok:
        return got
    if not needed:
        # ★결속할 것이 없다 — 빈 목록으로 지나간다(아무 줄도 안 만든다)
        return {"outlooks": [], "scene_assignments": []}
    raise CentralInputsError(
        "결속할 아웃룩 줄이 있는데 `outlook_phase3` 산출이 없다 — "
        "이 스텝은 phase3 **뒤**에 와야 한다")


def build_ledger(screen_cp: Optional[Dict[str, Any]],
                 plan_cp: Optional[Dict[str, Any]],
                 phase3_cp: Optional[Dict[str, Any]]) -> Dict[str, Any]:
    """CP 셋 → **갈래 중립 장부 하나**. ★갈래별 resolver 를 부르기만 한다.

    Raises:
        CentralInputsError: 판별 산출이 없다 · phase3 가 없다.
    """
    from app.modules.pipeline import grounding_acquisition_ledger as gl
    from app.modules.pipeline import grounding_outlook_binding as ob

    rows = ((screen_cp or {}).get("data") or {}).get("rows")
    if not isinstance(rows, list):
        raise CentralInputsError(
            "`grounding_screen` 산출에 `rows` 가 없다 — 무엇을 조사할지 모른다")
    by_owner = gl.split_by_owner(rows)
    finals = final_id_by_subject(plan_cp, screen_cp)
    from app.modules.pipeline.grounding_entity_contract import (
        GENERIC_PROMOTION_OWNERS)

    # ★엔티티 줄로 **바로** 해소되는 갈래 — 계약이 정한 것 그대로
    base = gl.resolve_entity_backed(
        [r for o in GENERIC_PROMOTION_OWNERS for r in by_owner[o]],
        final_id_by_subject=finals)
    facet = gl.resolve_facet_rows(by_owner["location_part"],
                                  final_id_by_subject=finals)
    outlook_rows = by_owner["outlook"]
    p3 = phase3_data(phase3_cp, needed=bool(outlook_rows))
    outlook = ob.bind(outlook_rows, p3)
    led = gl.merge(base, facet, outlook)
    # ★아웃룩 엔티티(옷 전체)의 이름·설명 — final_id 당 의무 하나의 뼈대가 여기서 온다
    led["outfits"] = {str(o.get("short_id") or o.get("outlook_id") or ""): {
        "name": str(o.get("name") or ""), "description": str(o.get("description") or o.get("summary") or "")}
        for o in (p3.get("outlooks") or ()) if (o.get("short_id") or o.get("outlook_id"))}
    return led


def build_obligations(screen_cp: Optional[Dict[str, Any]],
                      plan_cp: Optional[Dict[str, Any]],
                      phase3_cp: Optional[Dict[str, Any]]) -> Dict[str, Any]:
    """CP 셋 → **의무 장부**. ★조사 앞에서 서는 것이 이것이다."""
    from app.modules.pipeline import grounding_reference_obligations as ro

    return ro.plan(build_ledger(screen_cp, plan_cp, phase3_cp))


def owners_reaching_the_door(ledger: Dict[str, Any]) -> Sequence[str]:
    """★**공개 끝점** — 어느 갈래가 실제로 중앙에 닿았나."""
    from app.modules.pipeline import grounding_acquisition_ledger as gl

    return gl.owners_present(ledger)
