// src/tools/search/grep.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 HostMonitorService from "../../services/host-monitor.js";

const MAX_MATCHES = 500;
const MAX_FILE_SIZE = 1024 * 1024; // 1MB

class GrepTool extends DtpTool {
  get name(): string {
    return "GrepTool";
  }
  get slug(): string {
    return "grep";
  }
  get metadata() {
    return {
      name: this.definition.function.name || "grep",
      category: "search",
      tags: ["search", "find", "regex", "content", "grep"],
      modes: [
        ChatSessionMode.Plan,
        ChatSessionMode.Build,
        ChatSessionMode.Test,
        ChatSessionMode.Ship,
        ChatSessionMode.Develop,
      ],
    };
  }

  public definition: ToolDefinition = {
    type: "function",
    function: {
      name: "grep",
      description:
        "Search file contents using regular expressions. Returns matching lines with file paths and line numbers. Supports case-insensitive matching and context lines.",
      parameters: {
        type: "object",
        properties: {
          pattern: {
            type: "string",
            description:
              "Regular expression pattern to search for. Use standard JavaScript regex syntax.",
          },
          path: {
            type: "string",
            description:
              "File or directory path to search in. Can be a specific file or directory.",
          },
          caseInsensitive: {
            type: "boolean",
            description: "Case insensitive search (default: false).",
          },
          contextBefore: {
            type: "number",
            description:
              "Number of lines to show before each match (default: 0).",
          },
          contextAfter: {
            type: "number",
            description:
              "Number of lines to show after each match (default: 0).",
          },
        },
        required: ["pattern", "path"],
      },
    },
  };

  public async execute(
    _context: ToolContext,
    args: ToolArguments,
  ): Promise<string> {
    const pattern = args.pattern as string | undefined;
    const searchPath = args.path as string | undefined;
    const caseInsensitive = (args.caseInsensitive as boolean) || false;
    const contextBefore = (args.contextBefore as number) || 0;
    const contextAfter = (args.contextAfter as number) || 0;

    if (!pattern || pattern.trim().length === 0) {
      return this.error("MISSING_PARAMETER", "pattern is required.", {
        parameter: "pattern",
        recoveryHint: "Provide a regex pattern to search for.",
      });
    }

    if (!searchPath || searchPath.trim().length === 0) {
      return this.error("MISSING_PARAMETER", "path is required.", {
        parameter: "path",
        recoveryHint: "Provide a file or directory path to search in.",
      });
    }

    let regex: RegExp;
    try {
      regex = new RegExp(pattern, caseInsensitive ? "i" : "");
    } catch (error) {
      return this.error(
        "INVALID_PARAMETER",
        `Invalid regex pattern: ${error instanceof Error ? error.message : String(error)}`,
        { parameter: "pattern" },
      );
    }

    try {
      const stat = await fs.stat(searchPath);
      const matches: Array<{
        file: string;
        line: number;
        content: string;
      }> = [];

      if (stat.isFile()) {
        const fileMatches = await this.searchFile(
          searchPath,
          regex,
          contextBefore,
          contextAfter,
        );
        matches.push(...fileMatches);
      } else if (stat.isDirectory()) {
        await this.searchDirectory(
          searchPath,
          regex,
          matches,
          contextBefore,
          contextAfter,
        );
      }

      const limited = matches.slice(0, MAX_MATCHES);

      let output = "";
      if (matches.length === 0) {
        output = `No matches found for "${pattern}" in ${searchPath}`;
      } else {
        const truncated = matches.length > MAX_MATCHES;
        output = `Found ${matches.length} match(es) for "${pattern}" in ${searchPath}${
          truncated ? ` (showing first ${MAX_MATCHES})` : ""
        }:\n\n`;

        for (const match of limited) {
          output += `${match.file}:${match.line}: ${match.content}\n`;
        }

        if (truncated) {
          output += `\n... and ${matches.length - MAX_MATCHES} more matches`;
        }
      }

      const byteCount = Buffer.byteLength(output, "utf-8");
      HostMonitorService.toolCall(byteCount);
      return this.success(
        {
          matches: limited,
          total: matches.length,
          truncated: matches.length > MAX_MATCHES,
        },
        output,
      );
    } catch (error) {
      const errorMessage =
        error instanceof Error ? error.message : String(error);
      return this.error(
        "OPERATION_FAILED",
        `Failed to search: ${errorMessage}`,
      );
    }
  }

  private async searchFile(
    filePath: string,
    regex: RegExp,
    contextBefore: number,
    contextAfter: number,
  ): Promise<Array<{ file: string; line: number; content: string }>> {
    const matches: Array<{ file: string; line: number; content: string }> = [];

    let stat;
    try {
      stat = await fs.stat(filePath);
    } catch {
      return matches;
    }

    if (stat.size > MAX_FILE_SIZE) {
      return matches;
    }

    let content: string;
    try {
      content = await fs.readFile(filePath, "utf-8");
    } catch {
      return matches;
    }

    const lines = content.split("\n");

    for (let i = 0; i < lines.length; i++) {
      const line = lines[i];
      if (line !== undefined && regex.test(line)) {
        const startLine = Math.max(0, i - contextBefore);
        const endLine = Math.min(lines.length - 1, i + contextAfter);

        for (let j = startLine; j <= endLine; j++) {
          const contextLine = lines[j];
          if (contextLine !== undefined) {
            matches.push({
              file: filePath,
              line: j + 1,
              content: contextLine,
            });
          }
        }
        regex.lastIndex = 0;
      }
    }

    return matches;
  }

  private async searchDirectory(
    dir: string,
    regex: RegExp,
    matches: Array<{ file: string; line: number; content: string }>,
    contextBefore: number,
    contextAfter: number,
  ): Promise<void> {
    let entries;
    try {
      entries = await fs.readdir(dir, { withFileTypes: true });
    } catch {
      return;
    }

    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name);

      if (entry.isDirectory()) {
        if (
          entry.name !== "node_modules" &&
          entry.name !== ".git" &&
          entry.name !== "dist" &&
          entry.name !== "build"
        ) {
          await this.searchDirectory(
            fullPath,
            regex,
            matches,
            contextBefore,
            contextAfter,
          );
        }
      } else if (entry.isFile()) {
        const ext = path.extname(entry.name);
        if (
          ext === ".ts" ||
          ext === ".tsx" ||
          ext === ".js" ||
          ext === ".jsx" ||
          ext === ".json" ||
          ext === ".md" ||
          ext === ".txt"
        ) {
          const fileMatches = await this.searchFile(
            fullPath,
            regex,
            contextBefore,
            contextAfter,
          );
          matches.push(...fileMatches);
        }
      }

      if (matches.length >= MAX_MATCHES * 2) {
        return;
      }
    }
  }
}

export default new GrepTool();
