import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { discoverProject } from "./discovery.mjs";
import { loadTestkitSetup } from "./setup-loader.mjs";
import {
  matchesSuiteSelectors,
  parseSuiteSelectors,
  suiteSelectionType,
} from "../runner/suite-selection.mjs";
import {
  DEFAULT_FILE_TIMEOUT_SECONDS,
  normalizeDatabaseBinding,
  normalizeExecutionConfig,
  normalizeRuntimeMaxConcurrentTasks,
  normalizeRuntimeInstances,
} from "../runner/execution-config.mjs";
import { normalizeKnownFailureIssueValidationConfig } from "../known-failures/github.mjs";

const TESTKIT_K6_BIN = "TESTKIT_K6_BIN";
const DEFAULT_LOCAL_IMAGE = "pgvector/pgvector:pg16";
const DEFAULT_LOCAL_USER = "testkit";
const DEFAULT_LOCAL_PASSWORD = "testkit";

export function parseDotenv(filePath) {
  if (!fs.existsSync(filePath)) return {};
  return parseDotenvString(fs.readFileSync(filePath, "utf8"));
}

export async function loadConfigs(opts = {}) {
  const productDir = resolveProductDir(process.cwd(), opts.dir);
  const { setup, setupFile } = await loadTestkitSetup(productDir);
  const execution = normalizeRepoExecution(setup.execution);
  const reporting = normalizeReportingConfig(setup.reporting);
  const explicitServices = setup.services || {};
  const discovery = discoverProject(productDir, explicitServices);
  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,
        reporting,
        explicitService: explicitServices[name] || {},
        discoveredService: discovery.services[name] || null,
        suites: discovery.suitesByService[name] || {},
      })
    );

  validateConfigCoverage(configs);

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

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

function normalizeServiceConfig({
  name,
  productDir,
  setup,
  setupFile,
  execution,
  reporting,
  explicitService,
  discoveredService,
  suites,
}) {
  const local = normalizeLocalConfig(name, explicitService, discoveredService, productDir);
  const envFiles = inferEnvFiles(productDir, explicitService, local);
  const serviceEnv = {
    ...loadServiceEnv(productDir, envFiles),
    ...(explicitService.env || {}),
  };
  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);
  const skip = normalizeSkipConfig(explicitService.skip, {
    name,
    productDir,
    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 || [],
      database,
      databaseFrom: explicitService.databaseFrom,
      envFiles,
      requirements,
      serviceEnv,
      skip,
      runtime,
      local,
    },
  };
}

function normalizeReportingConfig(value) {
  if (!value) return null;

  const knownFailuresFile = normalizeOptionalString(value.knownFailuresFile);
  if (!knownFailuresFile) {
    throw new Error('testkit.setup.ts reporting.knownFailuresFile must be a non-empty string');
  }

  const issueValidation = normalizeKnownFailureIssueValidationConfig(value.issueValidation);

  return {
    knownFailuresFile,
    issueValidation,
  };
}

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

function inferLocalRuntime(productDir, cwd) {
  const absoluteCwd = resolveServiceCwd(productDir, cwd);
  if (!fs.existsSync(absoluteCwd)) return undefined;

  if (detectNextApp(absoluteCwd)) {
    return {
      cwd,
      start: "exec ./node_modules/.bin/next dev -p {port}",
      port: 3000,
      baseUrl: "http://127.0.0.1:{port}",
      readyUrl: "http://127.0.0.1:{port}",
      env: {},
    };
  }

  if (fs.existsSync(path.join(absoluteCwd, "cmd", "server"))) {
    return {
      cwd,
      start: "exec go run ./cmd/server",
      port: 3000,
      baseUrl: "http://127.0.0.1:{port}",
      readyUrl: "http://127.0.0.1:{port}/health",
      env: {},
    };
  }

  if (fs.existsSync(path.join(absoluteCwd, "package.json")) && fs.existsSync(path.join(absoluteCwd, "src"))) {
    return {
      cwd,
      start: "exec ./node_modules/.bin/tsx watch src/index.ts",
      port: 3000,
      baseUrl: "http://127.0.0.1:{port}",
      readyUrl: "http://127.0.0.1:{port}/health",
      env: {},
    };
  }

  return undefined;
}

