/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import fs from 'node:fs/promises';
import path from 'node:path';
import { BaseDeclarativeTool, BaseToolInvocation, Kind, } from './tools.js';
import { makeRelative, shortenPath } from '../utils/paths.js';
import { DEFAULT_FILE_FILTERING_OPTIONS } from '../config/constants.js';
import { ToolErrorType } from './tool-error.js';
import { LS_TOOL_NAME } from './tool-names.js';
import { buildDirPathArgsPattern } from '../policy/utils.js';
import { debugLogger } from '../utils/debugLogger.js';
import { LS_DEFINITION } from './definitions/coreTools.js';
import { resolveToolDeclaration } from './definitions/resolver.js';
import { discoverJitContext, appendJitContext } from './jit-context.js';
class LSToolInvocation extends BaseToolInvocation {
    config;
    constructor(config, params, messageBus, _toolName, _toolDisplayName) {
        super(params, messageBus, _toolName, _toolDisplayName);
        this.config = config;
    }
    /**
     * Checks if a filename matches any of the ignore patterns
     * @param filename Filename to check
     * @param patterns Array of glob patterns to check against
     * @returns True if the filename should be ignored
     */
    shouldIgnore(filename, patterns) {
        if (!patterns || patterns.length === 0) {
            return false;
        }
        for (const pattern of patterns) {
            // Convert glob pattern to RegExp
            const regexPattern = pattern
                .replace(/[.+^${}()|[\]\\]/g, '\\$&')
                .replace(/\*/g, '.*')
                .replace(/\?/g, '.');
            const regex = new RegExp(`^${regexPattern}$`);
            if (regex.test(filename)) {
                return true;
            }
        }
        return false;
    }
    /**
     * Gets a description of the file reading operation
     * @returns A string describing the file being read
     */
    getDescription() {
        const relativePath = makeRelative(this.params.dir_path, this.config.getTargetDir());
        return shortenPath(relativePath);
    }
    getPolicyUpdateOptions(_outcome) {
        return {
            argsPattern: buildDirPathArgsPattern(this.params.dir_path),
        };
    }
    // Helper for consistent error formatting
    errorResult(llmContent, returnDisplay, type) {
        return {
            llmContent,
            // Keep returnDisplay simpler in core logic
            returnDisplay: `Error: ${returnDisplay}`,
            error: {
                message: llmContent,
                type,
            },
        };
    }
    /**
     * Executes the LS operation with the given parameters
     * @returns Result of the LS operation
     */
    async execute(_signal) {
        const resolvedDirPath = path.resolve(this.config.getTargetDir(), this.params.dir_path);
        const validationError = this.config.validatePathAccess(resolvedDirPath, 'read');
        if (validationError) {
            return {
                llmContent: validationError,
                returnDisplay: 'Path not in workspace.',
                error: {
                    message: validationError,
                    type: ToolErrorType.PATH_NOT_IN_WORKSPACE,
                },
            };
        }
        try {
            const stats = await fs.stat(resolvedDirPath);
            if (!stats) {
                // fs.statSync throws on non-existence, so this check might be redundant
                // but keeping for clarity. Error message adjusted.
                return this.errorResult(`Error: Directory not found or inaccessible: ${resolvedDirPath}`, `Directory not found or inaccessible.`, ToolErrorType.FILE_NOT_FOUND);
            }
            if (!stats.isDirectory()) {
                return this.errorResult(`Error: Path is not a directory: ${resolvedDirPath}`, `Path is not a directory.`, ToolErrorType.PATH_IS_NOT_A_DIRECTORY);
            }
            const files = await fs.readdir(resolvedDirPath);
            if (files.length === 0) {
                // Changed error message to be more neutral for LLM
                return {
                    llmContent: `Directory ${resolvedDirPath} is empty.`,
                    returnDisplay: `Directory is empty.`,
                };
            }
            const relativePaths = files.map((file) => path.relative(this.config.getTargetDir(), path.join(resolvedDirPath, file)));
            const fileDiscovery = this.config.getFileService();
            const { filteredPaths, ignoredCount } = fileDiscovery.filterFilesWithReport(relativePaths, {
                respectGitIgnore: this.params.file_filtering_options?.respect_git_ignore ??
                    this.config.getFileFilteringOptions().respectGitIgnore ??
                    DEFAULT_FILE_FILTERING_OPTIONS.respectGitIgnore,
                respectGeminiIgnore: this.params.file_filtering_options?.respect_gemini_ignore ??
                    this.config.getFileFilteringOptions().respectGeminiIgnore ??
                    DEFAULT_FILE_FILTERING_OPTIONS.respectGeminiIgnore,
            });
            const entries = [];
            for (const relativePath of filteredPaths) {
                const fullPath = path.resolve(this.config.getTargetDir(), relativePath);
                if (this.shouldIgnore(path.basename(fullPath), this.params.ignore)) {
                    continue;
                }
                try {
                    const stats = await fs.stat(fullPath);
                    const isDir = stats.isDirectory();
                    entries.push({
                        name: path.basename(fullPath),
                        path: fullPath,
                        isDirectory: isDir,
                        size: isDir ? 0 : stats.size,
                        modifiedTime: stats.mtime,
                    });
                }
                catch (error) {
                    // Log error internally but don't fail the whole listing
                    debugLogger.debug(`Error accessing ${fullPath}: ${error}`);
                }
            }
            // Sort entries (directories first, then alphabetically)
            entries.sort((a, b) => {
                if (a.isDirectory && !b.isDirectory)
                    return -1;
                if (!a.isDirectory && b.isDirectory)
                    return 1;
                return a.name.localeCompare(b.name);
            });
            // Create formatted content for LLM
            const directoryContent = entries
                .map((entry) => {
                if (entry.isDirectory) {
                    return `[DIR] ${entry.name}`;
                }
                return `${entry.name} (${entry.size} bytes)`;
            })
                .join('\n');
            let resultMessage = `Directory listing for ${resolvedDirPath}:\n${directoryContent}`;
            if (ignoredCount > 0) {
                resultMessage += `\n\n(${ignoredCount} ignored)`;
            }
            // Discover JIT subdirectory context for the listed directory
            const jitContext = await discoverJitContext(this.config, resolvedDirPath);
            if (jitContext) {
                resultMessage = appendJitContext(resultMessage, jitContext);
            }
            let displayMessage = `Listed ${entries.length} item(s).`;
            if (ignoredCount > 0) {
                displayMessage += ` (${ignoredCount} ignored)`;
            }
            return {
                llmContent: resultMessage,
                returnDisplay: displayMessage,
            };
        }
        catch (error) {
            const errorMsg = `Error listing directory: ${error instanceof Error ? error.message : String(error)}`;
            return this.errorResult(errorMsg, 'Failed to list directory.', ToolErrorType.LS_EXECUTION_ERROR);
        }
    }
}
/**
 * Implementation of the LS tool logic
 */
export class LSTool extends BaseDeclarativeTool {
    config;
    static Name = LS_TOOL_NAME;
    constructor(config, messageBus) {
        super(LSTool.Name, 'ReadFolder', LS_DEFINITION.base.description, Kind.Search, LS_DEFINITION.base.parametersJsonSchema, messageBus, true, false);
        this.config = config;
    }
    /**
     * Validates the parameters for the tool
     * @param params Parameters to validate
     * @returns An error message string if invalid, null otherwise
     */
    validateToolParamValues(params) {
        const resolvedPath = path.resolve(this.config.getTargetDir(), params.dir_path);
        return this.config.validatePathAccess(resolvedPath, 'read');
    }
    createInvocation(params, messageBus, _toolName, _toolDisplayName) {
        return new LSToolInvocation(this.config, params, messageBus ?? this.messageBus, _toolName, _toolDisplayName);
    }
    getSchema(modelId) {
        return resolveToolDeclaration(LS_DEFINITION, modelId);
    }
}
//# sourceMappingURL=ls.js.map