import { BestAgentError, normalizeWhitespace, truncateText } from "@best-agent/foundation";
import type { CapabilityRequest, ThreadMetadata, ToolResultEnvelope } from "@best-agent/protocol";
import { basename, relative, resolve } from "node:path";
import { z } from "zod";
import { redactSecretLikeTextForHistory } from "../../policy/capability-history.js";
import { readsSensitiveShellCommand } from "../../policy/runtime-policy.js";
import type { ToolDefinition, ToolEvidenceRecord, ToolExecutionContext } from "../contracts.js";
import { createTextOverflowPreview, countTextLines } from "../output-overflow.js";
import { typedToolInputSchema } from "../schema.js";
import { SHELL_COMMAND_HISTORY_SAFE_MAX_CHARS } from "../tool-history-input.js";

type ShellToolInput = { command: string; cwd?: string; timeout_ms?: number };

const DEFAULT_SHELL_TIMEOUT_MS = 2 * 60 * 1000;
const LONG_RUNNING_SHELL_TIMEOUT_MS = 5 * 60 * 1000;
const PYTHON_SCRIPT_SHELL_TIMEOUT_MS = 15 * 60 * 1000;
const TIMEOUT_WRAPPER_GRACE_MS = 1_000;
const SHELL_INLINE_MAX_BYTES = 2 * 1024;
const SHELL_INLINE_MAX_LINES = 120;
const SHELL_LIVE_OUTPUT_MAX_BYTES = 4 * 1024;
const SHELL_TRUNCATION_HINT =
  "[truncated] Shell output clipped to stay within the model context budget. Re-run a narrower command or inspect the relevant portion with grep, tail, or sed.";
const SHELL_BACKGROUND_TIMEOUT_HINT =
  "Background commands can keep shell pipes open and be killed on timeout. If this command should keep a long-lived service running, restart it detached with stdin/stdout/stderr redirected, then verify readiness with a separate short protocol or application-level command. A port check only proves listener liveness.";
const SHELL_UNBOUNDED_LOG_FOLLOW_MESSAGE =
  "The shell tool does not run unbounded log-following commands. Use tail -n, sed, grep, or another snapshot command; if you intentionally need a short follow, wrap it with timeout or provide timeout_ms.";
const SHELL_REDACTED_SECRET = "[redacted]";
const HIGH_CONFIDENCE_SENSITIVE_ENV_NAME_PATTERN =
  /(?:^|_)(?:API[_-]?KEY|TOKEN|SECRET|AUTH|CREDENTIAL|PRIVATE[_-]?KEY)(?:_|$)/iu;
const GENERIC_CREDENTIAL_ENV_NAME_PATTERN = /^(?:PASSWORD|PASS)$/iu;
const ENV_DUMP_COMMANDS = new Set(["env", "printenv"]);
const LONG_RUNNING_DIRECT_COMMANDS = new Set([
  "apt",
  "apt-get",
  "apk",
  "brew",
  "cargo",
  "cmake",
  "dnf",
  "gradle",
  "gradlew",
  "jest",
  "make",
  "mvn",
  "pacman",
  "pip",
  "pip3",
  "playwright",
  "pytest",
  "turbo",
  "uv",
  "vitest",
  "xcodebuild",
  "yum",
  "zypper",
]);
const LONG_RUNNING_PACKAGE_MANAGER_COMMANDS = new Set([
  "bun",
  "bunx",
  "npm",
  "npx",
  "nx",
  "pnpm",
  "pnpx",
  "yarn",
]);
const LONG_RUNNING_PACKAGE_MANAGER_TASKS = new Set([
  "bench",
  "benchmark",
  "build",
  "jest",
  "playwright",
  "test",
  "vitest",
]);
const SHELL_COMMAND_INPUT_SCHEMA = z.strictObject({
  command: z
    .string()
    .min(1)
    .describe(
      "Shell command string. Use this for pipes, chaining, redirection, globs, cd, and other shell syntax.",
    ),
  cwd: z.string().optional().describe("Optional workspace directory to run the command from."),
  timeout_ms: z.int().positive().optional().describe("Optional positive timeout in milliseconds."),
});
const SHELL_INPUT_SCHEMA = typedToolInputSchema<ShellToolInput>(SHELL_COMMAND_INPUT_SCHEMA);

