"""facet 결속 — `outlook → character` · `location_part → location`. ★유료 0.

## 왜 필요한가

C(c) 가 다섯 갈래를 **낸 것**은 확인됐다. 그러나 **production SOT 까지 붙는
것**은 별개다. 「이 옷은 저 사람 것」을 **이름으로 옮기면 안 된다** — 같은
이름이 여럿이고, 이름이 다른 같은 것도 있다.

## ★더 단순한 길 — 새 관계를 안 만든다

merge 는 이미 `part_of` 를 낸다. 그리고 **갈래 조합이 관계를 이미 말한다** —

    outlook       ⊂ character   → 그 사람에게 붙은 시각 층
    location_part ⊂ location    → 그 공간에 고정된 구성 요소

그래서 **관계 종류를 새로 더하지 않고** 기존 `part_of` 와 **owner 조합**으로
결속한다. 이름·표면형·regex 는 **한 자도** 안 본다.

★관계 종류를 더하면 팩·schema·merge 지시문이 다 바뀌고, 모델이 새 낱말을
배워야 하며, 지금 있는 `part_of` 판정과 뜻이 겹친다. 재료가 이미 있는데
새 축을 만들면 두 벌이 된다.

## 못 붙이면 — **durable debt**

`entity_canon.entity_type` 은 지금 넷뿐이다(location · character · outlook ·
prop). **`location_part` 갈래가 없다.** 그래서 `location_part` 는 붙일 자리가
없고, `location` 으로 등록하면 그것이 바로 **base 갈래 우회 등록**이다.

★안 한다. 후보·의무를 **빚**으로 남기고, 갈래가 생기면 그때 붙인다.
"""
from __future__ import annotations

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

from app.modules.pipeline.grounding_entity_contract import (
    MATERIALIZABLE_OWNER_TYPES)

#: 갈래 조합 → 붙는 자리. ★**이 표가 관계다** — 이름을 안 본다.
FACET_PARENT = {
    "outlook": "character",
    "location_part": "location",
}

#: ★materialize 가능한 갈래 — **도메인 계약 한 곳**에서 온다. 여기 다시 적으면
#:  DB 관측값이 두 벌이 되고 한쪽만 고쳐진다.
CANON_TYPES = MATERIALIZABLE_OWNER_TYPES

#: 빚 사유.
DEBT_NO_CANON_TYPE = "no_canon_entity_type"
DEBT_NO_PARENT = "no_structural_parent"
DEBT_PARENT_NOT_REGISTERED = "parent_not_registered"
#: ★자기 자신이 등록 안 됐다 — 붙일 **자기 ID 가 없다**.
DEBT_FACET_NOT_REGISTERED = "facet_not_registered"


def parent_of(owner: str) -> Optional[str]:
    return FACET_PARENT.get(str(owner or ""))


def is_facet(owner: str) -> bool:
    return str(owner or "") in FACET_PARENT


