// 개요(절 목차) → 본문 deterministic 점프용 anchor offset 산출 (경로 A: FE-only).
//
// hier 절(section_title)은 md_content 의 heading 라인에서 나왔으나(builder.py build_hier_tree,
// md_content 순수함수), 비-ATX(제N조/Chapter)는 본문에 markdown heading 요소·id 가 안 생기고
// 중복 제목(표-1·Part UW…)이 흔해 슬러그·textContent 매칭이 깨진다. 그래서 md_content 에서
// 각 절의 heading 위치(char offset)를 직접 찾아 <a id="sec-{chunk_id}"> 를 주입할 좌표를 만든다.
//
// ★ false early match 방어 3중 (리뷰 반영):
//   1. 라인-시작(전체-라인) 매칭 — 본문 중간 상호참조("see Part UW for…")는 라인 전체가 제목과
//      같지 않으므로 제외. heading 라인(선두 #/리스트마커 제거 후 전체)만 매칭.
//   2. 전체 매칭 + truncation 처리 — 'first-N-chars' prefix 금지('제1조'가 '제1조의2' 오매칭 차단).
//      builder 가 KO/ENG 제목을 [:200] truncate 하므로 truncated(매우 긴 제목)일 때만 startsWith.
//   3. 단조 커서 + 코드펜스 회피 — 매칭은 직전 매칭 다음 라인부터(역행 불가) + ``` ~~~ 펜스 내부 제외.
//      미스/역행은 anchor 없음 = 점프 비활성(아코디언 폴백). 오점프보다 무점프.
//
// ⚠ 잔여 한계: 본문 앞 '목차(TOC)'가 절 제목을 단독 라인으로 순서대로 나열하면 커서가 TOC 를
//   먼저 잡을 수 있다(연쇄 시프트). 4-1 의 '정확도' 측정으로 검출 — 빈번하면 경로 B(builder offset).

import { cleanHeading, type DocumentSection } from './headingPath.ts';

const TRUNCATE_HINT = 180; // builder.py 가 KO/ENG 제목을 [:200] 으로 자름 → 거의 그 길이면 truncated 로 간주

function norm(s: string | null | undefined): string {
  return cleanHeading(s).toLowerCase();
}

/** 한 라인을 heading 후보 텍스트로: 선두 ATX #(1~6) / 리스트마커(-*+) / blockquote(>) 제거 후 정규화. */
function normLine(raw: string): string {
  const stripped = raw.replace(/^\s{0,3}(?:#{1,6}\s+|[-*+]\s+|>\s+)?/, '');
  return cleanHeading(stripped).toLowerCase();
}

export interface AnchorMapResult {
  /** chunk_id → md_content 내 heading 라인 시작 char offset. (없으면 점프 비활성) */
  anchors: Record<number, number>;
  /** 후보(비-window·제목有) 절 수 — 4-1 커버리지 분모. */
  total: number;
  /** 신뢰 anchor 수 — 4-1 커버리지 분자. (정확도는 별도 수작업 검증) */
  matched: number;
}

/**
 * sections 는 서버 chunk_index 순(문서 순서)으로 가정한다(GET /documents/{id}/sections ORDER BY).
 */
export function buildAnchorMap(
  mdContent: string | null | undefined,
  sections: DocumentSection[] | null | undefined,
): AnchorMapResult {
  const anchors: Record<number, number> = {};
  if (!mdContent || !sections || sections.length === 0) {
    return { anchors, total: 0, matched: 0 };
  }

  // 라인별 (offset, 정규화 텍스트, 펜스 여부) 사전계산.
  const rawLines = mdContent.split('\n');
  const lines: { off: number; norm: string }[] = [];
  let off = 0;
  let inFence = false;
  for (const raw of rawLines) {
    const fenceToggle = /^\s{0,3}(```|~~~)/.test(raw);
    const fencedHere = inFence || fenceToggle; // 펜스 경계 라인도 매칭 제외
    lines.push({ off, norm: fencedHere ? '' : normLine(raw) });
    if (fenceToggle) inFence = !inFence;
    off += raw.length + 1; // '\n'
  }

  let cursor = 0; // 단조 전진 라인 인덱스
  let total = 0;
  let matched = 0;

  for (const s of sections) {
    // window 조각 + %_split parent(chapter_split/clause_split/section_split)는 string-match 대상 아님 →
    // 건너뜀. (split-parent jump 은 Path B 의 BE char_start 로만 성립; Path A 폴백선 windowed 절 무점프=무회귀.)
    if (s.node_type === 'window' || s.node_type?.endsWith('_split')) continue;
    let nt = norm(s.section_title);
    if (!nt && s.heading_path) {
      const last = s.heading_path.split('>').pop();
      nt = norm(last);
    }
    if (!nt) continue;
    total++;

    const truncated = nt.length >= TRUNCATE_HINT;
    let foundIdx = -1;
    for (let i = cursor; i < lines.length; i++) {
      const ln = lines[i].norm;
      if (!ln) continue; // 빈 라인 / 펜스 내부
      if (ln === nt || (truncated && ln.startsWith(nt))) {
        foundIdx = i;
        break;
      }
    }
    if (foundIdx >= 0) {
      anchors[s.chunk_id] = lines[foundIdx].off;
      cursor = foundIdx + 1; // 단조: 다음 절은 이 라인 이후만
      matched++;
    }
    // 미스 → anchor 없음(점프 비활성, 폴백)
  }

  return { anchors, total, matched };
}
