import { buildTimingKey, estimateTaskDuration } from "../timing/index.mjs";
import {
  matchesSelectedTypes,
  matchesSuiteSelectors,
  suiteSelectionType,
} from "./suite-selection.mjs";

const TYPE_ORDER = ["dal", "integration", "e2e", "scenario", "load"];

export function taskNeedsLocalRuntime(task) {
  return task.type !== "dal";
}

export function resolveRuntimeConfigs(targetConfig, configMap) {
  const ordered = [];
  const visiting = new Set();
  const seen = new Set();

  const visit = (config) => {
    if (seen.has(config.name)) return;
    if (visiting.has(config.name)) {
      throw new Error(`Dependency cycle detected involving "${config.name}"`);
    }

    visiting.add(config.name);
    for (const depName of config.testkit.dependsOn || []) {
      const dep = configMap.get(depName);
      if (!dep) {
        throw new Error(`Service "${config.name}" depends on unknown service "${depName}"`);
      }
      visit(dep);
    }
    visiting.delete(config.name);
    seen.add(config.name);
    ordered.push(config);
  };

  visit(targetConfig);
  return ordered;
}

export function collectSuites(config, typeValues, suiteSelectors, fileNames = [], opts = {}) {
  const selectedFiles = new Set(fileNames.map(normalizePathSeparators));
  const suites = [];
  let orderIndex = 0;

  for (const type of orderedTypes(Object.keys(config.suites))) {
    for (const suite of config.suites[type] || []) {
      const framework = suite.framework || "k6";
      const displayType = suiteSelectionType(type, framework);
      const selectedSuiteFiles =
        selectedFiles.size === 0
          ? suite.files
          : suite.files.filter((file) => selectedFiles.has(normalizePathSeparators(file)));
      if (!matchesSelectedTypes(displayType, typeValues)) continue;
      if (!matchesSuiteSelectors(displayType, suite.name, suiteSelectors)) continue;
      if (selectedSuiteFiles.length === 0) continue;

      const { files, skippedFiles } = applySkipRules(
        config,
        displayType,
        suite.name,
        selectedSuiteFiles,
        opts
      );
      suites.push({
        ...suite,
        files,
        skippedFiles,
        framework,
        type,
        displayType,
        orderIndex,
        sortKey: `${type}:${suite.name}`,
        weight:
          suite.testkit?.weight ||
          (framework === "playwright"
            ? Math.max(2, Math.max(1, files.length))
            : Math.max(1, files.length)),
        totalFileCount: selectedSuiteFiles.length,
      });
      orderIndex += 1;
    }
  }

  return suites;
}

export function applyShard(suites, shard) {
  if (!shard) return suites;
  return suites.filter((_unused, index) => index % shard.total === shard.index - 1);
}

export function orderedTypes(types) {
  const ordered = [];
  for (const known of TYPE_ORDER) {
    if (types.includes(known)) ordered.push(known);
  }
  for (const type of types) {
    if (!ordered.includes(type)) ordered.push(type);
  }
  return ordered;
}

export function buildRuntimeGraphs(servicePlans) {
  const executed = servicePlans.filter((plan) => !plan.skipped);
  const graphs = [];
  const graphByRuntimeKey = new Map();

  for (const plan of executed) {
    plan.assignedGraphKey = plan.runtimeKey;
    const existing = graphByRuntimeKey.get(plan.runtimeKey);
    if (existing) {
      existing.targetNames.push(plan.config.name);
      existing.instanceCount = Math.max(existing.instanceCount, plan.config.testkit.runtime.instances);
      existing.maxConcurrentTasks = Math.min(
        existing.maxConcurrentTasks,
        resolveGraphMaxConcurrentTasks(plan.runtimeConfigs)
      );
      continue;
    }

    const graph = {
      key: plan.runtimeKey,
      runtimeNames: plan.runtimeNames,
      runtimeConfigs: plan.runtimeConfigs,
      targetNames: [plan.config.name],
      dirName: buildGraphDirName(plan.runtimeNames),
      instanceCount: plan.config.testkit.runtime.instances,
      maxConcurrentTasks: resolveGraphMaxConcurrentTasks(plan.runtimeConfigs),
    };
    graphs.push(graph);
    graphByRuntimeKey.set(plan.runtimeKey, graph);
  }

  const sortedGraphs = graphs.sort((left, right) => left.dirName.localeCompare(right.dirName));
  const maxInstanceCount = Math.max(1, ...sortedGraphs.map((graph) => graph.instanceCount));

  return sortedGraphs.map((graph, index) => ({
    ...graph,
    portNamespaceIndex: index,
    portNamespaceStride: maxInstanceCount,
  }));
}

