import { resolveServiceCwd } from "../config/index.mjs";
import {
  announceResolvedToolchain,
  applyToolchainEnv,
  resolveConfiguredToolchain,
} from "../toolchains/index.mjs";
import { buildExecutionEnv, numericPortFromUrl } from "./template.mjs";
import { DEFAULT_READY_TIMEOUT_MS, assertLocalServicePortsAvailable, isPortInUse, waitForReady } from "./readiness.mjs";
import { captureOutput, killChildProcess, startDetachedCommand, stopChildProcess, sleep } from "./processes.mjs";
import { readDatabaseUrl } from "./state-io.mjs";

export async function startLocalServices(runtimeConfigs, lifecycle, options = {}) {
  const started = [];

  try {
    for (const config of runtimeConfigs) {
      if (!config.testkit.local) continue;
      const proc = await startLocalService(config, lifecycle, options);
      started.push(proc);
    }
  } catch (error) {
    await stopLocalServices(started, lifecycle);
    throw error;
  }

  return started;
}

export async function startLocalService(config, lifecycle, options = {}) {
  const cwd = resolveServiceCwd(config.productDir, config.testkit.local.cwd);
  const resolvedToolchain = await resolveConfiguredToolchain(config);
  await announceResolvedToolchain(config, resolvedToolchain, options.reporter);
  const env = applyToolchainEnv(
    buildExecutionEnv(config, config.testkit.local.env, process.env),
    resolvedToolchain
  );
  const port = config.testkit.local.port || numericPortFromUrl(config.testkit.local.baseUrl);
  if (port) {
    env.PORT = String(port);
  }

  const dbUrl = readDatabaseUrl(config.stateDir);
  if (dbUrl) {
    env.DATABASE_URL = dbUrl;
  }

  await assertLocalServicePortsAvailable(config, isPortInUse);

  options.reporter?.localServiceStarting?.(config, config.testkit.local.start);
  const child = startDetachedCommand(config.testkit.local.start, cwd, env);
  const logRecord = options.logRegistry?.ensureServiceLogRecord(config);
  const liveWriter =
    options.reporter?.outputMode === "debug"
      ? (line) => options.reporter.writeDebugLine?.(line)
      : null;

  const outputDrains = [
    captureOutput(child.stdout, {
      livePrefix: `[${config.runtimeLabel}:${config.name}]`,
      liveWriter,
      onLine(line) {
        if (logRecord) options.logRegistry.append(logRecord, "stdout", line);
      },
    }),
    captureOutput(child.stderr, {
      livePrefix: `[${config.runtimeLabel}:${config.name}]`,
      liveWriter,
      onLine(line) {
        if (logRecord) options.logRegistry.append(logRecord, "stderr", line);
      },
    }),
  ];
  lifecycle.registerService(config, child, cwd, () => {
    killChildProcess(child, "SIGTERM");
  });

  const readyTimeoutMs = config.testkit.local.readyTimeoutMs || DEFAULT_READY_TIMEOUT_MS;

  try {
    await waitForReady({
      name: `${config.runtimeLabel}:${config.name}`,
      url: config.testkit.local.readyUrl,
      timeoutMs: readyTimeoutMs,
      process: child,
      signal: lifecycle.signal,
      sleep,
    });
  } catch (error) {
    await stopChildProcess(child, outputDrains);
    lifecycle.unregisterService(child.pid);
    throw error;
  }

  return { name: config.name, child, outputDrains };
}

export async function stopLocalServices(started, lifecycle) {
  for (const service of [...started].reverse()) {
    await stopChildProcess(service.child, service.outputDrains);
    lifecycle?.unregisterService(service.child.pid);
  }
}
