import path from 'node:path';
import { readFile as readTextFile } from 'node:fs/promises';
import { buildCodebaseIndex } from './build.js';
import { abortIfNeeded, countWords } from '../utils.js';
const DEFAULT_MAX_CHUNK_WORDS = 500;
const DEFAULT_OVERLAP_WORDS = 50;
const DEFAULT_MAX_CHUNK_TOKENS = 1_200;
const DEFAULT_OVERLAP_TOKENS = 120;
function estimateTokens(text) {
    if (!text.trim())
        return 0;
    return Math.max(1, Math.ceil(text.length / 4));
}
function resolveSizer(options) {
    if (options?.maxTokens != null) {
        return {
            maxUnits: options.maxTokens,
            overlapUnits: options.overlapTokens ?? DEFAULT_OVERLAP_TOKENS,
            count: options.countTokens ?? estimateTokens,
        };
    }
    return {
        maxUnits: options?.maxChunkWords ?? DEFAULT_MAX_CHUNK_WORDS,
        overlapUnits: options?.overlapWords ?? DEFAULT_OVERLAP_WORDS,
        count: countWords,
    };
}
function chunkFile(file, sizer) {
    const chunks = [];
    const lines = file.lines;
    if (!lines) {
        throw new Error(`chunkFromIndex requires full file lines for ${file.relativePath}.`);
    }
    const sortedSymbols = [...file.symbols].sort((a, b) => a.line - b.line);
    if (sortedSymbols.length === 0) {
        return chunkByUnits(file, lines, sizer);
    }
    // Build symbol boundaries: each symbol starts a new potential chunk
    const boundaries = [];
    for (let i = 0; i < sortedSymbols.length; i++) {
        const sym = sortedSymbols[i];
        const startLine = sym.line - 1; // 0-indexed
        const endLine = i + 1 < sortedSymbols.length
            ? sortedSymbols[i + 1].line - 2 // up to line before next symbol
            : lines.length - 1;
        boundaries.push({ startLine, endLine, symbols: [sym] });
    }
    // Add file header (before first symbol) if present
    if (boundaries.length > 0 && boundaries[0].startLine > 0) {
        boundaries.unshift({ startLine: 0, endLine: boundaries[0].startLine - 1, symbols: [] });
    }
    // Merge small adjacent sections into chunks that respect the selected budget.
    let currentStart = boundaries[0]?.startLine ?? 0;
    let currentEnd = boundaries[0]?.endLine ?? 0;
    let currentSymbols = [...(boundaries[0]?.symbols ?? [])];
    let currentUnits = sizer.count(lines.slice(currentStart, currentEnd + 1).join('\n'));
    for (let i = 1; i < boundaries.length; i++) {
        const section = boundaries[i];
        const sectionText = lines.slice(section.startLine, section.endLine + 1).join('\n');
        const sectionUnits = sizer.count(sectionText);
        if (currentUnits + sectionUnits <= sizer.maxUnits) {
            currentEnd = section.endLine;
            currentSymbols.push(...section.symbols);
            currentUnits += sectionUnits;
        }
        else {
            emitChunk(chunks, file, lines, currentStart, currentEnd, currentSymbols);
            const overlapStart = findOverlapStart(lines, currentEnd, sizer);
            currentStart = Math.min(overlapStart, section.startLine);
            currentEnd = section.endLine;
            currentSymbols = [...section.symbols];
            currentUnits = sizer.count(lines.slice(currentStart, currentEnd + 1).join('\n'));
        }
    }
    emitChunk(chunks, file, lines, currentStart, currentEnd, currentSymbols);
    return chunks;
}
function chunkByUnits(file, lines, sizer) {
    const chunks = [];
    let currentStart = 0;
    let currentUnits = 0;
    for (let i = 0; i < lines.length; i++) {
        currentUnits += sizer.count(lines[i]);
        if (currentUnits >= sizer.maxUnits || i === lines.length - 1) {
            emitChunk(chunks, file, lines, currentStart, i, []);
            const overlapStart = findOverlapStart(lines, i, sizer);
            currentStart = overlapStart;
            currentUnits = sizer.count(lines.slice(currentStart, i + 1).join('\n'));
        }
    }
    return chunks;
}
async function chunkFileCooperative(file, sizer, yielder) {
    const chunks = [];
    const lines = file.lines;
    if (!lines) {
        throw new Error(`chunkFromIndex requires full file lines for ${file.relativePath}.`);
    }
    const sortedSymbols = [...file.symbols].sort((a, b) => a.line - b.line);
    if (sortedSymbols.length === 0) {
        return chunkByUnitsCooperative(file, lines, sizer, yielder);
    }
    const boundaries = [];
    for (let i = 0; i < sortedSymbols.length; i++) {
        const sym = sortedSymbols[i];
        const startLine = sym.line - 1;
        const endLine = i + 1 < sortedSymbols.length ? sortedSymbols[i + 1].line - 2 : lines.length - 1;
        boundaries.push({ startLine, endLine, symbols: [sym] });
        await yielder.tick();
    }
    if (boundaries.length > 0 && boundaries[0].startLine > 0) {
        boundaries.unshift({ startLine: 0, endLine: boundaries[0].startLine - 1, symbols: [] });
    }
    let currentStart = boundaries[0]?.startLine ?? 0;
    let currentEnd = boundaries[0]?.endLine ?? 0;
    let currentSymbols = [...(boundaries[0]?.symbols ?? [])];
    let currentUnits = sizer.count(lines.slice(currentStart, currentEnd + 1).join('\n'));
    for (let i = 1; i < boundaries.length; i++) {
        const section = boundaries[i];
        const sectionText = lines.slice(section.startLine, section.endLine + 1).join('\n');
        const sectionUnits = sizer.count(sectionText);
        if (currentUnits + sectionUnits <= sizer.maxUnits) {
            currentEnd = section.endLine;
            currentSymbols.push(...section.symbols);
            currentUnits += sectionUnits;
        }
        else {
            emitChunk(chunks, file, lines, currentStart, currentEnd, currentSymbols);
            const overlapStart = findOverlapStart(lines, currentEnd, sizer);
            currentStart = Math.min(overlapStart, section.startLine);
            currentEnd = section.endLine;
            currentSymbols = [...section.symbols];
            currentUnits = sizer.count(lines.slice(currentStart, currentEnd + 1).join('\n'));
        }
        await yielder.tick();
    }
    emitChunk(chunks, file, lines, currentStart, currentEnd, currentSymbols);
    return chunks;
}
async function chunkByUnitsCooperative(file, lines, sizer, yielder) {
    const chunks = [];
    let currentStart = 0;
    let currentUnits = 0;
    for (let i = 0; i < lines.length; i++) {
        currentUnits += sizer.count(lines[i]);
        if (currentUnits >= sizer.maxUnits || i === lines.length - 1) {
            emitChunk(chunks, file, lines, currentStart, i, []);
            const overlapStart = findOverlapStart(lines, i, sizer);
            currentStart = overlapStart;
            currentUnits = sizer.count(lines.slice(currentStart, i + 1).join('\n'));
        }
        await yielder.tick();
    }
    return chunks;
}
function findOverlapStart(lines, endLine, sizer) {
    if (sizer.overlapUnits <= 0)
        return endLine + 1;
    let units = 0;
    for (let i = endLine; i >= 0; i--) {
        units += sizer.count(lines[i]);
        if (units >= sizer.overlapUnits)
            return i;
    }
    return 0;
}
function emitChunk(chunks, file, lines, startLine, endLine, symbols) {
    const content = lines.slice(startLine, endLine + 1).join('\n');
    if (!content.trim())
        return;
    const primarySymbol = symbols.find(s => s.exported) ?? symbols[0];
    const title = primarySymbol
        ? primarySymbol.containerName
            ? `${file.relativePath}:${primarySymbol.containerName}.${primarySymbol.name}`
            : `${file.relativePath}:${primarySymbol.name}`
        : `${file.relativePath}:${startLine + 1}-${endLine + 1}`;
    chunks.push({
        filePath: file.path,
        relativePath: file.relativePath,
        language: file.language,
        chunkIndex: chunks.length,
        title,
        content,
        symbolNames: symbols.map(s => s.name),
        symbolTypes: symbols.map(s => s.type),
        startLine: startLine + 1,
        endLine: endLine + 1,
        hasExports: symbols.some(s => s.exported),
    });
}
/** Normalize a caller-supplied path to `ParsedFile.relativePath` form. */
function normalizeScopedPath(repoRoot, entry) {
    const relative = path.isAbsolute(entry) ? path.relative(repoRoot, entry) : entry;
    return relative.replace(/\\/g, '/').replace(/^(\.\/)+/, '');
}
/** Resolve the `files` scope to the matching index files (undefined → all). */
function scopeFiles(index, files) {
    if (files === undefined)
        return index.files;
    const wanted = new Set(files.map(entry => normalizeScopedPath(index.repoRoot, entry)));
    return index.files.filter(file => wanted.has(file.relativePath));
}
async function hydrateChunkFile(file, options) {
    if (file.lines)
        return file;
    abortIfNeeded(options.signal);
    const content = await (options.readFile ?? ((filePath) => readTextFile(filePath, 'utf8')))(file.path);
    abortIfNeeded(options.signal);
    return {
        ...file,
        content,
        lines: content.split(/\r?\n/),
    };
}
/** Chunk files from an already-built CodebaseIndex (avoids double-parsing) */
export function chunkFromIndex(index, options) {
    if (index.contentMode !== 'full') {
        throw new Error(`chunkFromIndex requires contentMode "full". Current contentMode is "${index.contentMode}". Open a chunk-capable store with openChunkStore(...) or build with contentMode "full".`);
    }
    const sizer = resolveSizer(options);
    const chunks = [];
    for (const file of scopeFiles(index, options?.files)) {
        chunks.push(...chunkFile(file, sizer));
    }
    return chunks;
}
export async function chunkFromIndexCooperative(index, options, yielder) {
    if (index.contentMode !== 'full') {
        throw new Error(`chunkFromIndex requires contentMode "full". Current contentMode is "${index.contentMode}". Open a chunk-capable store with openChunkStore(...) or build with contentMode "full".`);
    }
    const sizer = resolveSizer(options);
    const chunks = [];
    for (const file of scopeFiles(index, options?.files)) {
        chunks.push(...(await chunkFileCooperative(file, sizer, yielder)));
        await yielder.tick();
    }
    return chunks;
}
export async function chunkFromIndexStreaming(index, options, streaming = {}) {
    const sizer = resolveSizer(options);
    const chunks = [];
    for (const file of scopeFiles(index, options?.files)) {
        abortIfNeeded(streaming.signal);
        const hydrated = await hydrateChunkFile(file, streaming);
        const fileChunks = streaming.yielder
            ? await chunkFileCooperative(hydrated, sizer, streaming.yielder)
            : chunkFile(hydrated, sizer);
        chunks.push(...fileChunks);
        await streaming.yielder?.tick();
    }
    return chunks;
}
/** Discover, parse, and chunk an entire codebase */
export async function chunkCodebase(options, onProgress) {
    const index = await buildCodebaseIndex({
        ...options,
        contentMode: options.contentMode ?? 'full',
    }, onProgress);
    const chunks = chunkFromIndex(index, options);
    onProgress?.({ phase: 'chunk', total: chunks.length, completed: chunks.length, detail: 'Chunking complete' });
    return chunks;
}
//# sourceMappingURL=chunk.js.map