import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ExecutionContext } from "../../../schema/src/records/context.js";
import {
  classify,
  type ClassifiedRun,
  type DetectorOutcome,
  type KernelStores,
} from "../../../core/src/classify/classifier.js";
import { loadContractStore } from "../../../core/src/contracts/store.js";
import { loadDecisionStore } from "../../../core/src/decisions/store.js";
import {
  buildAsyncJourneyOutcome,
  deriveProvenRungs,
  highestProvenRung,
} from "./completion-ladder.js";
import {
  QUEUE_BRANCH_SCENARIO_KINDS,
  type QueueBranchContract,
  type QueueBranchRunResult,
  type QueueBranchScenarioKind,
  type QueueBranchScenarioResult,
  type QueueBranchSystem,
} from "./types.js";

const QUEUE_DETECTOR = { id: "async-queue-branch", version: "1.0.0" } as const;

function assertNonEmptyString(value: string, field: string): void {
  if (value.trim().length === 0) {
    throw new Error(`${field} must be a non-empty string`);
  }
}

function assertScenarioKind(value: string): asserts value is QueueBranchScenarioKind {
  if (!(QUEUE_BRANCH_SCENARIO_KINDS as readonly string[]).includes(value)) {
    throw new Error(`unknown queue branch scenario: ${value}`);
  }
}

function validateContract(contract: QueueBranchContract): void {
  assertNonEmptyString(contract.branchId, "branchId");
  for (const scenario of contract.ownedScenarios) {
    assertScenarioKind(scenario);
  }
}

function validateSystem<TState>(system: QueueBranchSystem<TState>): void {
  assertNonEmptyString(system.id, "system.id");
  if (typeof system.createState !== "function") {
    throw new Error("createState must be a function");
  }
  if (typeof system.runScenario !== "function") {
    throw new Error("runScenario must be a function");
  }
  if (typeof system.snapshotCompletion !== "function") {
    throw new Error("snapshotCompletion must be a function");
  }
  if (typeof system.assertScenario !== "function") {
    throw new Error("assertScenario must be a function");
  }
}

function queueContext(branchId: string, scenario: QueueBranchScenarioKind): ExecutionContext {
  return {
    kind: "api",
    surfaceId: branchId,
    adapterId: "async-queue-branch",
    environment: {},
    seed: `${branchId}:${scenario}`,
  };
}

async function createDefaultStores(now = "2026-07-25T12:00:00.000Z"): Promise<KernelStores> {
  const root = await mkdtemp(join(tmpdir(), "invariantum-async-queue-branch-"));
  const [decisions, contracts] = await Promise.all([
    loadDecisionStore(root),
    loadContractStore(root),
  ]);
  return {
    decisions,
    contracts,
    authoritativeContracts: new Set<string>(),
    now,
  };
}

function buildScenarioViolationOutcome(
  branchId: string,
  systemId: string,
  scenario: QueueBranchScenarioKind,
  error: string,
): DetectorOutcome {
  const context = queueContext(branchId, scenario);
  const violation = { branchId, systemId, scenario, error };
  return {
    detector: QUEUE_DETECTOR,
    class: "async-queue-branch-violation",
    severity: "high",
    target: { kind: "journey-branch", canonical: branchId },
    context,
    summary: `Queue branch scenario ${scenario} failed for ${branchId}`,
    evidence: [
      {
        truthSource: "confirmed",
        payload: violation,
      },
    ],
    artifacts: [],
    laneEligibility: "blocking-eligible",
    proofConditionMet: true,
    scope: {
      id: `async-queue:${branchId}:${scenario}`,
      detectorId: QUEUE_DETECTOR.id,
      surfaceId: branchId,
    },
    violation,
    contractOrConfig: { branchId, scenario, systemId },
    contextDimensions: { scenario },
  };
}

async function classifyScenarioResults(input: {
  branchId: string;
  systemId: string;
  scenarioResults: QueueBranchScenarioResult[];
  runId: string;
}): Promise<ClassifiedRun> {
  const detectorOutcomes: DetectorOutcome[] = [];
  for (const result of input.scenarioResults) {
    if (!result.holds && result.error !== undefined) {
      detectorOutcomes.push(
        buildScenarioViolationOutcome(
          input.branchId,
          input.systemId,
          result.scenario,
          result.error,
        ),
      );
    }
  }
  const stores = await createDefaultStores();
  return classify({
    detectorOutcomes,
    harnessEvents: [],
    coverageEvents: [],
    stores,
    runId: input.runId,
  });
}

export async function runQueueBranch<TState>(
  contract: QueueBranchContract,
  system: QueueBranchSystem<TState>,
  options: { runId?: string } = {},
): Promise<QueueBranchRunResult> {
  validateContract(contract);
  validateSystem(system);
  const runId = options.runId ?? `async-queue:${contract.branchId}`;

  const owned = new Set(contract.ownedScenarios);
  const skippedScenarios = QUEUE_BRANCH_SCENARIO_KINDS.filter((scenario) => !owned.has(scenario));
  const scenarioResults: QueueBranchScenarioResult[] = [];

  for (const scenario of contract.ownedScenarios) {
    const state = await system.createState();
    let holds = false;
    let error: string | undefined;
    let evidence = system.snapshotCompletion(state);

    try {
      await system.runScenario(scenario, state);
      system.assertScenario(scenario, state);
      evidence = system.snapshotCompletion(state);
      holds = true;
    } catch (caught) {
      error = caught instanceof Error ? caught.message : String(caught);
      evidence = system.snapshotCompletion(state);
      holds = false;
    }

    const provenRungs = deriveProvenRungs(evidence);
    scenarioResults.push({
      scenario,
      branchId: contract.branchId,
      holds,
      provenRung: highestProvenRung(provenRungs),
      evidence,
      ...(error !== undefined ? { error } : {}),
    });
  }

  const allRungs = scenarioResults.flatMap((result) => deriveProvenRungs(result.evidence));
  const classified = await classifyScenarioResults({
    branchId: contract.branchId,
    systemId: system.id,
    scenarioResults,
    runId,
  });

  return {
    branchId: contract.branchId,
    systemId: system.id,
    scenarioResults,
    highestProvenRung: highestProvenRung(allRungs),
    holds: scenarioResults.every((result) => result.holds),
    classified,
    skippedScenarios,
  };
}

export { buildAsyncJourneyOutcome };