function normalizeDatabaseConfig(explicitService, serviceName) {
  if (explicitService.databaseFrom) return undefined;
  if (!explicitService.database) return undefined;

  const database =
    explicitService.database.provider === "local"
      ? explicitService.database
      : {
          provider: "local",
          ...explicitService.database,
        };

  return {
    ...database,
    binding: normalizeDatabaseBinding(
      database.binding || "per-runtime",
      `Service "${serviceName}" database.binding`
    ),
    provider: "local",
    selectedBackend: "local",
    reset: database.reset !== false,
    image: database.image || DEFAULT_LOCAL_IMAGE,
    user: database.user || DEFAULT_LOCAL_USER,
    password: database.password || DEFAULT_LOCAL_PASSWORD,
    template: normalizeDatabaseTemplateConfig(database.template, serviceName),
    serviceName,
  };
}

function normalizeRuntimeConfig(value, serviceName) {
  if (!value) {
    return {
      instances: 1,
      maxConcurrentTasks: Number.POSITIVE_INFINITY,
    };
  }

  return {
    instances: normalizeRuntimeInstances(
      value.instances ?? 1,
      `Service "${serviceName}" runtime.instances`
    ),
    maxConcurrentTasks: normalizeRuntimeMaxConcurrentTasks(
      value.maxConcurrentTasks,
      `Service "${serviceName}" runtime.maxConcurrentTasks`
    ),
  };
}

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

function normalizeDatabaseTemplateConfig(value, serviceName) {
  if (value == null) {
    return {
      inputs: [],
      migrate: [],
      seed: [],
      verify: [],
    };
  }
  if (!value || typeof value !== "object") {
    throw new Error(`Service "${serviceName}" database.template must be an object`);
  }

  return {
    inputs: normalizeTemplateInputs(value.inputs, serviceName),
    migrate: normalizeTemplateLifecycleSteps(
      value.migrate,
      `Service "${serviceName}" database.template.migrate`
    ),
    seed: normalizeTemplateLifecycleSteps(
      value.seed,
      `Service "${serviceName}" database.template.seed`
    ),
    verify: normalizeTemplateLifecycleSteps(
      value.verify,
      `Service "${serviceName}" database.template.verify`
    ),
  };
}

function normalizeTemplateInputs(value, serviceName) {
  if (value == null) return [];
  if (!Array.isArray(value)) {
    throw new Error(`Service "${serviceName}" database.template.inputs must be an array`);
  }

  return value.map((entry, index) => {
    const normalized = normalizeOptionalString(entry);
    if (!normalized) {
      throw new Error(
        `Service "${serviceName}" database.template.inputs[${index}] must be a non-empty string`
      );
    }
    return normalized;
  });
}

function normalizeTemplateLifecycleSteps(value, label) {
  if (value == null) return [];
  if (!Array.isArray(value)) {
    throw new Error(`${label} must be an array`);
  }

  return value.map((step, index) => normalizeTemplateLifecycleStep(step, `${label}[${index}]`));
}

function normalizeTemplateLifecycleStep(step, label) {
  if (!step || typeof step !== "object") {
    throw new Error(`${label} must be an object`);
  }

  const kind = normalizeOptionalString(step.kind);
  if (kind === "command") {
    const cmd = normalizeOptionalString(step.cmd);
    if (!cmd) throw new Error(`${label}.cmd must be a non-empty string`);
    return {
      kind,
      cmd,
      cwd: normalizeOptionalString(step.cwd),
      inputs: normalizeTemplateStepInputs(step.inputs, label),
    };
  }
  if (kind === "sql-file") {
    const filePath = normalizeOptionalString(step.path);
    if (!filePath) throw new Error(`${label}.path must be a non-empty string`);
    return {
      kind,
      path: filePath,
      cwd: normalizeOptionalString(step.cwd),
      inputs: normalizeTemplateStepInputs(step.inputs, label),
    };
  }
  if (kind === "module") {
    const specifier = normalizeOptionalString(step.specifier);
    if (!specifier) throw new Error(`${label}.specifier must be a non-empty string`);
    return {
      kind,
      specifier,
      cwd: normalizeOptionalString(step.cwd),
      inputs: normalizeTemplateStepInputs(step.inputs, label),
    };
  }

  throw new Error(`${label}.kind must be one of: command, sql-file, module`);
}

function normalizeTemplateStepInputs(value, label) {
  if (value == null) return [];
  if (!Array.isArray(value)) {
    throw new Error(`${label}.inputs must be an array`);
  }

  return value.map((entry, index) => {
    const normalized = normalizeOptionalString(entry);
    if (!normalized) {
      throw new Error(`${label}.inputs[${index}] must be a non-empty string`);
    }
    return normalized;
  });
}

