"""concept_parser — 개념노트 markdown 구조 파서 + 관련개념 백링크 해소 (이론 리더용).

정찰 실측 불변식(273/273): 개념노트는 고정 골격을 100% 따름 —
    # {H1 제목}                     (첫 줄, DB title 과 다른 표시용 제목)
    > **한 줄 요약**: {요약}          (blockquote, 라벨 고정)
    ## {본문 라벨}  ...              (BODY, 자유 라벨 H2 0~N, 트레일 ★ 가능)
    ## 빈출 포인트                    (항상, 관련개념 직전)
    ## 관련 개념                      (항상, 문서 최종 섹션)

코드펜스(``` ASCII 도식) 내부의 ##/- 는 무시. 헤딩 트레일 ★ 는 스트립(라벨 정규화).
'빈출 포인트'/'관련 개념' 앵커만 이름으로 잡고 나머지 BODY 는 순서·위치로 처리(라벨 화이트리스트 금지).
순수 함수 · LLM 0.
"""

from __future__ import annotations

import re

_FENCE = re.compile(r"^\s*```")
_H1 = re.compile(r"^#\s+(.+?)\s*$")
_H2 = re.compile(r"^##\s+(.+?)\s*$")  # ### 는 매칭 안 됨(## 뒤 \s 요구)
_SUMMARY = re.compile(r"^>\s*\*\*한 줄 요약\*\*:\s*(.+)$")
_STAR_SUFFIX = re.compile(r"\s*★+\s*$")
_TRAIL_STARS = re.compile(r"★+\s*$")
_BINCHEOL_ITEM = re.compile(r"^\s*-\s+(★*)\s*(.+)$")
_RELATED_ITEM = re.compile(r"^\s*-\s+(.+)$")
_PAREN = re.compile(r"\s*\(.*$")  # 괄호부터 끝(clarifier 힌트 절단)
_NUM_PREFIX = re.compile(r"^\d+_")
_STRIP_SYM = re.compile(r"[\s_·,./()\-]")

_ANCHOR_BINCHEOL = "빈출 포인트"
_ANCHOR_RELATED = "관련 개념"


def parse_concept(md: str) -> dict:
    """개념노트 md → {title, summary, body[{label,stars,md}], bincheol[{tier,text}], related[{raw,phrase,hint}]}."""
    lines = (md or "").split("\n")
    title: str | None = None
    summary: str | None = None
    body: list[dict] = []
    bincheol_lines: list[str] = []
    related_lines: list[str] = []

    in_fence = False
    zone = "pre"  # pre | body | bincheol | related
    body_cur: dict | None = None

    def emit(line: str) -> None:
        if body_cur is not None:
            body_cur["_lines"].append(line)
        elif zone == "bincheol":
            bincheol_lines.append(line)
        elif zone == "related":
            related_lines.append(line)
        # pre-zone 내용(요약 앞 잡음)은 버림

    for ln in lines:
        if _FENCE.match(ln):
            in_fence = not in_fence
            emit(ln)
            continue
        if in_fence:
            emit(ln)
            continue

        if title is None:
            m = _H1.match(ln)
            if m:
                title = m.group(1).strip()
                continue
        if summary is None:
            m = _SUMMARY.match(ln)
            if m:
                summary = m.group(1).strip()
                continue

        m2 = _H2.match(ln)
        if m2:
            raw_label = m2.group(1).strip()
            star_m = _TRAIL_STARS.search(raw_label)
            stars = len(star_m.group(0).strip()) if star_m else 0
            label = _STAR_SUFFIX.sub("", raw_label).strip()
            if label == _ANCHOR_BINCHEOL:
                zone = "bincheol"
                body_cur = None
                continue
            if label == _ANCHOR_RELATED:
                zone = "related"
                body_cur = None
                continue
            body_cur = {"label": label, "stars": stars, "_lines": []}
            body.append(body_cur)
            zone = "body"
            continue

        emit(ln)

    body_out = []
    for s in body:
        text = "\n".join(s["_lines"]).strip()
        if text or s["label"]:
            body_out.append({"label": s["label"], "stars": s["stars"], "md": text})

    bincheol = []
    for ln in bincheol_lines:
        m = _BINCHEOL_ITEM.match(ln)
        if m:
            bincheol.append({"tier": len(m.group(1)), "text": m.group(2).strip()})

    related = []
    for ln in related_lines:
        m = _RELATED_ITEM.match(ln)
        if m:
            raw = m.group(1).strip()
            phrase = _PAREN.sub("", raw).strip()
            hint = raw[len(phrase):].strip() if len(raw) > len(phrase) else ""
            if phrase:
                related.append({"raw": raw, "phrase": phrase, "hint": hint})

    return {
        "title": title,
        "summary": summary,
        "body": body_out,
        "bincheol": bincheol,
        "related": related,
    }


def _normalize(s: str) -> str:
    """해소용 정규화: NN_ 접두 제거 → 소문자 → 공백/기호 제거. 영문은 lowercase 유지."""
    s = _NUM_PREFIX.sub("", s or "")
    s = s.lower()
    s = _STRIP_SYM.sub("", s)
    return s


def resolve_related(related: list[dict], title_index: list[tuple]) -> list[dict]:
    """관련개념 구절 → 개념 doc 해소. title_index = [(doc_id, title, subject), ...].

    다단 fallback(정찰 ~79%): 정규화 exact → 양방향 substring(≥2자 가드) → 미해소=dangling(doc_id None).
    """
    norm_exact: dict[str, int] = {}
    norm_list: list[tuple[str, int, str]] = []
    for did, ttl, _subj in title_index:
        n = _normalize(ttl)
        if n:
            norm_exact.setdefault(n, did)
            norm_list.append((n, did, ttl))

    out = []
    for it in related:
        pn = _normalize(it["phrase"])
        did: int | None = None
        rtitle: str | None = None
        if pn and len(pn) >= 2:
            if pn in norm_exact:
                did = norm_exact[pn]
            else:
                # substring 폴백: title-norm ⊆ phrase-norm 방향만(짧은 phrase 가 더 큰 title 을
                # 삼키는 오결선 방지, 예: '염산'→'염산나트륨' X) + 길이차 최소(가장 구체적) +
                # doc_id tiebreak(순서 무관 결정성). 후보 없으면 dangling(doc_id None).
                cands = [
                    (abs(len(n) - len(pn)), cand, ttl)
                    for n, cand, ttl in norm_list
                    if len(n) >= 2 and n in pn
                ]
                if cands:
                    cands.sort(key=lambda c: (c[0], c[1]))
                    _, did, rtitle = cands[0]
        if did is not None and rtitle is None:
            rtitle = next((t for d, t, _ in title_index if d == did), None)
        out.append(
            {"phrase": it["phrase"], "hint": it["hint"], "doc_id": did, "title": rtitle}
        )
    return out
