import net from "net";
import {
  cleanupStaleRuns,
  findPortOwner,
  formatRunSummary,
  isPidRunning,
  listRunManifests,
} from "./lifecycle.mjs";
import { socketFromUrl } from "./template.mjs";

export const DEFAULT_READY_TIMEOUT_MS = 120_000;

export async function waitForReady({ name, url, timeoutMs, process, signal, sleep }) {
  const start = Date.now();

  while (Date.now() - start < timeoutMs) {
    if (signal?.aborted) {
      throw signal.reason || new Error(`Service "${name}" startup aborted`);
    }
    if (process.exitCode !== null) {
      throw new Error(`Service "${name}" exited before becoming ready`);
    }

    try {
      const response = await fetch(url);
      if (response.ok) return;
    } catch {
      // Service still warming up.
    }

    await sleep(1_000);
  }

  throw new Error(`Timed out waiting for "${name}" to become ready at ${url}`);
}

export async function assertLocalServicePortsAvailable(config, isPortInUse) {
  const endpoints = [config.testkit.local.baseUrl, config.testkit.local.readyUrl];
  const seen = new Set();

  for (const endpoint of endpoints) {
    const socket = socketFromUrl(endpoint);
    if (!socket) continue;

    const key = `${socket.host}:${socket.port}`;
    if (seen.has(key)) continue;
    seen.add(key);

    if (await isPortInUse(socket)) {
      await cleanupStaleRuns(config.productDir);
    }

    if (await isPortInUse(socket)) {
      const owner = findPortOwner(config.productDir, socket);
      const ownerDetail = owner
        ? owner.active
          ? ` Active testkit run ${formatRunSummary(owner.manifest)} owns ${key} via ${owner.service.runtimeLabel}:${owner.service.serviceName}.`
          : ` Stale testkit run ${formatRunSummary(owner.manifest)} owns ${key}.`
        : "";
      throw new Error(
        `Cannot start "${config.runtimeLabel}:${config.name}" because ${key} is already in use. ` +
          `Stop the existing process and rerun testkit.${ownerDetail}`
      );
    }
  }
}

export async function isPortInUse({ host, port }) {
  return new Promise((resolve, reject) => {
    const socket = new net.Socket();
    let settled = false;

    const finish = (value, error = null) => {
      if (settled) return;
      settled = true;
      socket.destroy();
      if (error) {
        reject(error);
        return;
      }
      resolve(value);
    };

    socket.setTimeout(1_000);
    socket.once("connect", () => finish(true));
    socket.once("timeout", () => finish(false));
    socket.once("error", (error) => {
      if (["ECONNREFUSED", "EHOSTUNREACH", "ENOTFOUND"].includes(error.code)) {
        finish(false);
        return;
      }
      finish(false, error);
    });

    socket.connect(port, host);
  });
}

export function printRunStatus(productDir) {
  const manifests = listRunManifests(productDir);
  if (manifests.length === 0) return;

  console.log("  runs/");
  for (const manifest of manifests) {
    const state = isPidRunning(manifest.pid) ? "active" : "stale";
    const ports = [
      ...new Set(
        (manifest.services || []).flatMap((service) =>
          (service.ports || []).map((socket) => `${socket.host}:${socket.port}`)
        )
      ),
    ];
    console.log(
      `    ${manifest.runId}: ${state} pid=${manifest.pid}${ports.length > 0 ? ` ports=${ports.join(",")}` : ""}`
    );
  }
}
