import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { discoverProject } from "./discovery.mjs";
import { loadTestkitSetup } from "./setup-loader.mjs";
import { normalizeToolchainRegistry } from "../toolchains/index.mjs";
import { mergeDiscoveryConfigs } from "../discovery/path-policy.mjs";
import { normalizeDatabaseConfig } from "./database.mjs";
import { normalizeRepoDiscoveryConfig, normalizeServiceDiscoveryConfig } from "./discovery-config.mjs";
import { inferEnvFiles, loadServiceEnv, parseDotenv } from "./env.mjs";
import { ensureExistingPath, resolveProductDir, resolveServiceCwd } from "./paths.mjs";
import {
  detectNextApp,
  inferLocalRuntime,
  normalizeBrowserServiceConfig,
  normalizeOptionalString,
  normalizeRepoExecution,
  normalizeReportingConfig,
  normalizeRuntimeConfig,
} from "./runtime.mjs";
import { normalizeServiceRequirements, normalizeSkipConfig } from "./skip-config.mjs";
import { normalizeTelemetryConfig } from "./telemetry.mjs";
import { validateConfigCoverage, validateServiceConfig } from "./validation.mjs";

const TESTKIT_K6_BIN = "TESTKIT_K6_BIN";

export { parseDotenv, resolveProductDir, resolveServiceCwd };

export async function loadConfigContext(opts = {}) {
  const productDir = resolveProductDir(process.cwd(), opts.dir);
  const setupContext = opts.setupContext || (await loadTestkitSetup(productDir));
  const { setup, setupFile } = setupContext;
  const execution = normalizeRepoExecution(setup.execution);
  const reporting = normalizeReportingConfig(setup.reporting);
  const toolchains = normalizeToolchainRegistry(setup.toolchains);
  const discoveryConfig = normalizeRepoDiscoveryConfig(setup.discovery);
  const explicitServices = setup.services || {};
  const discovery = discoverProject(productDir, explicitServices, {
    ...(opts.discoveryOptions || {}),
    discovery: discoveryConfig,
  });
  const serviceNames = new Set([
    ...Object.keys(explicitServices),
    ...Object.keys(discovery.suitesByService),
  ]);

  const configs = [...serviceNames]
    .sort((left, right) => left.localeCompare(right))
    .map((name) =>
      normalizeServiceConfig({
        name,
        productDir,
        setup,
        setupFile,
        execution,
        discovery: discoveryConfig,
        reporting,
        toolchains,
        explicitService: explicitServices[name] || {},
        discoveredService: discovery.services[name] || null,
        suites: discovery.suitesByService[name] || {},
      })
    );

  validateConfigCoverage(configs);

  return {
    productDir,
    setup,
    setupFile,
    execution,
    discovery: discoveryConfig,
    reporting,
    toolchains,
    explicitServices,
    discovery,
    configs,
  };
}

export async function loadConfigs(opts = {}) {
  const context = await loadConfigContext(opts);
  const { configs } = context;

  const filtered = opts.service ? configs.filter((config) => config.name === opts.service) : configs;

  if (opts.service && filtered.length === 0) {
    const available = configs.map((config) => config.name).join(", ");
    throw new Error(`Service "${opts.service}" not found. Available: ${available}`);
  }

  return filtered;
}

export function resolveK6Binary() {
  const override = process.env[TESTKIT_K6_BIN]?.trim();
  if (override) {
    const isPathLike =
      path.isAbsolute(override) ||
      override.includes(path.sep) ||
      override.includes(path.posix.sep) ||
      override.includes(path.win32.sep);
    const overridePath = isPathLike ? path.resolve(process.cwd(), override) : override;

    if (isPathLike && !fs.existsSync(overridePath)) {
      throw new Error(`${TESTKIT_K6_BIN} points to a missing file: ${overridePath}`);
    }

    return overridePath;
  }

  const thisFile = fileURLToPath(import.meta.url);
  const abs = path.resolve(path.dirname(thisFile), "..", "..", "vendor", "k6");
  if (!fs.existsSync(abs)) {
    throw new Error(`Bundled k6 binary not found: ${abs}`);
  }
  return abs;
}

export function resolveDalBinary() {
  return resolveK6Binary();
}

function normalizeServiceConfig({
  name,
  productDir,
  setup,
  setupFile,
  execution,
  discovery,
  reporting,
  toolchains,
  explicitService,
  discoveredService,
  suites,
}) {
  const local = normalizeLocalConfig(name, explicitService, discoveredService, productDir);
  const envFiles = inferEnvFiles(productDir, explicitService, local);
  const serviceEnv = {
    ...loadServiceEnv(productDir, envFiles),
    ...(explicitService.env || {}),
  };
  const browser = normalizeBrowserServiceConfig(explicitService.browser, name);
  const normalizedDiscovery = mergeDiscoveryConfigs(
    discovery,
    normalizeServiceDiscoveryConfig(explicitService.discovery, name)
  );
  if (explicitService.migrate || explicitService.seed) {
    throw new Error(
      `Service "${name}" uses removed migrate/seed hooks. Move template lifecycle to database.template.{migrate,seed,verify}.`
    );
  }
  const database = normalizeDatabaseConfig(explicitService, name);
  const runtime = normalizeRuntimeConfig(explicitService.runtime, name, toolchains);
  const skip = normalizeSkipConfig(explicitService.skip, { name, suites });
  const requirements = normalizeServiceRequirements(explicitService.requirements, { name, suites });

  validateServiceConfig({
    name,
    local,
    database,
    databaseFrom: explicitService.databaseFrom,
    runtime,
    dependsOn: explicitService.dependsOn || [],
    suites,
    productDir,
  });

  return {
    name,
    productDir,
    setupFile,
    stateDir: path.join(productDir, ".testkit", name),
    telemetry: normalizeTelemetryConfig(setup.telemetry),
    suites,
    testkit: {
      execution,
      reporting,
      dependsOn: explicitService.dependsOn || [],
      discovery: normalizedDiscovery,
      database,
      databaseFrom: explicitService.databaseFrom,
      envFiles,
      requirements,
      serviceEnv,
      skip,
      runtime,
      browser,
      local,
    },
  };
}

function normalizeLocalConfig(name, explicitService, discoveredService, productDir) {
  if (Object.prototype.hasOwnProperty.call(explicitService, "local")) {
    if (explicitService.local === false) {
      return undefined;
    }
    return {
      ...explicitService.local,
      cwd: explicitService.local.cwd || ".",
      env: explicitService.local.env || {},
    };
  }

  const inferredCwd = discoveredService?.inferredLocalCwd || ".";
  const detected = inferLocalRuntime(productDir, inferredCwd);
  if (!detected) return undefined;

  return detected;
}
