"""야외 구조물 **형태 참조**의 보충 획득 — 중앙 조사가 산 것에 없는 `structure_form` 의무를 21.915 가 **같은 경계**로 산다.

★설계 `docs/design/2026-09-03-grounding-v2-21915-adapter.md` §8 (HITL 0 · Codex 좁힌 (가)):
  - 의무는 **결정적**으로 만든다 — 그룹 → 실외 장소 하나(`members[].loc_id`) → 중앙 CP·보충 CP 에 `structure_form`
    줄이 없는 장소마다 하나. 뼈대는 그 장소의 중앙 ledger_row 를 복사하고 부류 이름을 place spec 이 낸
    `structure_desc`(런타임 LLM 산출 · 고정 명사 아님)로 바꾼다. 원고 근거는 그대로.
  - 사는 것은 중앙 조사기(`grounding_central_acquisition.run`) 그대로 — 5단계(텍스트 조사 → 좁은 검색 → CRITERIA
    판정 → 넓은 재검색 → 반드시 한 장). 여기서는 사지 않는다.
  - 남기는 곳은 **별도 CP**(`reference_acquisition_supplement`) — append-only. 중앙 CP 는 안 고친다.
  - 읽는 곳은 **merge view 하나**(`outdoor_reference_rows`) — base + 살아 있는 보충 줄. 같은 subject 가 양쪽에 있으면 선다.
★사람 대기 없음. 이름·부분문자열·regex 결속 없음 — ID(loc_id ↔ final_id)로만 잇는다.
"""
from __future__ import annotations

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

SUPPLEMENT_STEP_DIR = "reference_acquisition_supplement"
SUPPLEMENT_CONTRACT_VERSION = "1.202609030310"


class SupplementConflict(RuntimeError):
    """base 와 보충이 같은 대상을 둘 다 든다 — 어느 쪽을 쓸지 모르는 채 안 쓴다."""


def structure_form_subject_id(loc: str) -> str:
    from app.modules.pipeline.grounding_reference_obligations import (
        OBLIGATION_STRUCTURE_FORM, obligation_subject_id)
    return obligation_subject_id(loc, OBLIGATION_STRUCTURE_FORM)


def _rows(cp: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]:
    return list((((cp or {}).get("data") or {}).get("rows")) or [])


def _is_structure_form(row: Dict[str, Any]) -> bool:
    from app.modules.pipeline.grounding_reference_obligations import OBLIGATION_STRUCTURE_FORM
    led = row.get("ledger_row") or {}
    return str(led.get("obligation_kind") or "") == OBLIGATION_STRUCTURE_FORM


def live_supplement_rows(supp_cp: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]:
    """보충 CP 의 **살아 있는** 줄 — `superseded_by` 가 없는 것."""
    return [r for r in _rows(supp_cp) if not r.get("superseded_by")]


def outdoor_reference_rows(front_cp: Optional[Dict[str, Any]],
                           supp_cp: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]:
    """★야외 전용 merge view — base(중앙) + 살아 있는 보충. 같은 subject 가 양쪽에 있으면 선다."""
    base = _rows(front_cp)
    supp = live_supplement_rows(supp_cp)
    base_ids = {str(r.get("research_subject_id") or "") for r in base}
    dup = sorted({str(r.get("research_subject_id") or "") for r in supp} & base_ids)
    if dup:
        raise SupplementConflict(f"중앙 CP 와 보충 CP 가 같은 대상을 둘 다 든다 — {dup}")
    return base + supp


def validate_targets(front_cp: Optional[Dict[str, Any]], targets: Sequence[Dict[str, Any]]) -> List[str]:
    """★provider 앞에서 **한 번에** 본다 (Codex BLOCK 3 2026-09-03): 그룹의 실외 장소가 정확히 하나인가 ·
    structure_desc 가 있는가 · 그 장소 줄이 중앙 CP 에 있는가. 문제 목록을 돌려준다(비면 통과)."""
    by_loc = {str((r.get("ledger_row") or {}).get("final_id") or "")
              for r in _rows(front_cp)
              if str((r.get("ledger_row") or {}).get("owner_type") or "") == "location"}
    problems: List[str] = []
    for t in targets:
        gid = str(t.get("gid") or "")
        locs = list(t.get("loc_ids") if t.get("loc_ids") is not None else ([t.get("loc_id")] if t.get("loc_id") else []))
        locs = [str(x) for x in locs if x]
        if len(locs) != 1:
            problems.append(f"{gid}: 실외 장소가 {locs or '없음'} — 정확히 하나여야 한다")
            continue
        if not str(t.get("structure_desc") or "").strip():
            problems.append(f"{gid}→{locs[0]}: place spec 의 structure_desc 가 비었다")
        if locs[0] not in by_loc:
            problems.append(f"{gid}→{locs[0]}: 중앙 CP 에 그 장소 줄이 없다 — 뼈대를 지어내지 않는다")
    return problems


