import path from 'node:path';
import { compareCodeUnits } from '../utils.js';
import { normalizePath } from './languages.js';
import { toNormalizedSymbolKind, } from './types.js';
const WORD_BOUNDARY = /[^A-Za-z0-9]/;
function isBoundaryStart(text, index) {
    if (index === 0)
        return true;
    const previous = text[index - 1];
    if (WORD_BOUNDARY.test(previous))
        return true;
    // camelCase / PascalCase boundary
    const current = text[index];
    return current >= 'A' && current <= 'Z' && previous >= 'a' && previous <= 'z';
}
/**
 * Subsequence match of `queryLower` inside `text`, scored for symbol search:
 * boundary-aligned and consecutive matches score up; long spans score down.
 * Greedy forward pass anchored at each viable start, keeping the best window.
 */
export function fuzzyMatch(text, textLower, queryLower) {
    if (queryLower.length === 0 || queryLower.length > text.length)
        return undefined;
    let best;
    const firstChar = queryLower[0];
    for (let start = textLower.indexOf(firstChar); start !== -1; start = textLower.indexOf(firstChar, start + 1)) {
        const positions = [start];
        let queryIndex = 1;
        let textIndex = start + 1;
        while (queryIndex < queryLower.length && textIndex < text.length) {
            if (textLower[textIndex] === queryLower[queryIndex]) {
                positions.push(textIndex);
                queryIndex++;
            }
            textIndex++;
        }
        if (queryIndex < queryLower.length)
            break; // later starts only shrink the tail
        const span = positions[positions.length - 1] - positions[0] + 1;
        let score = 100;
        for (let i = 0; i < positions.length; i++) {
            if (isBoundaryStart(text, positions[i]))
                score += 8;
            if (i > 0 && positions[i] === positions[i - 1] + 1)
                score += 6;
        }
        score -= (span - queryLower.length) * 3;
        if (positions[0] === 0)
            score += 12;
        if (span === queryLower.length && text.length === queryLower.length)
            score += 20; // exact
        if (best === undefined || score > best.score)
            best = { score, positions, span };
    }
    return best;
}
const corpusCache = new WeakMap();
function buildCorpus(index) {
    const entries = [];
    for (const file of index.files) {
        const relativePath = normalizePath(file.relativePath !== '' ? file.relativePath : path.relative(index.repoRoot, file.path));
        const pathLower = relativePath.toLowerCase();
        for (const symbol of file.symbols) {
            const qualified = symbol.containerName ? `${symbol.containerName}.${symbol.name}` : symbol.name;
            entries.push({
                file,
                symbol,
                relativePath,
                name: symbol.name,
                nameLower: symbol.name.toLowerCase(),
                qualified,
                qualifiedLower: qualified.toLowerCase(),
                pathLower,
                kind: toNormalizedSymbolKind(symbol.type, symbol.containerName),
            });
        }
    }
    return entries;
}
function corpusFor(index) {
    const cached = corpusCache.get(index);
    if (cached)
        return cached;
    const corpus = buildCorpus(index);
    corpusCache.set(index, corpus);
    return corpus;
}
/** Field weights: a name hit outranks a container hit outranks a path hit. */
const FIELD_WEIGHT = { name: 1, container: 0.75, path: 0.5 };
/**
 * Fuzzy-search symbols across the index by name, container-qualified name,
 * and file path. Results carry match positions for highlighting. The search
 * corpus is memoized per index object, so repeated queries against the same
 * index (a session, a long-lived host) pay the corpus build once.
 */
export function searchSymbols(index, query, options = {}) {
    const trimmed = query.trim();
    if (trimmed === '')
        return [];
    const queryLower = trimmed.toLowerCase();
    const limit = Math.max(1, Math.floor(options.limit ?? 24));
    const kinds = options.kinds ? new Set(options.kinds) : undefined;
    const pathFilter = options.pathFilter?.toLowerCase();
    const results = [];
    for (const entry of corpusFor(index)) {
        if (kinds && !kinds.has(entry.kind))
            continue;
        if (pathFilter && !entry.pathLower.includes(pathFilter))
            continue;
        let field = 'name';
        let matchedText = entry.name;
        let match = fuzzyMatch(entry.name, entry.nameLower, queryLower);
        if (!match && entry.qualified !== entry.name) {
            field = 'container';
            matchedText = entry.qualified;
            match = fuzzyMatch(entry.qualified, entry.qualifiedLower, queryLower);
        }
        if (!match) {
            field = 'path';
            matchedText = `${entry.relativePath}:${entry.name}`;
            match = fuzzyMatch(matchedText, matchedText.toLowerCase(), queryLower);
        }
        if (!match)
            continue;
        const line = entry.symbol.line;
        const nodeId = entry.symbol.nodeId;
        results.push({
            ...(nodeId !== undefined ? { nodeId } : {}),
            name: entry.symbol.name,
            kind: entry.kind,
            type: entry.symbol.type,
            filePath: entry.relativePath,
            line,
            endLine: entry.symbol.endLine ?? line,
            ...(entry.symbol.containerName !== undefined ? { containerName: entry.symbol.containerName } : {}),
            ...(entry.file.language !== '' ? { language: entry.file.language } : {}),
            ...(entry.symbol.exported !== undefined ? { exported: entry.symbol.exported } : {}),
            score: match.score * FIELD_WEIGHT[field],
            positions: match.positions,
            span: match.span,
            field,
            matchedText,
        });
    }
    return results
        .sort((a, b) => {
        if (a.score !== b.score)
            return b.score - a.score;
        if (a.span !== b.span)
            return a.span - b.span;
        return compareCodeUnits(a.filePath, b.filePath) || a.line - b.line;
    })
        .slice(0, limit);
}
//# sourceMappingURL=search.js.map