export function createShellTool(): ToolDefinition<ShellToolInput> {
  return {
    description:
      "Run a terminal command in the workspace. Use command for normal shell syntax, including pipes, chaining, redirection, and cd. Each shell call is a separate process: cd, export, alias, and other shell-local state do not persist into later tool calls or external verifiers. To make a command available later, write a file or symlink into a directory already on PATH. Temporary global package installs are useful for this turn, but deliverable scripts should use available runtime dependencies, vendored code, or project-declared dependencies rather than relying on an ad hoc install. For multiline Python, Perl, Ruby, or shell scripts, use a quoted heredoc such as python3 - <<'PY' ... PY instead of embedding large code in -c quotes. Long-lived background services must detach and redirect stdin/stdout/stderr, then be verified with a separate protocol or application-level command; a port check only proves listener liveness. Safe inspection commands run automatically, while commands that change state may require approval.",
    effect: "execute",
    executionClass: "external-side-effect",
    idempotent: false,
    inputSchema: SHELL_INPUT_SCHEMA,
    name: "shell",
    replayPolicy: "never",
    source: "built-in",
    capabilities(input, context) {
      const normalized = input;
      const effectiveCwd = resolveShellCwd("shell", normalized.cwd, context.cwd);
      return createShellCapabilityRequests(resolveShellCapabilityArgv(normalized), effectiveCwd, {
        shellCommand: shouldExecuteWithShell(normalized.command),
      });
    },
    async execute(input, context) {
      const normalized = input;
      rejectUnboundedLogFollow(normalized);
      const argv = resolveShellArgv(normalized);
      const timeoutMs = resolveShellTimeoutMs(normalized);
      const effectiveCwd = resolveShellCwd("shell", normalized.cwd, context.cwd);
      if (!context.sandbox.isDirectory(effectiveCwd)) {
        throw new BestAgentError(
          "EXEC_INVALID",
          `The shell tool cwd must be an existing directory inside the workspace: ${normalized.cwd ?? context.cwd}`,
        );
      }
      try {
        const liveOutput = createShellLiveOutputReporter(context);
        const commandOptions = {
          ...(context.abortSignal ? { signal: context.abortSignal } : {}),
          ...(liveOutput
            ? {
                onHeartbeat: liveOutput.onHeartbeat,
                onOutput: liveOutput.onOutput,
              }
            : {}),
          timeoutMs,
          ...(context.sandboxProfile ? { sandboxProfile: context.sandboxProfile } : {}),
        };
        const capture = await context.sandbox
          .runCommandCapture(argv, effectiveCwd, commandOptions)
          .finally(() => liveOutput?.onDone());
        const shellOutput = buildShellExecutionPresentation(capture, normalized);
        context.setResultMetadata?.({
          envelope: shellOutput.envelope,
          forceTracePersistence: capture.timedOut || shellOutput.truncated,
          outputManaged: true,
          structuredContent: shellOutput.structuredContent,
          traceOutput: shellOutput.traceOutput,
        });
        if (capture.aborted) {
          throw createShellAbortError("Command aborted before completion.");
        }
        if (capture.timedOut) {
          throw new BestAgentError(
            "EXEC_FAILED",
            buildShellTimeoutFailureMessage(timeoutMs, normalized, shellOutput.output),
          );
        }
        if (capture.exitCode !== 0 || capture.signalCode) {
          const failureMessage =
            capture.signalCode || capture.exitCode === null
              ? buildShellFailureMessage(describeShellExitFailure(capture), shellOutput.output)
              : shellOutput.output.trim().length > 0
                ? shellOutput.output
                : describeShellExitFailure(capture);
          throw rewriteShellExecutionError(
            argv,
            normalized,
            new BestAgentError("EXEC_FAILED", failureMessage),
          );
        }
        return shellOutput.output;
      } catch (error) {
        throw rewriteShellExecutionError(argv, normalized, error);
      }
    },
    evidenceExtractor(input, _output, envelope) {
      const normalized = input;
      const sensitive = readsSensitiveShellCommand(normalized.command);
      const commandDisplay = renderShellEvidenceCommand(normalized);
      const commandForEvaluation = renderShellEvaluationCommand(normalized, sensitive);
      const commandForProvider = renderShellProviderEvidenceCommand(normalized, sensitive);
      return [
        {
          confidence: envelope.outcome === "success" ? 0.8 : 0.75,
          ...(envelope.outcome === "error" && envelope.failureKind
            ? { diagnosticCode: envelope.failureKind }
            : {}),
          kind: "tool-outcome",
          payload: {
            command: commandDisplay,
            commandDisplay,
            commandForEvaluation,
            commandForProvider,
            cwd: normalized.cwd,
            envelope: sensitive ? undefined : envelope,
            evidencePayload: sensitive ? undefined : envelope.evidencePayload,
            indexable: !sensitive,
            outcome: envelope.outcome,
            outputPreview: undefined,
            sensitivity: sensitive ? "sensitive-redacted" : "none",
            stderrSummary: sensitive ? undefined : envelope.stderrSummary,
            stdoutSummary: sensitive ? undefined : envelope.stdoutSummary,
          },
          summary: sensitive
            ? `Shell command ${envelope.outcome}: sensitive output omitted.`
            : commandForProvider
              ? `Shell command ${envelope.outcome}: ${truncateShellEvidence(commandForProvider)}.`
              : `Shell command ${envelope.outcome}: command omitted from provider evidence.`,
        } satisfies ToolEvidenceRecord,
      ];
    },
    formatInput(input) {
      const normalized = input;
      const renderedCommand = normalized.command;
      const suffixes = [
        normalized.cwd ? `cwd ${normalized.cwd}` : undefined,
        normalized.timeout_ms ? `timeout ${normalized.timeout_ms}ms` : undefined,
      ].filter(Boolean);
      return suffixes.length > 0 ? `${renderedCommand} (${suffixes.join(", ")})` : renderedCommand;
    },
  };
}

