// src/tools/file/edit.ts
// Copyright (C) 2025 DTP Technologies, LLC
// All Rights Reserved

import fs from "node:fs/promises";
import path from "node:path";
import type { ToolDefinition } from "../../lib/ai-client.js";

import {
  DtpTool,
  type ToolArguments,
  type ToolContext,
} from "../../lib/tool.js";
import { ChatSessionMode } from "@/models/chat-session.js";
import type { IChatFileOperation } from "../../models/chat-history.js";
import { validateProjectPath } from "../../lib/path-security.js";
import { PROJECT_ROOT } from "../../config/env.js";
import HostMonitorService from "../../services/host-monitor.js";

// Binary file extensions that should not be diffed or stored
const BINARY_EXTENSIONS = new Set([
  ".o",
  ".obj",
  ".a",
  ".lib",
  ".so",
  ".dll",
  ".exe",
  ".bin",
  ".png",
  ".jpg",
  ".jpeg",
  ".gif",
  ".bmp",
  ".ico",
  ".webp",
  ".pdf",
  ".zip",
  ".tar",
  ".gz",
  ".bz2",
  ".7z",
  ".wasm",
  ".pyc",
  ".class",
]);

function isBinaryPath(filePath: string): boolean {
  return BINARY_EXTENSIONS.has(path.extname(filePath).toLowerCase());
}

function countLineDelta(
  oldContent: string,
  newContent: string,
): { added: number; removed: number } {
  const oldLines = oldContent.split("\n");
  const newLines = newContent.split("\n");
  // Simple unified diff line count approximation
  let added = 0;
  let removed = 0;
  const maxLen = Math.max(oldLines.length, newLines.length);
  for (let i = 0; i < maxLen; i++) {
    const o = oldLines[i];
    const n = newLines[i];
    if (o === undefined && n !== undefined) {
      added++;
    } else if (n === undefined && o !== undefined) {
      removed++;
    } else if (o !== n) {
      added++;
      removed++;
    }
  }
  return { added, removed };
}

function buildUnifiedDiff(
  oldContent: string,
  newContent: string,
  filePath: string,
): string {
  const oldLines = oldContent.split("\n");
  const newLines = newContent.split("\n");
  const lines: string[] = [];
  lines.push(`--- a/${filePath}`);
  lines.push(`+++ b/${filePath}`);
  const maxLen = Math.max(oldLines.length, newLines.length);
  for (let i = 0; i < maxLen; i++) {
    const o = oldLines[i];
    const n = newLines[i];
    if (o === undefined) {
      lines.push(`+${n ?? ""}`);
    } else if (n === undefined) {
      lines.push(`-${o}`);
    } else if (o !== n) {
      lines.push(`-${o}`);
      lines.push(`+${n}`);
    } else {
      lines.push(` ${o}`);
    }
  }
  return lines.join("\n");
}

export class FileEditTool extends DtpTool {
  get name(): string {
    return "FileEditTool";
  }
  get slug(): string {
    return "file-edit";
  }
  get metadata() {
    return {
      name: this.definition.function.name || "file_edit",
      category: "file",
      tags: ["file", "edit", "replace", "modify", "search"],
      modes: [
        ChatSessionMode.Plan,
        ChatSessionMode.Build,
        ChatSessionMode.Test,
        ChatSessionMode.Ship,
        ChatSessionMode.Develop,
      ],
    };
  }

  public definition: ToolDefinition = {
    type: "function",
    function: {
      name: "file_edit",
      description:
        "Perform a search-and-replace edit on an existing file. Replaces the first occurrence of the search string with the replace string. Returns a diff context showing what changed.",
      parameters: {
        type: "object",
        properties: {
          path: {
            type: "string",
            description: "Path to the file to edit (relative or absolute).",
          },
          search: {
            type: "string",
            description:
              "The exact text to search for (first occurrence will be replaced).",
          },
          replace: {
            type: "string",
            description: "The text to replace the search string with.",
          },
        },
        required: ["path", "search", "replace"],
      },
    },
  };

