import { isTestFile } from "../utils/test-file.js";
import { CHARS_PER_TOKEN, RRF_K, ADJACENCY_GAP, LINE_NUMBER_PAD, QUERY_DECOMPOSE_THRESHOLD, SPLIT_WINDOW_LO, SPLIT_WINDOW_HI, } from "./retrieval-constants.js";
/**
 * Estimate token count from a string. ~4 chars per token.
 */
export function estimateTokens(text) {
    return Math.ceil(text.length / CHARS_PER_TOKEN);
}
/**
 * Filter embedding entries by file path substring and/or test file exclusion.
 * Uses a lookup map to resolve the file path for each embedding ID.
 */
export function filterEmbeddingsByFile(embeddings, fileLookup, fileFilter, excludeTests) {
    if (!fileFilter && !excludeTests)
        return embeddings;
    return new Map([...embeddings.entries()].filter(([id]) => {
        const file = fileLookup.get(id);
        if (!file)
            return false;
        if (fileFilter && !file.includes(fileFilter))
            return false;
        if (excludeTests && isTestFile(file))
            return false;
        return true;
    }));
}
/**
 * Compute RRF scores from multiple embedding query vectors against filtered embeddings.
 * Each vector produces a ranked list; scores are accumulated via RRF formula.
 */
export function computeRRFScores(vecs, filteredEmbeddings, cosSim) {
    const rrfScores = new Map();
    for (const vec of vecs) {
        if (!vec)
            continue;
        const qEmbed = new Float32Array(vec);
        const subScores = [];
        for (const [id, chunkVec] of filteredEmbeddings) {
            if (chunkVec.length === qEmbed.length) {
                subScores.push({ id, score: cosSim(qEmbed, chunkVec) });
            }
        }
        subScores.sort((a, b) => b.score - a.score);
        subScores.forEach((s, rank) => {
            rrfScores.set(s.id, (rrfScores.get(s.id) ?? 0) + 1 / (RRF_K + rank + 1));
        });
    }
    return rrfScores;
}
/**
 * Group top chunk IDs by file, merge overlapping/adjacent chunks, and format
 * as numbered plain text sections.
 */
export function formatChunksAsText(topIds, chunks, excludeTests) {
    const byFile = new Map();
    for (const id of topIds) {
        const chunk = chunks.get(id);
        if (!chunk)
            continue;
        if (excludeTests && isTestFile(chunk.file))
            continue;
        const existing = byFile.get(chunk.file) ?? [];
        existing.push({ startLine: chunk.startLine, endLine: chunk.endLine, text: chunk.text });
        byFile.set(chunk.file, existing);
    }
    const sections = ["The following code sections were retrieved:"];
    for (const [file, fileChunks] of byFile) {
        fileChunks.sort((a, b) => a.startLine - b.startLine);
        const merged = [];
        for (const chunk of fileChunks) {
            const last = merged[merged.length - 1];
            if (last && chunk.startLine <= last.endLine + ADJACENCY_GAP) {
                if (chunk.endLine > last.endLine) {
                    const overlapLines = last.endLine - chunk.startLine + 1;
                    const newLines = chunk.text.split("\n").slice(overlapLines);
                    last.text = last.text + "\n" + newLines.join("\n");
                    last.endLine = chunk.endLine;
                }
            }
            else {
                merged.push({ startLine: chunk.startLine, endLine: chunk.endLine, text: chunk.text });
            }
        }
        sections.push(`Path: ${file}`);
        for (const chunk of merged) {
            const lines = chunk.text.split("\n");
            const numbered = lines.map((line, i) => {
                const lineNo = String(chunk.startLine + i).padStart(LINE_NUMBER_PAD, " ");
                return `${lineNo}\t${line}`;
            }).join("\n");
            sections.push(numbered);
        }
        sections.push("...");
    }
    return sections.join("\n");
}
/**
 * Split a long query into sub-queries at natural connectors for RRF merging.
 */
export function decomposeQuery(query) {
    const words = query.split(/\s+/).filter(Boolean);
    if (words.length <= QUERY_DECOMPOSE_THRESHOLD)
        return [query];
    const splitWords = new Set(["and", "or", "from", "to", "with", "using", "for", "via", "then"]);
    const lo = Math.floor(words.length * SPLIT_WINDOW_LO);
    const hi = Math.floor(words.length * SPLIT_WINDOW_HI);
    for (let i = lo; i <= hi; i++) {
        if (splitWords.has((words[i] ?? "").toLowerCase())) {
            const a = words.slice(0, i).join(" ");
            const b = words.slice(i + 1).join(" ");
            if (a.trim() && b.trim())
                return [a, b];
        }
    }
    const mid = Math.floor(words.length / 2);
    return [words.slice(0, mid).join(" "), words.slice(mid).join(" ")];
}
/**
 * Truncate symbol source to a character limit, preserving the rest of the symbol object.
 */
export function truncateSymbolSource(sym, limit) {
    if (limit > 0 && sym.source && sym.source.length > limit) {
        return { ...sym, source: sym.source.slice(0, limit) };
    }
    return sym;
}
/**
 * Race a promise against a timeout. Rejects with a descriptive error on timeout.
 */
export function withTimeout(promise, ms, label) {
    let timer;
    return Promise.race([
        promise.finally(() => clearTimeout(timer)),
        new Promise((_resolve, reject) => {
            timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
        }),
    ]);
}
//# sourceMappingURL=retrieval-utils.js.map