/**
 * Shared command execution utilities for extensions and custom tools.
 */

import { spawn } from "node:child_process";

/**
 * Options for executing shell commands.
 */
export interface ExecOptions {
	/** AbortSignal to cancel the command */
	signal?: AbortSignal;
	/** Timeout in milliseconds */
	timeout?: number;
	/** Working directory */
	cwd?: string;
}

/**
 * Result of executing a shell command.
 */
export interface ExecResult {
	stdout: string;
	stderr: string;
	code: number;
	killed: boolean;
}

/**
 * Execute a shell command and return stdout/stderr/code.
 * Supports timeout and abort signal.
 */
export async function execCommand(
	command: string,
	args: string[],
	cwd: string,
	options?: ExecOptions,
): Promise<ExecResult> {
	// Combine user abort + timeout into one signal (Node 19+ AbortSignal.any).
	const sigs: AbortSignal[] = [];
	if (options?.signal) sigs.push(options.signal);
	if (options?.timeout && options.timeout > 0)
		sigs.push(AbortSignal.timeout(options.timeout));
	const signal =
		sigs.length === 0
			? undefined
			: sigs.length === 1
				? sigs[0]
				: AbortSignal.any(sigs);

	return new Promise((resolve) => {
		const proc = spawn(command, args, {
			cwd,
			// On Windows, npm/npx/tsc etc. are .cmd scripts that require shell
			// resolution.  Without this, spawn fails with ENOENT or EINVAL (#2854).
			shell: process.platform === "win32",
			stdio: ["ignore", "pipe", "pipe"],
			// spawn auto-sends killSignal when this aborts.
			signal,
			killSignal: "SIGTERM",
		});

		let stdout = "";
		let stderr = "";
		let killTimer: NodeJS.Timeout | undefined;

		// Force SIGKILL 5s after SIGTERM if the process refuses to exit.
		signal?.addEventListener(
			"abort",
			() => {
				killTimer = setTimeout(() => {
					if (!proc.killed) proc.kill("SIGKILL");
				}, 5000);
			},
			{ once: true },
		);

		proc.stdout?.on("data", (data) => {
			stdout += data.toString();
		});

		proc.stderr?.on("data", (data) => {
			stderr += data.toString();
		});

		const finish = (code: number) => {
			if (killTimer) clearTimeout(killTimer);
			resolve({
				stdout,
				stderr,
				code,
				killed: signal?.aborted ?? false,
			});
		};

		proc.on("close", (code) => finish(code ?? 0));
		proc.on("error", () => finish(1));
	});
}