function normalizeSkipConfig(value, { name, productDir, suites }) {
  if (!value) return undefined;

  const discoveredFiles = new Set();
  const discoveredSuites = [];
  for (const [type, typedSuites] of Object.entries(suites || {})) {
    for (const suite of typedSuites || []) {
      const displayType = suiteSelectionType(type, suite.framework || "k6");
      discoveredSuites.push({
        type,
        displayType,
        name: suite.name,
      });
      for (const file of suite.files || []) {
        discoveredFiles.add(normalizePath(file));
      }
    }
  }

  const seenFiles = new Set();
  const files = [];
  for (const rule of value.files || []) {
    if (!rule || typeof rule !== "object") {
      throw new Error(`Service "${name}" skip.files entries must be objects`);
    }
    const filePath = normalizePath(rule.path);
    const reason = normalizeSkipReason(rule.reason, `Service "${name}" skip.files["${filePath}"]`);
    if (!filePath) {
      throw new Error(`Service "${name}" skip.files entries require a non-empty path`);
    }
    if (seenFiles.has(filePath)) {
      throw new Error(`Service "${name}" defines duplicate skip.files path "${filePath}"`);
    }
    if (!discoveredFiles.has(filePath)) {
      throw new Error(
        `Service "${name}" skip.files path "${filePath}" did not match any discovered suite file`
      );
    }
    seenFiles.add(filePath);
    files.push({ path: filePath, reason });
  }

  const parsedSelectors = (value.suites || []).flatMap((rule, index) => {
    if (!rule || typeof rule !== "object") {
      throw new Error(`Service "${name}" skip.suites entries must be objects`);
    }
    const selector = String(rule.selector || "").trim();
    if (!selector) {
      throw new Error(`Service "${name}" skip.suites[${index}] requires a non-empty selector`);
    }
    const reason = normalizeSkipReason(
      rule.reason,
      `Service "${name}" skip.suites["${selector}"]`
    );
    const parsed = parseSuiteSelectors([selector]);
    if (parsed.length !== 1) {
      throw new Error(`Service "${name}" skip.suites["${selector}"] is invalid`);
    }
    return [{ selector: parsed[0], reason }];
  });

  const seenSelectors = new Set();
  const suitesWithReasons = [];
  for (const rule of parsedSelectors) {
    if (seenSelectors.has(rule.selector.raw)) {
      throw new Error(
        `Service "${name}" defines duplicate skip.suites selector "${rule.selector.raw}"`
      );
    }
    const matched = discoveredSuites.some((suite) =>
      matchesSuiteSelectors(suite.displayType, suite.name, [rule.selector])
    );
    if (!matched) {
      throw new Error(
        `Service "${name}" skip.suites selector "${rule.selector.raw}" did not match any discovered suite`
      );
    }
    seenSelectors.add(rule.selector.raw);
    suitesWithReasons.push(rule);
  }

  if (files.length === 0 && suitesWithReasons.length === 0) return undefined;

  return {
    files,
    fileReasonByPath: new Map(files.map((rule) => [rule.path, rule.reason])),
    suites: suitesWithReasons,
  };
}

function normalizeServiceRequirements(value, { name, suites }) {
  if (!value) return { suites: [], files: [], fileLocksByPath: new Map() };

  const discoveredSuites = [];
  const discoveredFiles = new Set();
  for (const [type, typedSuites] of Object.entries(suites || {})) {
    for (const suite of typedSuites || []) {
      discoveredSuites.push({
        displayType: suiteSelectionType(type, suite.framework || "k6"),
        name: suite.name,
      });
      for (const file of suite.files || []) {
        discoveredFiles.add(file);
      }
    }
  }

  const suiteRules = [];
  const seenSelectors = new Set();
  for (const rule of value.suites || []) {
    if (!rule || typeof rule !== "object") {
      throw new Error(`Service "${name}" requirements.suites entries must be objects`);
    }
    const selector = String(rule.selector || "").trim();
    if (!selector) {
      throw new Error(`Service "${name}" requirements.suites entries require a selector`);
    }
    const parsed = parseSuiteSelectors([selector]);
    if (parsed.length !== 1) {
      throw new Error(`Service "${name}" requirements.suites["${selector}"] is invalid`);
    }
    const parsedSelector = parsed[0];
    if (parsedSelector.kind !== "typed") {
      throw new Error(
        `Service "${name}" requirements.suites["${selector}"] must use a typed selector like int:health`
      );
    }
    if (seenSelectors.has(parsedSelector.raw)) {
      throw new Error(
        `Service "${name}" defines duplicate requirements.suites selector "${parsedSelector.raw}"`
      );
    }
    const matched = discoveredSuites.some((suite) =>
      matchesSuiteSelectors(suite.displayType, suite.name, [parsedSelector])
    );
    if (!matched) {
      throw new Error(
        `Service "${name}" requirements.suites selector "${parsedSelector.raw}" did not match any discovered suite`
      );
    }
    seenSelectors.add(parsedSelector.raw);
    suiteRules.push({
      selector: parsedSelector,
      locks: normalizeRequirementLocks(
        rule.locks,
        `Service "${name}" requirements.suites["${parsedSelector.raw}"].locks`
      ),
    });
  }

  const fileRules = [];
  const seenFiles = new Set();
  for (const [index, rule] of (value.files || []).entries()) {
    if (!rule || typeof rule !== "object") {
      throw new Error(`Service "${name}" requirements.files entries must be objects`);
    }

    const filePath = String(rule.path || "").trim();
    if (!filePath) {
      throw new Error(`Service "${name}" requirements.files[${index}] requires a path`);
    }
    if (!discoveredFiles.has(filePath)) {
      throw new Error(
        `Service "${name}" requirements.files["${filePath}"] did not match any discovered test file`
      );
    }
    if (seenFiles.has(filePath)) {
      throw new Error(`Service "${name}" defines duplicate requirements.files path "${filePath}"`);
    }

    seenFiles.add(filePath);
    fileRules.push({
      path: filePath,
      locks: normalizeRequirementLocks(
        rule.locks,
        `Service "${name}" requirements.files["${filePath}"].locks`
      ),
    });
  }

  return {
    suites: suiteRules,
    files: fileRules,
    fileLocksByPath: new Map(fileRules.map((rule) => [rule.path, rule.locks])),
  };
}

