
    -i                        d Z ddlmZ ddlZddlZddlmZmZmZm	Z	m
Z
  ej                  e      Z e
d      Z e
d      Z ej                   d      Z ej                   d      Zdd	Zdd
Z	 d	 	 	 	 	 	 	 ddZddZy)u  엔티티 이름 매칭 유틸 — 괄호 접미사 드리프트 대응.

**배경**
`character_angles[].character`(LLM 출력) 등 시나리오 단위 입력과 `EntityCanon.name`
(DB 고정) 사이에 표기 드리프트가 발생함:
  - "이도령" vs "이도령(혼)" (괄호 접미사 variant)
  - "수리영 " vs "수리영" (공백)
  - "민숙" vs " 민숙 " (전후 공백)

exact-match 전제로 작성된 기존 dict lookup(`name_to_uuid.get(name)`) 패턴은
이런 드리프트에서 정상 엔티티까지 "미등록"으로 분류하는 silent bug를 만든다.

**정규화 범위**
안전(공백만):
  - 앞뒤 공백 제거
  - 중간 연속 공백 단일화

별개 단계(괄호 제거):
  - variant 엔티티("서현 (5세)")와 base("서현")가 공존하는 실측 데이터가 있어
    괄호 제거 정규화 키를 variant 엔티티에 등록하면 base와 충돌 → silent 오매칭.
  - 따라서 `build_name_index`는 괄호 있는 엔티티를 저장할 때 **bare key(괄호 제거 키)를
    등록하지 않음**. base는 원본=bare이므로 자연 등록. variant는 원본 및 공백 정리만.
  - `lookup_name`은 query 쪽을 단계별로 완화(raw → 공백정리 → bare)해 최대한 맞춤.
    이 최후 수단이 base에 맞을 수도 있지만, 원본 표기가 일치하면 variant 우선이라 안전.

의도적으로 **제외**:
  - 조사 제거("민숙이"→"민숙"): "민숙이"가 본명일 수 있음.
  - 대소문자 통일: 한글은 불필요. 영문명은 원본 표기 의도 보존.

**사용**
```python
from app.core.name_matcher import build_name_index, lookup_name

idx = build_name_index(canons, key_fn=lambda c: c.name, value_fn=lambda c: c.id)
uuid = lookup_name(idx, llm_name)  # None이면 미등록
```
    )annotationsN)CallableDictIterableOptionalTypeVarTVu   [\(\[（［【〈《「『].*$z\s+c                    t        | t              r| sy| j                         }t        j	                  d|      j                         }t
        j	                  d|      }|S )u  이름을 "bare key"(괄호 제거 + 공백 정리)로 정규화.

    처리:
      1. 앞뒤 공백 제거
      2. 괄호 접미사 제거 (첫 괄호부터 끝까지)
      3. 중복 공백 단일화

    빈 문자열 / None / 비문자열 입력 시 "" 반환.

    주의: 반환값은 "variant/base 구분 정보가 사라진" bare key이므로
    `build_name_index`의 variant 엔티티 등록에는 쓰이지 않는다.
    lookup_name의 최후 fallback으로만 쓰임.
      )
isinstancestrstrip_BRACKET_PATTERNsub_WS_PATTERN)namess     K/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/name_matcher.pynormalize_namer   7   sN     dC 

AR#))+AQAH    c                p    t        | t              r| syt        j                  d|       j	                         S )u_   공백만 정리 (괄호는 보존). variant/base 둘 다 안전하게 등록 가능한 수준.r   r   )r   r   r   r   r   )r   s    r   _collapse_whitespacer   M   s+    dC ??3%++--r   c                  	 |d }i 	d	fd}| D ]w  } ||      xs d}|s ||      } ||||       t        |      }||k7  r
 ||||       t        j                  |      rTt        |      }|sb||k7  sh||k7  sn ||||       y 	S )u  items의 이름을 키로 하는 lookup 인덱스 구축.

    등록 정책 (variant/base 충돌 방지):
      - raw (원본 이름) 등록
      - collapsed (공백만 정리) 등록 — raw와 다를 때만
      - **bare** (괄호 제거 정규화) — raw에 괄호가 없을 때만 (variant면 bare 등록 안 함).
        이유: "서현"(base)과 "서현 (5세)"(variant)가 공존하는 실측 데이터에서
        variant의 bare "서현"을 등록하면 base와 충돌 → silent 오매칭.
      - 충돌 시 첫 등록 우선 (`setdefault`). raw/collapsed 충돌은 warning 로그.

    Args:
        items: 엔티티 컬렉션 (예: EntityCanon 리스트, 체크포인트 dict 리스트)
        key_fn: 각 item에서 **이름 문자열**을 추출하는 함수
        value_fn: 인덱스 값 생성 함수. 생략 시 item 자체(항등) 사용.

    Returns:
        {key: value_fn(item)}. key는 raw/collapsed/bare 중 하나.

    Raises:
        없음. 빈 이름 item은 스킵.
    c                    | S N )xs    r   <lambda>z"build_name_index.<locals>.<lambda>o   s    Qr   c                \    | sy| v r|    |urt         j                  d| |       y|| <   y)uG   key 등록. 기존 값과 다른 엔티티 val이 충돌하면 warning.NzIname_index collision: key=%r kept existing entry, skipped new from raw=%r)loggerwarning)keyvalraw_for_logindexs      r   	_registerz#build_name_index.<locals>._registers   s>    %<Sz$_ c
r   r   )r$   r   r%   r
   r&   r   returnNone)r   r   searchr   )
itemskey_fnvalue_fnr(   itemrawr%   	collapsedbarer'   s
            @r   build_name_indexr3   T   s    4 E Tl btn#sC (-	ic*&&s+!#&D	(9$S)  Lr   c                    |sy|| v r| |   S t        |      }|r||k7  r	|| v r| |   S t        |      }|r|||fvr	|| v r| |   S y)u  query로 index 조회. raw → 공백정리 → bare 단계적 완화.

    bare 단계에서 variant 엔티티가 등록 제외된 상태라 base에 맞을 수 있지만,
    index에 variant의 원본 키가 있으면 raw 단계에서 먼저 잡히므로 정상 우선순위.
    N)r   r   )r'   queryr1   r2   s       r   lookup_namer6      sm     ~U|$U+IY%'I,>Y% DUI..45=T{r   )r   r   r)   r   r   )r,   zIterable[T]r-   zCallable[[T], str]r.   zOptional[Callable[[T], V]]r)   Dict[str, V])r'   r7   r5   r   r)   zOptional[V])__doc__
__future__r   loggingretypingr   r   r   r   r   	getLogger__name__r"   r	   r
   compiler   r   r   r   r3   r6   r   r   r   <module>r@      s   $J #  	 > >			8	$CLCL 2::?@ bjj ,. ,0::: ): 	:zr   