import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { setTimeout as delay } from "node:timers/promises";
import { requiredEnvironment } from "./exact-journey.js";

export interface ControlPlaneServiceRuntime {
  readonly host: string;
  readonly service: string;
  readonly url: string;
  readonly timeoutMs: number;
}

export function controlPlaneServiceRuntime(
  environment: Readonly<Record<string, string | undefined>> = process.env,
): ControlPlaneServiceRuntime {
  return {
    host: requiredEnvironment(environment, "AWP_CONTROL_PLANE_HOST"),
    service: requiredEnvironment(environment, "AWP_CONTROL_PLANE_SERVICE"),
    url: requiredEnvironment(environment, "AWP_CONTROL_PLANE_URL").replace(/\/$/u, ""),
    timeoutMs: Number(environment.AWP_GOLIVE_TIMEOUT_MS ?? "120000"),
  };
}

function ssh(runtime: ControlPlaneServiceRuntime, args: readonly string[]): string {
  return execFileSync(
    "ssh",
    ["-o", "BatchMode=yes", "-o", "ConnectTimeout=5", runtime.host, ...args],
    { encoding: "utf8" },
  ).trim();
}

export function controlPlanePid(runtime: ControlPlaneServiceRuntime): number {
  const raw = ssh(runtime, [
    "systemctl",
    "--user",
    "show",
    runtime.service,
    "--property=MainPID",
    "--value",
  ]);
  const pid = Number(raw);
  assert.ok(Number.isInteger(pid) && pid > 1, `${runtime.service} must have a safe live MainPID`);
  const commandLine = ssh(runtime, ["ps", "-p", String(pid), "-o", "args="]);
  assert.match(
    commandLine,
    /apps\/control-plane\/dist\/server\.js/u,
    `refusing to kill PID ${pid}: ${runtime.service} does not identify the AWP control plane`,
  );
  return pid;
}

async function waitForHealthyReplacement(
  runtime: ControlPlaneServiceRuntime,
  priorPid: number,
): Promise<number> {
  const deadline = Date.now() + runtime.timeoutMs;
  while (Date.now() < deadline) {
    try {
      const nextPid = controlPlanePid(runtime);
      if (nextPid !== priorPid) {
        const response = await fetch(`${runtime.url}/health`, { cache: "no-store" });
        if (response.ok) return nextPid;
      }
    } catch {
      // The killed service has not produced and exposed its replacement process yet.
    }
    await delay(100);
  }
  throw new Error(`${runtime.service} did not become healthy with a replacement PID`);
}

export async function killAndRestartControlPlane(
  runtime: ControlPlaneServiceRuntime,
): Promise<{ readonly priorPid: number; readonly replacementPid: number }> {
  const priorPid = controlPlanePid(runtime);
  ssh(runtime, ["kill", "-KILL", String(priorPid)]);
  ssh(runtime, ["systemctl", "--user", "restart", runtime.service]);
  const replacementPid = await waitForHealthyReplacement(runtime, priorPid);
  return { priorPid, replacementPid };
}
