import { spawn } from "child_process";
import net from "net";
import chalk from "chalk";
import closeWithGrace from "close-with-grace";
import { execaCommand } from "execa";
import fkill from "fkill";
import waitOn from "wait-on";
import { singleton } from "./singleton.server.js";
const isDeployed = process.env.KCDSHOP_DEPLOYED === "true" || process.env.KCDSHOP_DEPLOYED === "1";
const devProcesses = singleton("dev_processes", getDevProcessesMap);
const testProcesses = singleton("test_processes", getTestProcessesMap);
function getDevProcessesMap() {
  const procs = /* @__PURE__ */ new Map();
  global.__process_dev_close_with_grace_return__?.uninstall();
  global.__process_dev_close_with_grace_return__ = closeWithGrace(async () => {
    for (const [name, proc] of procs.entries()) {
      console.log("closing", name);
      proc.process.kill();
    }
  });
  return procs;
}
function getTestProcessesMap() {
  const procs = /* @__PURE__ */ new Map();
  global.__process_test_close_with_grace_return__?.uninstall();
  global.__process_test_close_with_grace_return__ = closeWithGrace(async () => {
    for (const [id, proc] of procs.entries()) {
      if (proc.process) {
        console.log("closing", id);
        proc.process.kill();
      }
    }
  });
  return procs;
}
const colors = [
  "blue",
  "green",
  "yellow",
  "red",
  "magenta",
  "redBright",
  "greenBright",
  "yellowBright",
  "blueBright",
  "magentaBright"
];
async function runAppDev(app) {
  if (isDeployed)
    throw new Error("cannot run apps in deployed mode");
  const key = app.name;
  if (devProcesses.has(key)) {
    return { status: "process-running", running: true };
  }
  if (app.dev.type !== "script") {
    return { status: "error", error: "no-server" };
  }
  const { portNumber } = app.dev;
  if (!await isPortAvailable(portNumber)) {
    return { status: "port-unavailable", running: false, portNumber };
  }
  const availableColors = colors.filter(
    (color2) => Array.from(devProcesses.values()).every((p) => p.color !== color2)
  );
  const color = availableColors[devProcesses.size % availableColors.length] ?? "blue";
  const appProcess = spawn("npm", ["run", "dev"], {
    cwd: app.fullPath,
    shell: true,
    stdio: ["ignore", "pipe", "pipe"],
    env: {
      ...process.env,
      // TODO: support specifying the env
      NODE_ENV: "development",
      // TODO: support specifying the port
      PORT: String(portNumber),
      APP_SERVER_PORT: String(portNumber),
      // let it pick a random port...
      REMIX_DEV_SERVER_WS_PORT: ""
    }
  });
  const prefix = chalk[color](
    `[${app.name.replace(/^exercises\./, "")}:${portNumber}]`
  );
  function handleStdOutData(data) {
    console.log(
      data.toString("utf-8").split("\n").map((line) => `${prefix} ${line}`).join("\n")
    );
  }
  appProcess.stdout.on("data", handleStdOutData);
  function handleStdErrData(data) {
    console.error(
      data.toString("utf-8").split("\n").map((line) => `${prefix} ${line}`).join("\n")
    );
  }
  appProcess.stderr.on("data", handleStdErrData);
  devProcesses.set(key, { color, process: appProcess, port: portNumber });
  appProcess.on("exit", (code) => {
    appProcess.stdout.off("data", handleStdOutData);
    appProcess.stderr.off("data", handleStdErrData);
    console.log(`${prefix} exited (${code})`);
    devProcesses.delete(key);
  });
  return { status: "process-started", running: true };
}
async function runAppTests(app) {
  if (isDeployed)
    throw new Error("cannot run tests in deployed mode");
  const key = app.name;
  if (app.test.type !== "script") {
    return { status: "error", error: "no-test" };
  }
  const testProcess = execaCommand(app.test.script, {
    cwd: app.fullPath,
    shell: true,
    stdio: ["ignore", "pipe", "pipe"],
    env: {
      ...process.env,
      // TODO: support specifying the env
      NODE_ENV: "development",
      // TODO: support specifying the port
      PORT: app.dev.type === "script" ? String(app.dev.portNumber) : void 0,
      APP_SERVER_PORT: app.dev.type === "script" ? String(app.dev.portNumber) : void 0,
      // let it pick a random port...
      REMIX_DEV_SERVER_WS_PORT: ""
    }
  });
  const output = [];
  const entry = { process: testProcess, output };
  function handleStdOutData(data) {
    output.push({
      type: "stdout",
      content: data.toString("utf-8"),
      timestamp: Date.now()
    });
  }
  testProcess.stdout?.on("data", handleStdOutData);
  function handleStdErrData(data) {
    output.push({
      type: "stderr",
      content: data.toString("utf-8"),
      timestamp: Date.now()
    });
  }
  testProcess.stderr?.on("data", handleStdErrData);
  testProcess.on("exit", (code) => {
    testProcess.stdout?.off("data", handleStdOutData);
    testProcess.stderr?.off("data", handleStdErrData);
    entry.process = null;
    entry.exitCode = code;
  });
  testProcesses.set(key, entry);
  return testProcess;
}
async function waitOnApp(app) {
  if (app.dev.type === "script") {
    return waitOn({
      resources: [`http://localhost:${app.dev.portNumber}`],
      timeout: 2e4
    });
  }
}
function isPortAvailable(port) {
  return new Promise((resolve) => {
    const server = net.createServer();
    server.unref();
    server.on("error", () => resolve(false));
    server.listen(Number(port), () => {
      server.close(() => {
        resolve(true);
      });
    });
  });
}
function isAppRunning(app) {
  try {
    const devProcess = devProcesses.get(app.name);
    if (!devProcess)
      return false;
    if (devProcess.process === null)
      return false;
    devProcess.process.kill(0);
    return true;
  } catch {
    return false;
  }
}
function isTestRunning(app) {
  try {
    const testProcess = testProcesses.get(app.name);
    if (!testProcess)
      return false;
    if (testProcess.process === null)
      return false;
    testProcess.process.kill(0);
    return true;
  } catch {
    return false;
  }
}
function getTestProcessEntry(app) {
  return testProcesses.get(app.name);
}
function clearTestProcessEntry(app) {
  return testProcesses.delete(app.name);
}
function getProcesses() {
  return { devProcesses, testProcesses };
}
async function closeProcess(key) {
  if (isDeployed)
    throw new Error("cannot close processes in deployed mode");
  const proc = devProcesses.get(key);
  if (proc) {
    const exitedPromise = new Promise(
      (resolve) => proc.process.on("exit", resolve)
    );
    if (process.platform === "win32") {
      const { execa } = await import("execa");
      await execa("taskkill", ["/pid", String(proc.process.pid), "/f", "/t"]);
    } else {
      proc.process.kill();
    }
    await Promise.race([
      new Promise((resolve) => setTimeout(resolve, 500)),
      exitedPromise
    ]);
    await stopPort(proc.port);
    devProcesses.delete(key);
  }
}
const sleep = (t) => new Promise((resolve) => setTimeout(resolve, t));
async function stopPort(port) {
  if (isDeployed)
    throw new Error("cannot stop ports in deployed mode");
  await fkill(`:${port}`, { force: true, silent: true });
  await waitForPortToBeAvailable(port);
}
async function waitForPortToBeAvailable(port) {
  const timeout = Date.now() + 1e4;
  let portAvailable = false;
  do {
    portAvailable = await isPortAvailable(port);
    await sleep(100);
  } while (!portAvailable && Date.now() < timeout);
  if (!portAvailable) {
    console.error("Timed out waiting for the port to become available");
  }
}
export {
  clearTestProcessEntry,
  closeProcess,
  getProcesses,
  getTestProcessEntry,
  isAppRunning,
  isPortAvailable,
  isTestRunning,
  runAppDev,
  runAppTests,
  stopPort,
  waitForPortToBeAvailable,
  waitOnApp
};
