/**
 * Handler for fs_search messages.
 * Searches file contents within a project using ripgrep (preferred) or Node.js fallback.
 * Limits results to MAX_SEARCH_RESULTS matches, excludes node_modules and .git.
 */
import { readdir, readFile } from "node:fs/promises";
import { join, relative } from "node:path";
import { execFileSync } from "node:child_process";
import { ErrorCode, MAX_SEARCH_RESULTS } from "@pocketcoder/shared/constants";
import { getProject } from "../db/repositories/projects.js";
import { logger } from "../utils/logger.js";
const EXCLUDED_DIRS = new Set([
    "node_modules",
    ".git",
    ".hg",
    ".svn",
    "dist",
    "build",
    ".next",
    ".nuxt",
    "__pycache__",
    ".cache",
]);
let rgAvailable = null;
function isRipgrepAvailable() {
    if (rgAvailable !== null)
        return rgAvailable;
    try {
        execFileSync("rg", ["--version"], { stdio: "ignore" });
        rgAvailable = true;
    }
    catch {
        rgAvailable = false;
    }
    return rgAvailable;
}
async function searchWithRipgrep(projectPath, query, options) {
    const { execa } = await import("execa");
    const args = ["--json", "--no-heading", "--max-count", String(MAX_SEARCH_RESULTS)];
    if (!options.caseSensitive) {
        args.push("--ignore-case");
    }
    if (!options.regex) {
        args.push("--fixed-strings");
    }
    if (options.glob) {
        args.push("--glob", options.glob);
    }
    // Exclude common directories
    for (const dir of EXCLUDED_DIRS) {
        args.push("--glob", `!${dir}`);
    }
    args.push("--", query, ".");
    try {
        const result = await execa("rg", args, {
            cwd: projectPath,
            reject: false,
            timeout: 30000,
        });
        // Exit code 1 means no matches (not an error)
        if (result.exitCode !== 0 && result.exitCode !== 1) {
            throw new Error(`ripgrep failed with exit code ${result.exitCode}: ${result.stderr}`);
        }
        return parseRipgrepJson(result.stdout, projectPath);
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        throw new Error(`ripgrep search failed: ${msg}`);
    }
}
function parseRipgrepJson(output, _projectPath) {
    const files = new Map();
    let totalMatches = 0;
    if (!output.trim()) {
        return { files: [], totalMatches: 0, truncated: false };
    }
    for (const line of output.split("\n")) {
        if (!line.trim())
            continue;
        let parsed;
        try {
            parsed = JSON.parse(line);
        }
        catch {
            continue;
        }
        if (parsed.type !== "match")
            continue;
        const data = parsed.data;
        if (totalMatches >= MAX_SEARCH_RESULTS) {
            return {
                files: mapToFileResults(files),
                totalMatches,
                truncated: true,
            };
        }
        // rg runs from project dir with "." so paths are relative like "./src/foo.ts"
        let filePath = data.path.text;
        if (filePath.startsWith("./")) {
            filePath = filePath.slice(2);
        }
        const match = {
            line: data.line_number,
            content: data.lines.text.replace(/\n$/, ""),
            column: data.submatches?.[0]?.start,
        };
        const existing = files.get(filePath);
        if (existing) {
            existing.push(match);
        }
        else {
            files.set(filePath, [match]);
        }
        totalMatches++;
    }
    return {
        files: mapToFileResults(files),
        totalMatches,
        truncated: false,
    };
}
function mapToFileResults(files) {
    return Array.from(files.entries()).map(([path, matches]) => ({
        path,
        matches,
    }));
}
async function searchWithNodeJs(projectPath, query, options) {
    const files = new Map();
    let totalMatches = 0;
    let truncated = false;
    let pattern;
    if (options.regex) {
        pattern = new RegExp(query, options.caseSensitive ? "g" : "gi");
    }
    else {
        const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
        pattern = new RegExp(escaped, options.caseSensitive ? "g" : "gi");
    }
    // Simple glob matching for file filtering
    let globPattern = null;
    if (options.glob) {
        const globEscaped = options.glob
            .replace(/[.+^${}()|[\]\\]/g, "\\$&")
            .replace(/\*/g, ".*")
            .replace(/\?/g, ".");
        globPattern = new RegExp(`^${globEscaped}$`);
    }
    async function walk(dir) {
        if (truncated)
            return;
        let entries;
        try {
            entries = await readdir(dir, { withFileTypes: true });
        }
        catch {
            return;
        }
        for (const entry of entries) {
            if (truncated)
                return;
            if (entry.isDirectory()) {
                if (EXCLUDED_DIRS.has(entry.name))
                    continue;
                await walk(join(dir, entry.name));
            }
            else if (entry.isFile()) {
                const filePath = join(dir, entry.name);
                const relPath = relative(projectPath, filePath);
                // Apply glob filter on filename
                if (globPattern && !globPattern.test(entry.name))
                    continue;
                try {
                    const content = await readFile(filePath, "utf-8");
                    const lines = content.split("\n");
                    for (let i = 0; i < lines.length; i++) {
                        if (truncated)
                            break;
                        // Reset lastIndex for global regex
                        pattern.lastIndex = 0;
                        const match = pattern.exec(lines[i]);
                        if (match) {
                            const searchMatch = {
                                line: i + 1,
                                content: lines[i],
                                column: match.index,
                            };
                            const existing = files.get(relPath);
                            if (existing) {
                                existing.push(searchMatch);
                            }
                            else {
                                files.set(relPath, [searchMatch]);
                            }
                            totalMatches++;
                            if (totalMatches >= MAX_SEARCH_RESULTS) {
                                truncated = true;
                                break;
                            }
                        }
                    }
                }
                catch {
                    // Skip files that can't be read (binary, permissions, etc.)
                }
            }
        }
    }
    await walk(projectPath);
    return {
        files: mapToFileResults(files),
        totalMatches,
        truncated,
    };
}
export async function handleFsSearch(message, send) {
    const msg = message;
    const { requestId, sessionId, payload } = msg;
    const { query, glob, caseSensitive, regex } = payload;
    const project = getProject(sessionId);
    if (!project) {
        send({
            type: "fs_search_response",
            requestId,
            error: {
                code: ErrorCode.INVALID_REQUEST,
                message: `Project not found: ${sessionId}`,
            },
        });
        return;
    }
    // Validate query
    if (!query || query.length < 1) {
        send({
            type: "fs_search_response",
            requestId,
            error: {
                code: ErrorCode.QUERY_TOO_SHORT,
                message: "Search query must be at least 1 character",
            },
        });
        return;
    }
    // Validate regex if regex mode
    if (regex) {
        try {
            new RegExp(query);
        }
        catch {
            send({
                type: "fs_search_response",
                requestId,
                error: {
                    code: ErrorCode.INVALID_REGEX,
                    message: "Invalid regular expression",
                },
            });
            return;
        }
    }
    try {
        const options = {
            glob,
            caseSensitive: caseSensitive ?? false,
            regex: regex ?? false,
        };
        let result;
        if (isRipgrepAvailable()) {
            result = await searchWithRipgrep(project.path, query, options);
        }
        else {
            result = await searchWithNodeJs(project.path, query, options);
        }
        send({
            type: "fs_search_response",
            requestId,
            payload: {
                query,
                totalMatches: result.totalMatches,
                truncated: result.truncated,
                files: result.files,
            },
        });
    }
    catch (err) {
        const errorMessage = err instanceof Error ? err.message : String(err);
        logger.error(`fs_search error: ${errorMessage}`);
        send({
            type: "fs_search_response",
            requestId,
            error: {
                code: ErrorCode.UNKNOWN_ERROR,
                message: errorMessage,
            },
        });
    }
}
/** Reset cached ripgrep availability (for testing) */
export function resetRipgrepCache() {
    rgAvailable = null;
}
//# sourceMappingURL=fs-search.js.map