import { buildTimingKey, estimateTaskDuration } from "../timing/index.mjs";

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

export function batchNeedsLocalRuntime(batch) {
  return batch.tasks.some((task) => 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, suiteType, suiteNames, frameworkFilter) {
  const types =
    suiteType === "all"
      ? orderedTypes(Object.keys(config.suites))
      : [suiteType === "int" ? "integration" : suiteType];

  const selectedNames = new Set(suiteNames);
  const suites = [];
  let orderIndex = 0;

  for (const type of types) {
    for (const suite of config.suites[type] || []) {
      const framework = suite.framework || "k6";
      if (selectedNames.size > 0 && !selectedNames.has(suite.name)) continue;
      if (frameworkFilter && frameworkFilter !== "all" && framework !== frameworkFilter) continue;

      suites.push({
        ...suite,
        framework,
        type,
        orderIndex,
        sortKey: `${type}:${suite.name}`,
        weight:
          suite.testkit?.weight ||
          (framework === "playwright"
            ? Math.max(2, suite.files.length)
            : Math.max(1, suite.files.length)),
        maxFileConcurrency:
          framework === "k6" ? suite.testkit?.maxFileConcurrency || 1 : 1,
      });
      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 uniqueGraphs = [];
  const graphByRuntimeKey = new Map();

  for (const plan of executed) {
    if (graphByRuntimeKey.has(plan.runtimeKey)) {
      graphByRuntimeKey.get(plan.runtimeKey).exactTargets.push(plan.config.name);
      continue;
    }

    const graph = {
      key: plan.runtimeKey,
      runtimeNames: plan.runtimeNames,
      runtimeConfigs: plan.runtimeConfigs,
      exactTargets: [plan.config.name],
      assignedTargets: [],
      dirName: null,
      rootConfig: null,
    };
    uniqueGraphs.push(graph);
    graphByRuntimeKey.set(plan.runtimeKey, graph);
  }

  const maximalGraphs = uniqueGraphs.filter(
    (graph) =>
      !uniqueGraphs.some(
        (other) =>
          other.key !== graph.key &&
          isRuntimeSuperset(other.runtimeNames, graph.runtimeNames)
      )
  );

  for (const plan of executed) {
    const compatible = maximalGraphs.filter((graph) =>
      isRuntimeSuperset(graph.runtimeNames, plan.runtimeNames)
    );
    if (compatible.length === 0) {
      throw new Error(`No runtime graph found for service "${plan.config.name}"`);
    }

    const assigned = compatible.sort(compareGraphsForAssignment)[0];
    plan.assignedGraphKey = assigned.key;
    assigned.assignedTargets.push(plan.config.name);
  }

  for (const graph of maximalGraphs) {
    const rootName = [...graph.exactTargets].sort()[0];
    const rootPlan = executed.find((plan) => plan.config.name === rootName);
    if (!rootPlan) {
      throw new Error(`Missing root plan for graph "${graph.key}"`);
    }
    graph.rootConfig = rootPlan.config;
    graph.dirName = buildGraphDirName(graph.runtimeNames);
  }

  return maximalGraphs.sort((a, b) => a.dirName.localeCompare(b.dirName));
}

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,
          timingKey,
          estimatedDurationMs: estimateTaskDuration(timings, timingKey, suite),
          maxBatchSize:
            suite.framework === "playwright"
              ? Number.POSITIVE_INFINITY
              : suite.maxFileConcurrency || 1,
        });
        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 claimNextBatch(queue, preferredGraphKey) {
  if (queue.length === 0) return null;

  let index = -1;
  if (preferredGraphKey) {
    index = queue.findIndex((task) => task.graphKey === preferredGraphKey);
  }
  if (index === -1) index = 0;

  const seed = queue.splice(index, 1)[0];
  const tasks = [seed];

  if (seed.framework === "playwright") {
    for (let cursor = queue.length - 1; cursor >= 0; cursor -= 1) {
      const candidate = queue[cursor];
      if (
        candidate.framework === "playwright" &&
        candidate.graphKey === seed.graphKey &&
        candidate.targetName === seed.targetName
      ) {
        tasks.push(candidate);
        queue.splice(cursor, 1);
      }
    }
  } else if (seed.maxBatchSize > 1) {
    for (let cursor = queue.length - 1; cursor >= 0 && tasks.length < seed.maxBatchSize; cursor -= 1) {
      const candidate = queue[cursor];
      if (
        candidate.framework === seed.framework &&
        candidate.type === seed.type &&
        candidate.graphKey === seed.graphKey &&
        candidate.targetName === seed.targetName &&
        candidate.suiteKey === seed.suiteKey
      ) {
        tasks.push(candidate);
        queue.splice(cursor, 1);
      }
    }
  }

  tasks.sort(
    (a, b) =>
      a.orderIndex - b.orderIndex ||
      a.file.localeCompare(b.file)
  );

  return {
    graphKey: seed.graphKey,
    targetName: seed.targetName,
    framework: seed.framework,
    type: seed.type,
    tasks,
  };
}

export function isRuntimeSuperset(candidate, target) {
  return target.every((name) => candidate.includes(name));
}

export function compareGraphsForAssignment(left, right) {
  if (left.runtimeNames.length !== right.runtimeNames.length) {
    return left.runtimeNames.length - right.runtimeNames.length;
  }
  return left.key.localeCompare(right.key);
}

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

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