export function buildTaskQueue(servicePlans, graphs, timings) {
  const graphByKey = new Map(graphs.map((graph) => [graph.key, graph]));
  const tasks = [];
  let nextId = 1;

  for (const plan of servicePlans) {
    if (plan.skipped) continue;

    const graph = graphByKey.get(plan.assignedGraphKey);
    if (!graph) {
      throw new Error(`Assigned graph "${plan.assignedGraphKey}" not found for ${plan.config.name}`);
    }

    for (const suite of plan.suites) {
      for (const file of suite.files) {
        const timingKey = buildTimingKey(plan.config.name, suite, file);
        tasks.push({
          id: nextId,
          graphKey: graph.key,
          targetName: plan.config.name,
          serviceName: plan.config.name,
          suiteKey: `${suite.type}:${suite.name}`,
          suiteName: suite.name,
          type: suite.type,
          framework: suite.framework,
          orderIndex: suite.orderIndex,
          file,
          locks: resolveTaskLocks(plan.config, suite, file),
          resourceCost: 1,
          timingKey,
          estimatedDurationMs: estimateTaskDuration(timings, timingKey, suite),
        });
        nextId += 1;
      }
    }
  }

  return tasks.sort(
    (a, b) =>
      b.estimatedDurationMs - a.estimatedDurationMs ||
      a.serviceName.localeCompare(b.serviceName) ||
      a.suiteKey.localeCompare(b.suiteKey) ||
      a.file.localeCompare(b.file)
  );
}

export function claimNextTask(queue, preferredGraphKey, isRunnable = () => true) {
  if (queue.length === 0) return null;

  const preferredIndexes = [];
  const fallbackIndexes = [];
  for (let index = 0; index < queue.length; index += 1) {
    if (preferredGraphKey && queue[index].graphKey === preferredGraphKey) {
      preferredIndexes.push(index);
    } else {
      fallbackIndexes.push(index);
    }
  }

  for (const index of [...preferredIndexes, ...fallbackIndexes]) {
    if (!isRunnable(queue[index])) continue;
    return queue.splice(index, 1)[0];
  }

  return null;
}

export function buildGraphDirName(runtimeNames) {
  const slug = runtimeNames.map(slugSegment).join("__");
  return slug.length > 0 ? slug : "graph";
}

function applySkipRules(config, displayType, suiteName, files, opts = {}) {
  if (opts.ignoreSkipRules) {
    return {
      files,
      skippedFiles: [],
    };
  }
  const skip = config.testkit?.skip;
  if (!skip) {
    return {
      files,
      skippedFiles: [],
    };
  }

  const matchingSuiteRules = skip.suites.filter((rule) =>
    matchesSuiteSelectors(displayType, suiteName, [rule.selector])
  );
  const suiteReason = matchingSuiteRules[0]?.reason || null;
  const runnableFiles = [];
  const skippedFiles = [];

  for (const file of files) {
    const normalizedFile = normalizePathSeparators(file);
    const reason = skip.fileReasonByPath.get(normalizedFile) || suiteReason;
    if (reason) {
      skippedFiles.push({
        path: normalizedFile,
        reason,
      });
      continue;
    }
    runnableFiles.push(file);
  }

  return {
    files: runnableFiles,
    skippedFiles,
  };
}

function resolveTaskLocks(config, suite, file) {
  const locks = new Set();
  const matchedSuiteRules = config.testkit.requirements?.suites || [];
  for (const rule of matchedSuiteRules) {
    if (matchesSuiteSelectors(suite.displayType, suite.name, [rule.selector])) {
      for (const lockName of rule.locks || []) {
        locks.add(lockName);
      }
    }
  }

  const normalizedFile = normalizePathSeparators(file);
  for (const lockName of config.testkit.requirements?.fileLocksByPath?.get(normalizedFile) || []) {
    locks.add(lockName);
  }

  return [...locks].sort();
}

function normalizePathSeparators(filePath) {
  return String(filePath).split("\\").join("/");
}

function slugSegment(value) {
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "svc";
}

function resolveGraphMaxConcurrentTasks(runtimeConfigs) {
  return runtimeConfigs.reduce(
    (currentMin, config) => Math.min(currentMin, config.testkit.runtime.maxConcurrentTasks),
    Number.POSITIVE_INFINITY
  );
}
