"""검색 지시문 **저작** — production 중앙 경로용. ★새 팩·새 구매자 0.

## 왜 이것이 필요한가

실측 (2026-09-02, 유료 canary ①): chunk 팩이 후보를 낼 때
**「시대나 지역을 앞에 붙이지 마세요 — 그것은 뒤 단계가 붙입니다」**라고
적어 두었는데, 중앙 경로에는 그 「뒤 단계」가 **없었다**.
`grounding_acquisition_adapter.search_target` 이 `visual_brief` 를 질의로
그대로 넘기고 끝이었고, 저작기(`grounding_ref_brief` 팩)는 **canary 도구만**
불렀다. 그래서 —

    시대가 안 실린 질의  → 기록물은 **앞선 시기**가 훨씬 많아 옛것이 온다
    지역이 안 실린 질의  → **같은 언어권 옆 나라** 것이 온다

뒤에서 아무도 그것을 안 본다 — 검색은 이미지만 돌려주고, 심판은 「찾던
부류가 맞나」만 본다(시대·나라·품질을 심판에게 안 묻는 것이 이 판의 계약).

## 무엇을 하나 / 안 하나

    한다    런타임 좌표(시대·지역·세계 사실)를 **선언된 것만** 질의에 싣게
            지시문을 원어로 저작한다
    안 한다 장소·나라 이름을 코드나 프롬프트에 박지 않는다. 없는 좌표를
            지어내지 않는다. 시대가 없다고 대상에서 빼지 않는다

★★사용자 확정 (2026-09-02): 고증은 **과거 것만이 아니다.** 현대라도 나라·
지역마다 생김새가 다른 시설은 이미지 모델이 모르고 그곳 사람은 알아챈다.
그래서 **지역만 선언된 판**도 그 지역으로 찾는다.
★사례가 될 만한 낱말을 이 파일에 적지 않는다 — 원문 사례는
`docs/TASKS.md` 와 시험 fixture 에만 둔다.

★나가는 payload 가 **곧 신원**이다. 필드를 다시 나열하지 않으므로 저작
입력이 늘어도 재사용 신원이 저절로 따라 움직인다.
"""
from __future__ import annotations

from typing import Any, Callable, Dict, Optional

#: 저작에 쓰는 모델 별칭. ★검색 조율기와 같은 자리에서 온다.
BRIEF_MODEL_ALIAS = "gpt"

#: 이 모듈의 계약 판. ★저작 입력·조립이 바뀌면 올린다 — 재사용 신원이 본다.
#: 2.202609030010 — 저작기로 나가는 것이 **뼈대**만으로 바뀜(2026-09-03). 옛 신원 이관은 아래 LEGACY.
BRIEF_CONTRACT_VERSION = "2.202609030010"
LEGACY_BRIEF_CONTRACT_VERSION = "1.202609020400"


class BriefInputsMissing(RuntimeError):
    """저작 재료가 없다. ★빈 질의로 안 산다."""


def coordinates_of(world: Optional[Dict[str, Any]]) -> Dict[str, str]:
    """런타임 **구조화 칸**에서 좌표를 읽는다. ★전문에서 짐작하지 않는다.

    Returns:
        `{"era": …, "region": …}` — 없으면 빈 문자열. **지어내지 않는다.**
    """
    data = ((world or {}).get("data") or {}) if "data" in (world or {}) \
        else (world or {})
    return {"era": str(data.get("era") or "").strip(),
            "region": str(data.get("region") or "").strip()}


def skeleton_search_input(target: Dict[str, Any], *, era: str = "",
                          region: str = "") -> Dict[str, Any]:
    """검색 저작기로 나가는 **뼈대** — 그리고 구매 신원. ★같은 객체 하나 (Codex BLOCK 2026-09-02 밤).

    앞 판은 근거 문장·visual_brief·원문 표본까지 저작기에 보내고 그 전체를 신원으로 해싱했다.
    merge 가 근거 문장을 조금 다르게 합치기만 해도 같은 대상을 **다시 샀다**(실측 10건).
    사용자 확정은 「뼈대만 찾고 세부는 i2i」다. 그래서 종류 이름·넓은 검색어·언어 잠금·시대·
    지역·저작 팩/모델/계약만 나가고, 세부 묘사·근거 문장·원문은 여기 없다(장부 줄에는
    감사·i2i 용으로 그대로 남는다). 필드 목록은 이 함수 **한 곳**이다.
    """
    from app.modules.pipeline import grounding_ref_brief as grb

    terms = sorted({str(t).strip() for t in (target.get("terms_native") or ()) if str(t).strip()})
    return {
        "owner_type": str(target.get("owner_type") or ""),
        "coarse_type_label": str(target.get("coarse_type_label") or "").strip(),
        "terms_native": terms,
        "language_lock_native": str(target.get("language_lock_native") or ""),
        "era": str(era or ""), "region": str(region or ""),
        "brief_pack": grb.resolve_pack_version(),
        "brief_model": BRIEF_MODEL_ALIAS,
        "brief_contract": BRIEF_CONTRACT_VERSION,
    }


