/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import fs from 'node:fs/promises';
import * as Diff from 'diff';
import { coreEvents, debugLogger, getFileDiffFromResultDisplay, computeModelAddedAndRemovedLines, } from '@google/gemini-cli-core';
/**
 * Calculates file change statistics for a single turn.
 * A turn is defined as the sequence of messages starting after the given user message
 * and continuing until the next user message or the end of the conversation.
 *
 * @param conversation The full conversation record.
 * @param userMessage The starting user message for the turn.
 * @returns Statistics about lines added/removed and files touched, or null if no edits occurred.
 */
export function calculateTurnStats(conversation, userMessage) {
    const msgIndex = conversation.messages.indexOf(userMessage);
    if (msgIndex === -1)
        return null;
    let addedLines = 0;
    let removedLines = 0;
    const files = new Set();
    let hasEdits = false;
    // Look ahead until the next user message (single turn)
    for (let i = msgIndex + 1; i < conversation.messages.length; i++) {
        const msg = conversation.messages[i];
        if (msg.type === 'user')
            break; // Stop at next user message
        if (msg.type === 'gemini' && msg.toolCalls) {
            for (const toolCall of msg.toolCalls) {
                const fileDiff = getFileDiffFromResultDisplay(toolCall.resultDisplay);
                if (fileDiff) {
                    hasEdits = true;
                    const stats = fileDiff.diffStat;
                    const calculations = computeModelAddedAndRemovedLines(stats);
                    addedLines += calculations.addedLines;
                    removedLines += calculations.removedLines;
                    files.add(fileDiff.fileName);
                }
            }
        }
    }
    if (!hasEdits)
        return null;
    return {
        addedLines,
        removedLines,
        fileCount: files.size,
    };
}
/**
 * Calculates the cumulative file change statistics from a specific message
 * to the end of the conversation.
 *
 * @param conversation The full conversation record.
 * @param userMessage The message to start calculating impact from (exclusive).
 * @returns Aggregate statistics about lines added/removed and files touched, or null if no edits occurred.
 */
export function calculateRewindImpact(conversation, userMessage) {
    const msgIndex = conversation.messages.indexOf(userMessage);
    if (msgIndex === -1)
        return null;
    let addedLines = 0;
    let removedLines = 0;
    const files = new Set();
    const details = [];
    let hasEdits = false;
    // Look ahead to the end of conversation (cumulative)
    for (let i = msgIndex + 1; i < conversation.messages.length; i++) {
        const msg = conversation.messages[i];
        // Do NOT break on user message - we want total impact
        if (msg.type === 'gemini' && msg.toolCalls) {
            for (const toolCall of msg.toolCalls) {
                const fileDiff = getFileDiffFromResultDisplay(toolCall.resultDisplay);
                if (fileDiff) {
                    hasEdits = true;
                    const stats = fileDiff.diffStat;
                    const calculations = computeModelAddedAndRemovedLines(stats);
                    addedLines += calculations.addedLines;
                    removedLines += calculations.removedLines;
                    files.add(fileDiff.fileName);
                    details.push({
                        fileName: fileDiff.fileName,
                        diff: fileDiff.fileDiff,
                    });
                }
            }
        }
    }
    if (!hasEdits)
        return null;
    return {
        addedLines,
        removedLines,
        fileCount: files.size,
        details,
    };
}
/**
 * Reverts file changes made by the model from the end of the conversation
 * back to a specific target message.
 *
 * It iterates backwards through the conversation history and attempts to undo
 * any file modifications. It handles cases where the user might have subsequently
 * modified the file by attempting a smart patch (using the `diff` library).
 *
 * @param conversation The full conversation record.
 * @param targetMessageId The ID of the message to revert back to. Changes *after* this message will be undone.
 */
export async function revertFileChanges(conversation, targetMessageId) {
    const messageIndex = conversation.messages.findIndex((m) => m.id === targetMessageId);
    if (messageIndex === -1) {
        debugLogger.error('Requested message to rewind to was not found ');
        return;
    }
    // Iterate backwards from the end to the message being rewound (exclusive of the messageId itself)
    for (let i = conversation.messages.length - 1; i > messageIndex; i--) {
        const msg = conversation.messages[i];
        if (msg.type === 'gemini' && msg.toolCalls) {
            for (let j = msg.toolCalls.length - 1; j >= 0; j--) {
                const toolCall = msg.toolCalls[j];
                const fileDiff = getFileDiffFromResultDisplay(toolCall.resultDisplay);
                if (fileDiff) {
                    const { filePath, fileName, newContent, originalContent, isNewFile } = fileDiff;
                    try {
                        let currentContent = null;
                        try {
                            currentContent = await fs.readFile(filePath, 'utf8');
                        }
                        catch (e) {
                            // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
                            const error = e;
                            if ('code' in error && error.code === 'ENOENT') {
                                // File does not exist, which is fine in some revert scenarios.
                                debugLogger.debug(`File ${fileName} not found during revert, proceeding as it may be a new file deletion.`);
                            }
                            else {
                                // Other read errors are unexpected.
                                coreEvents.emitFeedback('error', `Error reading ${fileName} during revert: ${error.message}`, e);
                                // Continue to next tool call
                                return;
                            }
                        }
                        // 1. Exact Match: Safe to revert directly
                        if (currentContent === newContent) {
                            if (!isNewFile) {
                                await fs.writeFile(filePath, originalContent ?? '');
                            }
                            else {
                                // Original content was null (new file), so we delete the file
                                await fs.unlink(filePath);
                            }
                        }
                        // 2. Mismatch: Attempt Smart Revert (Patch)
                        else if (currentContent !== null) {
                            const originalText = originalContent ?? '';
                            // Create a patch that transforms Agent -> Original
                            const undoPatch = Diff.createPatch(fileName, newContent, originalText);
                            // Apply that patch to the Current content
                            const patchedContent = Diff.applyPatch(currentContent, undoPatch);
                            if (typeof patchedContent === 'string') {
                                if (patchedContent === '' && isNewFile) {
                                    // If the result is empty and the file didn't exist originally, delete it
                                    await fs.unlink(filePath);
                                }
                                else {
                                    await fs.writeFile(filePath, patchedContent);
                                }
                            }
                            else {
                                // Patch failed
                                coreEvents.emitFeedback('warning', `Smart revert for ${fileName} failed. The file may have been modified in a way that conflicts with the undo operation.`);
                            }
                        }
                        else {
                            // File was deleted by the user, but we expected content.
                            // This can happen if a file created by the agent is deleted before rewind.
                            coreEvents.emitFeedback('warning', `Cannot revert changes for ${fileName} because it was not found on disk. This is expected if a file created by the agent was deleted before rewind`);
                        }
                    }
                    catch (e) {
                        coreEvents.emitFeedback('error', `An unexpected error occurred while reverting ${fileName}.`, e);
                    }
                }
            }
        }
    }
}
//# sourceMappingURL=rewindFileOps.js.map