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

import { exec } from "node:child_process";
import path from "node:path";
import { promisify } from "node:util";

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

const execAsync = promisify(exec);
const DEFAULT_TIMEOUT = 30_000;
const MAX_TIMEOUT = 120_000;

export class ShellExecTool extends DroneTool {
  get name(): string {
    return "shell_exec";
  }

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

  public definition: IToolDefinition = {
    type: "function",
    function: {
      name: "shell_exec",
      description: "Execute a shell command in the project directory using Bash. Returns stdout, stderr, and exit code. For temporary/scratch work, create and use a tmp directory inside the .gadget workspace directory.",
      parameters: {
        type: "object",
        properties: {
          command: {
            type: "string",
            description: "The shell command to execute with Bash.",
          },
          cwd: {
            type: "string",
            description: "Working directory for the command, relative to the project root. Defaults to the project root.",
          },
          timeout: {
            type: "number",
            description: `Timeout in milliseconds. Default: ${DEFAULT_TIMEOUT / 1000}s, max: ${MAX_TIMEOUT / 1000}s.`,
          },
        },
        required: ["command"],
      },
    },
  };

  public async execute(args: IToolArguments, logger: IAiLogger): Promise<string> {
    const command = args.command;
    if (typeof command !== "string" || command.trim().length === 0) {
      return toolError({
        code: "MISSING_PARAMETER",
        message: "Command must not be empty.",
        parameter: "command",
        recoveryHint: "Provide a valid shell command to execute with Bash.",
      });
    }

    const projectRoot = getProjectRoot(this.toolbox);
    if (!projectRoot) {
      return toolError({
        code: "OPERATION_NOT_ALLOWED",
        message: "No active project workspace is configured for shell execution.",
        recoveryHint: "Run this tool during an active Agent work order.",
      });
    }

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

    const timeout = Math.min(
      typeof args.timeout === "number" ? args.timeout : DEFAULT_TIMEOUT,
      MAX_TIMEOUT,
    );

    try {
      const { stdout, stderr } = await execAsync(command, {
        cwd: resolvedCwd,
        shell: "/bin/bash",
        timeout,
        maxBuffer: 1024 * 1024 * 10,
      });

      return this.formatOutput(command, stdout, stderr, 0);
    } catch (error) {
      if (error instanceof Error) {
        const nodeError = error as NodeJS.ErrnoException & {
          code?: string;
          stdout?: string;
          stderr?: string;
        };

        if (nodeError.code === "ETIMEDOUT" || nodeError.message.includes("timeout")) {
          return toolError({
            code: "TIMEOUT",
            message: `Command timed out after ${timeout / 1000}s: ${command}`,
            recoveryHint: `Increase the timeout parameter (max ${MAX_TIMEOUT / 1000}s) or break the command into smaller steps.`,
          });
        }

        const stdout = nodeError.stdout ?? "";
        const stderr = nodeError.stderr ?? nodeError.message;
        const exitCode = typeof nodeError.code === "string"
          ? parseInt(nodeError.code.replace("ERR_", ""), 10)
          : 1;

        return this.formatOutput(
          command,
          stdout,
          stderr,
          isNaN(exitCode) ? 1 : exitCode,
        );
      }

      logger.error("failed to execute command", { command, error: String(error) });
      return toolError({
        code: "OPERATION_FAILED",
        message: `Failed to execute command: ${String(error)}`,
      });
    }
  }

  private formatOutput(
    command: string,
    stdout: string,
    stderr: string,
    exitCode: number,
  ): string {
    return [
      `COMMAND: ${command}`,
      `EXIT CODE: ${exitCode}`,
      "---[stdout]",
      stdout || "N/A",
      "---[stderr]",
      stderr || "N/A",
    ].join("\n");
  }
}
