/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import type {
  ConversationRecord,
  MessageRecord,
} from '@google/gemini-cli-core';
import fs from 'node:fs/promises';
import * as Diff from 'diff';
import {
  coreEvents,
  debugLogger,
  getFileDiffFromResultDisplay,
  computeModelAddedAndRemovedLines,
} from '@google/gemini-cli-core';

export interface FileChangeDetail {
  fileName: string;
  diff: string;
}

export interface FileChangeStats {
  addedLines: number;
  removedLines: number;
  fileCount: number;
  details?: FileChangeDetail[];
}

/**
 * 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: ConversationRecord,
  userMessage: MessageRecord,
): FileChangeStats | null {
  const msgIndex = conversation.messages.indexOf(userMessage);
  if (msgIndex === -1) return null;

  let addedLines = 0;
  let removedLines = 0;
  const files = new Set<string>();
  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: ConversationRecord,
  userMessage: MessageRecord,
): FileChangeStats | null {
  const msgIndex = conversation.messages.indexOf(userMessage);
  if (msgIndex === -1) return null;

  let addedLines = 0;
  let removedLines = 0;
  const files = new Set<string>();
  const details: FileChangeDetail[] = [];
  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: ConversationRecord,
  targetMessageId: string,
): Promise<void> {
  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: string | null = null;
            try {
              currentContent = await fs.readFile(filePath, 'utf8');
            } catch (e) {
              // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
              const error = e as Error;
              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,
            );
          }
        }
      }
    }
  }
}