function normalizeRequirementLocks(value, label) {
  const input = Array.isArray(value) ? value : value == null ? [] : [value];
  const seen = new Set();
  const locks = [];

  for (const rawLock of input) {
    const lockName = String(rawLock || "").trim();
    if (!lockName) {
      throw new Error(`${label} entries must be non-empty strings`);
    }
    if (seen.has(lockName)) continue;
    seen.add(lockName);
    locks.push(lockName);
  }

  return locks.sort();
}

function inferEnvFiles(productDir, explicitService, local) {
  if (explicitService.envFile || explicitService.envFiles) {
    const files = [];
    if (explicitService.envFile) files.push(explicitService.envFile);
    if (Array.isArray(explicitService.envFiles)) files.push(...explicitService.envFiles);
    return files;
  }

  const candidates = [];
  const serviceCwd = local?.cwd || ".";
  if (serviceCwd !== ".") {
    candidates.push(path.posix.join(serviceCwd, ".env.testkit"));
    candidates.push(path.posix.join(serviceCwd, ".env"));
  }
  candidates.push(".env.testkit");
  candidates.push(".env");

  return [...new Set(candidates)]
    .map((candidate) => candidate.split(path.sep).join("/"))
    .filter((candidate) => fs.existsSync(resolveServiceCwd(productDir, candidate)));
}

function normalizeSkipReason(reason, label) {
  const normalized = String(reason || "").trim();
  if (!normalized) {
    throw new Error(`${label} requires a non-empty reason`);
  }
  return normalized;
}

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

function validateConfigCoverage(configs) {
  const names = new Set(configs.map((config) => config.name));
  for (const config of configs) {
    for (const depName of config.testkit.dependsOn || []) {
      if (!names.has(depName)) {
        throw new Error(`Service "${config.name}" depends on "${depName}", but ${depName} is not defined`);
      }
    }
    const databaseFrom = config.testkit.databaseFrom;
    if (databaseFrom && !names.has(databaseFrom)) {
      throw new Error(
        `Service "${config.name}" databaseFrom "${databaseFrom}" is not defined`
      );
    }
  }
}

