import {
  applyShard,
  buildRuntimeGraphs,
  buildTaskQueue,
  claimNextBatch,
  collectSuites,
  resolveRuntimeConfigs,
} from "./planning.mjs";
import {
  addTrackerError,
  buildServiceTrackers,
  finalizeServiceResult,
  recordGraphError,
  recordTaskOutcome,
  summarizeDbBackend,
} from "./results.mjs";
import { buildRunArtifact, buildStatusArtifact } from "./reporting.mjs";
import { buildRunSummaryLines, formatError } from "./formatting.mjs";
import { loadTimings, saveTimings, writeRunArtifact, writeStatusArtifact } from "./artifacts.mjs";
import {
  cleanupRunById,
  cleanupRuns,
  cleanupStaleRuns,
  createRunLifecycle,
} from "./lifecycle.mjs";
import {
  collectGitMetadata,
  readPackageMetadata,
  safeHostname,
  safeUsername,
} from "./metadata.mjs";
import { createWorker, runWorker } from "./worker-loop.mjs";
import { findUnmatchedRequestedFiles, isFullRunSelection } from "./selection.mjs";
import { uploadTelemetryArtifact } from "../telemetry/index.mjs";

export async function runAll(configs, typeValues, suiteSelectors, opts, allConfigs = configs) {
  const configMap = new Map(allConfigs.map((config) => [config.name, config]));
  const startedAt = Date.now();
  const telemetry = configs[0]?.telemetry || null;
  const productDir = configs[0]?.productDir || process.cwd();
  await cleanupStaleRuns(productDir);
  const metadata = {
    git: collectGitMetadata(productDir),
    host: {
      hostname: safeHostname(),
      username: safeUsername(),
    },
    testkitVersion: readPackageMetadata().version,
  };
  const requestedFiles = opts.fileNames || [];
  if (requestedFiles.length > 0) {
    const unmatchedFiles = findUnmatchedRequestedFiles(
      configs,
      typeValues,
      suiteSelectors,
      requestedFiles,
      collectSuites,
      normalizePathSeparators
    );
    if (unmatchedFiles.length > 0) {
      throw new Error(
        `Requested file${unmatchedFiles.length === 1 ? "" : "s"} did not match any selected suites:\n` +
          unmatchedFiles.map((file) => `- ${file}`).join("\n")
      );
    }
  }
  if (
    opts.writeStatus &&
    !opts.allowPartialStatus &&
    !isFullRunSelection(
      typeValues,
      suiteSelectors,
      requestedFiles,
      opts.shard || null,
      opts.serviceFilter || null
    )
  ) {
    throw new Error(
      "Refusing to overwrite testkit.status.json from a filtered run. " +
        "Run the full suite with --write-status, or pass --allow-partial-status to opt in."
    );
  }

  const servicePlans = collectServicePlans(configs, configMap, typeValues, suiteSelectors, opts);
  const trackers = buildServiceTrackers(servicePlans, startedAt);
  const executedPlans = servicePlans.filter((plan) => !plan.skipped);
  let workerCount = 0;
  let exitCode = 0;
  const lifecycle = createRunLifecycle(productDir);
  lifecycle.markRunning();
  lifecycle.installSignalHandlers();
  let results = [];
  let finishedAt = Date.now();

  try {
    if (executedPlans.length > 0) {
      const timings = loadTimings(productDir);
      const graphs = buildRuntimeGraphs(executedPlans);
      const queue = buildTaskQueue(executedPlans, graphs, timings);
      workerCount = Math.max(1, Math.min(opts.jobs || 1, queue.length));
      const graphByKey = new Map(graphs.map((graph) => [graph.key, graph]));
      const workers = Array.from({ length: workerCount }, (_unused, index) =>
        createWorker(index + 1, productDir)
      );
      const timingUpdates = [];

      const workerResults = await Promise.allSettled(
        workers.map((worker) =>
          runWorker(
            worker,
            queue,
            graphByKey,
            trackers,
            timingUpdates,
            lifecycle,
            claimNextBatch,
            recordTaskOutcome,
            recordGraphError
          )
        )
      );

      for (const result of workerResults) {
        if (result.status === "rejected") {
          const message = formatError(result.reason);
          for (const tracker of trackers.values()) {
            if (!tracker.skipped) addTrackerError(tracker, message);
          }
        }
      }

      saveTimings(productDir, timings, timingUpdates);
    }

    finishedAt = Date.now();
    results = configs.map((config) =>
      finalizeServiceResult(trackers.get(config.name), startedAt, finishedAt)
    );
    const artifact = buildRunArtifact({
      productDir,
      results,
      startedAt,
      finishedAt,
      requestedJobs: opts.jobs || 1,
      workerCount,
      typeValues,
      suiteSelectors,
      fileNames: requestedFiles,
      shard: opts.shard || null,
      serviceFilter: opts.serviceFilter || null,
      metadata,
      summarizeDbBackend,
    });

    writeRunArtifact(productDir, artifact);
    if (opts.writeStatus) {
      writeStatusArtifact(
        productDir,
        buildStatusArtifact({
          productDir,
          results,
          typeValues,
          suiteSelectors,
          fileNames: requestedFiles,
          shard: opts.shard || null,
          serviceFilter: opts.serviceFilter || null,
          metadata,
        })
      );
    }

    printRunSummary(results, finishedAt - startedAt);
    await reportTelemetry(telemetry, artifact);
    if (results.some((result) => result.failed)) exitCode = 1;
    if (lifecycle.isStopRequested()) exitCode = Math.max(exitCode, 130);
  } finally {
    lifecycle.removeSignalHandlers();
    lifecycle.markFinished(
      exitCode === 0 ? "finished" : lifecycle.isStopRequested() ? "interrupted" : "failed"
    );
    await cleanupRunById(productDir, lifecycle.runId);
    await cleanupRuns(productDir, { includeActive: false });
    lifecycle.removeManifest();
    process.exitCode = exitCode;
  }
}

