"""선언된 시대·지역 좌표를 **글자 그대로** 옮긴다 — 모델이 다시 읽어 바꾸지 못하게.

★★★실측 (Codex BLOCK 2026-09-02, canary 69e821758f3d): 원고(fixture)는 `ERA = "1960년대"` ·
`REGION = "대한민국"` 으로 **선언**했는데, 그 값이 구조화된 칸으로 production 에 닿는 길이
없었다. `visual_world_rules` 가 원고 글을 읽어 시대를 「20세기 전반, 일제강점기 무렵의
근대 한국」으로 **다시 해석**했고, 그 값이 조사 좌표의 정본(`visual_world_rules.era`)이
되어 복장 두 건의 검색이 엉뚱한 시대를 찾았다.

계약:
  - 사용자/프로젝트가 `project_config` 에 `grounding_era` / `grounding_region` 을 선언하면
    `visual_world_rules` 의 `era` / `region` 은 **그 글자 그대로**다. 모델 값은 감사용으로만
    남긴다(`inferred_era` / `inferred_region`).
  - 선언이 없으면(빈 값) 모델이 읽은 값 그대로 — **지어내지 않는다**. 시대 없이 지역만
    선언해도 된다(현대 배경).
  - 어느 값이 어디서 왔는지 `coordinate_source` 에 남긴다: `declared` | `inferred`.
"""
from __future__ import annotations

from typing import Any, Dict, Mapping, Optional

DECLARED_ERA_KEY = "grounding_era"
DECLARED_REGION_KEY = "grounding_region"
SOURCE_DECLARED = "declared"
SOURCE_INFERRED = "inferred"
COORDINATES_CONTRACT_VERSION = "1.202609022200"


def declared_coordinates(project_config: Optional[Mapping[str, Any]]) -> Dict[str, str]:
    """선언된 좌표만 — 비어 있지 않은 것만 담는다."""
    cfg = dict(project_config or {})
    out: Dict[str, str] = {}
    for axis, key in (("era", DECLARED_ERA_KEY), ("region", DECLARED_REGION_KEY)):
        v = str(cfg.get(key) or "").strip()
        if v:
            out[axis] = v
    return out


def with_declared_coordinates(existing: Optional[Mapping[str, Any]], *,
                              era: Optional[str], region: Optional[str]) -> Dict[str, Any]:
    """프로젝트 설정 dict 에 선언 좌표를 **적거나 지운다** — 빈 값이면 키를 뺀다
    (빈 문자열을 남기면 「선언 없음」과 「빈 선언」이 한 칸에 겹친다)."""
    out = dict(existing or {})
    for key, val in ((DECLARED_ERA_KEY, era), (DECLARED_REGION_KEY, region)):
        v = str(val or "").strip()
        if v:
            out[key] = v
        else:
            out.pop(key, None)
    return out


def merge_step_config(existing: Optional[Mapping[str, Any]],
                      step_config: Mapping[str, Any]) -> Dict[str, Any]:
    """스텝별 모델 설정(값이 dict 인 키)을 **통째로** 바꾸되, 스칼라 설정 키(선언 좌표 ·
    grounding_mode 등)는 **남긴다**. ★실측: LLM 설정 저장이 dict 전체를 덮어 써서
    좌표 선언이 다음 저장에 사라질 자리였다."""
    kept = {k: v for k, v in dict(existing or {}).items() if not isinstance(v, dict)}
    kept.update(dict(step_config))
    return kept


def apply_declared_coordinates(result: Dict[str, Any],
                               project_config: Optional[Mapping[str, Any]]) -> Dict[str, Any]:
    """`visual_world_rules` 산출에 선언 좌표를 **덮어** 쓴다. ★모델 값은 옆 칸에 보존."""
    out = dict(result or {})
    declared = declared_coordinates(project_config)
    source: Dict[str, str] = {}
    for axis in ("era", "region"):
        inferred = str(out.get(axis) or "").strip()
        if axis in declared:
            out[f"inferred_{axis}"] = inferred
            out[axis] = declared[axis]
            source[axis] = SOURCE_DECLARED
        else:
            out[axis] = inferred
            source[axis] = SOURCE_INFERRED
    out["coordinate_source"] = source
    out["coordinates_contract"] = COORDINATES_CONTRACT_VERSION
    return out
