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 CoverageEvent,
  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 type { JourneyBranch, UiJourneyPlan } from "./schema.js";
import { parseJourneyBranch, parseUiJourneyPlan } from "./schema.js";
import { bootstrapConfirmedJourneyStores } from "./bootstrap-stores.js";
import type { JourneyOracles } from "./public.js";
import type {
  ExternalEffect,
  OracleVerdict,
  PersistenceOracle,
  SideEffectOracle,
} from "../oracles/types.js";

const JOURNEY_DETECTOR = { id: "journey-oracle", version: "1.0.0" } as const;

export type JourneyTriggerResult = Record<string, unknown>;

export type JourneyTrigger = () => Promise<JourneyTriggerResult>;

export type RunJourneyBranchOptions = {
  trigger: JourneyTrigger;
  certificationAllowed?: () => boolean;
  oracles: JourneyOracles;
  stores?: KernelStores;
  runId?: string;
};

export type JourneyPromiseCheck = {
  id: string;
  dimension: string;
  path: string;
  operator: "exists" | "absent" | "equals";
  passed: boolean;
  before?: unknown;
  after?: unknown;
};

export type RunUiJourneyPlanOptions = RunJourneyBranchOptions & {
  browserPromiseChecks?: () => readonly JourneyPromiseCheck[];
};

export type OracleRunResult = {
  kind: "persistence" | "side-effect";
  adapterId: string;
  verdict: OracleVerdict;
};

export type JourneyCoverageGap = {
  reason: "missing-domain-oracles" | "missing-browser-promises";
  declaredAdapters: string[];
};

export type JourneyTerminalStatus = "success" | "failure" | "coverage-incomplete";

export type JourneyAdjudicationItem = {
  kind: "journey-discrepancy";
  branchId: string;
  branchStatus: JourneyBranch["status"];
  oracleKind: "persistence" | "side-effect" | "browser-promise";
  violated: string[];
  evidence: Record<string, unknown>;
};

export type JourneyOutcome = {
  branchId: string;
  branchStatus: JourneyBranch["status"];
  triggerResult: JourneyTriggerResult;
  oracleResults: OracleRunResult[];
  certified: boolean;
  terminalStatus: JourneyTerminalStatus;
  uiOnly: boolean;
  browserPromiseChecks?: JourneyPromiseCheck[];
  coverageGap?: JourneyCoverageGap;
  adjudicationItems: JourneyAdjudicationItem[];
  classified: ClassifiedRun;
};

type DomainOracleKind = "persistence" | "side-effect";
type JourneyViolationKind = DomainOracleKind | "browser-promise";

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

function journeyContext(branch: JourneyBranch): ExecutionContext {
  return {
    kind: "api",
    surfaceId: branch.id,
    adapterId: "journey-runner",
    environment: {},
    seed: branch.id,
  };
}

function declaredDomainOracles(branch: JourneyBranch): Array<{
  kind: DomainOracleKind;
  id: string;
  version?: string;
}> {
  return branch.oracleAdapters.flatMap((adapter) => {
    if (adapter.kind === "persistence" || adapter.kind === "side-effect") {
      return [{ kind: adapter.kind, id: adapter.id, ...(adapter.version === undefined ? {} : { version: adapter.version }) }];
    }
    return [];
  });
}

function resolveOracle(
  adapter: { kind: DomainOracleKind; id: string; version?: string },
  oracles: JourneyOracles,
): PersistenceOracle | SideEffectOracle | undefined {
  const candidates = adapter.kind === "persistence" ? oracles.persistence : oracles.sideEffect;
  return candidates?.find(({ ref }) => ref.kind === adapter.kind && ref.id === adapter.id && ref.version === adapter.version)?.oracle;
}

function oracleAdapterKey(adapter: { kind: DomainOracleKind; id: string; version?: string }): string {
  return `${adapter.kind}:${adapter.id}:${adapter.version ?? ""}`;
}

function buildCoverageGapOutcome(
  branch: JourneyBranch,
  declared: ReturnType<typeof declaredDomainOracles>,
  runId: string,
  triggerResult: JourneyTriggerResult,
  reason: JourneyCoverageGap["reason"] = "missing-domain-oracles",
): Promise<JourneyOutcome> {
  const context = journeyContext(branch);
  const coverageEvent: CoverageEvent = {
    scope: {
      id: `journey:${branch.id}`,
      detectorId: JOURNEY_DETECTOR.id,
      surfaceId: branch.id,
    },
    context,
    reason: "unproven-precondition",
    witnessRefs: [
      {
        id: `journey-${reason}:${branch.id}`,
      },
    ],
  };

  return createDefaultStores().then((stores) => ({
    branchId: branch.id,
    branchStatus: branch.status,
    triggerResult,
    oracleResults: [],
    certified: false,
    terminalStatus: "coverage-incomplete",
    uiOnly: reason === "missing-browser-promises",
    coverageGap: {
      reason,
      declaredAdapters: declared.map((adapter) => `${adapter.kind}:${adapter.id}`),
    },
    adjudicationItems: [],
    classified: classify({
      detectorOutcomes: [],
      harnessEvents: [],
      coverageEvents: [coverageEvent],
      stores,
      runId,
    }),
  }));
}

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

