import { spawn } from "node:child_process";
import { closeSync, openSync } from "node:fs";
import { createServer } from "node:net";
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
import path from "node:path";

type ServiceName = "api" | "web";

export async function caserunNativeServiceCommand(input: { service: string; action: string; cwd: string; env: Record<string, string | undefined> }) {
  const service = validService(input.service);
  const stateDir = path.resolve(input.cwd, input.env.CASERUN_NATIVE_SERVICE_STATE_DIR ?? ".state/caserun-native/services");
  const stateFile = path.join(stateDir, `${service}.json`);
  const logFile = path.join(stateDir, `${service}.log`);
  if (input.action === "status") return status(service, stateFile, logFile);
  if (input.action === "logs") return logs(service, stateFile, logFile);
  if (input.action === "stop") return stop(service, stateFile, logFile);
  if (input.action === "restart") {
    await stop(service, stateFile, logFile);
    return start(service, stateDir, stateFile, logFile, input.cwd, input.env);
  }
  if (input.action === "start") {
    const current = await readState(stateFile);
    if (current?.pid && alive(current.pid)) await stop(service, stateFile, logFile);
    return start(service, stateDir, stateFile, logFile, input.cwd, input.env);
  }
  throw codedError("unsupported_service_action", `unsupported CaseRun service action: ${input.action}`);
}

async function start(service: ServiceName, stateDir: string, stateFile: string, logFile: string, cwd: string, env: Record<string, string | undefined>) {
  await mkdir(stateDir, { recursive: true });
  const endpoints = serviceEndpoints(service, env);
  await releaseStaleServicePort(service, env);
  const command = serviceCommand(service);
  const logFd = openSync(logFile, "w");
  const serviceCwd = path.join(cwd, "web/hwlab-cloud-web");
  const child = spawn(command[0], command.slice(1), { cwd: serviceCwd, env: serviceEnv(service, env), detached: true, stdio: ["ignore", logFd, logFd] });
  closeSync(logFd);
  child.unref();
  const state = { service, pid: child.pid, status: "starting", command, cwd, logFile, endpoints, startedAt: new Date().toISOString() };
  await writeFile(stateFile, `${JSON.stringify(state, null, 2)}\n`, "utf8");
  return { ok: true, operation: `caserun.service.${service}.start`, ...state, nextCommand: `hwlab-cli caserun service ${service} status` };
}

async function releaseStaleServicePort(service: ServiceName, env: Record<string, string | undefined>) {
  const bindHost = requiredEnv(env, service === "api" ? "CASERUN_API_BIND_HOST" : "CASERUN_WEB_BIND_HOST");
  const port = Number(requiredEnv(env, service === "api" ? "CASERUN_API_PORT" : "CASERUN_WEB_PORT"));
  if (await portAvailable(bindHost, port)) return;
  const expectedPortKey = service === "api" ? "HWLAB_CASERUN_NATIVE_PORT" : "WORKBENCH_WEB_PORT";
  const commandMarker = service === "api" ? "caserun-native-supervisor.ts" : "dev:native-caserun";
  const candidates = await matchingServiceGroups(commandMarker, expectedPortKey, String(port));
  if (candidates.length === 0) {
    throw codedError("native_port_owned_by_other_service", `CaseRun ${service} port ${port} is occupied by another service`);
  }
  for (const group of candidates) {
    try { process.kill(-group, "SIGTERM"); } catch (error: any) { if (error?.code !== "ESRCH") throw error; }
  }
  const deadline = Date.now() + 3_000;
  while (Date.now() < deadline) {
    if (await portAvailable(bindHost, port)) return;
    await new Promise((resolve) => setTimeout(resolve, 50));
  }
  for (const group of candidates) {
    try { process.kill(-group, "SIGKILL"); } catch (error: any) { if (error?.code !== "ESRCH") throw error; }
  }
  if (!await waitForPort(bindHost, port, 1_000)) throw codedError("native_port_release_timeout", `CaseRun ${service} port ${port} did not become available`);
}

async function matchingServiceGroups(commandMarker: string, portKey: string, port: string): Promise<number[]> {
  const groups = new Set<number>();
  for (const entry of await readdir("/proc").catch(() => [])) {
    if (!/^\d+$/u.test(entry)) continue;
    const base = `/proc/${entry}`;
    const [cmdline, environ, stat] = await Promise.all([
      readFile(`${base}/cmdline`, "utf8").catch(() => ""),
      readFile(`${base}/environ`, "utf8").catch(() => ""),
      readFile(`${base}/stat`, "utf8").catch(() => ""),
    ]);
    if (!cmdline.replace(/\0/gu, " ").includes(commandMarker)) continue;
    const variables = environ.split("\0");
    if (!variables.includes("HWLAB_CASERUN_NATIVE_TEST=1") || !variables.includes(`${portKey}=${port}`)) continue;
    const fields = stat.slice(stat.lastIndexOf(") ") + 2).trim().split(/\s+/u);
    const group = Number(fields[2]);
    if (Number.isInteger(group) && group > 1) groups.add(group);
  }
  return [...groups];
}

