/**
 * Read File Tool - Read files with multi-file, directory, and regex support
 *
 * Features:
 * - Read single or multiple files
 * - Read all files in a directory (with depth control)
 * - Filter files by glob/regex pattern
 * - Content search with regex
 * - Compact output format
 */
import { z } from 'zod';
import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
import { join, relative, extname } from 'path';
import { validatePath, getProjectRoot } from '../utils/path-validator.js';
// Max content per file (truncate if larger)
const MAX_CONTENT_LENGTH = 5000;
// Max files to return
const MAX_FILES = 50;
// Max total content length
const MAX_TOTAL_CONTENT = 50000;
// Define Zod schema for validation
const ParamsSchema = z.object({
    paths: z.union([z.string(), z.array(z.string())]).describe('File path(s) or directory'),
    pattern: z.string().optional().describe('Glob pattern to filter files (e.g., "*.ts", "**/*.js")'),
    contentPattern: z.string().optional().describe('Regex to search within file content'),
    maxDepth: z.number().default(3).describe('Max directory depth to traverse'),
    includeContent: z.boolean().default(true).describe('Include file content in result'),
    maxFiles: z.number().default(MAX_FILES).describe('Max number of files to return'),
    offset: z.number().min(0).optional().describe('Line offset to start reading from (0-based, for single file only)'),
    limit: z.number().min(1).optional().describe('Number of lines to read (for single file only)'),
});
// Common binary extensions to skip
const BINARY_EXTENSIONS = new Set([
    '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp', '.svg',
    '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
    '.zip', '.tar', '.gz', '.rar', '.7z',
    '.exe', '.dll', '.so', '.dylib',
    '.mp3', '.mp4', '.wav', '.avi', '.mov',
    '.woff', '.woff2', '.ttf', '.eot', '.otf',
    '.pyc', '.class', '.o', '.obj',
]);
/**
 * Check if file is likely binary
 */
function isBinaryFile(filePath) {
    const ext = extname(filePath).toLowerCase();
    return BINARY_EXTENSIONS.has(ext);
}
/**
 * Convert glob pattern to regex
 */
function globToRegex(pattern) {
    const escaped = pattern
        .replace(/[.+^${}()|[\]\\]/g, '\\$&')
        .replace(/\*/g, '.*')
        .replace(/\?/g, '.');
    return new RegExp(`^${escaped}$`, 'i');
}
/**
 * Check if filename matches glob pattern
 */
function matchesPattern(filename, pattern) {
    const regex = globToRegex(pattern);
    return regex.test(filename);
}
/**
 * Recursively collect files from directory
 */
function collectFiles(dir, pattern, maxDepth, currentDepth = 0) {
    if (currentDepth > maxDepth)
        return [];
    const files = [];
    try {
        const entries = readdirSync(dir, { withFileTypes: true });
        for (const entry of entries) {
            // Skip hidden files/dirs and node_modules
            if (entry.name.startsWith('.') || entry.name === 'node_modules')
                continue;
            const fullPath = join(dir, entry.name);
            if (entry.isDirectory()) {
                files.push(...collectFiles(fullPath, pattern, maxDepth, currentDepth + 1));
            }
            else if (entry.isFile()) {
                if (!pattern || matchesPattern(entry.name, pattern)) {
                    files.push(fullPath);
                }
            }
        }
    }
    catch {
        // Skip directories we can't read
    }
    return files;
}
/**
 * Read file content with truncation and optional line-based pagination
 */
function readFileContent(filePath, options) {
    const { maxLength, offset, limit } = options;
    if (isBinaryFile(filePath)) {
        return { content: '[Binary file]', truncated: false };
    }
    try {
        const content = readFileSync(filePath, 'utf8');
        const lines = content.split('\n');
        const totalLines = lines.length;
        // If offset/limit specified, use line-based pagination
        if (offset !== undefined || limit !== undefined) {
            const startLine = Math.min(offset ?? 0, totalLines);
            const endLine = limit !== undefined ? Math.min(startLine + limit, totalLines) : totalLines;
            const selectedLines = lines.slice(startLine, endLine);
            const selectedContent = selectedLines.join('\n');
            const actualEnd = endLine;
            const hasMore = actualEnd < totalLines;
            let finalContent = selectedContent;
            if (selectedContent.length > maxLength) {
                finalContent = selectedContent.substring(0, maxLength) + `\n... (+${selectedContent.length - maxLength} chars)`;
            }
            // Calculate actual line range (handle empty selection)
            const actualLineEnd = selectedLines.length > 0 ? startLine + selectedLines.length - 1 : startLine;
            return {
                content: finalContent,
                truncated: hasMore || selectedContent.length > maxLength,
                totalLines,
                lineRange: { start: startLine, end: actualLineEnd },
            };
        }
        // Default behavior: truncate by character length
        if (content.length > maxLength) {
            return {
                content: content.substring(0, maxLength) + `\n... (+${content.length - maxLength} chars)`,
                truncated: true,
                totalLines,
            };
        }
        return { content, truncated: false, totalLines };
    }
    catch (error) {
        return { content: `[Error: ${error.message}]`, truncated: false };
    }
}
/**
 * Find regex matches in content
 */
