import { spawnSync } from "node:child_process";
import process from "node:process";

/**
 * Return true while Node still considers the child process active.
 *
 * Do not use ChildProcess#killed for cleanup decisions. In Node, `killed`
 * only means a signal was sent successfully; it does not mean the process has
 * exited. That distinction matters for dev launchers because uvx/npm
 * wrappers can receive SIGTERM while their long-running child process keeps
 * serving on the original port.
 */
export function isProcessRunning(proc) {
  return proc.exitCode === null && proc.signalCode === null;
}

/**
 * Add spawn options needed for safe service launches and process-tree cleanup.
 *
 * Arguments must bypass shell parsing so values such as version constraints
 * containing `<` are forwarded literally. Callers that need shell behavior
 * must invoke the shell explicitly as the command.
 *
 * On POSIX, `detached: true` makes the spawned service the leader of a new
 * process group. Later we can signal `-pid` to terminate that whole group,
 * including wrapper chains like:
 *
 *   launcher -> uvx -> python agent-server
 *   launcher -> npm -> sh -> Vite
 *
 * Windows does not support POSIX process groups, so callers fall back to
 * signaling the direct child process there.
 */
export function getProcessTreeSpawnOptions(options = {}) {
  return {
    ...options,
    shell: false,
    detached: process.platform !== "win32",
  };
}

/**
 * Resolve a service command to a directly spawnable target on Windows.
 *
 * Services spawn without a shell so argument values reach the child verbatim.
 * Spawning `uvx` via cmd.exe instead makes it parse the args: a constraint like
 * `agent-client-protocol<0.11` is read as `<` input redirection and the spawn
 * dies with "The system cannot find the file specified." Resolving to an
 * absolute path lets callers spawn it shell-free.
 *
 * Returns `command` unchanged off Windows, when already a path, or if the lookup
 * fails.
 */
export function resolveWindowsCommand(
  command,
  platform = process.platform,
  lookup = whereCommandLookup,
) {
  if (platform !== "win32") {
    return command;
  }
  if (command.includes("/") || command.includes("\\")) {
    return command;
  }
  return lookup(command) || command;
}

function whereCommandLookup(command) {
  const result = spawnSync("where.exe", [command], { encoding: "utf8" });
  if (result.status !== 0 || !result.stdout) {
    return null;
  }
  return result.stdout.split(/\r?\n/).find(Boolean)?.trim() || null;
}

/**
 * Signal the whole spawned service tree when possible.
 *
 * POSIX `process.kill(-pid, signal)` targets the process group whose id is
 * `pid`; this only works because services are spawned with
 * `getProcessTreeSpawnOptions()`. Without the negative pid, shutdown would
 * often stop only the wrapper process and leave the actual server child
 * listening on its port.
 */
export function signalProcessTree(proc, signal) {
  if (!isProcessRunning(proc)) {
    return false;
  }

  try {
    if (process.platform === "win32" && proc.pid) {
      killWindowsProcessTree(proc, signal);
    } else if (!proc.pid) {
      proc.kill(signal);
    } else {
      process.kill(-proc.pid, signal);
    }
    return true;
  } catch (err) {
    if (err?.code === "ESRCH") {
      return false;
    }
    throw err;
  }
}

/**
 * Windows has no POSIX process groups: ChildProcess#kill reaches only the
 * direct child (e.g. the uvx wrapper), leaving grandchildren — the actual
 * python agent-server holding its port — running. `taskkill /t` walks the
 * child tree instead. Windows also has no graceful tree signal (taskkill
 * without /f posts WM_CLOSE, which console processes ignore), so SIGTERM and
 * SIGKILL both map to the same forceful /f kill; callers' delayed SIGKILL
 * pass skips already-exited trees via isProcessRunning, so the repeat is a
 * no-op. A non-zero taskkill exit just means the tree already exited — only
 * a failure to spawn taskkill itself falls back to the direct kill.
 */
function killWindowsProcessTree(proc, signal) {
  const result = spawnSync(
    "taskkill",
    ["/pid", String(proc.pid), "/t", "/f"],
    // windowsHide avoids a console window flash when invoked from the
    // packaged (GUI) Electron process.
    { stdio: "ignore", windowsHide: true },
  );
  if (result.error) {
    proc.kill(signal);
  }
}

export function createShutdownHookRegistry(onError) {
  const hooks = new Set();

  return {
    add(hook) {
      hooks.add(hook);
      return () => hooks.delete(hook);
    },

    run() {
      for (const hook of hooks) {
        try {
          hook();
        } catch (err) {
          onError?.(err);
        }
      }
    },
  };
}