// ── Shell helpers ───────────────────────────────────────────────────────

function renderShellEvidenceCommand(input: ShellToolInput): string {
  const command = input.command;
  if (command.length <= SHELL_COMMAND_HISTORY_SAFE_MAX_CHARS && !command.includes("\n")) {
    return redactSecretLikeTextForHistory(command);
  }
  return `[command omitted from shell evidence: ${command.length} chars, ${Buffer.byteLength(
    command,
    "utf8",
  )} bytes]`;
}

function renderShellEvaluationCommand(
  input: ShellToolInput,
  sensitive: boolean,
): string | undefined {
  const command = input.command;
  if (
    sensitive ||
    command.length > SHELL_COMMAND_HISTORY_SAFE_MAX_CHARS ||
    command.includes("\n") ||
    containsExplicitShellSecret(command)
  ) {
    return undefined;
  }
  return command;
}

function renderShellProviderEvidenceCommand(
  input: ShellToolInput,
  sensitive: boolean,
): string | undefined {
  const command = renderShellEvaluationCommand(input, sensitive);
  if (!command) {
    return undefined;
  }
  return redactSecretLikeTextForHistory(command) === command ? command : undefined;
}

function containsExplicitShellSecret(command: string): boolean {
  return (
    /\b(?:Authorization|X-API-Key|API-Key|API_KEY|Access-Token|AccessToken)\b/iu.test(command) ||
    /\bBearer\s+[A-Za-z0-9._~+/-]{8,}\b/iu.test(command) ||
    /\bsk-[A-Za-z0-9_-]{8,}\b/u.test(command)
  );
}

function truncateShellEvidence(value: string): string {
  return truncateText(normalizeWhitespace(value), 160);
}

function createShellLiveOutputReporter(context: ToolExecutionContext):
  | {
      onDone(): void;
      onHeartbeat(): void;
      onOutput(output: { stream: "stderr" | "stdout"; text: string }): void;
    }
  | undefined {
  if (!context.reportOutput) {
    return undefined;
  }
  let done = false;
  return {
    onDone() {
      done = true;
    },
    onHeartbeat() {
      if (!done) {
        context.heartbeat();
      }
    },
    onOutput(output) {
      if (done) {
        return;
      }
      context.heartbeat();
      context.reportOutput?.({
        stream: output.stream,
        text: clipShellLiveOutput(output.text),
      });
    },
  };
}

function clipShellLiveOutput(text: string): string {
  if (Buffer.byteLength(text, "utf8") <= SHELL_LIVE_OUTPUT_MAX_BYTES) {
    return text;
  }
  return createTextOverflowPreview(text, {
    maxBytes: SHELL_LIVE_OUTPUT_MAX_BYTES,
    maxLines: 80,
    omissionMessage: "[truncated] Live shell output chunk clipped.",
  }).preview;
}

function resolveShellTimeoutMs(input: ShellToolInput): number {
  if (input.timeout_ms !== undefined) {
    return input.timeout_ms;
  }
  const argv = splitShellCommandPrefix(input.command);
  const wrapperTimeoutMs = parseTimeoutWrapperMs(argv);
  if (wrapperTimeoutMs !== undefined) {
    return Math.max(DEFAULT_SHELL_TIMEOUT_MS, wrapperTimeoutMs + TIMEOUT_WRAPPER_GRACE_MS);
  }
  if (isPythonScriptShellCommand(argv)) {
    return PYTHON_SCRIPT_SHELL_TIMEOUT_MS;
  }
  if (shellCommandContainsLongRunningCommand(input.command)) {
    return LONG_RUNNING_SHELL_TIMEOUT_MS;
  }
  return isLongRunningShellCommand(argv) ? LONG_RUNNING_SHELL_TIMEOUT_MS : DEFAULT_SHELL_TIMEOUT_MS;
}