  public async execute(
    _context: ToolContext,
    args: ToolArguments,
  ): Promise<string> {
    const filePath = args.path as string | undefined;
    const search = args.search as string | undefined;
    const replace = args.replace as string | undefined;

    if (!filePath || filePath.trim().length === 0) {
      return this.error("MISSING_PARAMETER", "File path must not be empty.", {
        parameter: "path",
        recoveryHint: "Provide a valid file path.",
      });
    }

    if (search === undefined || search.length === 0) {
      return this.error(
        "MISSING_PARAMETER",
        "Search string must not be empty.",
        {
          parameter: "search",
          recoveryHint: "Provide the exact text to search for.",
        },
      );
    }

    if (replace === undefined) {
      return this.error(
        "MISSING_PARAMETER",
        "Replace string must not be undefined.",
        {
          parameter: "replace",
          recoveryHint:
            "Provide the replacement text (use empty string to delete).",
        },
      );
    }

    // Validate path security - prevent path traversal attacks
    const pathValidation = validateProjectPath(filePath, PROJECT_ROOT);
    if (!pathValidation.valid || !pathValidation.resolvedPath) {
      return this.error(
        "SECURITY_VIOLATION",
        pathValidation.error || "Invalid path",
        {
          parameter: "path",
          recoveryHint:
            "Provide a valid relative path within the project directory.",
        },
      );
    }

    const resolvedPath = pathValidation.resolvedPath;

    try {
      const content = await fs.readFile(resolvedPath, "utf-8");

      const searchIdx = content.indexOf(search);
      if (searchIdx === -1) {
        // Enhancement B: Show file content context on NOT_FOUND
        const contextInfo = this.buildNotFoundContext(content, search);
        return this.error(
          "NOT_FOUND",
          `Search string not found in ${filePath}.${contextInfo}`,
          {
            parameter: "search",
            recoveryHint:
              "Verify your search string matches exactly, including whitespace and line endings.",
          },
        );
      }

      const newContent = content.replace(search, replace);
      await fs.writeFile(resolvedPath, newContent, "utf-8");

      const diffContext = this.buildDiffContext(
        content,
        searchIdx,
        search,
        newContent,
      );

      const output = `File edited: ${filePath}\n\n${diffContext}`;

      // Build file operation metadata for the session panel
      const binary = isBinaryPath(filePath);
      const fileOperation: IChatFileOperation = {
        type: "edit",
        path: filePath,
        isBinary: binary,
      };
      if (!binary) {
        const delta = countLineDelta(content, newContent);
        fileOperation.linesAdded = delta.added;
        fileOperation.linesRemoved = delta.removed;
        fileOperation.diff = buildUnifiedDiff(content, newContent, filePath);
      }

      const byteCount = Buffer.byteLength(output, "utf-8");
      HostMonitorService.fileOperation(byteCount);
      HostMonitorService.toolCall(byteCount);

      // Return plain text format instead of JSON
      const plainTextResponse = `PATH: ${filePath}
FILE OPERATION: edit
SEARCH FOUND: true
---
${output}`;

      return plainTextResponse;
    } catch (error) {
      const errorMessage =
        error instanceof Error ? error.message : String(error);

      if (errorMessage.includes("ENOENT")) {
        return this.error("NOT_FOUND", `File not found: ${filePath}`, {
          parameter: "path",
          recoveryHint:
            "Check the file path and ensure the file exists. Use file_write to create it.",
        });
      }

      this.log.error("Failed to edit file", {
        path: filePath,
        error: errorMessage,
      });

      return this.error(
        "OPERATION_FAILED",
        `Failed to edit file: ${errorMessage}`,
      );
    }
  }

