"""이름이 **본문에서 왔는지** 확인한다 (2026-09-18).

## 무엇이 결함이었나

이름 없는 인물의 역할명을 모델이 지으면서 **본문에 없는 인종을 붙였다.**
대본에 「백인」은 한 곳(앰버)뿐인데 인물 셋이 백인이 됐고, 그 이름이
하류 전부로 퍼졌다 — 「백인 여성형 집사 로봇」(본문은 「집사로봇」).

금지문을 더 쌓아 봤지만 같은 판에서 11개가 나왔다. **글로 막는 대신 결속한다.**

## 어떻게 확인하나 — 뜻이 아니라 **출처**

두 가지를 **글자 그대로** 본다. 이것은 의미 판단이 아니라 인용 확인이다.

    ① `name_source_quote` 가 시나리오 본문에 그대로 있는가
    ② `name` 이 그 구절 안에 그대로 있는가

②가 핵심이다 — 지어낸 말은 근거 구절 안에 있을 수 없다. 모델이 「집사로봇」이
나오는 구절을 인용하면서 이름을 「백인 여성형 집사 로봇」이라 쓰면 ②에서 걸린다.

띄어쓰기와 줄바꿈은 **무시한다** — 시나리오 PDF 는 줄이 접혀 있어 같은 말이
공백만 다르게 들어온다. 그 밖의 글자는 그대로 견준다.
"""
from __future__ import annotations

import logging
import re
from typing import Dict, List, Tuple

logger = logging.getLogger(__name__)

_WS = re.compile(r"\s+")
#: 팩이 정한 **변형 표기** — 「앰버 (홀로그램)」·「현우 (어린 시절)」.
#: 괄호 안은 이 저장소의 약속이지 본문 인용이 아니므로 확인에서 뺀다.
#: (안 빼면 멀쩡한 변형 이름 19개가 전부 어긋남으로 잡힌다 — 실측)
_VARIANT_SUFFIX = re.compile(r"\s*\([^()]*\)\s*$")


def _flat(s: str) -> str:
    """공백·줄바꿈만 없앤 비교용 형태."""
    return _WS.sub("", s or "")


def base_name(name: str) -> str:
    """변형 표기를 떼어 낸 **본체 이름**."""
    return _VARIANT_SUFFIX.sub("", (name or "").strip()).strip()


def check_name_provenance(
    entities: List[Dict], fulltext: str,
) -> Tuple[List[Dict], List[Dict]]:
    """(근거가 맞는 것, 어긋난 것) 으로 가른다. 원본 dict 는 그대로 둔다.

    어긋난 항목에는 `_provenance_reason` 을 붙여 **다시 물을 때 그대로 보여준다** —
    무엇이 왜 틀렸는지 모르면 모델은 같은 답을 또 낸다.
    """
    flat_text = _flat(fulltext)
    ok: List[Dict] = []
    bad: List[Dict] = []
    for e in entities:
        name = (e.get("name") or "").strip()
        quote = (e.get("name_source_quote") or "").strip()
        if not name:
            continue
        if not quote:
            e = dict(e, _provenance_reason="근거 구절(name_source_quote)이 비었다")
            bad.append(e)
            continue
        if _flat(quote) not in flat_text:
            e = dict(e, _provenance_reason="근거 구절이 시나리오 본문에 없다(지어낸 인용)")
            bad.append(e)
            continue
        if _flat(base_name(name)) not in _flat(quote):
            e = dict(e, _provenance_reason=(
                "이름이 근거 구절 안에 없다 — 구절에 없는 말(인종·피부색·추정 나이 등)을"
                " 이름에 붙였다"))
            bad.append(e)
            continue
        ok.append(e)
    return ok, bad


def correction_block(bad: List[Dict]) -> str:
    """다시 물을 때 덧붙일 절. 비면 빈 문자열."""
    if not bad:
        return ""
    lines = ["\n\n[수정 요청] 아래 인물은 **이름의 근거가 확인되지 않았다.**",
             "본문에 있는 말로 이름을 고치고, 그 말이 들어 있는 구절을 그대로 인용하라.",
             "구절에 없는 말(인종·피부색·추정 나이 등)은 이름에 넣지 마라.", ""]
    for e in bad:
        lines.append(f"- 「{e.get('name','')}」 — {e.get('_provenance_reason','')}")
        q = (e.get("name_source_quote") or "").strip()
        if q:
            lines.append(f"    (낸 구절: {q[:120]})")
    return "\n".join(lines)


def log_summary(step: str, ok: List[Dict], bad: List[Dict]) -> None:
    if bad:
        logger.warning("%s 이름 근거: %d/%d 확인 · 어긋남 %s",
                       step, len(ok), len(ok) + len(bad),
                       [e.get("name") for e in bad][:8])
    else:
        logger.info("%s 이름 근거: %d개 전부 확인", step, len(ok))