function rejectUnboundedLogFollow(input: ShellToolInput): void {
  if (input.timeout_ms !== undefined) {
    return;
  }
  const argv = splitShellCommandPrefix(input.command);
  if (argv[0] === "timeout") {
    return;
  }
  if (argv[0] === "tail" && argv.slice(1).some(isTailFollowArg)) {
    throw new BestAgentError("EXEC_INVALID", SHELL_UNBOUNDED_LOG_FOLLOW_MESSAGE);
  }
  if (argv[0] === "journalctl" && argv.slice(1).some(isFollowFlag)) {
    throw new BestAgentError("EXEC_INVALID", SHELL_UNBOUNDED_LOG_FOLLOW_MESSAGE);
  }
  if (argv[0] === "watch") {
    throw new BestAgentError("EXEC_INVALID", SHELL_UNBOUNDED_LOG_FOLLOW_MESSAGE);
  }
  if (argv[0] === "less" && argv.slice(1).includes("+F")) {
    throw new BestAgentError("EXEC_INVALID", SHELL_UNBOUNDED_LOG_FOLLOW_MESSAGE);
  }
}

function isFollowFlag(arg: string): boolean {
  return arg === "-f" || arg === "--follow" || arg === "--follow=";
}

function isTailFollowArg(arg: string): boolean {
  return (
    arg === "--follow" || arg.startsWith("--follow=") || /^-[A-Za-z]*[fF][A-Za-z]*$/u.test(arg)
  );
}

function resolveShellCwd(
  toolName: string,
  inputCwd: string | undefined,
  workspaceCwd: string,
): string {
  if (!inputCwd) {
    return workspaceCwd;
  }
  const resolved = resolve(workspaceCwd, inputCwd);
  if (!isWithinRoot(resolved, workspaceCwd)) {
    throw new BestAgentError(
      "EXEC_INVALID",
      `The ${toolName} tool cwd must stay inside the workspace: ${JSON.stringify(inputCwd)}.`,
    );
  }
  return resolved;
}

function buildShellExecutionPresentation(
  capture: {
    exitCode: number | null;
    signalCode?: NodeJS.Signals | null;
    stderr: string;
    stdout: string;
    timedOut: boolean;
  },
  input: ShellToolInput,
): {
  envelope: Partial<ToolResultEnvelope>;
  output: string;
  structuredContent: ThreadMetadata;
  traceOutput: string;
  truncated: boolean;
} {
  const redactionOptions = { redactBareGenericCredentials: isEnvDumpCommand(input) };
  const stdout = redactShellSensitiveOutput(capture.stdout, redactionOptions);
  const stderr = redactShellSensitiveOutput(capture.stderr, redactionOptions);
  const streamCount = [stdout, stderr].filter((value) => value.length > 0).length || 1;
  const perStreamByteLimit = Math.max(1_024, Math.floor(SHELL_INLINE_MAX_BYTES / streamCount));
  const perStreamLineLimit = Math.max(40, Math.floor(SHELL_INLINE_MAX_LINES / streamCount));
  const stdoutPreview = createTextOverflowPreview(stdout, {
    maxBytes: perStreamByteLimit,
    maxLines: perStreamLineLimit,
    omissionMessage: SHELL_TRUNCATION_HINT,
  });
  const stderrPreview = createTextOverflowPreview(stderr, {
    maxBytes: perStreamByteLimit,
    maxLines: perStreamLineLimit,
    omissionMessage: SHELL_TRUNCATION_HINT,
  });
  const output = renderShellInlineOutput(stdoutPreview.preview, stderrPreview.preview);
  const truncated = stdoutPreview.truncated || stderrPreview.truncated;
  const traceOutput = renderShellTraceOutput(stdout, stderr);

  return {
    envelope: {
      stderrSummary: normalizeShellSummary(stderrPreview.preview),
      stdoutSummary: normalizeShellSummary(stdoutPreview.preview),
    },
    output,
    structuredContent: {
      exitCode: capture.exitCode,
      originalBytes:
        Buffer.byteLength(capture.stdout, "utf8") + Buffer.byteLength(capture.stderr, "utf8"),
      originalLines: countTextLines(capture.stdout) + countTextLines(capture.stderr),
      signalCode: capture.signalCode ?? null,
      stderrBytes: stderrPreview.originalBytes,
      stderrLines: stderrPreview.originalLines,
      stdoutBytes: stdoutPreview.originalBytes,
      stdoutLines: stdoutPreview.originalLines,
      timedOut: capture.timedOut,
      truncated,
    },
    traceOutput,
    truncated,
  };
}

function redactShellSensitiveOutput(
  value: string,
  options: { redactBareGenericCredentials: boolean },
): string {
  return value.replace(
    /^((?:(export\s+|declare\s+-x\s+))?([A-Za-z_][A-Za-z0-9_]*)=).+$/gmu,
    (line, prefix: string, declarationPrefix: string | undefined, name: string) => {
      const isGenericCredential =
        GENERIC_CREDENTIAL_ENV_NAME_PATTERN.test(name) &&
        (declarationPrefix !== undefined || options.redactBareGenericCredentials);
      return isHighConfidenceSensitiveEnvName(name) || isGenericCredential
        ? `${prefix}${SHELL_REDACTED_SECRET}`
        : line;
    },
  );
}