function validateServiceConfig({
  name,
  local,
  database,
  databaseFrom,
  runtime,
  dependsOn,
  suites,
  productDir,
}) {
  const usesLocalExecution = Object.entries(suites).some(([suiteType, discoveredSuites]) =>
    discoveredSuites.some(
      (suite) => (suite.framework && suite.framework !== "k6") || suiteType !== "dal"
    )
  );

  if (usesLocalExecution && !local) {
    throw new Error(
      `Service "${name}" defines non-DAL suites but no local runtime could be resolved. Add it in testkit.setup.ts.`
    );
  }

  if (database && databaseFrom) {
    throw new Error(`Service "${name}" cannot define both database and databaseFrom`);
  }
  if (runtime.instances < 1) {
    throw new Error(`Service "${name}" runtime.instances must be a positive integer`);
  }
  if (runtime.maxConcurrentTasks <= 0) {
    throw new Error(
      `Service "${name}" runtime.maxConcurrentTasks must be a positive integer when provided`
    );
  }

  for (const depName of dependsOn || []) {
    if (depName === name) {
      throw new Error(`Service "${name}" cannot depend on itself`);
    }
  }

  if (local?.cwd) {
    ensureExistingPath(productDir, local.cwd, `Service "${name}" local.cwd`);
  }
  for (const [stageName, steps] of Object.entries(database?.template || {})) {
    if (stageName === "inputs") continue;
    for (const step of steps || []) {
      if (step.cwd) {
        ensureExistingPath(
          productDir,
          step.cwd,
          `Service "${name}" database.template.${stageName} step cwd`
        );
      }
      if (step.kind === "sql-file") {
        ensureExistingPath(
          resolveServiceCwd(productDir, step.cwd || "."),
          step.path,
          `Service "${name}" database.template.${stageName} sql file`
        );
      }
      if (step.kind === "module") {
        const { modulePath } = parseModuleSpecifier(step.specifier);
        ensureExistingPath(
          resolveServiceCwd(productDir, step.cwd || "."),
          modulePath,
          `Service "${name}" database.template.${stageName} module`
        );
      }
      for (const input of step.inputs || []) {
        ensureExistingPath(
          resolveServiceCwd(productDir, step.cwd || "."),
          input,
          `Service "${name}" database.template.${stageName} step input`
        );
      }
    }
  }
  for (const input of database?.template?.inputs || []) {
    ensureExistingPath(productDir, input, `Service "${name}" database.template input`);
  }
}

function ensureExistingPath(productDir, relativePath, label) {
  const absolute = resolveServiceCwd(productDir, relativePath);
  if (!fs.existsSync(absolute)) {
    throw new Error(`${label} does not exist: ${relativePath}`);
  }
}

function normalizeTelemetryConfig(telemetry) {
  if (!telemetry) return null;
  if (telemetry.endpoint) {
    let parsed;
    try {
      parsed = new URL(telemetry.endpoint);
    } catch {
      throw new Error("testkit.setup telemetry.endpoint must be a valid URL");
    }
    if (!["http:", "https:"].includes(parsed.protocol)) {
      throw new Error("testkit.setup telemetry.endpoint must use http or https");
    }
  }
  if (telemetry.enabled === true) {
    if (!telemetry.endpoint) {
      throw new Error("testkit.setup telemetry.endpoint is required when telemetry.enabled is true");
    }
    if (!telemetry.tokenEnv) {
      throw new Error("testkit.setup telemetry.tokenEnv is required when telemetry.enabled is true");
    }
  }
  return {
    enabled: telemetry.enabled === true,
    endpoint: telemetry.endpoint,
    tokenEnv: telemetry.tokenEnv,
    timeoutMs: telemetry.timeoutMs || 3000,
  };
}

function normalizeRepoExecution(execution) {
  if (!execution) {
    return normalizeExecutionConfig({
      workers: 1,
      fileTimeoutSeconds: DEFAULT_FILE_TIMEOUT_SECONDS,
    });
  }
  return normalizeExecutionConfig(execution);
}

function normalizePath(value) {
  return String(value || "")
    .split(path.sep)
    .join("/")
    .replace(/^\.\/+/, "");
}

export function resolveProductDir(cwd, explicitDir) {
  const dir = explicitDir ? path.resolve(cwd, explicitDir) : cwd;
  if (!fs.existsSync(dir)) {
    throw new Error(`Product directory does not exist: ${dir}`);
  }
  return dir;
}

function parseDotenvString(source) {
  const env = {};
  for (const line of String(source).split("\n")) {
    const trimmed = line.trim();
    if (!trimmed || trimmed.startsWith("#")) continue;
    const eq = trimmed.indexOf("=");
    if (eq === -1) continue;
    const key = trimmed.slice(0, eq).trim();
    let val = trimmed.slice(eq + 1).trim();
    if (
      (val.startsWith("'") && val.endsWith("'")) ||
      (val.startsWith("\"") && val.endsWith("\""))
    ) {
      val = val.slice(1, -1);
    }
    env[key] = val;
  }
  return env;
}

function detectNextApp(cwd) {
  return (
    fs.existsSync(path.join(cwd, "next.config.js")) ||
    fs.existsSync(path.join(cwd, "next.config.mjs")) ||
    fs.existsSync(path.join(cwd, "next.config.ts"))
  );
}

function parseModuleSpecifier(specifier) {
  const [modulePath, exportName] = String(specifier).split("#", 2);
  return {
    modulePath,
    exportName: exportName || "default",
  };
}
