#!/usr/bin/env node

"use strict";

const os = require("os");
const { spawn } = require("child_process");
const { getBinaryPath } = require("../lib/platform");

const binaryPath = getBinaryPath();

// Spawn in its own process group (detached) to avoid double SIGINT delivery
// when the terminal sends SIGINT to the entire foreground process group.
// This makes explicit signal forwarding the only path, which is unambiguous.
// IMPORTANT: detached:true means the OS will NOT auto-kill the child if this
// wrapper dies -- the PPID polling below covers that case. The two are coupled.
const child = spawn(binaryPath, process.argv.slice(2), {
  stdio: "inherit",
  detached: true,
});

// Track whether we have already forwarded a signal. On double-signal
// (e.g. Ctrl+C twice), escalate to SIGKILL for a forced exit.
let signalForwarded = false;
let childExited = false;

function forwardSignal(signal) {
  if (childExited) return;
  if (signalForwarded) {
    // Second signal -- force kill.
    try {
      child.kill("SIGKILL");
    } catch {}
    return;
  }
  signalForwarded = true;
  try {
    child.kill(signal);
  } catch {}
}

// Forward termination signals to the child.
// Installing these overrides Node's default handlers, so the wrapper will not
// auto-exit on signal receipt -- the child's close event drives our exit.
const signals =
  process.platform === "win32"
    ? ["SIGINT"]
    : ["SIGINT", "SIGTERM", "SIGHUP"];

for (const sig of signals) {
  process.on(sig, () => forwardSignal(sig));
}

// If Node is shutting down for any reason, kill the child if still running.
process.on("exit", () => {
  if (!childExited) {
    try {
      child.kill("SIGKILL");
    } catch {}
  }
});

// Detect parent death via PPID polling. When backgrounded with & in npm scripts,
// the wrapper is outside the foreground process group and never receives signals.
// Polling process.ppid is the only reliable cross-platform detection: when the
// parent dies, the OS reparents this process (to PID 1, launchd, or systemd)
// and the PPID changes.
// IMPORTANT: this is load-bearing for detached:true above. Without PPID polling,
// a detached child would never be cleaned up if the wrapper crashes or is SIGKILL'd.
const originalPpid = process.ppid;
const ppidCheck = setInterval(() => {
  if (process.ppid !== originalPpid) {
    // Parent died. Send SIGTERM for graceful shutdown, then SIGKILL as fallback.
    forwardSignal("SIGTERM");
    setTimeout(() => {
      if (!childExited) {
        try {
          child.kill("SIGKILL");
        } catch {}
      }
    }, 2000).unref();
    clearInterval(ppidCheck);
  }
}, 1000);
ppidCheck.unref();

// Spawn errors (binary not found, permission denied).
child.on("error", (err) => {
  childExited = true;
  if (err.code === "ENOENT") {
    console.error(
      `Could not find the Dynoxide binary at: ${binaryPath}\n` +
        `The platform package may be corrupted. Try reinstalling: npm install dynoxide`
    );
  } else if (err.code === "EACCES") {
    console.error(
      `Permission denied running: ${binaryPath}\n` +
        `Try: chmod +x "${binaryPath}"`
    );
  } else {
    console.error(`Failed to run Dynoxide: ${err.message}`);
  }
  process.exit(1);
});

// Child exit drives wrapper exit. Propagate exit code or signal.
child.on("close", (code, signal) => {
  childExited = true;
  if (signal) {
    // Re-raise the signal so the parent sees it correctly.
    process.kill(process.pid, signal);
    const signum = os.constants.signals[signal] || 0;
    process.exit(128 + signum);
  }
  process.exit(code ?? 1);
});
