import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import {
  getServiceEnvFiles as getServiceEnvFilesModel,
  isDalSuiteType as isDalSuiteTypeModel,
  isObject as isObjectModel,
  normalizeTelemetryConfig as normalizeTelemetryConfigModel,
  normalizeTemplateConfig as normalizeTemplateConfigModel,
  parseDotenvString,
  requireString as requireStringModel,
  resolveLifecycleConfig as resolveLifecycleConfigModel,
  resolveSelectedDatabase as resolveSelectedDatabaseModel,
  validateConfigCoverage as validateConfigCoverageModel,
  validateDatabaseProviderConfig as validateDatabaseProviderConfigModel,
  validateHttpUrl as validateHttpUrlModel,
  validateLifecycleConfig as validateLifecycleConfigModel,
  validateRunnerManifest,
  validateServiceConfig as validateServiceConfigModel,
  validateTelemetryConfig as validateTelemetryConfigModel,
  validateTemplateConfig as validateTemplateConfigModel,
} from "./model.mjs";

const RUNNER_MANIFEST = "runner.manifest.json";
const TESTKIT_CONFIG = "testkit.config.json";
const TESTKIT_K6_BIN = "TESTKIT_K6_BIN";
export function parseDotenv(filePath) {
  if (!fs.existsSync(filePath)) return {};
  return parseDotenvString(fs.readFileSync(filePath, "utf8"));
}

export function getServiceNames(cwd) {
  const dir = cwd || process.cwd();
  const runnerPath = path.join(dir, RUNNER_MANIFEST);
  if (!fs.existsSync(runnerPath)) return [];
  const runner = JSON.parse(fs.readFileSync(runnerPath, "utf8"));
  if (!isObject(runner.services)) return [];
  return Object.keys(runner.services);
}

export function loadConfigs(opts = {}) {
  const productDir = resolveProductDir(process.cwd(), opts.dir);
  const runner = loadRunnerManifest(productDir);
  const config = loadTestkitConfig(productDir);
  validateConfigCoverage(runner, config);

  const serviceEntries = Object.entries(runner.services);
  const filtered = opts.service
    ? serviceEntries.filter(([name]) => name === opts.service)
    : serviceEntries;

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

  return filtered.map(([name, runnerService]) => {
    const serviceConfig = config.services[name];
    if (!serviceConfig) {
      throw new Error(
        `Service "${name}" exists in ${RUNNER_MANIFEST} but is missing from ${TESTKIT_CONFIG}`
      );
    }

    const resolvedDatabase = resolveSelectedDatabase(name, serviceConfig);
    const serviceEnv = loadServiceEnv(productDir, serviceConfig);
    const selectedBackend = resolvedDatabase?.selectedBackend;
    const resolvedMigrate = resolveLifecycleConfig(serviceConfig.migrate, selectedBackend);
    const resolvedSeed = resolveLifecycleConfig(serviceConfig.seed, selectedBackend);
    validateMergedService(
      name,
      runnerService,
      serviceConfig,
      resolvedDatabase,
      resolvedMigrate,
      resolvedSeed,
      productDir
    );

    return {
      name,
      productDir,
      stateDir: path.join(productDir, ".testkit", name),
      telemetry: normalizeTelemetryConfig(config.telemetry),
      suites: runnerService.suites,
      testkit: {
        ...serviceConfig,
        database: resolvedDatabase,
        migrate: resolvedMigrate,
        seed: resolvedSeed,
        envFiles: getServiceEnvFiles(serviceConfig),
        serviceEnv,
      },
    };
  });
}

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();
}

export function resolveServiceCwd(productDir, maybeRelative) {
  return path.resolve(productDir, maybeRelative || ".");
}

function loadRunnerManifest(productDir) {
  const manifestPath = path.join(productDir, RUNNER_MANIFEST);
  const raw = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
  validateRunnerManifest(raw, RUNNER_MANIFEST, manifestPath);
  return raw;
}

function loadTestkitConfig(productDir) {
  const configPath = path.join(productDir, TESTKIT_CONFIG);
  const raw = JSON.parse(fs.readFileSync(configPath, "utf8"));

  if (!isObject(raw.services)) {
    throw new Error(`${TESTKIT_CONFIG} must have a "services" object (${configPath})`);
  }

  if (raw.telemetry !== undefined) {
    validateTelemetryConfigModel(raw.telemetry, configPath, TESTKIT_CONFIG);
  }

  for (const [serviceName, service] of Object.entries(raw.services)) {
    validateServiceConfigModel(serviceName, service, configPath);
  }

  return raw;
}

function validateConfigCoverage(runner, config) {
  return validateConfigCoverageModel(runner, config, TESTKIT_CONFIG, RUNNER_MANIFEST);
}

