// 개요(절 목차) → 본문 점프 anchor 산출 공유 헬퍼 (경로 B: BE char_start primary + string-match 폴백).
//
// render-site 가 md_content 를 splice 할 때(trustBE=true)는 BE 가 builder 단계에서 박은 char_start 를
// 1순위로 쓰고, 비-md basis(3-pane extracted_text 등, trustBE=false)는 무조건 string-match(buildAnchorMap)로
// 폴백한다. char_start 가 비어 있으면(non-PASS doc, 또는 multi-night 재처리 중 아직 미백필 PASS doc) BE-only
// 가 아니라 string-match 로 graceful degrade 한다(B4: BE-first, NOT BE-only).
//
// ★ NEW-5 (must-not-miss): 폴백 트리거는 JUMP-TARGET-CANDIDATE 한정이다.
//   window-child(node_type='window')와 preamble(title 없음)은 char_start=NULL **BY DESIGN**(g2).
//   트리거가 'NULL char_start 가 하나라도 있으면 whole-doc 폴백' 이면, window-child 를 항상 보유한 windowed
//   doc 은 매번 폴백 → split-parent char_start(windowed 절의 단일 jump target)를 영영 안 쓰고 →
//   buildAnchorMap 은 split-parent 를 skip → windowed 코어 절이 영원히 점프 안 됨 = 이 플랜이 겨냥한
//   바로 그 절에서 Path A 0% 회귀. 따라서 트리거 분모 = jump-target-candidate 뿐.

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

export interface ResolveResult {
  /** chunk_id → splicedText 내 char offset (UTF-16). */
  anchors: Record<number, number>;
  /** jump-target candidate 수(BE 경로) 또는 buildAnchorMap.total(폴백). */
  total: number;
  /** 실제 anchor 부여 수. */
  matched: number;
  /** string-match(buildAnchorMap) 로 폴백했는지 — V-rail/검증용. */
  fellBack: boolean;
}

/** 표시 가능한 제목(또는 heading_path 말단)이 있는가. */
function hasTitle(s: DocumentSection): boolean {
  if (cleanHeading(s.section_title)) return true;
  const last = (s.heading_path || '').split('>').pop() || '';
  return !!cleanHeading(last);
}

/**
 * jump-target candidate = char_start 를 받아야 하는 절.
 *   = (비-window leaf) OR (%_split parent), 그리고 제목 보유.
 * window-child(node_type='window')·preamble(제목 없음)은 설계상 char_start NULL → candidate 아님(NEW-5).
 */
export function isJumpTargetCandidate(s: DocumentSection): boolean {
  const structural = (s.is_leaf && s.node_type !== 'window') || !!s.node_type?.endsWith('_split');
  return structural && hasTitle(s);
}

export function resolveAnchorMap(
  splicedText: string | null | undefined,
  sections: DocumentSection[] | null | undefined,
  opts: { trustBE: boolean },
): ResolveResult {
  const secs = sections ?? [];

  // basis 불일치(extracted_text 3-pane 등) → 무조건 string-match.
  if (!opts.trustBE) {
    const r = buildAnchorMap(splicedText, secs);
    return { ...r, fellBack: true };
  }

  // [B4 + NEW-5] BE-first: jump-target candidate 가 비었거나, candidate 중 char_start NULL 이 있으면 폴백.
  //   window-child/preamble NULL 은 candidate 가 아니라 트리거에 안 들어간다.
  const candidates = secs.filter(isJumpTargetCandidate);
  const beUnusable = candidates.length === 0 || candidates.some((s) => s.char_start == null);
  if (beUnusable) {
    const r = buildAnchorMap(splicedText, secs);
    return { ...r, fellBack: true };
  }

  // BE char_start 채택 (C1: window/null/no-title 제외 = candidate 집합과 동일).
  const anchors: Record<number, number> = {};
  const limit = (splicedText ?? '').length;
  let matched = 0;
  for (const s of candidates) {
    const cs = s.char_start as number;
    // char_start<=splicedText.length 가드(MarkdownDoc.svelte:58). 초과 = FE serve-truncate tail →
    // 그 anchor 만 비활성(폴백 안 함 — string-match 도 truncated tail 은 못 찾음).
    if (Number.isFinite(cs) && cs >= 0 && cs <= limit) {
      anchors[s.chunk_id] = cs;
      matched++;
    }
  }
  return { anchors, total: candidates.length, matched, fellBack: false };
}
