import { exec } from "node:child_process";
import { existsSync } from "node:fs";
import path from "node:path";
import { promisify } from "node:util";
import { z } from "zod";
import { getErrorMeta, getLogger } from "../../../../util/logger.js";
import { createToolFactory, PermissionLevel, ToolCategory } from "../types.js";
const execAsync = promisify(exec);
// Shell escape: wrap in single quotes and escape any single quotes within
const shellEscape = (arg) => {
    return `'${arg.replace(/'/g, "'\\''")}'`;
};
// Regex to match ripgrep --stats output at the end of stdout
const STATS_REGEX = /\n?\n?(\d+ matches[\s\S]*?seconds total)\s*$/;
/**
 * Parse ripgrep output to separate results from stats.
 * Stats appear at the end of stdout when using --stats flag.
 */
const parseStatsFromOutput = (output) => {
    const match = output.match(STATS_REGEX);
    if (match && match[1]) {
        const stats = match[1];
        const results = output.slice(0, match.index).trim();
        return { results, stats };
    }
    return { results: output, stats: "" };
};
/**
 * Grep tool - Powerful search built on ripgrep.
 */
export const grepToolFactory = createToolFactory("grep", () => ({
    description: `
A powerful search tool built on ripgrep

Usage:
- ALWAYS use Grep for search tasks. NEVER invoke grep or rg as a Bash command.
- Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")
- Use 'path' to search IN a specific file or directory (e.g., "pages/Page1/index.tsx", "components/")
- Use 'glob' to FILTER files by pattern (e.g., "*.tsx", "**/*.test.ts") - this is NOT for specifying paths
- Output modes: "content" shows matching lines, "files_with_matches" shows only file paths (default), "count" shows match counts
- Use Task tool for open-ended searches requiring multiple rounds
- Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping (use interface\\{\\} to find interface{} in Go code)
- Multiline matching: By default patterns match within single lines only. For cross-line patterns like struct \\{[\\s\\S]*?field, use multiline: true
`,
    category: ToolCategory.FILE,
    defaultPermission: PermissionLevel.ALLOW,
    readOnly: true,
    inputSchema: z.object({
        pattern: z.string().describe("Regex pattern to search"),
        path: z
            .string()
            .optional()
            .describe("File or directory to search IN (e.g., 'src/components/')"),
        output_mode: z
            .enum(["content", "files_with_matches", "count"])
            .optional()
            .describe("Output mode (default: files_with_matches)"),
        glob: z
            .string()
            .optional()
            .describe("Filter by file pattern (e.g., '*.tsx') - NOT for specific paths"),
        type: z.string().optional().describe("File type (e.g., js, py, rust)"),
        "-i": z.boolean().optional().describe("Case insensitive"),
        "-n": z.boolean().optional().describe("Show line numbers"),
        "-A": z.number().optional().describe("Context lines after match"),
        "-B": z.number().optional().describe("Context lines before match"),
        "-C": z.number().optional().describe("Context lines before and after"),
        head_limit: z.number().optional().describe("Limit output lines"),
        multiline: z.boolean().optional().describe("Enable multiline matching"),
    }),
    execute: async ({ pattern, path: searchPath, output_mode = "files_with_matches", glob: globPattern, type, "-i": caseInsensitive, "-n": lineNumbers, "-A": afterContext, "-B": beforeContext, "-C": context, head_limit, multiline, }, options) => {
        const args = ["rg", "--stats"];
        try {
            // Output mode flags
            if (output_mode === "files_with_matches") {
                args.push("-l");
            }
            else if (output_mode === "count") {
                args.push("-c");
            }
            // Optional flags
            if (caseInsensitive)
                args.push("-i");
            if (lineNumbers && output_mode === "content")
                args.push("-n");
            if (afterContext !== undefined && output_mode === "content")
                args.push(`-A ${afterContext}`);
            if (beforeContext !== undefined && output_mode === "content")
                args.push(`-B ${beforeContext}`);
            if (context !== undefined && output_mode === "content")
                args.push(`-C ${context}`);
            if (multiline)
                args.push("-U", "--multiline-dotall");
            if (globPattern)
                args.push(`--glob '${globPattern}'`);
            if (type)
                args.push(`--type ${type}`);
            // Exclude API subdirectories (they contain YAML artifacts we want to hide)
            args.push("--glob '!apis/*/'");
            args.push(shellEscape(pattern));
            // Add searchPath if provided, otherwise search current directory
            if (searchPath) {
                if (!existsSync(searchPath)) {
                    return {
                        output: `Path does not exist: ${searchPath}`,
                        matches: 0,
                    };
                }
                args.push(shellEscape(searchPath));
            }
            // Also search scratch directory where virtual API TS files live (if it exists)
            const scratchPath = path.resolve(process.cwd(), ".superblocks/scratch");
            if (existsSync(scratchPath)) {
                args.push("'.superblocks/scratch/'");
            }
            const command = args.join(" ");
            const { stdout } = await execAsync(command, {
                maxBuffer: 10 * 1024 * 1024, // 10MB
                timeout: 15_000, // 15 second timeout
                signal: options?.abortSignal,
            });
            const { results: searchResults } = parseStatsFromOutput(stdout);
            let result = searchResults;
            // map scratch path to virtual api path
            result = result.replace(/\.superblocks\/scratch\/apis%2F([^:\s]+)\.ts/g, "apis/$1.ts");
            if (head_limit) {
                const lines = result.split("\n");
                result = lines.slice(0, head_limit).join("\n");
            }
            const matchCount = output_mode === "files_with_matches"
                ? result.split("\n").filter((l) => l.trim()).length
                : undefined;
            return {
                output: result,
                matches: matchCount,
            };
        }
        catch (error) {
            // ripgrep returns exit code 1 when no matches found
            if (error.code === 1) {
                return { output: "", matches: 0 };
            }
            getLogger().error("Grep failed to execute with:", getErrorMeta({
                args: args.join(" "),
                error: error?.message,
                signal: error?.signal,
                code: error?.code,
                killed: error?.killed,
            }));
            // Handle timeout
            if (error.killed && error.signal === "SIGTERM") {
                const partialStdout = error.stdout ?? "";
                const { results: partialResults, stats: partialStats } = parseStatsFromOutput(partialStdout);
                const partialLines = partialResults
                    .split("\n")
                    .filter((l) => l.trim());
                const diagnostics = [
                    `Command: ${args.join(" ")}`,
                    `Pattern: ${pattern}`,
                    `Path: ${searchPath ?? "(project root)"}`,
                ];
                if (partialStats) {
                    diagnostics.push(`Partial stats: ${partialStats}`);
                }
                if (partialLines.length > 0) {
                    diagnostics.push(`Partial output (${partialLines.length} lines captured before timeout)`);
                }
                // Run a quick diagnostic to count files in the search scope
                try {
                    const scopeArgs = ["rg", "--files"];
                    if (globPattern)
                        scopeArgs.push(`--glob '${globPattern}'`);
                    if (type)
                        scopeArgs.push(`--type ${type}`);
                    scopeArgs.push("--glob '!apis/*/'");
                    if (searchPath)
                        scopeArgs.push(shellEscape(searchPath));
                    const { stdout: fileListOutput } = await execAsync(`${scopeArgs.join(" ")} | wc -l`, { timeout: 5000 });
                    diagnostics.push(`Files in search scope: ${fileListOutput.trim()}`);
                }
                catch {
                    // Ignore diagnostic failures
                }
                getLogger().error("Grep command timed out", getErrorMeta({
                    diagnostics: diagnostics.join("; "),
                    partialStats,
                    partialLinesCount: partialLines.length,
                }));
                throw new Error(`Grep command timed out after 15 seconds.\n${diagnostics.join("\n")}`);
            }
            if (error instanceof Error) {
                throw new Error(`Failed to grep: ${error.message}`);
            }
            throw new Error("Failed to grep: Unknown error");
        }
    },
    getSummary: (input, output) => {
        const pattern = input.pattern;
        const matches = output.matches ?? 0;
        const pathInfo = input.path ? ` in ${input.path}` : "";
        return {
            input: `pattern: "${pattern}"${pathInfo}`,
            output: matches === 0
                ? "No matches found"
                : `Found ${matches} match${matches === 1 ? "" : "es"}`,
        };
    },
}));
//# sourceMappingURL=grep.js.map