import {
  applyShard,
  buildRuntimeGraphs,
  buildTaskQueue,
  claimNextTask,
  collectSuites,
  resolveRuntimeConfigs,
} from "./planning.mjs";
import {
  addTrackerError,
  buildServiceTrackers,
  finalizeServiceResult,
  recordGraphError,
  recordTaskOutcome,
  summarizeDbBackend,
} from "./results.mjs";
import { buildRunArtifact, buildStatusArtifact } from "./reporting.mjs";
import { applyKnownFailuresToArtifacts, loadKnownFailuresConfig } from "./triage.mjs";
import { formatError } from "./formatting.mjs";
import {
  shouldFailKnownFailureIssueValidation,
  validateKnownFailureIssues,
} from "../known-failures/github.mjs";
import {
  loadTimings,
  resetResultArtifacts,
  saveTimings,
  writeRunArtifact,
  writeStatusArtifact,
} from "./artifacts.mjs";
import { createRunLogRegistry } from "./logs.mjs";
import {
  cleanupRunById,
  cleanupRuns,
  cleanupStaleRuns,
  createRunLifecycle,
} from "./lifecycle.mjs";
import {
  collectGitMetadata,
  readPackageMetadata,
  safeHostname,
  safeUsername,
} from "./metadata.mjs";
import { resolveExecutionConfig } from "./execution-config.mjs";
import { createRuntimeManager } from "./runtime-manager.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);
  resetResultArtifacts(productDir);
  const metadata = {
    git: collectGitMetadata(productDir),
    host: {
      hostname: safeHostname(),
      username: safeUsername(),
    },
    testkitVersion: readPackageMetadata().version,
  };
  const knownFailures = loadKnownFailuresConfig(
    productDir,
    configs[0]?.testkit?.reporting || null
  );
  const reporter = opts.reporter || null;
  const logRegistry = createRunLogRegistry(productDir);
  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 execution = resolveExecutionConfig({
    cli: opts,
    repo: configs[0]?.testkit?.execution || null,
  });
  const servicePlans = collectServicePlans(
    configs,
    configMap,
    typeValues,
    suiteSelectors,
    opts,
    execution,
    reporter
  );
  const trackers = buildServiceTrackers(servicePlans, startedAt);
  const executedPlans = servicePlans.filter((plan) => !plan.skipped);
  let workerCount = 0;
  let runtimeInstanceCount = 0;
  let runtimeStats = [];
  let exitCode = 0;
  const lifecycle = createRunLifecycle(productDir);
  lifecycle.markRunning();
  lifecycle.installSignalHandlers();
  let results = [];
  let finishedAt = Date.now();
  let knownFailureIssueValidation = null;

  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(execution.workers, queue.length));
      runtimeInstanceCount = graphs.reduce((sum, graph) => sum + graph.instanceCount, 0);
      const workers = Array.from({ length: workerCount }, (_unused, index) =>
        createWorker(index + 1, productDir)
      );
      const runtimeManager = createRuntimeManager({
        productDir,
        graphs,
        lifecycle,
        runtimeOptions: {
          reporter,
          logRegistry,
        },
      });
      const timingUpdates = [];

      try {
        const workerResults = await Promise.allSettled(
          workers.map((worker) =>
            runWorker(
              worker,
              queue,
              runtimeManager,
              trackers,
              timingUpdates,
              lifecycle,
              claimNextTask,
              recordTaskOutcome,
              recordGraphError,
              reporter
            )
          )
        );

        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);
            }
          }
        }
        runtimeStats = runtimeManager.getStats();
      } finally {
        await runtimeManager.cleanupAll();
      }

      saveTimings(productDir, timings, timingUpdates);
    }

    finishedAt = Date.now();
    results = configs.map((config) =>
      finalizeServiceResult(trackers.get(config.name), startedAt, finishedAt)
    );
    const runArtifact = buildRunArtifact({
      productDir,
      results,
      startedAt,
      finishedAt,
      execution,
      workerCount,
      runtimeInstanceCount,
      runtimeStats,
      typeValues,
      suiteSelectors,
      fileNames: requestedFiles,
      shard: opts.shard || null,
      serviceFilter: opts.serviceFilter || null,
      metadata,
      summarizeDbBackend,
      serviceLogs: logRegistry.listServiceLogs(),
    });
    const statusArtifact = opts.writeStatus
      ? buildStatusArtifact({
          productDir,
          results,
          typeValues,
          suiteSelectors,
          fileNames: requestedFiles,
          shard: opts.shard || null,
          serviceFilter: opts.serviceFilter || null,
          metadata,
        })
      : null;
    const enrichedArtifacts = applyKnownFailuresToArtifacts(
      runArtifact,
      statusArtifact,
      knownFailures
    );
    knownFailureIssueValidation = await validateKnownFailureIssues({
      productDir,
      document: knownFailures,
      runArtifact: enrichedArtifacts.runArtifact,
      statusArtifact: enrichedArtifacts.statusArtifact,
      config: configs[0]?.testkit?.reporting?.issueValidation || null,
      gitMetadata: metadata.git,
    });
    attachKnownFailureIssueValidation(
      enrichedArtifacts.runArtifact,
      enrichedArtifacts.statusArtifact,
      knownFailureIssueValidation
    );

    writeRunArtifact(productDir, enrichedArtifacts.runArtifact);
    if (opts.writeStatus) {
      writeStatusArtifact(productDir, enrichedArtifacts.statusArtifact);
    }

    reporter?.runSummary?.(results, finishedAt - startedAt, knownFailureIssueValidation);
    await reportTelemetry(telemetry, enrichedArtifacts.runArtifact, reporter);
    if (results.some((result) => result.failed)) exitCode = 1;
    if (shouldFailKnownFailureIssueValidation(knownFailureIssueValidation)) {
      exitCode = 1;
    }
    if (lifecycle.isStopRequested()) exitCode = Math.max(exitCode, 130);
    return {
      runArtifact: enrichedArtifacts.runArtifact,
      statusArtifact: enrichedArtifacts.statusArtifact,
      results,
      exitCode,
    };
  } finally {
    if (lifecycle.isStopRequested()) {
      exitCode = Math.max(exitCode, 130);
    }
    lifecycle.removeSignalHandlers();
    lifecycle.markFinished(
      exitCode === 0 ? "finished" : lifecycle.isStopRequested() ? "interrupted" : "failed"
    );
    await cleanupRunById(productDir, lifecycle.runId);
    await cleanupRuns(productDir, { includeActive: false });
    lifecycle.removeManifest();
    lifecycle.dispose();
    logRegistry.closeAll();
    process.exitCode = exitCode;
  }
}