function isHighConfidenceSensitiveEnvName(name: string): boolean {
  const upperName = name.toUpperCase();
  return (
    HIGH_CONFIDENCE_SENSITIVE_ENV_NAME_PATTERN.test(name) ||
    upperName.includes("_PASSWORD") ||
    upperName.includes("PASSWORD_") ||
    upperName.includes("_PASS") ||
    upperName.includes("PASS_")
  );
}

function createShellAbortError(message: string): Error {
  const error = new Error(message);
  error.name = "AbortError";
  return error;
}

function isEnvDumpCommand(input: ShellToolInput): boolean {
  const argv = splitShellCommandPrefix(input.command);
  const command = basename(argv[0] ?? "");
  if (command === "sudo") {
    return ENV_DUMP_COMMANDS.has(basename(argv[1] ?? ""));
  }
  return ENV_DUMP_COMMANDS.has(command);
}

function buildShellFailureMessage(prefix: string, output: string): string {
  return output.trim().length > 0 ? `${prefix}\n\n${output}` : prefix;
}

function buildShellTimeoutFailureMessage(
  timeoutMs: number,
  input: ShellToolInput,
  output: string,
): string {
  const hints = [`Command timed out after ${timeoutMs}ms.`];
  if (hasBackgroundShellOperator(input.command)) {
    hints.push(SHELL_BACKGROUND_TIMEOUT_HINT);
  }
  return buildShellFailureMessage(hints.join("\n"), output);
}

function hasBackgroundShellOperator(command: string): boolean {
  return /(^|[^&])&(?![&0-9>])/u.test(command);
}

function describeShellExitFailure(capture: {
  exitCode: number | null;
  signalCode?: NodeJS.Signals | null;
}): string {
  if (capture.signalCode) {
    return `Command terminated by signal ${capture.signalCode}.`;
  }
  return capture.exitCode === null
    ? "Command terminated without an exit code."
    : `Command exited with code ${capture.exitCode}`;
}

function renderShellInlineOutput(stdout: string, stderr: string): string {
  const normalizedStdout = stdout.trimEnd();
  const normalizedStderr = stderr.trimEnd();
  if (!normalizedStdout && !normalizedStderr) {
    return "";
  }
  if (!normalizedStdout) {
    return normalizedStderr;
  }
  if (!normalizedStderr) {
    return normalizedStdout;
  }
  return `stdout:\n${normalizedStdout}\n\nstderr:\n${normalizedStderr}`;
}

function renderShellTraceOutput(stdout: string, stderr: string): string {
  const sections: string[] = [];
  if (stdout.length > 0) {
    sections.push(`[stdout]\n${stdout}`);
  }
  if (stderr.length > 0) {
    sections.push(`[stderr]\n${stderr}`);
  }
  return sections.join("\n\n");
}

function normalizeShellSummary(value: string): string | undefined {
  const normalized = value.trim();
  return normalized.length > 0 ? normalized : undefined;
}

function isLongRunningShellCommand(argv: string[]): boolean {
  const [command, ...args] = argv;
  if (!command) {
    return false;
  }
  if (LONG_RUNNING_DIRECT_COMMANDS.has(command)) {
    return true;
  }
  if (command === "go") {
    return args[0] === "build" || args[0] === "test";
  }
  if (command === "python" || command === "python3") {
    return args[0] === "-m" && (args[1] === "pip" || args[1] === "pytest");
  }
  if (command === "swift") {
    return args[0] === "build" || args[0] === "test";
  }
  if (command === "xcrun") {
    return (
      args[0] === "xcodebuild" ||
      (args[0] === "swift" && (args[1] === "build" || args[1] === "test"))
    );
  }
  if (LONG_RUNNING_PACKAGE_MANAGER_COMMANDS.has(command)) {
    return args.some((arg) => LONG_RUNNING_PACKAGE_MANAGER_TASKS.has(arg));
  }
  return false;
}

function isPythonScriptPath(scriptPath: string | undefined): boolean {
  if (!scriptPath || scriptPath.startsWith("-")) {
    return false;
  }
  const scriptName = basename(scriptPath);
  return scriptName.endsWith(".py");
}

function isPythonScriptShellCommand(argv: string[]): boolean {
  const [command, firstArg] = argv;
  return (command === "python" || command === "python3") && isPythonScriptPath(firstArg);
}

function parseTimeoutWrapperMs(argv: string[]): number | undefined {
  if (basename(argv[0] ?? "") !== "timeout") {
    return undefined;
  }
  return parseTimeoutDurationMs(argv[1]);
}

