/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import fs from 'node:fs';
import fsPromises from 'node:fs/promises';
import path from 'node:path';
import os from 'node:os';
import * as Diff from 'diff';
import { WRITE_FILE_TOOL_NAME, WRITE_FILE_DISPLAY_NAME } from './tool-names.js';
import { BaseDeclarativeTool, BaseToolInvocation, Kind, } from './tools.js';
import { buildFilePathArgsPattern } from '../policy/utils.js';
import { ToolErrorType } from './tool-error.js';
import { makeRelative, shortenPath } from '../utils/paths.js';
import { getErrorMessage, isNodeError } from '../utils/errors.js';
import { ensureCorrectFileContent } from '../utils/editCorrector.js';
import { detectLineEnding } from '../utils/textUtils.js';
import { DEFAULT_DIFF_OPTIONS, getDiffStat } from './diffOptions.js';
import { getDiffContextSnippet } from './diff-utils.js';
import { IdeClient } from '../ide/ide-client.js';
import { logFileOperation } from '../telemetry/loggers.js';
import { FileOperationEvent } from '../telemetry/types.js';
import { FileOperation } from '../telemetry/metrics.js';
import { getSpecificMimeType } from '../utils/fileUtils.js';
import { getLanguageFromFilePath } from '../utils/language-detection.js';
import { debugLogger } from '../utils/debugLogger.js';
import { WRITE_FILE_DEFINITION } from './definitions/coreTools.js';
import { resolveToolDeclaration } from './definitions/resolver.js';
import { detectOmissionPlaceholders } from './omissionPlaceholderDetector.js';
import { resolveAndValidatePlanPath } from '../utils/planUtils.js';
import { isGemini3Model } from '../config/models.js';
import { discoverJitContext, appendJitContext } from './jit-context.js';
export function isWriteFileToolParams(args) {
    if (typeof args !== 'object' || args === null) {
        return false;
    }
    return ('file_path' in args &&
        typeof args.file_path === 'string' &&
        'content' in args &&
        typeof args.content === 'string');
}
export async function getCorrectedFileContent(config, filePath, proposedContent, abortSignal) {
    let originalContent = '';
    let fileExists = false;
    let correctedContent = proposedContent;
    try {
        originalContent = await config
            .getFileSystemService()
            .readTextFile(filePath);
        fileExists = true; // File exists and was read
    }
    catch (err) {
        if (isNodeError(err) && err.code === 'ENOENT') {
            fileExists = false;
            originalContent = '';
        }
        else {
            // File exists but could not be read (permissions, etc.)
            fileExists = true; // Mark as existing but problematic
            originalContent = ''; // Can't use its content
            const error = {
                message: getErrorMessage(err),
                code: isNodeError(err) ? err.code : undefined,
            };
            // Return early as we can't proceed with content correction meaningfully
            return { originalContent, correctedContent, fileExists, error };
        }
    }
    const aggressiveUnescape = !isGemini3Model(config.getActiveModel());
    correctedContent = await ensureCorrectFileContent(proposedContent, config.getBaseLlmClient(), abortSignal, config.getDisableLLMCorrection(), aggressiveUnescape);
    return { originalContent, correctedContent, fileExists };
}
class WriteFileToolInvocation extends BaseToolInvocation {
    config;
    resolvedPath;
    constructor(config, params, messageBus, toolName, displayName) {
        super(params, messageBus, toolName, displayName, undefined, undefined, true, () => this.config.getApprovalMode());
        this.config = config;
        if (this.config.isPlanMode()) {
            try {
                this.resolvedPath = resolveAndValidatePlanPath(this.params.file_path, this.config.storage.getPlansDir(), this.config.getProjectRoot());
            }
            catch (e) {
                debugLogger.error('Failed to resolve plan path during WriteFileTool invocation setup', e);
                // Validation fails, set resolvedPath to something that will fail validation downstream or just the raw path.
                this.resolvedPath = this.params.file_path;
            }
        }
        else {
            this.resolvedPath = path.resolve(this.config.getTargetDir(), this.params.file_path);
        }
    }
    toolLocations() {
        return [{ path: this.resolvedPath }];
    }
    getPolicyUpdateOptions(_outcome) {
        return {
            argsPattern: buildFilePathArgsPattern(this.params.file_path),
        };
    }
    getDescription() {
        const relativePath = makeRelative(this.resolvedPath, this.config.getTargetDir());
        return `Writing to ${shortenPath(relativePath)}`;
    }
    async getConfirmationDetails(abortSignal) {
        const correctedContentResult = await getCorrectedFileContent(this.config, this.resolvedPath, this.params.content, abortSignal);
        if (correctedContentResult.error) {
            // If file exists but couldn't be read, we can't show a diff for confirmation.
            return false;
        }
        const { originalContent, correctedContent } = correctedContentResult;
        const relativePath = makeRelative(this.resolvedPath, this.config.getTargetDir());
        const fileName = path.basename(this.resolvedPath);
        const fileDiff = Diff.createPatch(fileName, originalContent, // Original content (empty if new file or unreadable)
        correctedContent, // Content after potential correction
        'Current', 'Proposed', DEFAULT_DIFF_OPTIONS);
        const ideClient = await IdeClient.getInstance();
        const ideConfirmation = this.config.getIdeMode() && ideClient.isDiffingEnabled()
            ? ideClient.openDiff(this.resolvedPath, correctedContent)
            : undefined;
        const confirmationDetails = {
            type: 'edit',
            title: `Confirm Write: ${shortenPath(relativePath)}`,
            fileName,
            filePath: this.resolvedPath,
            fileDiff,
            originalContent,
            newContent: correctedContent,
            onConfirm: async (_outcome) => {
                // Mode transitions (e.g. AUTO_EDIT) and policy updates are now
                // handled centrally by the scheduler.
                if (ideConfirmation) {
                    const result = await ideConfirmation;
                    if (result.status === 'accepted' && result.content) {
                        this.params.content = result.content;
                    }
                }
            },
            ideConfirmation,
        };
        return confirmationDetails;
    }
    async execute({ abortSignal: abortSignal, }) {
        const validationError = this.config.validatePathAccess(this.resolvedPath);
        if (validationError) {
            return {
                llmContent: validationError,
                returnDisplay: 'Error: Path not in workspace.',
                error: {
                    message: validationError,
                    type: ToolErrorType.PATH_NOT_IN_WORKSPACE,
                },
            };
        }
        const { content, ai_proposed_content, modified_by_user } = this.params;
        const correctedContentResult = await getCorrectedFileContent(this.config, this.resolvedPath, content, abortSignal);
        if (correctedContentResult.error) {
            const errDetails = correctedContentResult.error;
            const errorMsg = errDetails.code
                ? `Error checking existing file '${this.resolvedPath}': ${errDetails.message} (${errDetails.code})`
                : `Error checking existing file: ${errDetails.message}`;
            return {
                llmContent: errorMsg,
                returnDisplay: errorMsg,
                error: {
                    message: errorMsg,
                    type: ToolErrorType.FILE_WRITE_FAILURE,
                },
            };
        }
        const { originalContent, correctedContent: fileContent, fileExists, } = correctedContentResult;
        // fileExists is true if the file existed (and was readable or unreadable but caught by readError).
        // fileExists is false if the file did not exist (ENOENT).
        const isNewFile = !fileExists ||
            (correctedContentResult.error !== undefined &&
                !correctedContentResult.fileExists);
        try {
            const dirName = path.dirname(this.resolvedPath);
            try {
                await fsPromises.access(dirName);
            }
            catch {
                await fsPromises.mkdir(dirName, { recursive: true });
            }
            let finalContent = fileContent;
            const useCRLF = !isNewFile && originalContent
                ? detectLineEnding(originalContent) === '\r\n'
                : os.EOL === '\r\n';
            if (useCRLF) {
                finalContent = finalContent.replace(/\r?\n/g, '\r\n');
            }
            await this.config
                .getFileSystemService()
                .writeTextFile(this.resolvedPath, finalContent);
            // Generate diff for display result
            const fileName = path.basename(this.resolvedPath);
            // If there was a readError, originalContent in correctedContentResult is '',
            // but for the diff, we want to show the original content as it was before the write if possible.
            // However, if it was unreadable, currentContentForDiff will be empty.
            const currentContentForDiff = correctedContentResult.error
                ? '' // Or some indicator of unreadable content
                : originalContent;
            const fileDiff = Diff.createPatch(fileName, currentContentForDiff, fileContent, 'Original', 'Written', DEFAULT_DIFF_OPTIONS);
            const originallyProposedContent = ai_proposed_content || content;
            const diffStat = getDiffStat(fileName, currentContentForDiff, originallyProposedContent, content);
            const llmSuccessMessageParts = [
                isNewFile
                    ? `Successfully created and wrote to new file: ${this.resolvedPath}.`
                    : `Successfully overwrote file: ${this.resolvedPath}.`,
            ];
            if (modified_by_user) {
                llmSuccessMessageParts.push(`User modified the \`content\` to be: ${content}`);
            }
            // Return a diff of the file before and after the write so that the agent
            // can avoid the need to spend a turn doing a verification read.
            const snippet = getDiffContextSnippet(isNewFile ? '' : originalContent, finalContent, 5);
            llmSuccessMessageParts.push(`Here is the updated code:\n${snippet}`);
            // Log file operation for telemetry (without diff_stat to avoid double-counting)
            const mimetype = getSpecificMimeType(this.resolvedPath);
            const programmingLanguage = getLanguageFromFilePath(this.resolvedPath);
            const extension = path.extname(this.resolvedPath);
            const operation = isNewFile ? FileOperation.CREATE : FileOperation.UPDATE;
            logFileOperation(this.config, new FileOperationEvent(WRITE_FILE_TOOL_NAME, operation, fileContent.split('\n').length, mimetype, extension, programmingLanguage));
            const displayResult = {
                fileDiff,
                fileName,
                filePath: this.resolvedPath,
                originalContent: correctedContentResult.originalContent,
                newContent: correctedContentResult.correctedContent,
                diffStat,
                isNewFile,
            };
            // Discover JIT subdirectory context for the written file path
            const jitContext = await discoverJitContext(this.config, this.resolvedPath);
            let llmContent = llmSuccessMessageParts.join(' ');
            if (jitContext) {
                llmContent = appendJitContext(llmContent, jitContext);
            }
            return {
                llmContent,
                returnDisplay: displayResult,
            };
        }
        catch (error) {
            // Capture detailed error information for debugging
            let errorMsg;
            let errorType = ToolErrorType.FILE_WRITE_FAILURE;
            if (isNodeError(error)) {
                // Handle specific Node.js errors with their error codes
                errorMsg = `Error writing to file '${this.resolvedPath}': ${error.message} (${error.code})`;
                // Log specific error types for better debugging
                if (error.code === 'EACCES') {
                    errorMsg = `Permission denied writing to file: ${this.resolvedPath} (${error.code})`;
                    errorType = ToolErrorType.PERMISSION_DENIED;
                }
                else if (error.code === 'ENOSPC') {
                    errorMsg = `No space left on device: ${this.resolvedPath} (${error.code})`;
                    errorType = ToolErrorType.NO_SPACE_LEFT;
                }
                else if (error.code === 'EISDIR') {
                    errorMsg = `Target is a directory, not a file: ${this.resolvedPath} (${error.code})`;
                    errorType = ToolErrorType.TARGET_IS_DIRECTORY;
                }
                // Include stack trace in debug mode for better troubleshooting
                if (this.config.getDebugMode() && error.stack) {
                    debugLogger.error('Write file error stack:', error.stack);
                }
            }
            else if (error instanceof Error) {
                errorMsg = `Error writing to file: ${error.message}`;
            }
            else {
                errorMsg = `Error writing to file: ${String(error)}`;
            }
            return {
                llmContent: errorMsg,
                returnDisplay: errorMsg,
                error: {
                    message: errorMsg,
                    type: errorType,
                },
            };
        }
    }
}
/**
 * Implementation of the WriteFile tool logic
 */