function collectServicePlans(configs, configMap, typeValues, suiteSelectors, opts, execution, reporter) {
  return configs.map((config) => {
    const suites = applyShard(
      collectSuites(config, typeValues, suiteSelectors, opts.fileNames || [], opts),
      opts.shard
    );

    if (suites.length === 0) {
      reporter?.serviceSkipped?.(
        config,
        `no matching files (types=${typeValues.join(",") || "all"} suites=${suiteSelectors.map((selector) => selector.raw).join(",") || "all"} files=${(opts.fileNames || []).join(",") || "all"})`
      );
      return {
        config,
        execution,
        skipped: true,
        suites: [],
        runtimeConfigs: [],
        runtimeNames: [],
        runtimeKey: null,
      };
    }

    for (const suite of suites) {
      for (const skippedFile of suite.skippedFiles || []) {
        reporter?.plannedSkip?.({
          serviceName: config.name,
          type: suite.displayType || suite.type,
          file: skippedFile.path,
          reason: skippedFile.reason,
        });
      }
    }

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

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

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

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

function attachKnownFailureIssueValidation(runArtifact, statusArtifact, validation) {
  if (!validation) return;
  runArtifact.knownFailuresIssueValidation = validation;
  if (statusArtifact) {
    statusArtifact.knownFailuresIssueValidation = validation;
  }
}
