"""레인2 재설계 — 구조물 seed 순수 T2I + seed 기반 항공뷰 맵 (2026-07-15).

사용자 지시(설계 v2 레인2 대체): 복잡 구조물은 seed 실사를 먼저 T2I 로
확정 → 그 seed 를 참조로 항공뷰 맵 생성 → 이후는 레인1(마커 맵) 파이프
(geometry/PIL 합성/스케치/leakage 는 outdoor_marker_map 재사용).
여기는 두 생성 계약의 결정론 프롬프트 조립만 담당한다.
"""
from __future__ import annotations

from pathlib import Path
from typing import Any, Dict, Mapping, Sequence

SEED_PACK_MODULE = "structure_seed"

SEED_PACK_VERSION_MAP = {
    "1": "1.202607151600",
    # v2 (Codex 리뷰 NARROW_2): ①INTERIOR SPEC=size-only evidence 봉인
    # (실내 시각화·cutaway·투명 벽 금지 — seed 가 look SOT 라 오류가 전
    # 단계로 고착) ②시간 불변 계약(parked/moving 차량·임시 장비·활동
    # 소품 배제, 'typical of the region' 창작 개방 제거, aerial 은
    # permanent 요소만 계승).
    "2": "2.202607151900",
    # v3 (Codex 재리뷰 NARROW_1): v2 의 절대 금지가 target 자체와 모순
    # 가능(개방형 고정 구조물 vs 'no transparent or open walls',
    # 이동수단형 target vs 'no vehicles parked or moving') →
    # target-relative 계약으로 좁힘: ①cutaway 방어=enclosed 표면 투명화·
    # 인위적 벽 제거 금지 + intentional open sides 보존 ②transient 배제
    # 유지하되 target 구조물·context 필수 permanent 요소는 예외 명시
    # (사람·동물 절대 배제는 유지). aerial 에도 동일 예외.
    "3": "3.202607152200",
    # v4 (E2E 6회차 육안 진단, 2026-07-16 승인): 위상 중심 spec 서술만으로
    # seed 를 그리면 낡음·재질·규모 성격이 소실(5→6회차 비교 실측) →
    # entity_merge 실외 loc 외관 서술을 EXTERIOR APPEARANCE 절(look
    # evidence)로 병기. 위상 SOT=spec 유지(배치·존재는 THE STRUCTURE 가
    # 단독 권위), transient=기존 EXCLUDED negative 가 우선 방어.
    "4": "4.202607161253",
    # v5 (2026-07-22 E2E11 fix①): v4 전체 사본 + human_scale_clause —
    # 휴먼 스케일 앵커(문≈2m·창·층고·난간 기준 치수 도출) **무조건 주입**.
    # 실측: 기존 scale_clause(STRUCTURE SCALE SPEC)는 interior_note 결손
    # 시 절 통째 생략 → E2E11 10/10 그룹 전부 규모 계약 0 (옥탑 과대의
    # 직접 원인). human_scale 은 interior 증거와 무관한 상시 앵커.
    "5": "5.202607221100",
}

# human_scale_clause 스템이 실리는 팩 — build_structure_seed_prompt 조립
_HUMAN_SCALE_PACKS = frozenset({"5"})


def resolve_seed_pack_version(version: str) -> str:
    if version not in SEED_PACK_VERSION_MAP:
        raise ValueError(f"structure_seed 팩 버전 없음: {version}")
    return SEED_PACK_VERSION_MAP[version]