function parseTimeoutDurationMs(value: string | undefined): number | undefined {
  const match = value?.match(/^(\d+(?:\.\d+)?)([smhd]?)$/u);
  if (!match) {
    return undefined;
  }
  const amount = Number(match[1]);
  if (!Number.isFinite(amount) || amount <= 0) {
    return undefined;
  }
  const unit = match[2] ?? "";
  const multiplier =
    unit === "d"
      ? 24 * 60 * 60 * 1000
      : unit === "h"
        ? 60 * 60 * 1000
        : unit === "m"
          ? 60 * 1000
          : 1000;
  return Math.ceil(amount * multiplier);
}

function shellCommandContainsLongRunningCommand(command: string): boolean {
  const longRunningCommands = [...LONG_RUNNING_DIRECT_COMMANDS].join("|");
  return (
    new RegExp(`(?:^|[;&|()])\\s*(?:sudo\\s+)?(?:${longRunningCommands})(?:\\s|$)`, "u").test(
      command,
    ) || /(?:^|[;&|()])\s*(?:sudo\s+)?python3?\s+-m\s+pip(?:\s|$)/u.test(command)
  );
}

function splitArgv(input: string): string[] {
  const argv = splitShellWords(input);

  if (argv.length === 0) {
    throw new BestAgentError("EXEC_INVALID", "Command execution requires a non-empty input.");
  }

  return normalizeShellArgv(argv);
}

function splitShellWords(input: string): string[] {
  const words: string[] = [];
  let current = "";
  let quote: "'" | '"' | undefined;
  let hasToken = false;

  for (let index = 0; index < input.length; index += 1) {
    const char = input[index] ?? "";
    if (quote === "'") {
      if (char === "'") {
        quote = undefined;
      } else {
        current += char;
        hasToken = true;
      }
      continue;
    }

    if (quote === '"') {
      if (char === '"') {
        quote = undefined;
        continue;
      }
      if (char === "\\" && index + 1 < input.length) {
        const next = input[index + 1] ?? "";
        current +=
          next === '"' || next === "\\" || next === "$" || next === "`" || next === "\n"
            ? next
            : `${char}${next}`;
        hasToken = true;
        index += 1;
        continue;
      }
      current += char;
      hasToken = true;
      continue;
    }

    if (/\s/u.test(char)) {
      if (hasToken) {
        words.push(current);
        current = "";
        hasToken = false;
      }
      continue;
    }
    if (char === "'" || char === '"') {
      quote = char;
      hasToken = true;
      continue;
    }
    if (char === "\\" && index + 1 < input.length) {
      current += input[index + 1] ?? "";
      hasToken = true;
      index += 1;
      continue;
    }
    current += char;
    hasToken = true;
  }

  if (quote) {
    throw new BestAgentError(
      "EXEC_INVALID",
      "Command execution received an unterminated quoted string.",
    );
  }
  if (hasToken) {
    words.push(current);
  }
  return words;
}

function normalizeShellArgv(argv: string[]): string[] {
  if (argv[0] !== "find") {
    return argv;
  }

  const normalized: string[] = [];
  for (const arg of argv) {
    if (arg.length > 1 && arg.startsWith("!")) {
      normalized.push("!", arg.slice(1));
      continue;
    }
    normalized.push(arg);
  }

  return normalized;
}

function resolveShellArgv(input: ShellToolInput): string[] {
  return shouldExecuteWithShell(input.command)
    ? shellCommandArgv(input.command)
    : splitArgv(input.command);
}

function resolveShellCapabilityArgv(input: ShellToolInput): string[] {
  return shouldExecuteWithShell(input.command)
    ? shellCommandArgv(input.command)
    : splitArgv(input.command);
}

function shellCommandArgv(command: string): string[] {
  if (process.platform === "win32") {
    return [process.env.ComSpec ?? "cmd.exe", "/d", "/s", "/c", command];
  }
  if (containsShellPipeline(command)) {
    return ["/bin/bash", "-lc", `set -o pipefail; ${command}`];
  }
  return ["/bin/sh", "-lc", command];
}

function containsShellPipeline(command: string): boolean {
  return /(^|[^|])\|(?!\|)/u.test(command);
}

