import fs from "fs";
import path from "path";
import { execaCommand } from "execa";
import { resolveServiceCwd } from "../config/index.mjs";
import { prepareDatabaseRuntime } from "../database/index.mjs";
import { buildExecutionEnv, resolveWorkerRuntimeConfigs } from "./template.mjs";
import { writeGraphMetadata } from "./state.mjs";
import { startLocalServices, stopLocalServices } from "./services.mjs";

export function createGraphContext(worker, graph) {
  const graphDir = path.join(worker.productDir, ".testkit", "_graphs", graph.dirName);
  const workerStateDir = path.join(graphDir, "workers", `worker-${worker.workerId}`);
  fs.mkdirSync(workerStateDir, { recursive: true });
  writeGraphMetadata(graphDir, graph);

  const runtimeConfigs = resolveWorkerRuntimeConfigs(
    graph.rootConfig,
    graph.runtimeConfigs,
    worker.workerId,
    workerStateDir
  );

  return {
    graphKey: graph.key,
    graphDir,
    workerStateDir,
    runtimeConfigs,
    configByName: new Map(runtimeConfigs.map((config) => [config.name, config])),
    prepared: false,
    started: false,
    startedServices: [],
  };
}

export async function ensureWorkerGraph(worker, batch, graphByKey, lifecycle) {
  const graph = graphByKey.get(batch.graphKey);
  if (!graph) {
    throw new Error(`Unknown graph "${batch.graphKey}"`);
  }

  if (worker.currentGraphKey && worker.currentGraphKey !== batch.graphKey) {
    await deactivateGraphContext(worker.graphContexts.get(worker.currentGraphKey), lifecycle);
    worker.graphSwitches += 1;
    worker.currentGraphKey = null;
  }

  let context = worker.graphContexts.get(batch.graphKey);
  if (!context) {
    context = createGraphContext(worker, graph);
    worker.graphContexts.set(batch.graphKey, context);
    lifecycle.trackGraphContext(context);
  }

  if (!context.prepared) {
    await prepareDatabases(context.runtimeConfigs);
    context.prepared = true;
  }

  if (batchNeedsLocalRuntime(batch) && !context.started) {
    context.startedServices = await startLocalServices(context.runtimeConfigs, lifecycle);
    context.started = true;
  }

  worker.currentGraphKey = batch.graphKey;
  return context;
}

export async function deactivateGraphContext(context, lifecycle) {
  if (!context?.started) return;
  await stopLocalServices(context.startedServices, lifecycle);
  context.started = false;
  context.startedServices = [];
}

export async function resetCurrentGraph(worker, lifecycle) {
  if (!worker.currentGraphKey) return;
  await deactivateGraphContext(worker.graphContexts.get(worker.currentGraphKey), lifecycle);
  worker.currentGraphKey = null;
}

export async function cleanupWorker(worker, lifecycle) {
  for (const context of worker.graphContexts.values()) {
    await deactivateGraphContext(context, lifecycle);
  }
  worker.currentGraphKey = null;
}

export async function prepareDatabases(runtimeConfigs) {
  for (const config of runtimeConfigs) {
    await prepareDatabaseRuntime(config, {
      runMigrate: config.testkit.migrate
        ? (databaseUrl) => runMigrate(config, databaseUrl)
        : null,
      runSeed: config.testkit.seed ? (databaseUrl) => runSeed(config, databaseUrl) : null,
    });
  }
}

async function runMigrate(config, databaseUrl) {
  const migrate = config.testkit.migrate;
  if (!migrate) return;

  const env = buildExecutionEnv(config, {}, process.env);
  if (databaseUrl) env.DATABASE_URL = databaseUrl;

  console.log(`\n── migrate:${config.workerLabel}:${config.name} ──`);
  await execaCommand(migrate.cmd, {
    cwd: resolveServiceCwd(config.productDir, migrate.cwd),
    env,
    stdio: "inherit",
    shell: true,
  });
}

async function runSeed(config, databaseUrl) {
  const seed = config.testkit.seed;
  if (!seed) return;

  const env = buildExecutionEnv(config, {}, process.env);
  if (databaseUrl) env.DATABASE_URL = databaseUrl;

  console.log(`\n── seed:${config.workerLabel}:${config.name} ──`);
  await execaCommand(seed.cmd, {
    cwd: resolveServiceCwd(config.productDir, seed.cwd),
    env,
    stdio: "inherit",
    shell: true,
  });
}

function batchNeedsLocalRuntime(batch) {
  return batch.type !== "dal";
}
