import { Type } from '@sinclair/typebox';
import { spawn } from 'child_process';
import { readFile, stat } from 'fs/promises';
import { createInterface } from 'node:readline';
import path from 'path';
import { resolveToCwd } from '../../shared/files/path-utils.js';
import { GREP } from '../../tool-names.js';
import { ABORT_AGENT_MESSAGE } from '../../utils/abort.js';
import { DEFAULT_MAX_CHARS, formatSize, GREP_MAX_LINE_LENGTH, truncateHead, truncateLine } from '../../utils/truncate.js';
import { enforceFilesystemPolicy } from '../permissions/filesystem-policy.js';
import { requirePermissionPolicy } from '../permissions/require-policy.js';
import { ensureTool } from './tool-binaries.js';
const grepSchema = Type.Object({
    pattern: Type.String({ description: 'Search pattern (regex or literal string)' }),
    path: Type.Optional(Type.String({ description: 'Directory or file to search (default: current directory)' })),
    glob: Type.Optional(Type.String({ description: "Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'" })),
    ignoreCase: Type.Optional(Type.Boolean({ description: 'Case-insensitive search (default: false)' })),
    literal: Type.Optional(Type.Boolean({
        description: 'Treat pattern as literal string instead of regex (default: false)'
    })),
    context: Type.Optional(Type.Number({ description: 'Number of lines to show before and after each match (default: 0)' })),
    limit: Type.Optional(Type.Number({ description: 'Maximum number of matches to return (default: 100)' }))
});
const DEFAULT_LIMIT = 100;
const defaultGrepOperations = {
    isDirectory: async (p) => (await stat(p)).isDirectory(),
    readFile: (p) => readFile(p, 'utf-8')
};
export function createGrepTool(cwd, options) {
    const customOps = options?.operations;
    return {
        name: GREP,
        label: GREP,
        isReadOnly: true,
        description: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} matches or ${DEFAULT_MAX_CHARS} characters (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`,
        parameters: grepSchema,
        execute: async (_toolCallId, { pattern, path: searchDir, glob, ignoreCase, literal, context, limit }, signal, _onUpdate, ctx) => {
            return new Promise((resolve, reject) => {
                if (signal?.aborted) {
                    reject(new Error(ABORT_AGENT_MESSAGE));
                    return;
                }
                let settled = false;
                const settle = (fn) => {
                    if (!settled) {
                        settled = true;
                        fn();
                    }
                };
                (async () => {
                    try {
                        const permissionPolicy = requirePermissionPolicy(ctx, GREP);
                        const searchPath = resolveToCwd(searchDir || '.', cwd);
                        const ops = customOps ?? defaultGrepOperations;
                        await enforceFilesystemPolicy({
                            path: searchPath,
                            policy: permissionPolicy.filesystem,
                            access: 'read',
                            sessionState: ctx?.sessionState,
                            approvalGrants: ctx?.approvalGrants?.filesystem
                        });
                        const rgPath = await ensureTool('rg', true);
                        if (!rgPath) {
                            settle(() => reject(new Error('ripgrep (rg) is not available and could not be downloaded')));
                            return;
                        }
                        let isDirectory;
                        try {
                            isDirectory = await ops.isDirectory(searchPath);
                        }
                        catch (_err) {
                            settle(() => reject(new Error(`Path not found: ${searchPath}`)));
                            return;
                        }
                        const contextValue = context && context > 0 ? context : 0;
                        const effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT);
                        const formatPath = (filePath) => {
                            if (isDirectory) {
                                const relative = path.relative(searchPath, filePath);
                                if (relative && !relative.startsWith('..')) {
                                    return relative.replace(/\\/g, '/');
                                }
                            }
                            return path.basename(filePath);
                        };
                        const fileCache = new Map();
                        const getFileLines = async (filePath) => {
                            let lines = fileCache.get(filePath);
                            if (!lines) {
                                try {
                                    const content = await ops.readFile(filePath);
                                    lines = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
                                }
                                catch {
                                    lines = [];
                                }
                                fileCache.set(filePath, lines);
                            }
                            return lines;
                        };
                        const args = ['--json', '--line-number', '--color=never', '--hidden'];
                        if (ignoreCase) {
                            args.push('--ignore-case');
                        }
                        if (literal) {
                            args.push('--fixed-strings');
                        }
                        if (glob) {
                            args.push('--glob', glob);
                        }
                        args.push(pattern, searchPath);
                        const child = spawn(rgPath, args, { stdio: ['ignore', 'pipe', 'pipe'] });
                        const rl = createInterface({ input: child.stdout });
                        let stderr = '';
                        let matchCount = 0;
                        let matchLimitReached = false;
                        let linesTruncated = false;
                        let aborted = false;
                        let killedDueToLimit = false;
                        const outputLines = [];
                        const cleanup = () => {
                            rl.close();
                            signal?.removeEventListener('abort', onAbort);
                        };
                        const stopChild = (dueToLimit = false) => {
                            if (!child.killed) {
                                killedDueToLimit = dueToLimit;
                                child.kill();
                            }
                        };
                        const onAbort = () => {
                            aborted = true;
                            stopChild();
                        };
                        signal?.addEventListener('abort', onAbort, { once: true });
                        child.stderr?.on('data', (chunk) => {
                            stderr += chunk.toString();
                        });
                        const formatBlock = async (filePath, lineNumber) => {
                            const relativePath = formatPath(filePath);
                            const lines = await getFileLines(filePath);
                            if (!lines.length) {
                                return [`${relativePath}:${lineNumber}: (unable to read file)`];
                            }
                            const block = [];
                            const start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;
                            const end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;
                            for (let current = start; current <= end; current++) {
                                const lineText = lines[current - 1] ?? '';
                                const sanitized = lineText.replace(/\r/g, '');
                                const isMatchLine = current === lineNumber;
                                // Truncate long lines
                                const { text: truncatedText, wasTruncated } = truncateLine(sanitized);
                                if (wasTruncated) {
                                    linesTruncated = true;
                                }
                                if (isMatchLine) {
                                    block.push(`${relativePath}:${current}: ${truncatedText}`);
                                }
                                else {
                                    block.push(`${relativePath}-${current}- ${truncatedText}`);
                                }
                            }
                            return block;
                        };
                        // Collect matches during streaming, format after
                        const matches = [];
                        rl.on('line', (line) => {
                            if (!line.trim() || matchCount >= effectiveLimit) {
                                return;
                            }
                            let event;
                            try {
                                event = JSON.parse(line);
                            }
                            catch {
                                return;
                            }
                            if (event.type === 'match') {
                                matchCount++;
                                const filePath = event.data?.path?.text;
                                const lineNumber = event.data?.line_number;
                                if (filePath && typeof lineNumber === 'number') {
                                    matches.push({ filePath, lineNumber });
                                }
                                if (matchCount >= effectiveLimit) {
                                    matchLimitReached = true;
                                    stopChild(true);
                                }
                            }
                        });
                        child.on('error', (error) => {
                            cleanup();
                            settle(() => reject(new Error(`Failed to run ripgrep: ${error.message}`)));
                        });
                        child.on('close', async (code) => {
                            cleanup();
                            if (aborted) {
                                settle(() => reject(new Error(ABORT_AGENT_MESSAGE)));
                                return;
                            }
                            if (!killedDueToLimit && code !== 0 && code !== 1) {
                                const errorMsg = stderr.trim() || `ripgrep exited with code ${code}`;
                                settle(() => reject(new Error(errorMsg)));
                                return;
                            }
                            if (matchCount === 0) {
                                settle(() => resolve({
                                    content: [{ type: 'text', text: 'No matches found' }],
                                    details: undefined
                                }));
                                return;
                            }
                            // Format matches (async to support remote file reading)
                            for (const match of matches) {
                                const block = await formatBlock(match.filePath, match.lineNumber);
                                outputLines.push(...block);
                            }
                            // Apply byte truncation (no line limit since we already have match limit)
                            const rawOutput = outputLines.join('\n');
                            const truncation = truncateHead(rawOutput);
                            let output = truncation.content;
                            const details = {};
                            // Build notices
                            const notices = [];
                            if (matchLimitReached) {
                                notices.push(`${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`);
                                details.matchLimitReached = effectiveLimit;
                            }
                            if (truncation.truncated) {
                                notices.push(`${formatSize(DEFAULT_MAX_CHARS)} limit reached`);
                                details.truncation = truncation;
                            }
                            if (linesTruncated) {
                                notices.push(`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`);
                                details.linesTruncated = true;
                            }
                            if (notices.length > 0) {
                                output += `\n\n[${notices.join('. ')}]`;
                            }
                            settle(() => resolve({
                                content: [{ type: 'text', text: output }],
                                details: Object.keys(details).length > 0 ? details : undefined
                            }));
                        });
                    }
                    catch (err) {
                        settle(() => reject(err));
                    }
                })();
            });
        }
    };
}
//# sourceMappingURL=grep.js.map