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

import fs from "node:fs/promises";
import path from "node:path";

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

const MAX_MATCHES = 500;
const MAX_FILE_SIZE = 1024 * 1024;

// TODO: Consider broadening this list or making it user-configurable
// Currently limited to common source/doc extensions for performance.
const SEARCH_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".json", ".md", ".txt"]);

export class GrepTool extends DroneTool {
  get name(): string {
    return "grep";
  }

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

  public definition: IToolDefinition = {
    type: "function",
    function: {
      name: this.name,
      description: "Search project 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, relative to the project root.",
          },
          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(args: IToolArguments, logger: IAiLogger): Promise<string> {
    const pattern = typeof args.pattern === "string" ? args.pattern : undefined;
    const searchPath = typeof args.path === "string" ? args.path : undefined;
    const caseInsensitive = args.caseInsensitive === true;
    const contextBefore = typeof args.contextBefore === "number" ? args.contextBefore : 0;
    const contextAfter = typeof args.contextAfter === "number" ? args.contextAfter : 0;

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

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

    const resolved = resolveProjectPath(this.toolbox, searchPath);
    if (typeof resolved === "string") return resolved;

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

    const projectRoot = getProjectRoot(this.toolbox);

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

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

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

      if (matches.length === 0) {
        return `No matches found for "${pattern}" in ${resolved.displayPath}`;
      }

      const truncated = matches.length > MAX_MATCHES;
      const lines: string[] = [
        `Found ${matches.length} match(es) for "${pattern}" in ${resolved.displayPath}${truncated ? ` (showing first ${MAX_MATCHES})` : ""}:`,
        "",
      ];

      for (const match of limited) {
        const displayFile = projectRoot ? path.relative(projectRoot, match.file) : match.file;
        lines.push(`${displayFile}:${match.line}: ${match.content}`);
      }

      if (truncated) {
        lines.push("", `... and ${matches.length - MAX_MATCHES} more matches`);
      }

      return lines.join("\n");
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      logger.error("failed to search", { path: resolved.displayPath, pattern, error: errorMessage });
      return toolError({
        code: "OPERATION_FAILED",
        message: `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 }> = [];

    try {
      const stat = await fs.stat(filePath);
      if (stat.size > MAX_FILE_SIZE) return matches;
    } catch {
      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 (SEARCH_EXTENSIONS.has(ext)) {
          const fileMatches = await this.searchFile(fullPath, regex, contextBefore, contextAfter);
          matches.push(...fileMatches);
        }
      }

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