function outcomeHasSuppliedAuthority(
  outcome: DetectorOutcome,
  stores: KernelStores,
): boolean {
  const probe = classify({
    detectorOutcomes: [outcome],
    harnessEvents: [],
    coverageEvents: [],
    stores,
    runId: "journey-store-authority-probe",
  });
  const finding = probe.findings[0] ?? probe.hiddenFindings[0];
  return finding !== undefined && finding.lane !== "advisory";
}

async function resolveClassificationStores(
  branch: JourneyBranch,
  detectorOutcome: DetectorOutcome,
  stores: KernelStores | undefined,
): Promise<KernelStores> {
  if (branch.status === "confirmed") {
    if (stores !== undefined && outcomeHasSuppliedAuthority(detectorOutcome, stores)) {
      return stores;
    }
    return bootstrapConfirmedJourneyStores(branch, [detectorOutcome]);
  }
  return stores ?? createDefaultStores();
}

function buildAdjudicationItem(
  branch: JourneyBranch,
  oracleKind: JourneyViolationKind,
  verdict: OracleVerdict,
): JourneyAdjudicationItem {
  return {
    kind: "journey-discrepancy",
    branchId: branch.id,
    branchStatus: branch.status,
    oracleKind,
    violated: verdict.violated ?? [],
    evidence: verdict.evidence,
  };
}

function buildViolationOutcome(
  branch: JourneyBranch,
  oracleKind: JourneyViolationKind,
  adapterId: string,
  verdict: OracleVerdict,
): DetectorOutcome {
  const context = journeyContext(branch);
  const violation = {
    branchId: branch.id,
    branchStatus: branch.status,
    oracleKind,
    adapterId,
    violated: verdict.violated ?? [],
    evidence: verdict.evidence,
  };

  return {
    detector: JOURNEY_DETECTOR,
    class: "journey-oracle-violation",
    severity: branch.status === "confirmed" ? "high" : "medium",
    target: { kind: "journey-branch", canonical: branch.id },
    context,
    summary: `Journey branch ${branch.id} ${oracleKind} oracle violation (${adapterId})`,
    evidence: [
      {
        truthSource: branch.status === "confirmed" ? "confirmed" : "inferred",
        payload: violation,
      },
    ],
    artifacts: [],
    laneEligibility: branch.status === "confirmed" ? "blocking-eligible" : "advisory",
    ...(branch.status === "confirmed" ? { proofConditionMet: true } : {}),
    scope: {
      id: `journey:${branch.id}:${oracleKind}:${adapterId}`,
      detectorId: JOURNEY_DETECTOR.id,
      surfaceId: branch.id,
    },
    violation,
    contractOrConfig: { branchId: branch.id, status: branch.status },
    contextDimensions: { branch: branch.id },
  };
}

async function runDeclaredOracle(
  branch: JourneyBranch,
  adapter: { kind: DomainOracleKind; id: string; version?: string },
  oracles: JourneyOracles,
  persistenceBefore: unknown,
): Promise<OracleRunResult> {
  if (adapter.kind === "persistence") {
    const persistence = resolveOracle(adapter, oracles);
    if (persistence === undefined) {
      throw new Error(
        `journey branch ${branch.id} declares persistence oracle ${adapter.id} but none was provided`,
      );
    }
    const typedPersistence = persistence as PersistenceOracle;
    const after = await typedPersistence.snapshot();
    const verdict = typedPersistence.assertPostconditions(branch, persistenceBefore, after);
    return { kind: "persistence", adapterId: adapter.id, verdict };
  }

  const sideEffect = resolveOracle(adapter, oracles);
  if (sideEffect === undefined) {
    throw new Error(
      `journey branch ${branch.id} declares side-effect oracle ${adapter.id} but none was provided`,
    );
  }
  const typedSideEffect = sideEffect as SideEffectOracle;
  await typedSideEffect.observed();
  const verdict = typedSideEffect.assertEffects(branch);
  return { kind: "side-effect", adapterId: adapter.id, verdict };
}