def structure_form_obligations(front_cp: Optional[Dict[str, Any]],
                               targets: Sequence[Dict[str, Any]]) -> Dict[str, Any]:
    """`targets` = `[{"gid", "loc_id", "structure_desc"}, …]` → 보충 의무 장부 `{"rows": [...]}`.
    ★중앙(base) CP 에 `structure_form` 줄이 있는 장소만 뺀다. **기존 보충 CP 는 의무를 막지 않는다** —
    (Codex BLOCK 2 2026-09-03) 옛 줄이 retryable/unavailable 이거나 desc·좌표·팩이 바뀌어 새 신원이 필요해도
    `ca.run` 에 닿아야 한다. 되쓰기/재시도/신원 판단은 `ca.run` 과 장부가 한다. 같은 장소를 두 그룹이
    가리키면 하나. ★뼈대는 그 장소의 중앙 ledger_row 복사 — 없으면 선다(뜻을 지어내지 않는다)."""
    from app.modules.pipeline.grounding_entity_contract import PRODUCER_PAYLOAD
    from app.modules.pipeline.grounding_reference_obligations import OBLIGATION_STRUCTURE_FORM
    problems = validate_targets(front_cp, targets)
    if problems:
        raise ValueError("보충 의무를 세울 수 없다: " + " | ".join(problems))
    have = {str(r.get("research_subject_id") or "") for r in _rows(front_cp) if _is_structure_form(r)}
    by_loc: Dict[str, Dict[str, Any]] = {}
    for r in _rows(front_cp):
        led = r.get("ledger_row") or {}
        if str(led.get("owner_type") or "") == "location" and led.get("final_id"):
            by_loc.setdefault(str(led["final_id"]), led)
    rows: List[Dict[str, Any]] = []
    seen: set = set()
    for t in targets:
        loc = str(t.get("loc_id") or "")
        desc = str(t.get("structure_desc") or "").strip()
        sid = structure_form_subject_id(loc)
        if sid in have or loc in seen:
            continue
        seen.add(loc)
        led = copy.deepcopy(by_loc[loc])
        payload = dict(led.get(PRODUCER_PAYLOAD) or {})
        payload.update({"coarse_type_label": desc, "surface_form": desc, "visual_brief": desc})
        led.update({
            "research_subject_id": sid, "purpose": None,
            "obligation_kind": OBLIGATION_STRUCTURE_FORM, "covers": [loc],
            "origin_subject_ids": sorted({str(led.get("research_subject_id") or "")} - {""}),
            "supplement_for_group": str(t.get("gid") or ""),
            PRODUCER_PAYLOAD: payload,
        })
        rows.append(led)
    return {"rows": rows, "contract": SUPPLEMENT_CONTRACT_VERSION}


def effective_result(row: Dict[str, Any]) -> Dict[str, Any]:
    """줄의 **유효 획득 결과** — 같은 신원이라도 이것이 달라지면 새 줄이다 (Codex BLOCK 2)."""
    acq = row.get("acquisition") or {}
    return {"status": str(row.get("status") or ""), "outcome": str(row.get("outcome") or ""),
            "chosen_path": str(acq.get("chosen_path") or ((acq.get("chosen") or {}).get("path")) or ""),
            "match_quality": str(acq.get("match_quality") or ""),
            "why_unbought": str(row.get("why_unbought") or "")}


def merge_supplement(existing: Optional[Dict[str, Any]], new_rows: Sequence[Dict[str, Any]],
                     *, config_hash: str) -> Dict[str, Any]:
    """★append-only: 있던 줄은 지우지 않는다. 같은 subject 에 대해 **신원과 유효 결과가 완전히 같을 때만**
    no-op. 신원이 다르거나(desc·좌표·팩 변경) 같은 신원의 유효 결과가 달라졌으면(예: incomplete → selected)
    옛 살아 있는 줄에 `superseded_by` 를 적고 새 줄을 덧붙인다. 살아 있는 줄이 subject 당 둘 이상이면 선다."""
    rows = [dict(r) for r in _rows(existing)]
    for n in new_rows:
        sid = str(n.get("research_subject_id") or "")
        ident = str(n.get("identity") or "")
        live = [r for r in rows if str(r.get("research_subject_id") or "") == sid and not r.get("superseded_by")]
        if len(live) > 1:
            raise SupplementConflict(f"보충 CP 에 {sid!r} 의 살아 있는 줄이 {len(live)}개다 — 어느 것을 쓸지 모른다")
        if live and str(live[0].get("identity") or "") == ident and effective_result(live[0]) == effective_result(n):
            continue
        stamp = f"{ident}:{effective_result(n)['status']}"
        for r in live:
            r["superseded_by"] = stamp
        rows.append(copy.deepcopy(n))
    live_ids = [str(r.get("research_subject_id") or "") for r in rows if not r.get("superseded_by")]
    dup = sorted({x for x in live_ids if live_ids.count(x) > 1})
    if dup:
        raise SupplementConflict(f"살아 있는 줄이 겹친다 — {dup}")
    return {"status": "completed", "schema_version": 1, "config_hash": config_hash,
            "data": {"contract": SUPPLEMENT_CONTRACT_VERSION, "rows": rows, "live_count": len(live_ids)}}