function hasShellControlSyntax(command: string): boolean {
  return (
    /[;&|><`\r\n]/u.test(command) || command.includes("$(") || hasLeadingEnvAssignment(command)
  );
}

function hasShellExpansionSyntax(command: string): boolean {
  if (/^(?:find|\/usr\/bin\/find|\/bin\/find)(?:\s|$)/u.test(command.trim())) {
    return false;
  }
  let quote: "'" | '"' | undefined;
  let tokenStart = true;
  for (let index = 0; index < command.length; index += 1) {
    const char = command[index] ?? "";
    if (quote === "'") {
      if (char === "'") {
        quote = undefined;
      }
      continue;
    }
    if (quote === '"') {
      if (char === '"') {
        quote = undefined;
        continue;
      }
      if (char === "\\" && index + 1 < command.length) {
        index += 1;
      }
      continue;
    }
    if (char === "'" || char === '"') {
      quote = char;
      tokenStart = false;
      continue;
    }
    if (/\s/u.test(char)) {
      tokenStart = true;
      continue;
    }
    if (char === "~" && tokenStart) {
      const next = command[index + 1];
      if (next === undefined || next === "/" || /\s/u.test(next)) {
        return true;
      }
    }
    if (char === "*" || char === "?" || char === "[" || char === "]" || char === "{") {
      return true;
    }
    if (char === "}") {
      return true;
    }
    tokenStart = false;
  }
  return false;
}

function hasLeadingEnvAssignment(command: string): boolean {
  return /^(?:env\s+)?[A-Za-z_][A-Za-z0-9_]*=\S+(?:\s|$)/u.test(command.trim());
}

function splitShellCommandPrefix(command: string): string[] {
  return command.trim().split(/\s+/u).filter(Boolean).slice(0, 4);
}

function shouldExecuteWithShell(command: string): boolean {
  return (
    hasShellControlSyntax(command) ||
    hasShellExpansionSyntax(command) ||
    /^command\s+-v(?:\s|$)/u.test(command.trim())
  );
}

export function createShellCapabilityRequests(
  argv: string[],
  cwd: string,
  options: { shellCommand?: boolean } = {},
): CapabilityRequest[] {
  if (!options.shellCommand) {
    assertDirectShellArgv("shell", argv);
  }

  return [
    {
      group: "shell",
      argv,
      cwd,
    },
    ...(options.shellCommand ? [] : inferShellFilesystemReads(argv, cwd)),
  ];
}

function assertDirectShellArgv(toolName: string, argv: string[]): void {
  const command = argv[0];
  if (!command) {
    throw new BestAgentError("EXEC_INVALID", "Command execution requires a non-empty argv.");
  }

  if (/[;&|><`]/.test(command) || command.includes("$(")) {
    throw invalidShellSyntaxError(toolName, 0, command);
  }

  if (/\s/.test(command)) {
    throw new BestAgentError(
      "EXEC_INVALID",
      `The ${toolName} tool requires argv[0] to contain only the executable path. Split flags and subcommands into separate argv entries instead of ${JSON.stringify(command)}.`,
    );
  }

  if (usesShellEval(command, argv.slice(1))) {
    throw new BestAgentError(
      "EXEC_INVALID",
      `The ${toolName} tool does not run shell interpreters with -c/-lc style command strings. Pass the executable and each argument directly instead.`,
    );
  }

  const packedEvalFlag = detectPackedEvalProgram(command, argv.slice(1));
  if (packedEvalFlag) {
    throw new BestAgentError(
      "EXEC_INVALID",
      `The ${toolName} tool could not safely infer the inline program from the command string. Use the command field with proper shell quoting or a heredoc, for example: python3 - <<'PY' ... PY.`,
    );
  }

  for (const [index, arg] of argv.slice(1).entries()) {
    if (!isForbiddenShellArgToken(arg)) {
      continue;
    }

    throw invalidShellSyntaxError(toolName, index + 1, arg);
  }

  if (
    command !== "ls" &&
    command !== "cat" &&
    command !== "head" &&
    command !== "tail" &&
    command !== "wc" &&
    command !== "file"
  ) {
    return;
  }

  for (const pathArg of extractShellPathArgs(argv.slice(1))) {
    if (!pathArg.startsWith("~") && !/[*?[\]{}]/.test(pathArg)) {
      continue;
    }

    throw new BestAgentError(
      "EXEC_INVALID",
      `The ${toolName} tool does not expand shell globs or ~. Pass a concrete workspace-relative or cwd-relative path instead of ${JSON.stringify(pathArg)}.`,
    );
  }
}

function usesShellEval(command: string, args: string[]): boolean {
  if (
    command !== "bash" &&
    command !== "sh" &&
    command !== "zsh" &&
    command !== "fish" &&
    command !== "dash"
  ) {
    return false;
  }

  return args.some((arg) => arg === "-c" || arg === "-lc" || arg === "-ic" || arg === "--command");
}

function detectPackedEvalProgram(command: string, args: string[]): string | undefined {
  if ((command === "python" || command === "python3") && args[0] === "-c" && args.length > 2) {
    return "-c";
  }

  if (command === "node" && (args[0] === "-e" || args[0] === "--eval") && args.length > 2) {
    return args[0];
  }

  return undefined;
}