function isCertified(
  branch: JourneyBranch,
  oracleResults: OracleRunResult[],
  coverageGap: JourneyCoverageGap | undefined,
): boolean {
  if (coverageGap !== undefined) {
    return false;
  }
  if (oracleResults.length === 0) {
    return false;
  }
  if (branch.status === "confirmed") {
    return oracleResults.every((result) => result.verdict.holds);
  }
  return oracleResults.every((result) => result.verdict.holds);
}

function matchesPromiseContract(
  plan: UiJourneyPlan,
  checks: readonly JourneyPromiseCheck[],
): boolean {
  if (checks.length !== plan.promise.expectations.length) {
    return false;
  }
  return plan.promise.expectations.every((expectation, index) => {
    const check = checks[index];
    return check !== undefined &&
      check.id === expectation.id &&
      check.dimension === expectation.dimension &&
      check.path === expectation.path &&
      check.operator === expectation.operator;
  });
}

function browserPromiseVerdict(checks: readonly JourneyPromiseCheck[]): OracleVerdict {
  const violated = checks.filter((check) => !check.passed).map((check) => check.id);
  return {
    holds: false,
    violated,
    evidence: {
      checks: checks.map((check) => ({
        id: check.id,
        dimension: check.dimension,
        path: check.path,
        operator: check.operator,
        passed: check.passed,
        ...(check.before === undefined ? {} : { before: check.before }),
        ...(check.after === undefined ? {} : { after: check.after }),
      })),
      violated,
    },
  };
}

function mergeClassifiedRuns(left: ClassifiedRun, right: ClassifiedRun): ClassifiedRun {
  const unique = <T extends { id: string }>(values: T[]): T[] =>
    [...new Map(values.map((value) => [value.id, value])).values()];
  return {
    findings: unique([...left.findings, ...right.findings]),
    hiddenFindings: unique([...left.hiddenFindings, ...right.hiddenFindings]),
    coverageOutcomes: unique([...left.coverageOutcomes, ...right.coverageOutcomes]),
    harnessOutcomes: unique([...left.harnessOutcomes, ...right.harnessOutcomes]),
  };
}

async function classifyJourneyOutcomes(
  branch: JourneyBranch,
  detectorOutcomes: DetectorOutcome[],
  stores: KernelStores | undefined,
  runId: string,
): Promise<ClassifiedRun> {
  if (detectorOutcomes.length === 0) {
    const resolvedStores = stores ?? await createDefaultStores();
    return classify({
      detectorOutcomes,
      harnessEvents: [],
      coverageEvents: [],
      stores: resolvedStores,
      runId,
    });
  }

  if (branch.status !== "confirmed") {
    const resolvedStores = stores ?? await createDefaultStores();
    return classify({
      detectorOutcomes,
      harnessEvents: [],
      coverageEvents: [],
      stores: resolvedStores,
      runId,
    });
  }

  const classifiedRuns = await Promise.all(
    detectorOutcomes.map(async (detectorOutcome) => {
      const resolvedStores = await resolveClassificationStores(
        branch,
        detectorOutcome,
        stores,
      );
      return classify({
        detectorOutcomes: [detectorOutcome],
        harnessEvents: [],
        coverageEvents: [],
        stores: resolvedStores,
        runId,
      });
    }),
  );

  return classifiedRuns.reduce(mergeClassifiedRuns);
}

async function runUiOnlyJourney(
  plan: UiJourneyPlan,
  options: RunUiJourneyPlanOptions,
): Promise<JourneyOutcome> {
  const branch = plan.journey;
  const runId = options.runId ?? "journey-run";
  const triggerResult = await options.trigger();
  const browserPromiseChecks = [...(options.browserPromiseChecks?.() ?? [])];

  if (
    !matchesPromiseContract(plan, browserPromiseChecks) ||
    (options.certificationAllowed !== undefined && !options.certificationAllowed())
  ) {
    const coverage = await buildCoverageGapOutcome(
      branch,
      [],
      runId,
      triggerResult,
      "missing-browser-promises",
    );
    return { ...coverage, uiOnly: true, browserPromiseChecks };
  }

  const mismatches = browserPromiseChecks.filter((check) => !check.passed);
  const detectorOutcomes: DetectorOutcome[] = [];
  const adjudicationItems: JourneyAdjudicationItem[] = [];
  if (mismatches.length > 0) {
    const verdict = browserPromiseVerdict(browserPromiseChecks);
    const outcome = buildViolationOutcome(
      branch,
      "browser-promise",
      "promise-contract",
      verdict,
    );
    detectorOutcomes.push(outcome);
    if (branch.status === "inferred") {
      adjudicationItems.push(buildAdjudicationItem(branch, "browser-promise", verdict));
    }
  }

  const classified = await classifyJourneyOutcomes(
    branch,
    detectorOutcomes,
    options.stores,
    runId,
  );
  return {
    branchId: branch.id,
    branchStatus: branch.status,
    triggerResult,
    oracleResults: [],
    certified: false,
    terminalStatus: mismatches.length === 0 ? "success" : "failure",
    uiOnly: true,
    browserPromiseChecks,
    adjudicationItems,
    classified,
  };
}