def derive_seed_inputs(
    *,
    spec: dict,
    building_group: dict,
    locations_by_id: dict,
) -> dict:
    """seed 생성 입력 파생 (Stage D — Codex 배선 조건 ⑥).

    structure_desc SOT = outdoor_place_spec 의 persistent_site **검증
    서술**(layout_narration_en + items name/placement) — raw entity_merge
    서술 금지(미검증 transient 가 seed 에 고착되는 것 차단).
    excluded_transient_en = spec excluded_transient_elements 를 negative
    contract 로 전달. interior_note_en = 같은 building 그룹 실내 loc
    서술(규모 SOT — 육안 라운드1 계약). exterior_note_en = 같은 그룹
    실외 loc 서술(외관 look evidence — E2E 6회차 승인: 위상은 여전히
    spec 단독 SOT, 서술 속 transient 는 EXCLUDED negative 가 방어).
    """
    # Codex Stage D NARROW-6: 소비 시점 잠금 — persisted spec 에 transient
    # item 이 섞여 있으면 fail-closed (영구 seed 로 고착 차단). upstream
    # validator 를 신뢰하지 않는다.
    bad = [
        (it.get("code") or it.get("name_en") or "?")
        for it in spec.get("items") or []
        if isinstance(it, dict)
        and it.get("temporal_scope") != "persistent_site"
    ]
    if bad:
        raise ValueError(
            "outdoor_place_spec items 에 persistent_site 아닌 항목 — "
            f"seed 입력 SOT 위반: {bad[:8]}"
        )
    lines = [(spec.get("layout_narration_en") or "").strip()]
    name_only: list = []
    for it in spec.get("items") or []:
        if not isinstance(it, dict):
            continue
        name = (it.get("name_en") or "").strip()
        placement = (it.get("placement_en") or "").strip()
        if name:
            lines.append(f"- {name}: {placement}" if placement
                         else f"- {name}")
            name_only.append(f"- {name}")
    structure_desc = "\n".join(x for x in lines if x)
    # 사진 고르기(form_ref pick) 전용 — **무엇이 있는지만** 담는다.
    # pick_system 이 스스로 "배치는 판단 근거가 아니다" 라고 두 번 못박는다:
    #   "NO PHOTOGRAPH HAS TO SHOW THEM ALL, OR SHOW THEM ARRANGED THAT WAY …
    #    a different arrangement of them, is NEVER a reason to reject" /
    #   "JUDGE THE STRUCTURE, NOT ITS SURROUNDINGS".
    # 그런데도 layout_narration + placement 서술이 콜마다 실려 있었다.
    # 이름은 남긴다 — 심판이 골라야 할 "주된 구조물 종류"의 근거다.
    structure_names_en = "\n".join(name_only)

    excluded = [
        (x.get("name_en") or "").strip()
        for x in spec.get("excluded_transient_elements") or []
        if isinstance(x, dict) and (x.get("name_en") or "").strip()
    ]
    excluded_transient_en = "; ".join(excluded)

    interior_lines = []
    exterior_lines = []
    for mem in (building_group or {}).get("members") or []:
        if not isinstance(mem, dict):
            continue
        loc = locations_by_id.get(mem.get("loc_id")) or {}
        desc = (loc.get("description") or "").strip()
        if not desc:
            continue
        line = f"{loc.get('name', '')}: {desc}"
        if mem.get("is_indoor") is True:
            interior_lines.append(line)
        elif mem.get("is_indoor") is False:
            exterior_lines.append(line)
        # None/비bool = 미분류 — default-deny(저장소 구조 계약), 양쪽 제외.
        # legacy CP 의 실내 서술이 EXTERIOR NOTES 로 주입되는 경로 차단
        # (Codex 리뷰 NARROW-1).
    return {
        "structure_desc": structure_desc,
        "structure_names_en": structure_names_en,
        "interior_note_en": " / ".join(interior_lines),
        "exterior_note_en": " / ".join(exterior_lines),
        "excluded_transient_en": excluded_transient_en,
    }


def collect_interior_scene_indices(
    *,
    building_group: Mapping[str, Any],
    scene_primary_loc: Mapping[int, Any],
    exclude: Sequence[int] = (),
) -> list:
    """같은 building 그룹 실내 멤버(is_indoor=True) loc 의 씬 목록.

    v9 room_inventory 배선(Codex v8b ③ 실측 결손): seed 저작 입력이
    야외 씬(lane scene_indices)뿐이라 실내 씬(안방·자기방 증거)이
    미전달 — 방 전수를 원리적으로 열거 못했다. scene→loc 매핑 SOT=
    scene_director 의 scene 별 primary_location (코드 자동 구축 관례).
    exclude=이미 공급되는 야외 씬(중복 제외). 결정론 정렬.
    """
    interior_locs = {
        m.get("loc_id")
        for m in (building_group or {}).get("members") or []
        if isinstance(m, dict) and m.get("is_indoor") is True
        and m.get("loc_id")
    }
    excluded = set(exclude)
    return sorted(
        si for si, loc in (scene_primary_loc or {}).items()
        if isinstance(si, int) and loc in interior_locs
        and si not in excluded
    )