function isForbiddenShellArgToken(arg: string): boolean {
  if (arg.includes("$(") || arg.includes("`")) {
    return true;
  }

  if (
    arg === "&&" ||
    arg === "||" ||
    arg === "|" ||
    arg === ";" ||
    arg === "&" ||
    arg === ">" ||
    arg === ">>" ||
    arg === "<" ||
    arg === "<<" ||
    arg === "<<<"
  ) {
    return true;
  }

  return /^(?:\d*(?:>>?|<<<?|[<>]&)\S*)$/u.test(arg);
}

function invalidShellSyntaxError(toolName: string, index: number, value: string): BestAgentError {
  return new BestAgentError(
    "EXEC_INVALID",
    `The ${toolName} tool received shell syntax in a direct execution context. Use the command field as a single shell command string with proper quoting. Offending token ${index}: ${JSON.stringify(value)}.`,
  );
}

function rewriteShellExecutionError(
  argv: string[],
  input: ShellToolInput,
  error: unknown,
): unknown {
  if (error instanceof BestAgentError && error.code === "EXEC_FAILED") {
    if (
      error.message === "Command exited with code 1" &&
      ((argv[0] === "which" && argv.length === 2) || isMissingCommandProbe(input))
    ) {
      const commandName =
        argv[0] === "which" && argv.length === 2
          ? argv[1]
          : extractMissingCommandProbeName(input.command ?? "");
      return createMissingCommandError(commandName);
    }

    const missingCommand = extractMissingCommandFromShellFailure(error.message);
    if (missingCommand) {
      return createMissingCommandError(missingCommand);
    }
  }

  return error;
}

function createMissingCommandError(commandName: string | undefined): BestAgentError {
  return new BestAgentError(
    "EXEC_COMMAND_NOT_FOUND",
    commandName
      ? `Command ${commandName} is not available in this environment.`
      : "Command is not available in this environment.",
  );
}

function extractMissingCommandFromShellFailure(message: string): string | undefined {
  const trimmed = message.trim();
  const spawnMatch = trimmed.match(/^spawn\s+(\S+)\s+ENOENT$/i);
  if (spawnMatch?.[1]) {
    return spawnMatch[1];
  }
  const shellNotFoundMatch = trimmed.match(
    /(?:^|\n)(?:\/bin\/sh:\s*\d+:\s*|sh:\s*|bash:\s*)(\S+):\s+(?:not found|command not found)\b/i,
  );
  if (shellNotFoundMatch?.[1]) {
    return shellNotFoundMatch[1];
  }
  const lineCommandNotFoundMatch = trimmed.match(
    /(?:^|\n)[^:\n]+:\s+line\s+\d+:\s+(\S+):\s+command not found\b/i,
  );
  if (lineCommandNotFoundMatch?.[1]) {
    return lineCommandNotFoundMatch[1];
  }
  const envMissingMatch = trimmed.match(/(?:^|\n)env:\s+(\S+):\s+No such file or directory\b/i);
  if (envMissingMatch?.[1]) {
    return envMissingMatch[1];
  }
  return undefined;
}

function isMissingCommandProbe(input: ShellToolInput): boolean {
  return extractMissingCommandProbeName(input.command) !== undefined;
}

function extractMissingCommandProbeName(command: string): string | undefined {
  const normalized = command.trim();
  const match = normalized.match(/^(?:which|command\s+-v)\s+([A-Za-z0-9_.+-]+)$/u);
  return match?.[1];
}

function inferShellFilesystemReads(argv: string[], cwd: string): CapabilityRequest[] {
  const command = argv[0];
  const paths =
    command === "find"
      ? extractFindPathArgs(argv.slice(1))
      : command === "ls" ||
          command === "cat" ||
          command === "head" ||
          command === "tail" ||
          command === "wc" ||
          command === "file"
        ? extractShellPathArgs(argv.slice(1))
        : [];
  if (paths.length === 0) {
    return [];
  }

  return paths.map((path) => ({
    group: "filesystem" as const,
    action: "read" as const,
    path: resolve(cwd, path),
  }));
}

function isWithinRoot(path: string, root: string): boolean {
  const rel = relative(root, path);
  return rel === "" || (!rel.startsWith("..") && !rel.startsWith("/") && !/^[A-Za-z]:/.test(rel));
}

function extractFindPathArgs(args: string[]): string[] {
  const paths: string[] = [];

  for (const arg of args) {
    if (arg === "--") {
      continue;
    }
    if (arg.startsWith("-") || arg === "(" || arg === ")" || arg === "!") {
      break;
    }
    paths.push(arg);
  }

  return paths.length > 0 ? paths : ["."];
}

function extractShellPathArgs(args: string[]): string[] {
  const paths: string[] = [];
  let positionalOnly = false;

  for (const arg of args) {
    if (arg === "--") {
      positionalOnly = true;
      continue;
    }

    if (arg === "-") {
      continue;
    }

    if (!positionalOnly && arg.startsWith("-")) {
      continue;
    }

    paths.push(arg);
  }

  return paths;
}
