import fs from "fs";
import os from "os";
import path from "path";
import { loadConfigs, resolveProductDir } from "../config/index.mjs";
import { captureDatabaseTemplateSnapshot, prepareDatabaseRuntime } from "../database/index.mjs";
import { createRunReporter } from "./presentation/run-reporter.mjs";
import { createRunLogRegistry } from "../runner/logs.mjs";
import { createSetupOperationRegistry } from "../runner/setup-operations.mjs";
import { resolveRuntimeInstanceConfigs } from "../runner/template.mjs";

export async function runDatabaseSnapshotCaptureCommand(options = {}) {
  const productDir = resolveProductDir(process.cwd(), options.dir);
  const configs = await loadConfigs({ dir: productDir });
  const target = resolveTargetConfig(configs, options.service);
  const outputPath = normalizeOptionalString(options.output);
  if (!outputPath) {
    throw new Error("Snapshot capture requires --output");
  }

  const selectedConfigs = collectRequiredConfigs(configs, target.name);
  const runtimeRoot = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-snapshot-"));
  const runtimeDir = path.join(runtimeRoot, "runtime-1");
  const resolvedConfigs = resolveRuntimeInstanceConfigs(selectedConfigs, "runtime-1", runtimeDir, {
    graphDirName: `snapshot-${target.name}`,
    portNamespaceIndex: 0,
    portNamespaceStride: 1,
  });
  const resolvedTarget = resolvedConfigs.find((config) => config.name === target.name);
  if (!resolvedTarget) {
    throw new Error(`Resolved runtime config missing target service "${target.name}"`);
  }

  const absoluteOutputPath = path.resolve(productDir, outputPath);
  const reporter = createRunReporter({ outputMode: options.debug ? "debug" : "compact" });
  const logRegistry = createRunLogRegistry(productDir);
  const setupRegistry = createSetupOperationRegistry({ logRegistry });
  try {
    for (const config of topologicallySortConfigs(resolvedConfigs)) {
      if (config.name === resolvedTarget.name) continue;
      if (config.testkit.database?.provider === "local") {
        await prepareDatabaseRuntime(config, {
          reporter,
          logRegistry,
          setupRegistry,
        });
      }
    }
    await captureDatabaseTemplateSnapshot(resolvedTarget, absoluteOutputPath, {
      reporter,
      logRegistry,
      setupRegistry,
    });
    console.log(`Wrote ${path.relative(productDir, absoluteOutputPath)}`);
  } finally {
    logRegistry.closeAll();
    fs.rmSync(runtimeRoot, { recursive: true, force: true });
  }
}

function resolveTargetConfig(configs, serviceName) {
  if (serviceName) {
    const match = configs.find((config) => config.name === serviceName);
    if (!match) {
      const available = configs.map((config) => config.name).join(", ");
      throw new Error(`Service "${serviceName}" not found. Available: ${available}`);
    }
    return match;
  }

  if (configs.length === 1) return configs[0];
  const withLocalDb = configs.filter((config) => config.testkit.database?.provider === "local");
  if (withLocalDb.length === 1) return withLocalDb[0];

  const available = configs.map((config) => config.name).join(", ");
  throw new Error(`Multiple services available. Pass --service. Available: ${available}`);
}

function normalizeOptionalString(value) {
  if (typeof value !== "string") return null;
  const normalized = value.trim();
  return normalized.length > 0 ? normalized : null;
}

function collectRequiredConfigs(configs, targetName) {
  const byName = new Map(configs.map((config) => [config.name, config]));
  const required = new Set();

  const visit = (name) => {
    if (required.has(name)) return;
    const config = byName.get(name);
    if (!config) {
      throw new Error(`Missing config for dependency "${name}"`);
    }
    required.add(name);
    for (const depName of config.testkit.dependsOn || []) {
      visit(depName);
    }
  };

  visit(targetName);
  return configs.filter((config) => required.has(config.name));
}

function topologicallySortConfigs(configs) {
  const byName = new Map(configs.map((config) => [config.name, config]));
  const visited = new Set();
  const visiting = new Set();
  const ordered = [];

  const visit = (config) => {
    if (visited.has(config.name)) return;
    if (visiting.has(config.name)) {
      throw new Error(`Dependency cycle while resolving snapshot capture for "${config.name}"`);
    }
    visiting.add(config.name);
    for (const depName of config.testkit.dependsOn || []) {
      const dependency = byName.get(depName);
      if (dependency) visit(dependency);
    }
    visiting.delete(config.name);
    visited.add(config.name);
    ordered.push(config);
  };

  for (const config of configs) {
    visit(config);
  }

  return ordered;
}