export class WriteFileTool extends BaseDeclarativeTool {
    config;
    static Name = WRITE_FILE_TOOL_NAME;
    constructor(config, messageBus) {
        super(WriteFileTool.Name, WRITE_FILE_DISPLAY_NAME, WRITE_FILE_DEFINITION.base.description, Kind.Edit, WRITE_FILE_DEFINITION.base.parametersJsonSchema, messageBus, true, false);
        this.config = config;
    }
    validateToolParamValues(params) {
        const filePath = params.file_path;
        if (!filePath) {
            return `Missing or empty "file_path"`;
        }
        let resolvedPath;
        if (this.config.isPlanMode()) {
            try {
                resolvedPath = resolveAndValidatePlanPath(filePath, this.config.storage.getPlansDir(), this.config.getProjectRoot());
            }
            catch (err) {
                return err instanceof Error ? err.message : String(err);
            }
        }
        else {
            resolvedPath = path.resolve(this.config.getTargetDir(), filePath);
        }
        const validationError = this.config.validatePathAccess(resolvedPath);
        if (validationError) {
            return validationError;
        }
        try {
            if (fs.existsSync(resolvedPath)) {
                const stats = fs.lstatSync(resolvedPath);
                if (stats.isDirectory()) {
                    return `Path is a directory, not a file: ${resolvedPath}`;
                }
            }
        }
        catch (statError) {
            return `Error accessing path properties for validation: ${resolvedPath}. Reason: ${statError instanceof Error ? statError.message : String(statError)}`;
        }
        const omissionPlaceholders = detectOmissionPlaceholders(params.content);
        if (omissionPlaceholders.length > 0) {
            return "`content` contains an omission placeholder (for example 'rest of methods ...'). Provide complete file content.";
        }
        return null;
    }
    createInvocation(params, messageBus) {
        return new WriteFileToolInvocation(this.config, params, messageBus ?? this.messageBus, this.name, this.displayName);
    }
    getSchema(modelId) {
        return resolveToolDeclaration(WRITE_FILE_DEFINITION, modelId);
    }
    getModifyContext(abortSignal) {
        return {
            getFilePath: (params) => params.file_path,
            getCurrentContent: async (params) => {
                const correctedContentResult = await getCorrectedFileContent(this.config, params.file_path, params.content, abortSignal);
                return correctedContentResult.originalContent;
            },
            getProposedContent: async (params) => {
                const correctedContentResult = await getCorrectedFileContent(this.config, params.file_path, params.content, abortSignal);
                return correctedContentResult.correctedContent;
            },
            createUpdatedParams: (_oldContent, modifiedProposedContent, originalParams) => {
                const content = originalParams.content;
                return {
                    ...originalParams,
                    ai_proposed_content: content,
                    content: modifiedProposedContent,
                    modified_by_user: true,
                };
            },
        };
    }
}
//# sourceMappingURL=write-file.js.map