def outbound(target: Dict[str, Any], *, world_facts: str, source_text: str,
             narrow: bool, era: str = "", region: str = "") -> Dict[str, Any]:
    """저작기로 **실제 나가는 것** — `skeleton_search_input` 에서만 조립한다.
    `world_facts`/`source_text` 는 서명 호환으로 받되 **보내지 않는다**(옛 신원 계산에만 쓴다)."""
    from app.modules.pipeline import grounding_ref_brief as grb

    sk = skeleton_search_input(target, era=era, region=region)
    kind = sk["coarse_type_label"]
    desc = "; ".join(sk["terms_native"]) or kind
    if not kind:
        raise BriefInputsMissing(
            f"{target.get('subject_id')!r} 에 저작 재료가 없다 — 부류 {kind!r}")
    user = grb.build_brief_user(
        owner_type=sk["owner_type"],
        coarse_type_label=kind, subject_description=desc,
        world_facts_block="", era_declaration=sk["era"],
        region_declaration=sk["region"], source_text="",
        researched_facts_block="")
    if narrow:
        user += "\n\n" + grb.load_narrow_retry_hint()
    return {"system": grb.load_brief_system(), "user": user,
            "schema": grb.build_brief_schema(),
            "model_alias": BRIEF_MODEL_ALIAS,
            "pack": grb.resolve_pack_version(),
            "contract": BRIEF_CONTRACT_VERSION,
            "skeleton": sk}


def _legacy_outbound(target: Dict[str, Any], *, world_facts: str, source_text: str,
                     narrow: bool, era: str = "", region: str = "") -> Dict[str, Any]:
    """★옛 신원 계산 전용 — 2026-09-02 이전 판이 실제로 보냈던 모양. 전송에 안 쓴다."""
    from app.modules.pipeline import grounding_ref_brief as grb

    kind = str(target.get("coarse_type_label") or "").strip()
    desc = "\n".join(x for x in (str(target.get("surface_form") or ""),
                                 str(target.get("visual_brief") or "")) if x)
    if not desc or not kind:
        raise BriefInputsMissing("legacy: 저작 재료가 없다")
    user = grb.build_brief_user(
        owner_type=str(target.get("owner_type") or ""),
        coarse_type_label=kind, subject_description=desc,
        world_facts_block=world_facts, era_declaration=era,
        region_declaration=region, source_text=source_text,
        researched_facts_block="")
    if narrow:
        user += "\n\n" + grb.load_narrow_retry_hint()
    return {"system": grb.load_brief_system(), "user": user,
            "schema": grb.build_brief_schema(),
            "model_alias": BRIEF_MODEL_ALIAS,
            "pack": grb.resolve_pack_version(),
            "contract": LEGACY_BRIEF_CONTRACT_VERSION}


def identity_inputs(*, world_facts: str, era: str, region: str
                    ) -> Dict[str, Any]:
    """판 전체에 공통인 저작 입력. ★대상별 것은 `target_identity` 가 낸다."""
    from app.modules.pipeline import grounding_ref_brief as grb

    # ★world_facts 는 이제 저작기로 안 나간다 — 신원에도 없다(뼈대 계약). 옛 신원은 아래 legacy.
    return {"brief_pack": grb.resolve_pack_version(),
            "brief_contract": BRIEF_CONTRACT_VERSION,
            "brief_model": BRIEF_MODEL_ALIAS,
            "era": str(era or ""), "region": str(region or "")}


def legacy_identity_inputs(*, world_facts: str, era: str, region: str) -> Dict[str, Any]:
    """옛 판(2026-09-02 이전)의 공통 저작 입력 — 장부 이관(alias)에만 쓴다."""
    return {**identity_inputs(world_facts=world_facts, era=era, region=region),
            "brief_contract": LEGACY_BRIEF_CONTRACT_VERSION,
            "world_facts": str(world_facts or "")}


