/**
 * ============================================================
 * Markdown Section Chunker (Phase 1-가)
 *
 * 긴 노트를 `#`~`######` 헤딩 경계로 *섹션 청크* 로 나눈다. 파일 단위 색인은 5000자
 * 다주제 문서를 하나의 흐릿한 단위로 만들어 검색 정밀도를 떨어뜨린다 — 섹션 단위로
 * 쪼개면 질의가 정확히 해당 섹션에 매치된다 (제2뇌의 "문서 청킹 전략" 지식 그대로).
 *
 * 규칙:
 *   - 각 헤딩 ~ 다음 헤딩 직전까지가 raw 섹션. 첫 헤딩 이전 본문(preamble)도 한 섹션.
 *   - 헤딩 breadcrumb(상위 헤딩 경로)을 함께 보존 → 청크가 문맥을 잃지 않음.
 *   - minChars 미만의 짧은 섹션은 다음 섹션과 병합(헤딩만 있고 본문 적은 경우 흔함).
 *   - targetChars 초과 누적 시 청크 확정. maxChars 초과 단일 섹션은 문단 경계로 재분할.
 *
 * 순수 함수 (fs/네트워크 의존 없음) — 단위 테스트·재현 용이.
 * ============================================================
 */

export interface Section {
    /** 이 섹션의 헤딩 텍스트 ('' = preamble). */
    heading: string;
    /** 루트→자기까지 헤딩 경로 (문맥용 breadcrumb). preamble 이면 []. */
    headingPath: string[];
    /** 섹션 본문(헤딩 라인 포함, 원문 그대로). */
    text: string;
    /** 원문 내 시작/끝 문자 오프셋 (디버그/추적용). */
    charStart: number;
    charEnd: number;
}

export interface ChunkOptions {
    /** 청크 목표 길이. 누적이 이 값을 넘으면 확정. 기본 1200. */
    targetChars: number;
    /** 이보다 짧은 섹션은 다음과 병합. 기본 200. */
    minChars: number;
    /** 단일 청크가 이보다 길면 문단 경계로 재분할. 기본 = targetChars * 2. */
    maxChars: number;
}

const DEFAULTS: ChunkOptions = { targetChars: 1200, minChars: 200, maxChars: 2400 };

interface RawSection {
    heading: string;
    headingPath: string[];
    start: number;
    end: number;
}

const HEADING_RE = /^(#{1,6})[ \t]+(.+?)[ \t]*#*$/;

/**
 * 원문을 헤딩 경계 raw 섹션으로 분해. fenced code block(```) 안의 `#` 라인은
 * 헤딩으로 보지 않는다 (코드 주석이 섹션을 깨는 것 방지).
 */
function parseRawSections(content: string): RawSection[] {
    const lines = content.split('\n');
    const sections: RawSection[] = [];
    const stack: Array<{ level: number; title: string }> = [];

    let offset = 0;
    let curStart = 0;
    let curHeading = '';
    let curPath: string[] = [];
    let inFence = false;
    let started = false;

    const pushCurrent = (end: number) => {
        if (!started) return;
        sections.push({ heading: curHeading, headingPath: [...curPath], start: curStart, end });
    };

    for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        const lineStart = offset;
        offset += line.length + 1; // +1 for the '\n' we split on

        const fence = line.trimStart().startsWith('```');
        if (fence) { inFence = !inFence; }

        const m = !inFence ? line.match(HEADING_RE) : null;
        if (m) {
            // close previous section at this heading's start
            pushCurrent(lineStart);
            const level = m[1].length;
            const title = m[2].trim();
            // maintain breadcrumb stack by level
            while (stack.length && stack[stack.length - 1].level >= level) stack.pop();
            stack.push({ level, title });
            curStart = lineStart;
            curHeading = title;
            curPath = stack.map((s) => s.title);
            started = true;
        } else if (!started) {
            // preamble before the first heading
            started = true;
            curStart = 0;
            curHeading = '';
            curPath = [];
        }
    }
    pushCurrent(content.length);
    return sections.filter((s) => s.end > s.start);
}

/** 긴 텍스트를 문단(\n\n) 경계로 target 길이 이하 조각으로. 단일 문단이 maxChars 초과면 하드 컷. */
function splitLongText(text: string, target: number, maxChars: number): string[] {
    if (text.length <= maxChars) return [text];
    const paras = text.split(/\n{2,}/);
    const pieces: string[] = [];
    let buf = '';
    const flush = () => { if (buf.trim()) pieces.push(buf); buf = ''; };
    for (const para of paras) {
        if (para.length > maxChars) {
            flush();
            // hard slice a giant paragraph
            for (let i = 0; i < para.length; i += target) pieces.push(para.slice(i, i + target));
            continue;
        }
        if (buf && (buf.length + para.length + 2) > target) flush();
        buf = buf ? `${buf}\n\n${para}` : para;
    }
    flush();
    return pieces.length ? pieces : [text];
}

/**
 * 원문을 섹션 청크로 분해. 짧은 섹션 병합 + 긴 섹션 재분할 적용.
 * 결과가 비면(빈 파일 등) 전체를 한 청크로 반환.
 */
export function splitIntoSections(content: string, opts?: Partial<ChunkOptions>): Section[] {
    const o: ChunkOptions = { ...DEFAULTS, ...(opts || {}) };
    if (o.maxChars < o.targetChars) o.maxChars = o.targetChars * 2;

    const raw = parseRawSections(content);
    if (raw.length === 0) {
        const t = content.trim();
        return t ? [{ heading: '', headingPath: [], text: content, charStart: 0, charEnd: content.length }] : [];
    }

    // 1) 짧은 섹션 병합 — 연속이므로 [firstStart, lastEnd] 로 span 유지.
    const merged: RawSection[] = [];
    let buf: RawSection | null = null;
    for (const s of raw) {
        if (!buf) { buf = { ...s }; continue; }
        const bufLen = buf.end - buf.start;
        if (bufLen < o.minChars) {
            buf = { heading: buf.heading, headingPath: buf.headingPath, start: buf.start, end: s.end };
        } else {
            merged.push(buf);
            buf = { ...s };
        }
        if ((buf.end - buf.start) >= o.targetChars) { merged.push(buf); buf = null; }
    }
    if (buf) merged.push(buf);

    // 2) 긴 섹션 재분할 + Section 객체화.
    const out: Section[] = [];
    for (const s of merged) {
        const text = content.slice(s.start, s.end);
        if (text.length <= o.maxChars) {
            out.push({ heading: s.heading, headingPath: s.headingPath, text, charStart: s.start, charEnd: s.end });
            continue;
        }
        let cursor = s.start;
        for (const piece of splitLongText(text, o.targetChars, o.maxChars)) {
            const idx = content.indexOf(piece, cursor);
            const start = idx >= 0 ? idx : cursor;
            const end = start + piece.length;
            out.push({ heading: s.heading, headingPath: s.headingPath, text: piece, charStart: start, charEnd: end });
            cursor = end;
        }
    }
    return out.filter((s) => s.text.trim().length > 0);
}