async function waitForPort(host: string, port: number, timeoutMs: number): Promise<boolean> {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if (await portAvailable(host, port)) return true;
    await new Promise((resolve) => setTimeout(resolve, 50));
  }
  return portAvailable(host, port);
}

function portAvailable(host: string, port: number): Promise<boolean> {
  return new Promise((resolve) => {
    const server = createServer();
    server.once("error", () => resolve(false));
    server.listen({ host, port, exclusive: true }, () => server.close(() => resolve(true)));
  });
}

async function stop(service: ServiceName, stateFile: string, logFile: string) {
  const state = await readState(stateFile);
  if (!state?.pid || !alive(state.pid)) { await rm(stateFile, { force: true }); return { ok: true, operation: `caserun.service.${service}.stop`, service, status: "stopped", alreadyStopped: true, logFile }; }
  try { process.kill(-state.pid, "SIGTERM"); } catch (error: any) { if (error?.code !== "ESRCH") throw error; }
  if (!await waitForExit(state.pid, 3_000)) {
    try { process.kill(-state.pid, "SIGKILL"); } catch (error: any) { if (error?.code !== "ESRCH") throw error; }
    await waitForExit(state.pid, 1_000);
  }
  await rm(stateFile, { force: true });
  return { ok: true, operation: `caserun.service.${service}.stop`, service, status: "stopped", pid: state.pid, logFile };
}

async function status(service: ServiceName, stateFile: string, logFile: string) {
  const state = await readState(stateFile);
  const running = Boolean(state?.pid && alive(state.pid));
  return { ok: running, operation: `caserun.service.${service}.status`, service, status: running ? "running" : "stopped", pid: running ? state.pid : null, endpoints: state?.endpoints ?? null, stateFile, logFile, nextCommand: `hwlab-cli caserun service ${service} logs` };
}

async function logs(service: ServiceName, stateFile: string, logFile: string) {
  const state = await status(service, stateFile, logFile);
  const lines = (await readFile(logFile, "utf8").catch(() => "")).split("\n").filter(Boolean).slice(-80);
  return { ...state, operation: `caserun.service.${service}.logs`, lines, truncated: lines.length === 80 };
}

function serviceCommand(service: ServiceName) {
  return service === "api"
    ? ["bun", "scripts/caserun-native-supervisor.ts"]
    : ["bun", "run", "dev:native-caserun"];
}
function serviceEnv(service: ServiceName, env: Record<string, string | undefined>) {
  return { ...process.env, ...env, HWLAB_CASERUN_NATIVE_TEST: "1", ...(service === "web"
    ? {
        WORKBENCH_WEB_HOST: requiredEnv(env, "CASERUN_WEB_BIND_HOST"),
        WORKBENCH_WEB_PORT: requiredEnv(env, "CASERUN_WEB_PORT"),
        HWLAB_CASERUN_NATIVE_URL: `http://${requiredEnv(env, "CASERUN_PROBE_HOST")}:${requiredEnv(env, "CASERUN_API_PORT")}`
      }
    : { HWLAB_CASERUN_NATIVE_HOST: requiredEnv(env, "CASERUN_API_BIND_HOST"), HWLAB_CASERUN_NATIVE_PORT: requiredEnv(env, "CASERUN_API_PORT") }) };
}
function serviceEndpoints(service: ServiceName, env: Record<string, string | undefined>) {
  const port = service === "api" ? requiredEnv(env, "CASERUN_API_PORT") : requiredEnv(env, "CASERUN_WEB_PORT");
  const publicBaseUrl = fixedHttpsOrigin(requiredEnv(env, "CASERUN_PUBLIC_BASE_URL"), "CASERUN_PUBLIC_BASE_URL");
  validPort(port);
  return { url: service === "api" ? publicBaseUrl : `${publicBaseUrl}/caserun` };
}
function validPort(value: string) { const port = Number(value); if (!Number.isInteger(port) || port < 1 || port > 65535) throw codedError("invalid_service_port", `invalid CaseRun service port: ${value}`); return port; }
function validService(value: string): ServiceName { if (value === "api" || value === "web") return value; throw codedError("invalid_service", "CaseRun service must be api or web"); }
async function readState(file: string) { return JSON.parse(await readFile(file, "utf8").catch(() => "null")); }
function alive(pid: number) { try { process.kill(pid, 0); return true; } catch { return false; } }
async function waitForExit(pid: number, timeoutMs: number) {
  const deadline = Date.now() + timeoutMs;
  while (alive(pid) && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 50));
  return !alive(pid);
}
function requiredEnv(env: Record<string, string | undefined>, key: string) { const value = String(env[key] ?? "").trim(); if (!value) throw codedError("native_exposure_config_required", `${key} is required from YAML-first native CaseRun config`); return value; }
function fixedHttpsOrigin(value: string, key: string) {
  try {
    const parsed = new URL(value);
    if (parsed.protocol === "https:" && parsed.pathname === "/" && !parsed.search && !parsed.hash && !parsed.port) return parsed.origin;
  } catch {
    // Report one stable configuration error below.
  }
  throw codedError("native_exposure_config_invalid", `${key} must be a pathless HTTPS origin`);
}
function codedError(code: string, message: string) { return Object.assign(new Error(message), { code }); }
