"""building_fp_link — W-G (2026-07-03): same-building indoor FP → outdoor 산출물 구조 참조.

같은 ``building_groups`` 그룹(=하나의 건물/부지)에 실내·실외 location 이 공존하면,
그 건물의 **실내 floor plan** 이 건물 크기·층수·개구부(문/창) 배치의 구조 SOT 다.
외부 산출물(aerial base / bg plate)이 이 fp 를 참조하지 않으면 건물 규모·구조를
근거 없이 상상해 축소/왜곡한다 — 이 모듈은 그 연결을 **구조 필드만으로** 조인한다.

데이터 소스 (전부 checkpoint 구조 필드 — 파일명/라벨/substring 파싱 금지):
  - ``background_classify.data.building_groups[].members[{loc_id, is_indoor,
    shot_count}]`` + ``anchor_loc``
  - ``background_master_plan.data.background_catalog[bg_id].{loc_id, depends_on_fp}``
    (fp_id↔loc 매핑의 SOT — floor_plan_render ``_compute_fp_primary_loc_ids`` 와
    동일한 depends_on_fp 역참조 조인)

순수 함수만 — DB/파일 접근 없음. 소비자(outdoor_site_layout / background_render)가
flag ``outdoor_building_fp_ref_enabled`` ON 일 때만 호출한다(OFF = 미호출 = 기존
경로 byte-identical).
"""
from __future__ import annotations

from typing import Any, Dict, List

__all__ = [
    "indoor_fp_ids_by_loc",
    "build_outdoor_building_fp_link",
    "build_group_membership_by_loc",
]


def build_group_membership_by_loc(
    building_groups: List[Dict[str, Any]],
) -> Dict[str, Dict[str, Any]]:
    """W-K (2026-07-03) — 모든 ``building_groups`` 멤버 loc 의 그룹 멤버십.

    반환: ``{loc_sid: {"group_id": str, "is_indoor": bool, "group_mixed": bool}}``.

    ``build_outdoor_building_fp_link`` 와 달리 **전 멤버**(실내 포함, mixed
    여부 무관)를 담는다 — same-place 렌더 체이닝의 그룹 anchor 는 등록
    (outdoor ok 렌더만)과 첨부(그룹 전 멤버) 판정에 실내 loc 멤버십이 필요하다.
    ``group_mixed`` 는 그룹에 실내·실외 멤버가 공존하는지(lane 재배열 정렬
    키). ``is_indoor`` 가 bool 이 아닌 멤버는 제외(구조 필드 계약 위반 —
    추측 금지). 같은 loc 이 여러 그룹에 등장하면 first-wins(그룹 순서 =
    classify 산출 순서, 결정론). 소비자(background_render)가 flag
    ``same_place_render_chain_enabled`` ON 일 때만 호출한다.
    """
    out: Dict[str, Dict[str, Any]] = {}
    for group in building_groups or []:
        if not isinstance(group, dict):
            continue
        members = [
            m for m in (group.get("members") or [])
            if isinstance(m, dict) and isinstance(m.get("is_indoor"), bool)
        ]
        if not members:
            continue
        group_id = str(group.get("group_id") or "")
        if not group_id:
            continue
        has_indoor = any(m["is_indoor"] for m in members)
        has_outdoor = any(not m["is_indoor"] for m in members)
        mixed = has_indoor and has_outdoor
        for m in members:
            loc = str(m.get("loc_id") or "")
            if not loc or loc in out:
                continue
            out[loc] = {
                "group_id": group_id,
                "is_indoor": bool(m["is_indoor"]),
                "group_mixed": mixed,
            }
    return out


def indoor_fp_ids_by_loc(
    background_catalog: Dict[str, Any],
) -> Dict[str, List[str]]:
    """``background_catalog`` → ``{loc_sid: sorted [fp_id, ...]}``.

    catalog 의 각 bg entry 가 가진 ``loc_id`` + ``depends_on_fp`` 구조 필드만
    사용한다. loc 하나에 fp 가 여러 개면 정렬 리스트(결정론) — 선택은
    ``build_outdoor_building_fp_link`` 가 첫 번째를 취한다.
    """
    out: Dict[str, set] = {}
    for entry in (background_catalog or {}).values():
        if not isinstance(entry, dict):
            continue
        loc = entry.get("loc_id")
        if not isinstance(loc, str) or not loc:
            continue
        for fp in entry.get("depends_on_fp") or []:
            if isinstance(fp, str) and fp:
                out.setdefault(loc, set()).add(fp)
    return {loc: sorted(fps) for loc, fps in out.items()}


def build_outdoor_building_fp_link(
    building_groups: List[Dict[str, Any]],
    fp_ids_by_loc: Dict[str, List[str]],
) -> Dict[str, Dict[str, str]]:
    """실내·실외 공존(building) 그룹의 outdoor loc → 대표 indoor fp 링크.

    반환: ``{outdoor_loc_sid: {"fp_id", "indoor_loc_sid", "group_id"}}``.

    규칙(전부 결정론):
      - 같은 그룹에 ``is_indoor`` True/False 멤버가 **공존**할 때만 링크를 만든다
        (순수 실외/실내 그룹 = 대상 아님).
      - indoor 대표 loc 선택 — **fp 를 실제 보유한** indoor 멤버 중에서:
          1. 그룹 ``anchor_loc`` 이 그 후보면 최우선 (classify 가 이미 '가장 많은
             실내 샷을 가진 주 공간'으로 뽑은 구조 필드),
          2. 아니면 ``shot_count`` 내림차순 → ``loc_id`` 오름차순.
      - fp 선택 — 대표 loc 의 정렬된 fp 리스트 첫 번째 (주 fp 1장).
      - 그룹 내 모든 outdoor 멤버가 같은 링크를 공유한다(같은 건물).
    """
    link: Dict[str, Dict[str, str]] = {}
    for group in building_groups or []:
        if not isinstance(group, dict):
            continue
        members = [m for m in (group.get("members") or []) if isinstance(m, dict)]
        indoor = [m for m in members if m.get("is_indoor") is True]
        outdoor = [m for m in members if m.get("is_indoor") is False]
        if not indoor or not outdoor:
            continue

        candidates = [
            m for m in indoor
            if fp_ids_by_loc.get(str(m.get("loc_id") or ""))
        ]
        if not candidates:
            continue

        anchor = str(group.get("anchor_loc") or "")
        chosen = next(
            (m for m in candidates if str(m.get("loc_id")) == anchor), None
        )
        if chosen is None:
            chosen = sorted(
                candidates,
                key=lambda m: (
                    -int(m.get("shot_count") or 0),
                    str(m.get("loc_id") or ""),
                ),
            )[0]

        indoor_loc = str(chosen.get("loc_id"))
        fp_id = fp_ids_by_loc[indoor_loc][0]
        group_id = str(group.get("group_id") or "")
        for m in outdoor:
            out_loc = str(m.get("loc_id") or "")
            if not out_loc or out_loc in link:
                # first-wins — 동일 loc 이 여러 그룹에 등장하면 먼저 정의된
                # 그룹 유지(그룹 순서 = classify 산출 순서, 결정론).
                continue
            link[out_loc] = {
                "fp_id": fp_id,
                "indoor_loc_sid": indoor_loc,
                "group_id": group_id,
            }
    return link