def target_identity(target: Dict[str, Any], *, world_facts: str,
                    source_text: str, era: str, region: str) -> str:
    """이 대상의 **저작 입력 지문**. ★나가는 payload 자체에서 나온다.

    ★★★필드 목록을 부르는 쪽에 다시 적지 않는다 (Codex BLOCK 2026-09-02).
    앞 판 신원은 판 공통 여섯 칸만 접어서, `owner_type`·부류·표기·
    `visual_brief`·원문 언어 표본이 **바뀌어도 옛 검색 결과를 되썼다** —
    다른 질의가 나가는데 같은 것으로 본 것이다.
    이제 **저작기로 실제 나가는 것**(system+user+schema+모델+팩+계약)을
    그대로 해싱한다. 저작 입력이 늘어도 신원이 **저절로** 따라 움직인다.
    ★좁힘 라운드는 같은 대상의 **같은 구매**이므로 `narrow=False` 로 잰다.
    """
    import hashlib
    import json

    sk = skeleton_search_input(target, era=era, region=region)
    raw = json.dumps(sk, sort_keys=True, ensure_ascii=False, default=str)
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:24]


def legacy_target_identity(target: Dict[str, Any], *, world_facts: str,
                           source_text: str, era: str, region: str) -> str:
    """옛 판의 대상 신원(저작 payload 전체 해시) — 장부 이관(alias)에만 쓴다."""
    import hashlib
    import json

    o = _legacy_outbound(target, world_facts=world_facts, source_text=source_text,
                         narrow=False, era=era, region=region)
    raw = json.dumps({k: o[k] for k in sorted(o)}, sort_keys=True,
                     ensure_ascii=False, default=str)
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:24]


def make_writer(*, world_facts: str, source_text: str, era: str = "",
                region: str = "", request_lock: Optional[Dict[str, Any]] = None,
                call: Optional[Callable[..., Dict[str, Any]]] = None
                ) -> Callable[..., Dict[str, Any]]:
    """`acquire_one(write_brief=…)` 에 넣을 저작기. ★공용 경계를 그대로 쓴다.

    Args:
        call: 시험이 갈아 끼우는 자리. 안 주면 `call_structured`.
    """
    def _write(target: Dict[str, Any], *, narrow: bool = False
               ) -> Dict[str, Any]:
        o = outbound(target, world_facts=world_facts, source_text=source_text,
                     narrow=narrow, era=era, region=region)
        fn = call
        if fn is None:
            from app.modules.llm.llm_client import call_structured

            fn = call_structured
        tag = "grounding_search_brief"
        return fn(tag, o["system"], o["user"], o["schema"],
                  project_config={tag: {"model": o["model_alias"]}},
                  schema_name=tag,
                  opik_metadata={"tags": ["op:grounding-search-brief"]},
                  **dict(request_lock or {}))

    # ★★저작기가 **제 신원 입력을 들고 다닌다** — 부르는 쪽이 그것을 다시
    #  조립하면 두 벌이 되고, 한쪽만 고쳐진다.
    _write.identity_inputs = identity_inputs(
        world_facts=world_facts, era=era, region=region)
    # ★★★시대 조각도 **여기서** 나온다 (2026-09-02). `era_coverage` 는
    #  「질의에 시대가 붙었나」를 재는 칸인데, `acquire_one(era_tokens=…)` 에
    #  값을 넣는 곳이 **한 군데도 없어서** 유료 주행 12대상 24라운드가 전부
    #  `measured: false · 시대 SOT 가 없거나 비교 불능` 으로 적혔다.
    #  계약만 있고 내는 곳이 없으면 그것은 선언이지 기능이 아니다.
    from app.modules.pipeline.reference_acquisition_rounds import (
        era_tokens_of as _tok)

    _write.era_tokens = _tok(era)
    # ★★선언된 좌표 **원문**도 들고 다닌다 — 나가는 질의가 그것을 글자 그대로
    #  실었는지 `acquire_one` 이 provider 앞에서 본다. 부르는 쪽이 세계 사실을
    #  다시 파싱하지 않는다.
    _write.coordinates = coordinates_of({"era": era, "region": region})
    # ★대상별 지문 — **나가는 payload** 에서 나온다. 부르는 쪽이 필드를
    #  다시 나열하지 않는다.
    _write.target_identity = lambda t: target_identity(
        t, world_facts=world_facts, source_text=source_text, era=era,
        region=region)
    # ★옛 신원 — 장부 이관(alias)에만. 없는 것처럼 두면 옛 구매 38건을 다시 산다.
    _write.legacy_identity_inputs = legacy_identity_inputs(
        world_facts=world_facts, era=era, region=region)
    _write.legacy_target_identity = lambda t: legacy_target_identity(
        t, world_facts=world_facts, source_text=source_text, era=era, region=region)
    return _write
