import { spawn as nodeSpawn } from "node:child_process";
import { AsyncLocalStorage } from "node:async_hooks";

const runContext = new AsyncLocalStorage();

class ProcessSupervisor {
  constructor() {
    this.children = new Map();
    this.childrenByRun = new Map();
  }

  enterRun(runId) {
    if (runId) runContext.enterWith({ runId });
  }

  currentRunId() {
    return runContext.getStore()?.runId || "";
  }

  spawn(command, args = [], options = {}, metadata = {}) {
    const runId = metadata.runId || this.currentRunId();
    const killGroup = Boolean(metadata.killGroup || options.detached);
    const child = nodeSpawn(command, args, options);
    const record = {
      child,
      pid: child.pid,
      runId,
      killGroup,
      label: metadata.label || command
    };

    if (child.pid) {
      this.children.set(child.pid, record);
      if (runId) {
        if (!this.childrenByRun.has(runId)) this.childrenByRun.set(runId, new Set());
        this.childrenByRun.get(runId).add(child.pid);
      }
    }

    const untrack = () => this.untrack(child.pid);
    child.once("close", untrack);
    child.once("error", untrack);
    return child;
  }

  async killRun(runId, { signal = "SIGTERM", forceAfterMs = 5000 } = {}) {
    const pids = [...(this.childrenByRun.get(runId) || [])];
    if (!pids.length) return 0;
    for (const pid of pids) this.killPid(pid, signal);
    if (forceAfterMs > 0) {
      await delay(forceAfterMs);
      for (const pid of pids) {
        if (this.children.has(pid)) this.killPid(pid, "SIGKILL");
      }
    }
    return pids.length;
  }

  killChild(child, signal = "SIGTERM") {
    if (!child?.pid) return;
    this.killPid(child.pid, signal);
  }

  killPid(pid, signal) {
    const record = this.children.get(pid);
    try {
      if (process.platform !== "win32" && record?.killGroup) {
        process.kill(-pid, signal);
      } else {
        process.kill(pid, signal);
      }
    } catch {
      try {
        record?.child?.kill(signal);
      } catch {
        // Process already exited.
      }
    }
  }

  untrack(pid) {
    if (!pid) return;
    const record = this.children.get(pid);
    if (!record) return;
    this.children.delete(pid);
    if (record.runId && this.childrenByRun.has(record.runId)) {
      const pids = this.childrenByRun.get(record.runId);
      pids.delete(pid);
      if (!pids.size) this.childrenByRun.delete(record.runId);
    }
  }
}

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

export const processSupervisor = new ProcessSupervisor();
