// src/tools/file/shell.ts
// Copyright (C) 2025 DTP Technologies, LLC
// All Rights Reserved

import { exec } from "node:child_process";
import { promisify } from "node:util";
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 { validateWorkingDirectory } from "../../lib/path-security.js";
import { PROJECT_ROOT } from "../../config/env.js";
import HostMonitorService from "../../services/host-monitor.js";

const execAsync = promisify(exec);

const DEFAULT_TIMEOUT = 30_000; // 30 seconds
const MAX_TIMEOUT = 120_000; // 120 seconds

export class ShellExecTool extends DtpTool {
  get name(): string {
    return "ShellExecTool";
  }
  get slug(): string {
    return "shell-exec";
  }
  get metadata() {
    return {
      name: this.definition.function.name || "shell_exec",
      category: "file",
      tags: ["shell", "command", "exec", "run", "process"],
      modes: [
        ChatSessionMode.Plan,
        ChatSessionMode.Build,
        ChatSessionMode.Test,
        ChatSessionMode.Ship,
        ChatSessionMode.Develop,
      ],
    };
  }

  public definition: ToolDefinition = {
    type: "function",
    function: {
      name: "shell_exec",
      description:
        "Execute a shell command and capture its output. Returns stdout, stderr, and exit code. Use with caution — commands run with the same permissions as this process.",
      parameters: {
        type: "object",
        properties: {
          command: {
            type: "string",
            description: "The shell command to execute.",
          },
          cwd: {
            type: "string",
            description:
              "Working directory for the command. Defaults to current directory.",
          },
          timeout: {
            type: "number",
            description: `Timeout in milliseconds. Default: ${DEFAULT_TIMEOUT / 1000}s, max: ${MAX_TIMEOUT / 1000}s.`,
          },
        },
        required: ["command"],
      },
    },
  };

  public async execute(
    _context: ToolContext,
    args: ToolArguments,
  ): Promise<string> {
    const command = args.command as string | undefined;

    if (!command || command.trim().length === 0) {
      return this.error("MISSING_PARAMETER", "Command must not be empty.", {
        parameter: "command",
        recoveryHint: "Provide a valid shell command to execute.",
      });
    }

    // Validate cwd if provided - prevent path traversal attacks
    let resolvedCwd: string;
    if (args.cwd !== undefined) {
      const cwdPath = args.cwd as string;
      const cwdValidation = validateWorkingDirectory(cwdPath, PROJECT_ROOT);
      if (!cwdValidation.valid || !cwdValidation.resolvedPath) {
        return this.error(
          "SECURITY_VIOLATION",
          cwdValidation.error || "Invalid working directory",
          {
            parameter: "cwd",
            recoveryHint:
              "Provide a valid relative path within the project directory.",
          },
        );
      }
      resolvedCwd = cwdValidation.resolvedPath;
    } else {
      resolvedCwd = process.cwd();
    }

    const timeout = Math.min(
      (args.timeout as number | undefined) ?? DEFAULT_TIMEOUT,
      MAX_TIMEOUT,
    );

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

      const output = this.formatOutput(command, stdout, stderr, 0);
      const byteCount = Buffer.byteLength(output, "utf-8");
      HostMonitorService.toolCall(byteCount);
      return this.success({ command, exitCode: 0 }, output);
    } 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 this.error(
            "TIMEOUT",
            `Command timed out after ${timeout / 1000}s: ${command}`,
            {
              parameter: "timeout",
              recoveryHint: `Increase the timeout parameter (max ${MAX_TIMEOUT / 1000}s) or break the command into smaller steps.`,
            },
          );
        }

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

        const stdout = nodeError.stdout ?? "";
        const stderr = nodeError.stderr ?? nodeError.message;

        const output = this.formatOutput(
          command,
          stdout,
          stderr,
          isNaN(exitCode) ? 1 : exitCode,
        );
        const byteCount = Buffer.byteLength(output, "utf-8");
        HostMonitorService.toolCall(byteCount);
        return this.success(
          { command, exitCode: isNaN(exitCode) ? 1 : exitCode },
          output,
        );
      }

      this.log.error("Failed to execute command", {
        command,
        error: String(error),
      });

      return this.error(
        "OPERATION_FAILED",
        `Failed to execute command: ${String(error)}`,
      );
    }
  }

  private formatOutput(
    command: string,
    stdout: string,
    stderr: string,
    exitCode: number,
  ): string {
    const parts: string[] = [];

    parts.push(`COMMAND: ${command}`);
    parts.push(`EXIT CODE: ${exitCode}`);
    parts.push(`---[stdout]`);
    parts.push(stdout || "N/A");
    parts.push(`---[stderr]`);
    parts.push(stderr || "N/A");

    return parts.join("\n");
  }
}

export default new ShellExecTool();