def bind(rows: Sequence[Dict[str, Any]],
         part_of: Sequence[Dict[str, Any]],
         registered: Dict[str, Any]) -> Dict[str, Any]:
    """facet 행을 부모에 **구조적으로** 잇는다.

    Args:
        rows: `reduce_episode(...)["rows"]`.
        part_of: `reduce_episode(...)["part_of"]` — `{part, whole}` 목록.
        registered: `reduce_episode(...)["registered"]`.

    Returns:
        ``{"bindings": [...], "debt": [...]}``

        `bindings` 는 **구조화 결속**이다 — `local_id` 와 `final_id` 로만
        잇고 이름을 안 쓴다. `debt` 는 못 붙인 것이고, **base 갈래로 우회
        등록하지 않는다**.
    """
    by_id = {str(r.get("local_id")): r for r in rows}
    # ★부모 후보는 **관계 장부**에서만 온다. 이름으로 찾지 않는다.
    #  ★★같은 관계가 두 줄 와도 **부모 둘이 아니다** — 같은 사실의 중복이다
    #   (Codex 2026-08-31). 결정적으로 dedupe 하고 몇 번 왔는지는 남긴다.
    parents: Dict[str, List[str]] = {}
    dup_count: Dict[str, int] = {}
    for rel in part_of or ():
        p, w = str(rel.get("part")), str(rel.get("whole"))
        if p not in by_id or w not in by_id:
            continue
        key = f"{p}->{w}"
        dup_count[key] = dup_count.get(key, 0) + 1
        if w not in parents.setdefault(p, []):
            parents[p].append(w)
    for k in list(parents):
        parents[k] = sorted(parents[k])

    bindings: List[Dict[str, Any]] = []
    debt: List[Dict[str, Any]] = []
    for lid, r in sorted(by_id.items()):
        owner = str(r.get("owner_type") or "")
        if not is_facet(owner):
            continue
        want = parent_of(owner)
        cand = [w for w in parents.get(lid, [])
                if str(by_id[w].get("owner_type")) == want]

        rec = registered.get(lid) or {}
        base = {"local_id": lid, "owner_type": owner,
                "final_id": rec.get("final_id"),
                "surface_form": r.get("surface_form")}

        if owner not in CANON_TYPES:
            # ★붙일 갈래 자체가 SOT 에 없다 — **빚**이다. 우회 등록 안 한다.
            #  ★이 검사가 **먼저**다: 이 판의 등록 여부와 무관하게 붙일 자리
            #   자체가 없다는 뜻이라 더 근본이다.
            debt.append({**base, "reason": DEBT_NO_CANON_TYPE,
                         "wanted_parent_owner": want,
                         "parent_candidates": sorted(cand),
                         "note": (f"`entity_canon.entity_type` 에 {owner!r} 가 "
                                  f"없다. {want!r} 로 등록하면 base 갈래 우회 "
                                  "등록이라 안 한다")})
            continue

        # ★★**자기 자신이 등록돼야** 붙일 ID 가 생긴다 (Codex 2026-08-31).
        #  앞 판은 부모만 봐서, `final_id: None` 인 outlook 을 **성공 결속**
        #  으로 냈다 — 그 상태로는 `character_outlook` 에 넣을 ID 가 없다.
        if rec.get("registered") is not True or not rec.get("final_id"):
            debt.append({**base, "reason": DEBT_FACET_NOT_REGISTERED,
                         "wanted_parent_owner": want,
                         "parent_candidates": sorted(cand),
                         "note": ("이 행이 아직 등록 안 됐다 — 붙일 자기 ID 가 "
                                  "없다. 등록되면 그때 붙인다")})
            continue

        if not cand:
            debt.append({**base, "reason": DEBT_NO_PARENT,
                         "wanted_parent_owner": want,
                         "parent_candidates": [],
                         "note": ("관계 장부에 이 행을 담는 부모가 없다. "
                                  "이름으로 짐작해 붙이지 않는다")})
            continue
        if len(cand) > 1:
            debt.append({**base, "reason": DEBT_NO_PARENT,
                         "wanted_parent_owner": want,
                         "parent_candidates": sorted(cand),
                         "note": "부모 후보가 여럿이다 — 아무 쪽에나 안 붙인다"})
            continue

        parent = cand[0]
        prec = registered.get(parent) or {}
        if prec.get("registered") is not True or not prec.get("final_id"):
            debt.append({**base, "reason": DEBT_PARENT_NOT_REGISTERED,
                         "wanted_parent_owner": want,
                         "parent_candidates": [parent],
                         "note": ("부모가 아직 등록 안 됐다 — 붙일 자리가 "
                                  "확정되지 않았다")})
            continue

        bindings.append({**base, "parent_local_id": parent,
                         "parent_owner_type": want,
                         "parent_final_id": prec.get("final_id")})

    return {"bindings": bindings, "debt": debt,
            # ★같은 사실이 몇 번 왔는지 — 감사용. 판정에는 안 쓴다.
            "relation_duplicates": {k: n for k, n in sorted(dup_count.items())
                                    if n > 1}}


def assert_owner_pairs(bindings: Sequence[Dict[str, Any]]) -> None:
    """결속의 **갈래 조합**을 구조적으로 본다. ★어긋나면 선다."""
    for b in bindings or ():
        owner = str(b.get("owner_type"))
        want = parent_of(owner)
        got = str(b.get("parent_owner_type"))
        if want is None:
            raise AssertionError(f"{owner!r} 는 facet 갈래가 아니다")
        if got != want:
            raise AssertionError(
                f"{owner!r} 가 {got!r} 에 붙었다 — {want!r} 여야 한다")