function collectServicePlans(configs, configMap, typeValues, suiteSelectors, opts) {
  return configs.map((config) => {
    console.log(`\n══ ${config.name} ══`);
    const suites = applyShard(
      collectSuites(config, typeValues, suiteSelectors, opts.fileNames || [], opts),
      opts.shard
    );

    if (suites.length === 0) {
      console.log(
        `No test files for ${config.name} types=${typeValues.join(",") || "all"} suites=${suiteSelectors.map((selector) => selector.raw).join(",") || "all"} files=${(opts.fileNames || []).join(",") || "all"} — skipping`
      );
      return {
        config,
        skipped: true,
        suites: [],
        runtimeConfigs: [],
        runtimeNames: [],
        runtimeKey: null,
      };
    }

    const runtimeConfigs = resolveRuntimeConfigs(config, configMap);
    return {
      config,
      skipped: false,
      suites,
      runtimeConfigs,
      runtimeNames: runtimeConfigs.map((runtimeConfig) => runtimeConfig.name).sort(),
      runtimeKey: runtimeConfigs.map((runtimeConfig) => runtimeConfig.name).sort().join("|"),
    };
  });
}

function printRunSummary(results, durationMs) {
  for (const line of buildRunSummaryLines(results, durationMs)) {
    console.log(line);
  }
}

async function reportTelemetry(telemetry, artifact) {
  if (!telemetry?.enabled) return;

  try {
    const outcome = await uploadTelemetryArtifact(telemetry, artifact);
    if (outcome?.ok) {
      console.log("Telemetry: uploaded run artifact");
      return;
    }
    if (outcome?.reason === "missing-token") {
      console.log(
        `Telemetry: skipped upload because ${telemetry.tokenEnv || "configured token env"} is not set`
      );
      return;
    }
    if (outcome?.reason && !outcome.skipped) return;
  } catch (error) {
    console.log(`Telemetry: upload failed (${formatError(error)})`);
  }
}

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