  /**
   * Build context information when search string is not found.
   * Shows file content around the approximate location where the search might have been expected.
   */
  private buildNotFoundContext(content: string, search: string): string {
    const lines = content.split("\n");
    const searchLower = search.toLowerCase();
    const searchWords = searchLower.split(/\s+/).filter((w) => w.length > 2);

    // Try to find a line that contains some words from the search
    let bestMatchLine = -1;
    let bestMatchScore = 0;

    for (let i = 0; i < lines.length; i++) {
      const line = lines[i];
      if (line === undefined) continue;
      const lineLower = line.toLowerCase();
      let score = 0;
      for (const word of searchWords) {
        if (lineLower.includes(word)) {
          score++;
        }
      }
      if (score > bestMatchScore) {
        bestMatchScore = score;
        bestMatchLine = i;
      }
    }

    if (bestMatchLine === -1) {
      // No similar content found, show first few lines
      const previewLines = lines.slice(0, 5);
      const preview = previewLines
        .map((line, i) => `  ${i + 1}: ${line ?? ""}`)
        .join("\n");
      return `\n\nFile content (first 5 lines):\n${preview}`;
    }

    // Show context around the best match
    const contextStart = Math.max(0, bestMatchLine - 2);
    const contextEnd = Math.min(lines.length, bestMatchLine + 3);
    const contextLines = lines.slice(contextStart, contextEnd);
    const context = contextLines
      .map((line, i) => `  ${contextStart + i + 1}: ${line ?? ""}`)
      .join("\n");

    return `\n\nFile content around line ${bestMatchLine + 1} (possible match location):\n${context}`;
  }

  private buildDiffContext(
    original: string,
    matchStart: number,
    search: string,
    newContent: string,
  ): string {
    const contextLines = 2;
    const oldLines = original.split("\n");
    const newLines = newContent.split("\n");

    // Find the starting line of the match
    let charOffset = 0;
    let matchStartLine = 0;
    for (let i = 0; i < oldLines.length; i++) {
      const line = oldLines[i];
      if (line === undefined) break;
      const lineLen = line.length + 1; // +1 for newline
      if (charOffset + lineLen > matchStart) {
        matchStartLine = i;
        break;
      }
      charOffset += lineLen;
    }

    // Calculate how many lines the search string spans
    const searchLines = search.split("\n");
    const matchEndLine = matchStartLine + searchLines.length - 1;

    // Enhancement A: Show all affected lines for multi-line changes
    const affectedStartLine = Math.max(0, matchStartLine - contextLines);

    const diffLines: string[] = [];

    // Add summary header
    const numChangedLines = matchEndLine - matchStartLine + 1;
    if (numChangedLines === 1) {
      const lineNum = matchStartLine + 1;
      const oldLineText = oldLines[matchStartLine] ?? "";
      const newLineText = newLines[matchStartLine] ?? "";
      const oldLen = oldLineText.length;
      const newLen = newLineText.length;

      diffLines.push(`Changed line ${lineNum}:`);
      diffLines.push(`  Removed (${oldLen} chars): ${oldLineText}`);
      diffLines.push(`  Added (${newLen} chars): ${newLineText}`);
    } else {
      diffLines.push(
        `Changed lines ${matchStartLine + 1}-${matchEndLine + 1}:`,
      );
      diffLines.push(`  Search spanned ${numChangedLines} lines`);
      diffLines.push(`  --- Old:`);
      for (let i = matchStartLine; i <= matchEndLine; i++) {
        const oldLine = oldLines[i];
        if (oldLine !== undefined) {
          diffLines.push(`  ${i + 1}: ${oldLine}`);
        }
      }
      diffLines.push(`  --- New:`);
      // For multi-line replacements, show the corresponding new lines
      const numReplaceLines = searchLines.length;
      for (let i = 0; i < numReplaceLines; i++) {
        const newLineIdx = matchStartLine + i;
        const newLine = newLines[newLineIdx];
        if (newLine !== undefined) {
          diffLines.push(`  ${newLineIdx + 1}: ${newLine}`);
        }
      }
    }

    // Add context before (if any)
    if (matchStartLine > affectedStartLine) {
      diffLines.push("");
      diffLines.push("Context before:");
      for (let i = affectedStartLine; i < matchStartLine; i++) {
        const ctxLine = oldLines[i];
        if (ctxLine !== undefined) {
          diffLines.push(`  ${i + 1}: ${ctxLine}`);
        }
      }
    }

    // Add context after (if any)
    const actualEndLine = Math.min(
      newLines.length,
      matchEndLine + contextLines + 1,
    );
    if (matchEndLine + 1 < actualEndLine) {
      diffLines.push("");
      diffLines.push("Context after:");
      for (let i = matchEndLine + 1; i < actualEndLine; i++) {
        const ctxLine = newLines[i];
        if (ctxLine !== undefined) {
          diffLines.push(`  ${i + 1}: ${ctxLine}`);
        }
      }
    }

    return diffLines.join("\n");
  }
}

export default new FileEditTool();
