import { matchesSuiteSelectors, parseSuiteSelectors, suiteSelectionType } from "../runner/suite-selection.mjs";
import { normalizePath } from "./paths.mjs";

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

export function normalizeSkipConfig(value, { name, 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,
  };
}

export 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])),
  };
}

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