// 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_ENTRIES = 1000;

export class ListTool extends DroneTool {
  get name(): string {
    return "list";
  }

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

  public definition: IToolDefinition = {
    type: "function",
    function: {
      name: this.name,
      description: "List project directory contents with optional filtering. Shows file types, sizes, and modification dates. Supports filtering by glob pattern.",
      parameters: {
        type: "object",
        properties: {
          path: {
            type: "string",
            description: "Directory path to list, relative to the project root. Defaults to project root.",
          },
          pattern: {
            type: "string",
            description: "Optional glob pattern to filter results (e.g., '*.ts', 'src/**/*').",
          },
          recursive: {
            type: "boolean",
            description: "List subdirectories recursively (default: false).",
          },
          showHidden: {
            type: "boolean",
            description: "Show hidden files (files starting with dot) (default: false).",
          },
          maxDepth: {
            type: "number",
            description: "Maximum directory depth for recursive listing (default: 3).",
          },
        },
        required: [],
      },
    },
  };

  public async execute(args: IToolArguments, logger: IAiLogger): Promise<string> {
    const projectRoot = getProjectRoot(this.toolbox);
    if (!projectRoot) {
      return toolError({
        code: "OPERATION_NOT_ALLOWED",
        message: "No active project workspace is configured for file tools.",
        recoveryHint: "Run this tool during an active Agent work order.",
      });
    }

    let targetPath: string;
    if (args.path !== undefined && typeof args.path === "string") {
      const resolved = resolveProjectPath(this.toolbox, args.path);
      if (typeof resolved === "string") return resolved;
      targetPath = resolved.absolutePath;
    } else {
      targetPath = path.resolve(projectRoot);
    }

    const patternStr = typeof args.pattern === "string" ? args.pattern : undefined;
    let pattern: RegExp | undefined;
    if (patternStr) {
      try {
        const globPattern = patternStr
          .replace(/[.+^${}()|[\]\\]/g, "\\$&")
          .replace(/\*/g, ".*")
          .replace(/\?/g, ".");
        pattern = new RegExp("^" + globPattern + "$");
      } catch {
        // invalid glob pattern — skip filtering
      }
    }

    const recursive = args.recursive === true;
    const showHidden = args.showHidden === true;
    const maxDepth = typeof args.maxDepth === "number" ? args.maxDepth : 3;
    const displayPath = path.relative(projectRoot, targetPath) || ".";

    try {
      const stat = await fs.stat(targetPath);
      if (!stat.isDirectory()) {
        return toolError({
          code: "INVALID_PARAMETER",
          message: `"${displayPath}" is not a directory.`,
          parameter: "path",
          recoveryHint: "Provide a directory path to list.",
        });
      }

      const entries = await this.listDirectory(targetPath, pattern, recursive, showHidden, 0, maxDepth, projectRoot);
      const limited = entries.slice(0, MAX_ENTRIES);

      if (entries.length === 0) {
        return `No entries found in "${displayPath}"`;
      }

      const truncated = entries.length > MAX_ENTRIES;
      const lines: string[] = [
        `Contents of "${displayPath}" (${truncated ? `showing first ${MAX_ENTRIES} of ` : ""}${entries.length} entries):`,
        "",
      ];

      for (const entry of limited) {
        const typeIndicator = entry.isDirectory ? "d" : entry.isSymlink ? "l" : "-";
        const size = entry.isDirectory ? "-" : entry.size.toString();
        const modified = entry.modified ? new Date(entry.modified).toISOString().split("T")[0] : "-";
        lines.push(`${typeIndicator} ${size.padStart(10)} ${modified} ${entry.name}`);
      }

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

      return lines.join("\n");
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      logger.error("failed to list directory", { path: displayPath, error: errorMessage });
      return toolError({
        code: "OPERATION_FAILED",
        message: `Failed to list directory: ${errorMessage}`,
      });
    }
  }

  private async listDirectory(
    dir: string,
    pattern: RegExp | undefined,
    recursive: boolean,
    showHidden: boolean,
    depth: number,
    maxDepth: number,
    projectRoot: string,
  ): Promise<Array<{ name: string; isDirectory: boolean; isSymlink: boolean; size: number; modified: Date | null }>> {
    const results: Array<{ name: string; isDirectory: boolean; isSymlink: boolean; size: number; modified: Date | null }> = [];

    let entries;
    try {
      entries = await fs.readdir(dir, { withFileTypes: true });
    } catch {
      return results;
    }

    for (const entry of entries) {
      if (!showHidden && entry.name.startsWith(".")) continue;
      if (entry.name === "node_modules" || entry.name === ".git") continue;

      const fullPath = path.join(dir, entry.name);
      const relativePath = path.relative(projectRoot, fullPath);

      if (pattern && !pattern.test(relativePath) && !pattern.test(entry.name)) continue;

      let stat;
      try {
        stat = await fs.stat(fullPath);
      } catch {
        continue;
      }

      results.push({
        name: relativePath,
        isDirectory: entry.isDirectory(),
        isSymlink: entry.isSymbolicLink(),
        size: stat.size,
        modified: stat.mtime,
      });

      if (recursive && entry.isDirectory() && depth < maxDepth) {
        const subResults = await this.listDirectory(fullPath, pattern, recursive, showHidden, depth + 1, maxDepth, projectRoot);
        results.push(...subResults);
      }
    }

    return results;
  }
}