function findMatches(content, pattern) {
    try {
        const regex = new RegExp(pattern, 'gm');
        const matches = [];
        let match;
        while ((match = regex.exec(content)) !== null && matches.length < 10) {
            // Get line containing match
            const lineStart = content.lastIndexOf('\n', match.index) + 1;
            const lineEnd = content.indexOf('\n', match.index);
            const line = content.substring(lineStart, lineEnd === -1 ? undefined : lineEnd).trim();
            matches.push(line.substring(0, 200)); // Truncate long lines
        }
        return matches;
    }
    catch {
        return [];
    }
}
// Tool schema for MCP
export const schema = {
    name: 'read_file',
    description: `Read files with multi-file, directory, regex support, and line-based pagination.

Usage:
  read_file(paths="file.ts")                              # Single file (full content)
  read_file(paths="file.ts", offset=100, limit=50)        # Lines 100-149 (0-based)
  read_file(paths=["a.ts", "b.ts"])                       # Multiple files
  read_file(paths="src/", pattern="*.ts")                 # Directory with pattern
  read_file(paths="src/", contentPattern="TODO")          # Search content

Supports both absolute and relative paths. Relative paths are resolved from project root.
Returns compact file list with optional content. Use offset/limit for large file pagination.`,
    inputSchema: {
        type: 'object',
        properties: {
            paths: {
                oneOf: [
                    { type: 'string', description: 'Single file or directory path' },
                    { type: 'array', items: { type: 'string' }, description: 'Array of file paths' }
                ],
                description: 'File path(s) or directory to read',
            },
            pattern: {
                type: 'string',
                description: 'Glob pattern to filter files (e.g., "*.ts", "*.{js,ts}")',
            },
            contentPattern: {
                type: 'string',
                description: 'Regex pattern to search within file content',
            },
            maxDepth: {
                type: 'number',
                description: 'Max directory depth to traverse (default: 3)',
                default: 3,
            },
            includeContent: {
                type: 'boolean',
                description: 'Include file content in result (default: true)',
                default: true,
            },
            maxFiles: {
                type: 'number',
                description: `Max number of files to return (default: ${MAX_FILES})`,
                default: MAX_FILES,
            },
            offset: {
                type: 'number',
                description: 'Line offset to start reading from (0-based, for single file only)',
                minimum: 0,
            },
            limit: {
                type: 'number',
                description: 'Number of lines to read (for single file only)',
                minimum: 1,
            },
        },
        required: ['paths'],
    },
};
// Handler function
export async function handler(params) {
    const parsed = ParamsSchema.safeParse(params);
    if (!parsed.success) {
        return { success: false, error: `Invalid params: ${parsed.error.message}` };
    }
    const { paths, pattern, contentPattern, maxDepth, includeContent, maxFiles, offset, limit, } = parsed.data;
    const cwd = getProjectRoot();
    // Normalize paths to array
    const inputPaths = Array.isArray(paths) ? paths : [paths];
    // Collect all files to read
    const allFiles = [];
    for (const inputPath of inputPaths) {
        const resolvedPath = await validatePath(inputPath);
        if (!existsSync(resolvedPath)) {
            continue; // Skip non-existent paths
        }
        const stat = statSync(resolvedPath);
        if (stat.isDirectory()) {
            // Collect files from directory
            const dirFiles = collectFiles(resolvedPath, pattern, maxDepth);
            allFiles.push(...dirFiles);
        }
        else if (stat.isFile()) {
            // Add single file (check pattern if provided)
            if (!pattern || matchesPattern(relative(cwd, resolvedPath), pattern)) {
                allFiles.push(resolvedPath);
            }
        }
    }
    // Limit files
    const limitedFiles = allFiles.slice(0, maxFiles);
    const totalFiles = allFiles.length;
    // Process files
    const files = [];
    let totalContent = 0;
    // Only apply offset/limit for single file mode
    const isSingleFile = limitedFiles.length === 1;
    const useLinePagination = isSingleFile && (offset !== undefined || limit !== undefined);
    for (const filePath of limitedFiles) {
        if (totalContent >= MAX_TOTAL_CONTENT)
            break;
        const stat = statSync(filePath);
        const entry = {
            path: relative(cwd, filePath) || filePath,
            size: stat.size,
        };
        if (includeContent) {
            const remainingSpace = MAX_TOTAL_CONTENT - totalContent;
            const maxLen = Math.min(MAX_CONTENT_LENGTH, remainingSpace);
            // Pass offset/limit only for single file mode
            const readOptions = { maxLength: maxLen };
            if (useLinePagination) {
                if (offset !== undefined)
                    readOptions.offset = offset;
                if (limit !== undefined)
                    readOptions.limit = limit;
            }
            const { content, truncated, totalLines, lineRange } = readFileContent(filePath, readOptions);
            // If contentPattern provided, only include files with matches
            if (contentPattern) {
                const matches = findMatches(content, contentPattern);
                if (matches.length > 0) {
                    entry.matches = matches;
                    entry.content = content;
                    entry.truncated = truncated;
                    entry.totalLines = totalLines;
                    entry.lineRange = lineRange;
                    totalContent += content.length;
                }
                else {
                    continue; // Skip files without matches
                }
            }
            else {
                entry.content = content;
                entry.truncated = truncated;
                entry.totalLines = totalLines;
                entry.lineRange = lineRange;
                totalContent += content.length;
            }
        }
        files.push(entry);
    }
    // Build message
    let message = `Read ${files.length} file(s)`;
    if (totalFiles > maxFiles) {
        message += ` (showing ${maxFiles} of ${totalFiles})`;
    }
    if (useLinePagination && files.length > 0 && files[0].lineRange) {
        const { start, end } = files[0].lineRange;
        message += ` [lines ${start}-${end} of ${files[0].totalLines}]`;
    }
    if (contentPattern) {
        message += ` matching "${contentPattern}"`;
    }
    return {
        success: true,
        result: {
            files,
            totalFiles,
            message,
        },
    };
}
//# sourceMappingURL=read-file.js.map