// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
// Licensed under the Apache License, Version 2.0

import fs from "node:fs/promises";

import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai";
import { DroneTool } from "../tool.ts";
import { toolError } from "../system/common.ts";
import { getGadgetDir, resolvePlanPath } from "./common.ts";

export class PlanFileEditTool extends DroneTool {
  get name(): string {
    return "plan_file_edit";
  }

  get category(): string {
    return "plan";
  }

  public definition: IToolDefinition = {
    type: "function",
    function: {
      name: this.name,
      description:
        "Perform an exact search-and-replace edit on a file in Gadget's .gadget directory. Replaces the first occurrence only and returns changed-line context. This Plan-mode tool works in Gadget's own directory (.gadget) for storing and updating plans, todos, and other knowledge. It does not affect project files.",
      parameters: {
        type: "object",
        properties: {
          path: { type: "string", description: "Path to the file to edit, relative to the .gadget directory." },
          search: { type: "string", description: "Exact text to search for. It must match whitespace and line endings exactly." },
          replace: { type: "string", description: "Replacement text. Empty string is allowed to delete the match." },
        },
        required: ["path", "search", "replace"],
      },
    },
  };

  public async execute(args: IToolArguments, logger: IAiLogger): Promise<string> {
    const filePath = args.path;
    const search = args.search;
    const replace = args.replace;

    if (typeof filePath !== "string" || filePath.trim().length === 0) {
      return toolError({
        code: "MISSING_PARAMETER",
        message: "File path must not be empty.",
        parameter: "path",
        recoveryHint: "Provide a valid file path within the .gadget directory.",
      });
    }
    if (typeof search !== "string" || search.length === 0) {
      return toolError({
        code: "MISSING_PARAMETER",
        message: "Search string must not be empty.",
        parameter: "search",
        recoveryHint: "Provide the exact text to search for.",
      });
    }
    if (typeof replace !== "string") {
      return toolError({
        code: "MISSING_PARAMETER",
        message: "Replace string must be a string and must not be undefined.",
        parameter: "replace",
        recoveryHint: "Provide the replacement text. Use an empty string to delete the match.",
      });
    }

    const resolved = resolvePlanPath(this.toolbox, filePath);
    if (typeof resolved === "string") return resolved;

    try {
      const content = await fs.readFile(resolved.absolutePath, "utf-8");
      const searchIdx = content.indexOf(search);
      if (searchIdx === -1) {
        const contextInfo = this.buildNotFoundContext(content, search);
        return toolError({
          code: "NOT_FOUND",
          message: `Search string not found in ${resolved.displayPath}.${contextInfo}`,
          parameter: "search",
          recoveryHint: "Verify your search string matches exactly, including whitespace and line endings.",
        });
      }

      const newContent = content.replace(search, replace);
      await fs.writeFile(resolved.absolutePath, newContent, "utf-8");
      const diffContext = this.buildDiffContext(content, searchIdx, search, newContent);

      return [
        `PATH: ${resolved.displayPath}`,
        "FILE OPERATION: edit",
        "SEARCH FOUND: true",
        "---",
        `File edited: ${resolved.displayPath}`,
        "",
        diffContext,
      ].join("\n");
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      if (errorMessage.includes("ENOENT")) {
        const gadgetDir = getGadgetDir(this.toolbox);
        return toolError({
          code: "NOT_FOUND",
          message: `File not found in .gadget: ${resolved.displayPath}${gadgetDir ? `. The .gadget directory exists but this file does not.` : ". Plan storage is empty. Create a file with plan_file_write first."}`,
          parameter: "path",
          recoveryHint: "Check the file path. Use plan_list to see available files, or plan_file_write to create the file first.",
        });
      }
      logger.error("failed to edit plan file", { path: resolved.displayPath, error: errorMessage });
      return toolError({
        code: "OPERATION_FAILED",
        message: `Failed to edit file: ${errorMessage}`,
      });
    }
  }

  private buildNotFoundContext(content: string, search: string): string {
    const lines = content.split("\n");
    const searchWords = search.toLowerCase().split(/\s+/).filter((word) => word.length > 2);
    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) {
      const preview = lines.slice(0, 5).map((line, index) => `  ${index + 1}: ${line}`).join("\n");
      return `\n\nFile content (first 5 lines):\n${preview}`;
    }

    const contextStart = Math.max(0, bestMatchLine - 2);
    const contextEnd = Math.min(lines.length, bestMatchLine + 3);
    const context = lines
      .slice(contextStart, contextEnd)
      .map((line, index) => `  ${contextStart + index + 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");
    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;
      if (charOffset + lineLen > matchStart) {
        matchStartLine = i;
        break;
      }
      charOffset += lineLen;
    }

    const searchLines = search.split("\n");
    const matchEndLine = matchStartLine + searchLines.length - 1;
    const affectedStartLine = Math.max(0, matchStartLine - contextLines);
    const diffLines: string[] = [];
    const numChangedLines = matchEndLine - matchStartLine + 1;

    if (numChangedLines === 1) {
      const lineNum = matchStartLine + 1;
      const oldLineText = oldLines[matchStartLine] ?? "";
      const newLineText = newLines[matchStartLine] ?? "";
      diffLines.push(`Changed line ${lineNum}:`);
      diffLines.push(`  Removed (${oldLineText.length} chars): ${oldLineText}`);
      diffLines.push(`  Added (${newLineText.length} 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 (let i = 0; i < searchLines.length; i++) {
        const newLineIdx = matchStartLine + i;
        const newLine = newLines[newLineIdx];
        if (newLine !== undefined) diffLines.push(`  ${newLineIdx + 1}: ${newLine}`);
      }
    }

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

    const actualEndLine = Math.min(newLines.length, matchEndLine + contextLines + 1);
    if (matchEndLine + 1 < actualEndLine) {
      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");
  }
}