export async function runJourneyBranch(
  branchInput: JourneyBranch,
  options: RunJourneyBranchOptions,
): Promise<JourneyOutcome> {
  const branch = parseJourneyBranch(branchInput);
  assertNonEmptyString(options.runId ?? "journey-run", "runId");
  const runId = options.runId ?? "journey-run";

  const declared = declaredDomainOracles(branch);
  const hasDomainOracle = declared.length > 0 && declared.every((adapter) => resolveOracle(adapter, options.oracles) !== undefined);

  if (declared.length === 0 || !hasDomainOracle) {
    return buildCoverageGapOutcome(branch, declared, runId, await options.trigger());
  }
  const persistenceBefore = new Map<string, unknown>();
  for (const adapter of declared) {
    if (adapter.kind !== "persistence") {
      continue;
    }
    const persistence = resolveOracle(adapter, options.oracles);
    if (persistence === undefined) {
      throw new Error(
        `journey branch ${branch.id} declares persistence oracle ${adapter.id} but none was provided`,
      );
    }
    persistenceBefore.set(
      oracleAdapterKey(adapter),
      await (persistence as PersistenceOracle).snapshot(),
    );
  }

  const triggerResult = await options.trigger();
  if (options.certificationAllowed !== undefined && !options.certificationAllowed()) {
    const coverage = await buildCoverageGapOutcome(branch, declared, runId, triggerResult);
    return { ...coverage, triggerResult };
  }

  const oracleResults: OracleRunResult[] = [];
  const adjudicationItems: JourneyAdjudicationItem[] = [];
  const detectorOutcomes: DetectorOutcome[] = [];

  for (const adapter of declared) {
    const result = await runDeclaredOracle(
      branch,
      adapter,
      options.oracles,
      persistenceBefore.get(oracleAdapterKey(adapter)),
    );
    oracleResults.push(result);
    if (!result.verdict.holds) {
      if (branch.status === "inferred") {
        adjudicationItems.push(
          buildAdjudicationItem(branch, adapter.kind, result.verdict),
        );
      }
      detectorOutcomes.push(
        buildViolationOutcome(branch, adapter.kind, adapter.id, result.verdict),
      );
    }
  }

  const classified = await classifyJourneyOutcomes(
    branch,
    detectorOutcomes,
    options.stores,
    runId,
  );

  return {
    branchId: branch.id,
    branchStatus: branch.status,
    triggerResult,
    oracleResults,
    certified: isCertified(branch, oracleResults, undefined),
    terminalStatus: detectorOutcomes.length === 0 ? "success" : "failure",
    uiOnly: false,
    adjudicationItems,
    classified,
  };
}

export async function runUiJourneyPlan(
  planInput: UiJourneyPlan,
  options: RunUiJourneyPlanOptions,
): Promise<JourneyOutcome> {
  const plan = parseUiJourneyPlan(planInput);
  if (declaredDomainOracles(plan.journey).length === 0) {
    return runUiOnlyJourney(plan, options);
  }

  const domainOutcome = await runJourneyBranch(plan.journey, options);
  const browserPromiseChecks = [...(options.browserPromiseChecks?.() ?? [])];
  if (domainOutcome.coverageGap !== undefined) {
    return { ...domainOutcome, browserPromiseChecks };
  }
  if (!matchesPromiseContract(plan, browserPromiseChecks)) {
    const coverage = await buildCoverageGapOutcome(
      plan.journey,
      declaredDomainOracles(plan.journey),
      options.runId ?? "journey-run",
      domainOutcome.triggerResult,
      "missing-browser-promises",
    );
    return { ...coverage, browserPromiseChecks };
  }

  const mismatches = browserPromiseChecks.filter((check) => !check.passed);
  if (mismatches.length === 0) {
    return { ...domainOutcome, browserPromiseChecks };
  }

  const verdict = browserPromiseVerdict(browserPromiseChecks);
  const browserOutcome = buildViolationOutcome(
    plan.journey,
    "browser-promise",
    "promise-contract",
    verdict,
  );
  const classifiedBrowser = await classifyJourneyOutcomes(
    plan.journey,
    [browserOutcome],
    options.stores,
    options.runId ?? "journey-run",
  );

  return {
    ...domainOutcome,
    certified: false,
    terminalStatus: "failure",
    browserPromiseChecks,
    classified: mergeClassifiedRuns(domainOutcome.classified, classifiedBrowser),
  };
}

export type { ExternalEffect, OracleVerdict, PersistenceOracle, SideEffectOracle };