function resolveProductDir(cwd, explicitDir) {
  if (explicitDir) {
    const dir = path.resolve(cwd, explicitDir);
    ensureProductFiles(dir);
    return dir;
  }

  ensureProductFiles(cwd);
  return cwd;
}

function ensureProductFiles(dir) {
  const missing = [RUNNER_MANIFEST, TESTKIT_CONFIG].filter(
    (file) => !fs.existsSync(path.join(dir, file))
  );
  if (missing.length > 0) {
    throw new Error(
      `Expected ${missing.join(" and ")} in ${dir}. Either cd into a product directory or use --dir.`
    );
  }
}

function validateMergedService(
  name,
  runnerService,
  serviceConfig,
  resolvedDatabase,
  resolvedMigrate,
  resolvedSeed,
  productDir
) {
  const usesLocalExecution = Object.values(runnerService.suites).some((suites) =>
    suites.some(
      (suite) =>
        (suite.framework && suite.framework !== "k6") ||
        !isDalSuiteType(suite, runnerService, suites)
    )
  );

  if (usesLocalExecution && !isObject(serviceConfig.local)) {
    throw new Error(
      `Service "${name}" defines non-DAL suites but has no local runtime in ${TESTKIT_CONFIG}`
    );
  }

  if (serviceConfig.dependsOn) {
    for (const dep of serviceConfig.dependsOn) {
      if (dep === name) {
        throw new Error(`Service "${name}" cannot depend on itself`);
      }
    }
  }

  if (
    resolvedDatabase?.provider === "local" &&
    (serviceConfig.migrate || serviceConfig.seed) &&
    resolvedDatabase.template.inputs.length === 0
  ) {
    throw new Error(
      `Service "${name}" uses local database provisioning with migrations or seeds, so database.template.inputs must list the files/directories that define the template cache`
    );
  }

  if (serviceConfig.local?.cwd) {
    const cwdPath = resolveServiceCwd(productDir, serviceConfig.local.cwd);
    if (!fs.existsSync(cwdPath)) {
      throw new Error(
        `Service "${name}" local.cwd does not exist: ${serviceConfig.local.cwd}`
      );
    }
  }

  if (resolvedMigrate?.cwd) {
    const cwdPath = resolveServiceCwd(productDir, resolvedMigrate.cwd);
    if (!fs.existsSync(cwdPath)) {
      throw new Error(
        `Service "${name}" migrate.cwd does not exist: ${resolvedMigrate.cwd}`
      );
    }
  }

  if (resolvedSeed?.cwd) {
    const cwdPath = resolveServiceCwd(productDir, resolvedSeed.cwd);
    if (!fs.existsSync(cwdPath)) {
      throw new Error(
        `Service "${name}" seed.cwd does not exist: ${resolvedSeed.cwd}`
      );
    }
  }

}

function validateServiceConfig(name, service, configPath) {
  return validateServiceConfigModel(name, service, configPath);
}

function validateTelemetryConfig(telemetry, configPath) {
  return validateTelemetryConfigModel(telemetry, configPath, TESTKIT_CONFIG);
}

function loadServiceEnv(productDir, serviceConfig) {
  const env = {};
  for (const envFile of getServiceEnvFiles(serviceConfig)) {
    Object.assign(env, parseDotenv(resolveServiceCwd(productDir, envFile)));
  }
  return env;
}

function getServiceEnvFiles(serviceConfig) {
  return getServiceEnvFilesModel(serviceConfig);
}

function resolveSelectedDatabase(name, serviceConfig) {
  return resolveSelectedDatabaseModel(name, serviceConfig);
}

function validateDatabaseProviderConfig(name, db, label) {
  return validateDatabaseProviderConfigModel(name, db, label);
}

function validateTemplateConfig(name, template, label) {
  return validateTemplateConfigModel(name, template, label);
}

function normalizeTemplateConfig(template) {
  return normalizeTemplateConfigModel(template);
}

function validateLifecycleConfig(name, value, label) {
  return validateLifecycleConfigModel(name, value, label);
}

function resolveLifecycleConfig(value, selectedBackend) {
  return resolveLifecycleConfigModel(value, selectedBackend);
}

function requireString(obj, key, label) {
  return requireStringModel(obj, key, label);
}

function isDalSuiteType(suite, runnerService, suitesForType) {
  return isDalSuiteTypeModel(suite, runnerService, suitesForType);
}

function isObject(value) {
  return isObjectModel(value);
}

function normalizeTelemetryConfig(telemetry) {
  return normalizeTelemetryConfigModel(telemetry);
}

function validateHttpUrl(value, label) {
  return validateHttpUrlModel(value, label);
}