def build_structure_seed_prompt(
    *,
    structure_desc: str,
    interior_note_en: str = "",
    exterior_note_en: str = "",
    world_anchor: str = "",
    place_text: str = "",
    excluded_transient_en: str = "",
    prompt_version: str = "4",
) -> str:
    """구조물 seed 순수 T2I 프롬프트 — 참조 이미지 없는 생성.

    structure_desc = persistent_site 검증 서술(derive_seed_inputs SOT),
    interior_note_en = 같은 building 실내 loc 서술(규모 SOT, 없으면
    SCALE 절 자체를 생략 — dangling 금지, Stage C H2 교훈).
    exterior_note_en = 같은 building 실외 loc 외관 서술(look evidence,
    v4 — 빈 문자열=절 생략. 위상 권위는 THE STRUCTURE, transient 는
    EXCLUDED 절이 우선한다는 계약이 팩 exterior_clause 에 명시됨).
    excluded_transient_en = spec 이 transient 로 판정한 요소의 negative
    contract (빈 문자열=절 생략, 기존 조립 byte-identical).
    """
    from app.modules.prompt_loader import load_prompt

    if not (structure_desc or "").strip():
        raise ValueError("structure_desc 결손 — seed 생성 대상 서술 필수")

    resolved = resolve_seed_pack_version(prompt_version)
    parts = [
        f"A photographic plate{world_anchor}.",
        load_prompt(SEED_PACK_MODULE, "seed_head", version=resolved).strip(),
        f"THE STRUCTURE: {structure_desc}",
    ]
    if prompt_version in _HUMAN_SCALE_PACKS:
        # fix① (E2E11): 휴먼 스케일 앵커 — interior 증거 유무와 무관하게
        # 상시 주입(기존 scale_clause 는 interior_note 결손 시 생략돼 규모
        # 계약이 0 이 되던 실측 대응). v4 이하=기존 조립 byte-identical.
        parts.append(
            load_prompt(
                SEED_PACK_MODULE, "human_scale_clause", version=resolved
            ).strip()
        )
    if (exterior_note_en or "").strip():
        # v4 미만 팩에는 exterior_clause 가 없음 — load_prompt 가 명시
        # 실패(fail-closed)하므로 조용한 절 누락은 발생하지 않는다.
        parts.append(
            load_prompt(SEED_PACK_MODULE, "exterior_clause",
                        version=resolved).strip()
            + f"\nEXTERIOR NOTES: {exterior_note_en}"
        )
    if (interior_note_en or "").strip():
        parts.append(
            load_prompt(SEED_PACK_MODULE, "scale_clause",
                        version=resolved).strip()
            + f"\nINTERIOR: {interior_note_en}"
        )
    if (place_text or "").strip():
        parts.append(f"THE LOCATION: {place_text}")
    if (excluded_transient_en or "").strip():
        parts.append(
            "EXCLUDED — TRANSIENT ELEMENTS (confirmed not part of the "
            "permanent site; never depict): " + excluded_transient_en
        )
    parts.append(
        load_prompt(SEED_PACK_MODULE, "no_people", version=resolved).strip()
    )
    return "\n\n".join(parts)


def build_aerial_map_prompt(*, prompt_version: str = "3") -> str:
    """(DEPRECATED — 육안 라운드3, 2026-07-15) seed→실사 탑다운 항공뷰.

    사용자 판정: 실사 항공사진은 레인1 canon 맵과 매체가 다름 — 레인2
    맵도 outdoor_place_canon map_prompt(2D SITE PLAN 재투영, 참조=seed)를
    재사용한다(v4 canary 실증, outdoor_structure_seed_step 배선). 신규
    소비 금지 — 모듈 삭제 금지 규칙에 따라 이력용으로만 보존.
    """
    from app.modules.prompt_loader import load_prompt

    resolved = resolve_seed_pack_version(prompt_version)
    return "\n\n".join([
        load_prompt(SEED_PACK_MODULE, "aerial_head",
                    version=resolved).strip(),
        load_prompt(SEED_PACK_MODULE, "no_people", version=resolved).strip(),
    ])


# ── 2026-07-19 재설계 A: seed 플레이트 참조 (aerial 전면 대체) ─────────
# 사용자 확정: 야외 배경 플레이트의 공간·룩 참조=장소 seed(정면 실사 앵커).
# 위성/항공 배치도(aerial) 형태 참조·산출 금지.

SEED_SITE_GUIDANCE = (
    "\n\nSITE LOOK REFERENCE: the attached photograph shows this exact "
    "place — the confirmed look of its structure and surroundings. Keep "
    "the structure's massing, storey count, materials, wear and the "
    "character of the surroundings consistent with that photograph while "
    "composing the viewpoint this plate needs. Do not copy its camera "
    "angle or framing."
)


def build_seed_plate_refs(
    seed_groups: Mapping[str, Any],
    building_groups: Sequence[Mapping[str, Any]],
) -> Dict[str, Dict[str, str]]:
    """loc_id → {group_id, path} — 야외(is_indoor False) loc 에 그 그룹의
    확정 seed 실사를 플레이트 공간·룩 참조로 매핑.

    status!='ok'/경로 결손/파일 부재 그룹은 제외(호출측 fail-safe — 기존
    경로 유지). 순수 함수 — 스텝 게이트(flag)는 호출측 소유.
    """
    out: Dict[str, Dict[str, str]] = {}
    for g in building_groups or []:
        gid = str((g or {}).get("group_id") or "")
        entry = (seed_groups or {}).get(gid) or {}
        if entry.get("status") != "ok":
            continue
        p = str(entry.get("seed_png_path") or "")
        if not p or not Path(p).is_file():
            continue
        for m in (g or {}).get("members") or []:
            if (m or {}).get("is_indoor") is False:
                # background_classify building_groups member 의 loc 키는
                # ``loc_id`` (E2E9 실측 — ``location_id`` 로 읽던 배선
                # 버그로 매핑이 항상 비어 seed 가 한 번도 첨부되지 않았다).
                # legacy 호출 호환으로 location_id 도 수용.
                loc = str(
                    (m or {}).get("loc_id")
                    or (m or {}).get("location_id") or "")
                if loc:
                    out[loc] = {"group_id": gid, "path": p}
    return out
