import { lstat, readdir, readFile } from 'node:fs/promises';
import path from 'node:path';
import { BINARY_FILE_EXTENSIONS } from '../constants.js';
import { loadGitignore } from '../utils/gitignore-loader.js';
const MAX_CONTEXT_CONTENT_LENGTH = 1500;
const MAX_MATCH_CONTENT_LENGTH = 300;
const DEFAULT_SEARCH_TIMEOUT_MS = 30_000;
export class SearchTimeoutError extends Error {
    constructor(timeoutMs) {
        super(`Search timed out after ${Math.round(timeoutMs / 1000)} seconds. Try a more specific query or narrower path.`);
        this.name = 'SearchTimeoutError';
    }
}
function normalizePathForMatch(filePath) {
    return filePath.replace(/\\/g, '/');
}
function escapeRegexChar(char) {
    return /[|\\{}()[\]^$+?.]/.test(char) ? `\\${char}` : char;
}
function expandBraces(pattern) {
    const match = pattern.match(/\{([^{}]+)\}/);
    if (!match || match.index === undefined) {
        return [pattern];
    }
    const before = pattern.slice(0, match.index);
    const after = pattern.slice(match.index + match[0].length);
    return match[1]
        .split(',')
        .flatMap(part => expandBraces(`${before}${part.trim()}${after}`));
}
function globToRegExpSource(pattern) {
    let source = '';
    for (let index = 0; index < pattern.length; index++) {
        const current = pattern[index];
        const next = pattern[index + 1];
        if (current === '*') {
            if (next === '*') {
                const afterNext = pattern[index + 2];
                if (afterNext === '/') {
                    source += '(?:.*/)?';
                    index += 2;
                }
                else {
                    source += '.*';
                    index += 1;
                }
            }
            else {
                source += '[^/]*';
            }
            continue;
        }
        if (current === '?') {
            source += '[^/]';
            continue;
        }
        if (current === '/') {
            source += '/';
            continue;
        }
        source += escapeRegexChar(current);
    }
    return source;
}
function buildGlobRegexes(pattern) {
    const normalizedPattern = normalizePathForMatch(pattern);
    return expandBraces(normalizedPattern).map(expanded => 
    // nosemgrep: detect-non-literal-regexp
    new RegExp(`^${globToRegExpSource(expanded)}$`));
}
export function matchesGlob(filePath, pattern, matchBasename = false) {
    const normalizedPath = normalizePathForMatch(filePath);
    const target = matchBasename
        ? path.posix.basename(normalizedPath)
        : normalizedPath;
    return buildGlobRegexes(pattern).some(regex => regex.test(target));
}
function isIgnoredByBinaryHeuristics(filePath, content) {
    const ext = path.extname(filePath).toLowerCase();
    if (BINARY_FILE_EXTENSIONS.has(ext)) {
        return true;
    }
    return content.includes('\0');
}
function formatMatchContent(content, maxLength) {
    if (content.length <= maxLength) {
        return content;
    }
    return `${content.slice(0, maxLength)}…`;
}
function buildSearchRegex(query, caseSensitive, wholeWord) {
    const flags = caseSensitive ? 'g' : 'gi';
    const source = wholeWord ? `\\b(?:${query})\\b` : query;
    // nosemgrep: detect-non-literal-regexp
    return new RegExp(source, flags);
}
export async function walkProjectEntries(cwd, startPath, onEntry, signal) {
    const ig = loadGitignore(cwd);
    const rootPath = startPath ?? cwd;
    await lstat(rootPath);
    const checkAborted = () => {
        if (signal?.aborted) {
            throw signal.reason ?? new Error('Walk aborted');
        }
    };
    const visit = async (absolutePath, relativePath) => {
        checkAborted();
        if (relativePath && ig.ignores(normalizePathForMatch(relativePath))) {
            return false;
        }
        const stats = await lstat(absolutePath);
        const isDirectory = stats.isDirectory();
        const isSymlink = stats.isSymbolicLink();
        if (relativePath) {
            const shouldStop = await onEntry({
                absolutePath,
                relativePath,
                isDirectory,
            });
            if (shouldStop) {
                return true;
            }
        }
        if (!isDirectory || isSymlink) {
            return false;
        }
        let children = await readdir(absolutePath, { withFileTypes: true });
        children = children.sort((a, b) => a.name.localeCompare(b.name));
        for (const child of children) {
            const childAbsolutePath = path.join(absolutePath, child.name);
            const childRelativePath = relativePath
                ? path.join(relativePath, child.name)
                : child.name;
            if (await visit(childAbsolutePath, childRelativePath)) {
                return true;
            }
        }
        return false;
    };
    const rootRelativePath = path.relative(cwd, rootPath);
    await visit(rootPath, rootRelativePath === '' ? '' : rootRelativePath);
}
export async function findMatchingPaths(pattern, cwd, maxResults) {
    const hasSlash = normalizePathForMatch(pattern).includes('/');
    const files = [];
    let truncated = false;
    await walkProjectEntries(cwd, undefined, entry => {
        if (matchesGlob(entry.relativePath, pattern, !hasSlash)) {
            files.push(normalizePathForMatch(entry.relativePath));
            if (files.length >= maxResults) {
                truncated = true;
                return true;
            }
        }
        return false;
    });
    return { files, truncated };
}
export async function searchProjectContents(query, cwd, maxResults, caseSensitive, include, searchPath, wholeWord, contextLines, timeoutMs = DEFAULT_SEARCH_TIMEOUT_MS) {
    const matches = [];
    let truncated = false;
    const regex = buildSearchRegex(query, caseSensitive, wholeWord ?? false);
    const hasContext = contextLines !== undefined && contextLines > 0;
    const normalizedContextLines = Math.max(0, contextLines ?? 0);
    const includeHasSlash = include
        ? normalizePathForMatch(include).includes('/')
        : false;
    const controller = new AbortController();
    const timeoutError = new SearchTimeoutError(timeoutMs);
    const timer = setTimeout(() => controller.abort(timeoutError), timeoutMs);
    try {
        await walkProjectEntries(cwd, searchPath, async (entry) => {
            if (entry.isDirectory) {
                return false;
            }
            if (include &&
                !matchesGlob(entry.relativePath, include, !includeHasSlash)) {
                return false;
            }
            let content;
            try {
                content = await readFile(entry.absolutePath, 'utf-8');
            }
            catch {
                return false;
            }
            if (isIgnoredByBinaryHeuristics(entry.relativePath, content)) {
                return false;
            }
            const lines = content.split(/\r?\n/);
            for (let index = 0; index < lines.length; index++) {
                const currentLine = lines[index] ?? '';
                regex.lastIndex = 0;
                if (!regex.test(currentLine)) {
                    continue;
                }
                const lineNumber = index + 1;
                let matchContent = currentLine.trim();
                if (hasContext) {
                    const start = Math.max(0, index - normalizedContextLines);
                    const end = Math.min(lines.length - 1, index + normalizedContextLines);
                    const contextContent = lines
                        .slice(start, end + 1)
                        .map((line, offset) => `${start + offset + 1}: ${line}`)
                        .join('\n');
                    matchContent = formatMatchContent(contextContent, MAX_CONTEXT_CONTENT_LENGTH);
                }
                else {
                    matchContent = formatMatchContent(matchContent, MAX_MATCH_CONTENT_LENGTH);
                }
                matches.push({
                    file: normalizePathForMatch(entry.relativePath),
                    line: lineNumber,
                    content: matchContent,
                });
                if (matches.length >= maxResults) {
                    truncated = true;
                    return true;
                }
            }
            return false;
        }, controller.signal);
    }
    finally {
        clearTimeout(timer);
    }
    return { matches, truncated };
}
//# sourceMappingURL=file-search.js.map