import type { JsonObject, JsonValue } from "./project-config.ts";

export const KXM_COMPILED_WORKFLOW_SCHEMA = "kxm.compiled-workflow.v1";
export const KXM_RESERVED_STEP_KINDS = ["workflow"] as const;

const WORKFLOW_SCHEMA = "kxm.workflow.v1";
const STEP_ID_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
const SUPPORTED_STEP_KINDS = new Set(["agent", "moa", "gate", "approval", "wait"]);
const TERMINAL_STATUSES = new Set(["completed", "failed", "cancelled"]);
const JOIN_STRATEGIES = new Set(["all", "all-settled", "quorum", "first-success"]);
const EVIDENCE_KINDS = new Set(["assignment-result", "gate", "receipt", "approval", "artifact"]);
const REPOSITORY_ACCESS = new Set(["none", "read", "write"]);
const DISTINCT_BY = new Set(["provider", "model", "profile"]);
const BANNED_RUNTIME_HINT = "not yet supported";

export type ReadonlyJsonValue =
  | string
  | number
  | boolean
  | null
  | readonly ReadonlyJsonValue[]
  | { readonly [key: string]: ReadonlyJsonValue };
export type ReadonlyJsonObject = { readonly [key: string]: ReadonlyJsonValue };

export type KxmTerminalStatus = "completed" | "failed" | "cancelled";
export type KxmCompiledStepKind = "agent" | "moa" | "gate" | "approval" | "wait";
export type KxmRepositoryAccess = "none" | "read" | "write";

export type KxmCompiledTransition =
  | { readonly to: "step"; readonly target: string; readonly edge: "forward" | "back"; readonly maxTransitions?: number }
  | { readonly to: "terminal"; readonly terminalStatus: KxmTerminalStatus; readonly maxTransitions?: number };

export interface KxmCompiledProducerPolicy {
  readonly minimumProducers: number;
  readonly eligibleAgents: readonly string[];
  readonly acceptedStatuses: readonly ["passed"];
  readonly degradation?: { readonly minimumProducers: number };
}

export interface KxmCompiledEvidence {
  readonly key: string;
  readonly kind: "assignment-result" | "gate" | "receipt" | "approval" | "artifact";
  readonly minimum: number;
  readonly reusableAcrossAttempts: boolean;
  readonly producerPolicy?: KxmCompiledProducerPolicy;
}

export interface KxmCompiledAssignments {
  readonly allowedAgents: readonly string[]; // declared, else [step.agent] when present, else []
  readonly minimum: number;
  readonly target: number;
  readonly maximum: number;
  readonly maxParallel: number;
  readonly maxAttemptsPerAssignment: number;
  readonly maxWriteRepositories?: number;
  readonly distinctBy: readonly ("provider" | "model" | "profile")[];
}

export interface KxmCompiledJoin {
  readonly strategy: "all" | "all-settled" | "quorum" | "first-success";
  readonly minimumPassed?: number;
  readonly cancelRemaining?: boolean;
}

interface KxmCompiledStepBase {
  readonly id: string;
  readonly index: number;
  readonly description?: string;
  readonly instructions?: string;
  readonly maxAttempts: number;
  readonly timeoutMs?: number;
  readonly safeSpeculation: boolean;
  readonly repositories: Readonly<Record<string, KxmRepositoryAccess>>;
  readonly tools?: ReadonlyJsonObject;
  readonly secrets: readonly ReadonlyJsonObject[];
  readonly model?: ReadonlyJsonValue;
  readonly requiredEvidence: readonly KxmCompiledEvidence[];
  readonly outcomes: readonly string[];
  readonly transitions: Readonly<Record<string, KxmCompiledTransition>>;
  readonly requiresPlanHash: boolean;
  readonly assignments: KxmCompiledAssignments;
  readonly join: KxmCompiledJoin;
}

export type KxmCompiledStep =
  | (KxmCompiledStepBase & { readonly kind: "agent" | "moa"; readonly agent: string })
  | (KxmCompiledStepBase & { readonly kind: "gate"; readonly gate: string; readonly expect: "pass" | "fail" })
  | (KxmCompiledStepBase & { readonly kind: "approval" })
  | (KxmCompiledStepBase & { readonly kind: "wait"; readonly signal: string });

export interface KxmCompiledPlan {
  readonly schema: typeof KXM_COMPILED_WORKFLOW_SCHEMA;
  readonly workflowId: string;
  readonly sourcePath?: string;
  readonly coordinator: string;
  readonly limits: {
    readonly maxTransitions?: number;
    readonly maxRunDurationMs?: number;
    readonly maxAgentTimeMs?: number;
    readonly maxModelCost?: number;
    readonly currency?: string;
  };
  readonly transitionBudget: number;
  readonly hasBackEdges: boolean;
  readonly entryStepId: string;
  readonly order: readonly string[];
  readonly steps: Readonly<Record<string, KxmCompiledStep>>;
  readonly reproOracle?: { readonly stageId: string; readonly evidenceKey: string };
  readonly planHash?: { readonly stageId: string; readonly evidenceKey: string };
  readonly requirePlanHash: readonly string[];
}

export interface KxmEngineCompileIssue {
  readonly code: string;
  readonly workflowId: string;
  readonly stepId?: string;
  readonly outcome?: string;
  readonly message: string;
}

export class KxmEngineCompileError extends Error {
  readonly issues: readonly KxmEngineCompileIssue[];

  constructor(issues: readonly KxmEngineCompileIssue[]) {
    const sorted = sortIssues(issues);
    super(sorted.map((issue) => `${issue.workflowId}: ${issue.code}: ${issue.message}`).join("\n"));
    this.name = "KxmEngineCompileError";
    this.issues = sorted;
  }
}

export interface KxmWorkflowCompileInput {
  readonly id: string;
  readonly value: JsonObject;
  readonly logicalPath?: string;
}

interface IssueSink {
  workflowId: string;
  issues: KxmEngineCompileIssue[];
}

export function compileKxmWorkflow(input: KxmWorkflowCompileInput): KxmCompiledPlan {
  const sink: IssueSink = { workflowId: input.id, issues: [] };
  const value = input.value;
  if (value.schema !== WORKFLOW_SCHEMA) {
    pushIssue(sink, { code: "workflow_schema_unsupported", message: "schema is not kxm.workflow.v1" });
  }

  const rawSteps = valuesOf(value, "steps").map(objectValue).filter((step): step is JsonObject => Boolean(step));
  if (rawSteps.length === 0) {
    pushIssue(sink, { code: "steps_empty", message: "workflow has no steps" });
  }

  const stepIndex = new Map<string, number>();
  for (const [index, step] of rawSteps.entries()) {
    const id = stringValue(step.id);
    if (!id || !isStepId(id)) {
      pushIssue(sink, { code: "step_id_invalid", message: "step id is invalid", ...(id ? { stepId: id } : {}) });
      continue;
    }
    if (stepIndex.has(id)) {
      pushIssue(sink, { code: "step_id_duplicate", stepId: id, message: `step ${id} is duplicated` });
      continue;
    }
    stepIndex.set(id, index);
  }

  const limitsObject = objectValue(value.limits);
  const limits = compileLimits(limitsObject, sink);
  const requirePlanHash = names(value.requirePlanHash);
  const compiledSteps: KxmCompiledStep[] = [];
  let hasBackEdges = false;

  for (const [index, step] of rawSteps.entries()) {
    const compiled = compileStep(step, index, stepIndex, requirePlanHash, sink);
    if (!compiled) continue;
    compiledSteps.push(compiled);
    if (Object.values(compiled.transitions).some((transition) => transition.to === "step" && transition.edge === "back")) {
      hasBackEdges = true;
    }
  }

  if (hasBackEdges && typeof limitsObject?.maxTransitions !== "number") {
    pushIssue(sink, { code: "workflow_cycle_unbounded", message: "workflow with back-edges requires limits.maxTransitions" });
  }

  const reproOracle = compileOracle("reproOracle", objectValue(value.reproOracle), compiledSteps, sink);
  const planHash = compileOracle("planHash", objectValue(value.planHash), compiledSteps, sink);
  for (const stageId of requirePlanHash) {
    if (!stepIndex.has(stageId)) {
      pushIssue(sink, { code: "plan_hash_stage_unknown", message: `requirePlanHash references unknown stage ${stageId}` });
    }
  }

  if (sink.issues.length > 0) throw new KxmEngineCompileError(sink.issues);

  const order = compiledSteps.map((step) => step.id);
  const steps = Object.create(null) as Record<string, KxmCompiledStep>;
  for (const step of compiledSteps) steps[step.id] = step;
  const entryStepId = order[0];
  if (!entryStepId) throw new KxmEngineCompileError(sink.issues);
  const coordinator = stringValue(value.coordinator) ?? "coordinator";
  const transitionBudget = limits.maxTransitions ?? order.length;
  const plan: KxmCompiledPlan = {
    schema: KXM_COMPILED_WORKFLOW_SCHEMA,
    workflowId: input.id,
    ...(input.logicalPath !== undefined ? { sourcePath: input.logicalPath } : {}),
    coordinator,
    limits,
    transitionBudget,
    hasBackEdges,
    entryStepId,
    order,
    steps,
    ...(reproOracle ? { reproOracle } : {}),
    ...(planHash ? { planHash } : {}),
    requirePlanHash: [...requirePlanHash],
  };
  return deepFreeze(plan);
}

function compileLimits(
  limits: JsonObject | undefined,
  sink: IssueSink,
): KxmCompiledPlan["limits"] {
  if (!limits) return {};
  const compiled: {
    maxTransitions?: number;
    maxRunDurationMs?: number;
    maxAgentTimeMs?: number;
    maxModelCost?: number;
    currency?: string;
  } = {};
  if (limits.maxTransitions !== undefined) {
    if (isCount(limits.maxTransitions)) compiled.maxTransitions = limits.maxTransitions;
    else pushIssue(sink, { code: "count_invalid", message: "limits.maxTransitions must be an integer >= 1" });
  }
  if (limits.maxRunDurationMs !== undefined) {
    if (isDuration(limits.maxRunDurationMs)) compiled.maxRunDurationMs = limits.maxRunDurationMs;
    else pushIssue(sink, { code: "duration_invalid", message: "limits.maxRunDurationMs must be an integer >= 0" });
  }
  if (limits.maxAgentTimeMs !== undefined) {
    if (isDuration(limits.maxAgentTimeMs)) compiled.maxAgentTimeMs = limits.maxAgentTimeMs;
    else pushIssue(sink, { code: "duration_invalid", message: "limits.maxAgentTimeMs must be an integer >= 0" });
  }
  if (limits.maxModelCost !== undefined) {
    if (isCost(limits.maxModelCost)) compiled.maxModelCost = limits.maxModelCost;
    else pushIssue(sink, { code: "cost_invalid", message: "limits.maxModelCost must be a finite number > 0" });
  }
  if (limits.currency !== undefined) {
    if (typeof limits.currency === "string") compiled.currency = limits.currency;
  }
  return compiled;
}

function compileStep(
  step: JsonObject,
  index: number,
  stepIndex: ReadonlyMap<string, number>,
  requirePlanHash: readonly string[],
  sink: IssueSink,
): KxmCompiledStep | undefined {
  const id = stringValue(step.id);
  if (!id || !isStepId(id) || stepIndex.get(id) !== index) return undefined;
  const kind = stringValue(step.kind);
  if (kind === "workflow") {
    pushIssue(sink, { code: "step_kind_reserved", stepId: id, message: `kind workflow is ${BANNED_RUNTIME_HINT}` });
    return undefined;
  }
  if (!kind || !SUPPORTED_STEP_KINDS.has(kind)) {
    pushIssue(sink, { code: "step_kind_unknown", stepId: id, message: `kind ${String(kind)} is unknown` });
    return undefined;
  }

  const agent = stringValue(step.agent);
  const gate = stringValue(step.gate);
  const signal = stringValue(step.signal);
  if ((kind === "agent" || kind === "moa") && !agent) {
    pushIssue(sink, { code: "step_agent_missing", stepId: id, message: `${id} is missing agent` });
  }
  if (kind === "gate" && !gate) {
    pushIssue(sink, { code: "step_gate_missing", stepId: id, message: `${id} is missing gate` });
  }
  if (kind === "wait" && !signal) {
    pushIssue(sink, { code: "step_signal_missing", stepId: id, message: `${id} is missing signal` });
  }

  if (step.expect !== undefined && (kind !== "gate" || (step.expect !== "pass" && step.expect !== "fail"))) {
    pushIssue(sink, { code: "gate_expect_invalid", stepId: id, message: "expect is gate-only pass or fail" });
  }
  if (kind === "gate" && Object.keys(objectValue(step.on) ?? {}).some((outcome) => outcome === "implementation_failure" || outcome === "repro_missing")) {
    pushIssue(sink, { code: "gate_outcome_renamed", stepId: id, message: "use implementation-failure and repro-missing" });
  }
  const maxAttempts = compileCountField(step.maxAttempts, 1, `${id}.maxAttempts`, id, sink);
  const timeoutMs = compileOptionalDuration(step.timeoutMs, `${id}.timeoutMs`, id, sink);
  const assignments = compileAssignments(step, id, agent, sink);
  const join = compileJoin(step, id, sink);
  const requiredEvidence = compileEvidence(step, id, sink);
  const transitions = compileTransitions(step, id, index, stepIndex, sink);
  const outcomes = Object.keys(transitions).sort(compareCodeUnits);
  const orderedTransitions = Object.create(null) as Record<string, KxmCompiledTransition>;
  for (const outcome of outcomes) {
    const transition = transitions[outcome];
    if (transition) orderedTransitions[outcome] = transition;
  }

  const description = stringValue(step.description);
  const instructions = stringValue(step.instructions);
  const base: KxmCompiledStepBase = {
    id,
    index,
    ...(description !== undefined ? { description } : {}),
    ...(instructions !== undefined ? { instructions } : {}),
    maxAttempts,
    ...(timeoutMs !== undefined ? { timeoutMs } : {}),
    safeSpeculation: step.safeSpeculation === true,
    repositories: compileRepositories(step.repositories),
    ...(isJsonObject(step.tools) ? { tools: cloneJsonObject(step.tools) } : {}),
    secrets: compileSecrets(step.secrets),
    ...(step.model !== undefined ? { model: cloneJsonValue(step.model) } : {}),
    requiredEvidence,
    outcomes,
    transitions: orderedTransitions,
    requiresPlanHash: requirePlanHash.includes(id),
    assignments,
    join,
  };

  if (kind === "agent" || kind === "moa") {
    if (!agent) return undefined;
    return { ...base, kind, agent };
  }
  if (kind === "gate") {
    if (!gate) return undefined;
    return { ...base, kind, gate, expect: step.expect === "fail" ? "fail" : "pass" };
  }
  if (kind === "wait") {
    if (!signal) return undefined;
    return { ...base, kind, signal };
  }
  return { ...base, kind: "approval" };
}

function compileAssignments(
  step: JsonObject,
  stepId: string,
  primaryAgentId: string | undefined,
  sink: IssueSink,
): KxmCompiledAssignments {
  const assignment = objectValue(step.assignments);
  const declaredAgents = assignment ? names(assignment.allowedAgents) : [];
  const allowedAgents = declaredAgents.length > 0 ? declaredAgents : primaryAgentId ? [primaryAgentId] : [];
  // Mirrors validateWorkflow in project-config.ts (minimum 1, target = minimum, maximum = target, maxParallel = maximum) for every kind. Do not resolve kind-conditional defaults here; the loader is the ceiling authority. Phase 7 changes both together.
  const minimum = compileCountField(assignment?.minimum, 1, `${stepId}.assignments.minimum`, stepId, sink);
  const target = compileCountField(assignment?.target, minimum, `${stepId}.assignments.target`, stepId, sink);
  const maximum = compileCountField(assignment?.maximum, target, `${stepId}.assignments.maximum`, stepId, sink);
  const maxParallel = compileCountField(assignment?.maxParallel, maximum, `${stepId}.assignments.maxParallel`, stepId, sink);
  const maxAttemptsPerAssignment = compileCountField(
    assignment?.maxAttemptsPerAssignment,
    1,
    `${stepId}.assignments.maxAttemptsPerAssignment`,
    stepId,
    sink,
  );
  const maxWriteRepositories = compileOptionalCount(
    assignment?.maxWriteRepositories,
    `${stepId}.assignments.maxWriteRepositories`,
    stepId,
    sink,
  );
  const distinctBy = names(assignment?.distinctBy).filter((value): value is "provider" | "model" | "profile" => DISTINCT_BY.has(value));
  return {
    allowedAgents: [...allowedAgents],
    minimum,
    target,
    maximum,
    maxParallel,
    maxAttemptsPerAssignment,
    ...(maxWriteRepositories !== undefined ? { maxWriteRepositories } : {}),
    distinctBy,
  };
}

function compileJoin(step: JsonObject, stepId: string, sink: IssueSink): KxmCompiledJoin {
  const join = objectValue(step.join);
  if (!join) return { strategy: "all" };
  const declared = stringValue(join.strategy);
  const strategy = declared && JOIN_STRATEGIES.has(declared)
    ? declared as KxmCompiledJoin["strategy"]
    : "all";
  const minimumPassed = compileOptionalCount(join.minimumPassed, `${stepId}.join.minimumPassed`, stepId, sink);
  const compiled: KxmCompiledJoin = {
    strategy,
    ...(minimumPassed !== undefined ? { minimumPassed } : {}),
    ...(typeof join.cancelRemaining === "boolean" ? { cancelRemaining: join.cancelRemaining } : {}),
  };
  return compiled;
}

function compileEvidence(step: JsonObject, stepId: string, sink: IssueSink): KxmCompiledEvidence[] {
  const evidence: KxmCompiledEvidence[] = [];
  const seen = new Set<string>();
  for (const candidate of valuesOf(step, "requiredEvidence")) {
    const requirement = objectValue(candidate);
    if (!requirement) continue;
    const key = stringValue(requirement.key);
    if (!key) continue;
    if (seen.has(key)) {
      pushIssue(sink, { code: "evidence_key_duplicate", stepId, message: `${stepId} evidence key ${key} is duplicated` });
      continue;
    }
    seen.add(key);
    const kind = stringValue(requirement.kind);
    if (!kind || !EVIDENCE_KINDS.has(kind)) continue;
    const minimum = compileCountField(requirement.minimum, 1, `${stepId}.${key}.minimum`, stepId, sink);
    const producerPolicy = objectValue(requirement.producerPolicy);
    const compiled: KxmCompiledEvidence = {
      key,
      kind: kind as KxmCompiledEvidence["kind"],
      minimum,
      reusableAcrossAttempts: requirement.reusableAcrossAttempts === true,
      ...(producerPolicy ? { producerPolicy: compileProducerPolicy(producerPolicy, stepId, key, sink) } : {}),
    };
    evidence.push(compiled);
  }
  return evidence;
}

function compileProducerPolicy(
  policy: JsonObject,
  stepId: string,
  key: string,
  sink: IssueSink,
): KxmCompiledProducerPolicy {
  const minimumProducers = compileCountField(
    policy.minimumProducers,
    1,
    `${stepId}.${key}.producerPolicy.minimumProducers`,
    stepId,
    sink,
  );
  const degradation = objectValue(policy.degradation);
  const degradationMinimum = compileOptionalCount(
    degradation?.minimumProducers,
    `${stepId}.${key}.producerPolicy.degradation.minimumProducers`,
    stepId,
    sink,
  );
  return {
    minimumProducers,
    eligibleAgents: names(policy.eligibleAgents),
    acceptedStatuses: ["passed"],
    ...(degradationMinimum !== undefined ? { degradation: { minimumProducers: degradationMinimum } } : {}),
  };
}

function compileTransitions(
  step: JsonObject,
  stepId: string,
  index: number,
  stepIndex: ReadonlyMap<string, number>,
  sink: IssueSink,
): Record<string, KxmCompiledTransition> {
  const outcomes = objectValue(step.on);
  const compiled = Object.create(null) as Record<string, KxmCompiledTransition>;
  if (!outcomes || Object.keys(outcomes).length === 0) {
    pushIssue(sink, { code: "transitions_missing", stepId, message: `${stepId} has no declared transitions` });
    return compiled;
  }
  for (const [outcome, raw] of Object.entries(outcomes)) {
    const parsed = parseTransition(raw);
    const maxTransitions = compileOptionalCount(
      parsed.maxTransitions,
      `${stepId}.${outcome}.maxTransitions`,
      stepId,
      sink,
      outcome,
    );
    if (parsed.target === "$terminal") {
      const status = parsed.terminalStatus;
      if (status === undefined) {
        pushIssue(sink, {
          code: "transition_terminal_status_missing",
          stepId,
          outcome,
          message: `${stepId}.${outcome} terminal transition requires terminalStatus`,
        });
        continue;
      }
      if (!TERMINAL_STATUSES.has(status)) {
        pushIssue(sink, {
          code: "transition_terminal_status_invalid",
          stepId,
          outcome,
          message: `${stepId}.${outcome} terminalStatus is invalid`,
        });
        continue;
      }
      compiled[outcome] = {
        to: "terminal",
        terminalStatus: status as KxmTerminalStatus,
        ...(maxTransitions !== undefined ? { maxTransitions } : {}),
      };
      continue;
    }
    if (parsed.terminalStatus !== undefined) {
      pushIssue(sink, {
        code: "transition_terminal_status_unexpected",
        stepId,
        outcome,
        message: `${stepId}.${outcome} step target must not declare terminalStatus`,
      });
    }
    if (!parsed.target || !stepIndex.has(parsed.target)) {
      pushIssue(sink, {
        code: "transition_target_unknown",
        stepId,
        outcome,
        message: `${stepId}.${outcome} references unknown target ${String(parsed.target)}`,
      });
      continue;
    }
    const targetIndex = stepIndex.get(parsed.target)!;
    const back = targetIndex <= index;
    if (back && maxTransitions === undefined && parsed.maxTransitions === undefined) {
      pushIssue(sink, {
        code: "back_edge_unbounded",
        stepId,
        outcome,
        message: `${stepId}.${outcome} back-edge requires maxTransitions`,
      });
    }
    compiled[outcome] = {
      to: "step",
      target: parsed.target,
      edge: back ? "back" : "forward",
      ...(maxTransitions !== undefined ? { maxTransitions } : {}),
    };
  }
  return compiled;
}

function parseTransition(value: JsonValue): { target?: string; maxTransitions?: JsonValue; terminalStatus?: string } {
  if (typeof value === "string") return { target: value };
  const object = objectValue(value);
  if (!object) return {};
  const target = stringValue(object.target);
  const terminalStatus = stringValue(object.terminalStatus);
  return {
    ...(target ? { target } : {}),
    ...(object.maxTransitions !== undefined ? { maxTransitions: object.maxTransitions } : {}),
    ...(terminalStatus ? { terminalStatus } : {}),
  };
}

function compileOracle(
  field: "reproOracle" | "planHash",
  oracle: JsonObject | undefined,
  steps: readonly KxmCompiledStep[],
  sink: IssueSink,
): { stageId: string; evidenceKey: string } | undefined {
  if (!oracle) return undefined;
  const stageId = stringValue(oracle.stageId);
  const evidenceKey = stringValue(oracle.evidenceKey);
  const step = stageId === undefined ? undefined : steps.find((candidate) => candidate.id === stageId);
  if (!stageId || !step) {
    pushIssue(sink, { code: "oracle_stage_unknown", message: `${field} references unknown stage ${String(stageId)}` });
    return undefined;
  }
  if (!evidenceKey || !step.requiredEvidence.some((item) => item.key === evidenceKey)) {
    pushIssue(sink, {
      code: "oracle_evidence_unknown",
      message: `${field} references undeclared evidence ${String(evidenceKey)} in ${stageId}`,
    });
    return undefined;
  }
  return { stageId, evidenceKey };
}

function compileRepositories(value: JsonValue | undefined): Readonly<Record<string, KxmRepositoryAccess>> {
  const source = objectValue(value) ?? {};
  const repositories: Record<string, KxmRepositoryAccess> = {};
  for (const [id, access] of Object.entries(source)) {
    if (typeof access === "string" && REPOSITORY_ACCESS.has(access)) {
      repositories[id] = access as KxmRepositoryAccess;
    }
  }
  return repositories;
}

function compileSecrets(value: JsonValue | undefined): ReadonlyJsonObject[] {
  if (!Array.isArray(value)) return [];
  return value.map(objectValue).filter((entry): entry is JsonObject => Boolean(entry)).map(cloneJsonObject);
}

function compileCountField(
  value: JsonValue | undefined,
  fallback: number,
  path: string,
  stepId: string,
  sink: IssueSink,
): number {
  if (value === undefined) return fallback;
  if (isCount(value)) return value;
  pushIssue(sink, { code: "count_invalid", stepId, message: `${path} must be an integer >= 1` });
  return fallback;
}

function compileOptionalCount(
  value: JsonValue | undefined,
  path: string,
  stepId: string,
  sink: IssueSink,
  outcome?: string,
): number | undefined {
  if (value === undefined) return undefined;
  if (isCount(value)) return value;
  pushIssue(sink, {
    code: "count_invalid",
    stepId,
    ...(outcome ? { outcome } : {}),
    message: `${path} must be an integer >= 1`,
  });
  return undefined;
}

function compileOptionalDuration(
  value: JsonValue | undefined,
  path: string,
  stepId: string,
  sink: IssueSink,
): number | undefined {
  if (value === undefined) return undefined;
  if (isDuration(value)) return value;
  pushIssue(sink, { code: "duration_invalid", stepId, message: `${path} must be an integer >= 0` });
  return undefined;
}

function isCount(value: JsonValue): value is number {
  return typeof value === "number" && Number.isInteger(value) && value >= 1;
}

function isDuration(value: JsonValue): value is number {
  return typeof value === "number" && Number.isInteger(value) && value >= 0;
}

function isCost(value: JsonValue): value is number {
  return typeof value === "number" && Number.isFinite(value) && value > 0;
}

function isStepId(value: string): boolean {
  return value.length >= 1 && value.length <= 64 && STEP_ID_PATTERN.test(value);
}

function isJsonObject(value: JsonValue | undefined): value is JsonObject {
  return value !== undefined && value !== null && typeof value === "object" && !Array.isArray(value);
}

function objectValue(value: JsonValue | undefined): JsonObject | undefined {
  return isJsonObject(value) ? value : undefined;
}

function stringValue(value: JsonValue | undefined): string | undefined {
  return typeof value === "string" ? value : undefined;
}

function valuesOf(object: JsonObject, field: string): JsonValue[] {
  const value = object[field];
  return Array.isArray(value) ? value : [];
}

function names(value: JsonValue | undefined): string[] {
  return Array.isArray(value) ? value.filter((candidate): candidate is string => typeof candidate === "string") : [];
}

function cloneJsonValue(value: JsonValue): ReadonlyJsonValue {
  if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
  if (Array.isArray(value)) return value.map((entry) => cloneJsonValue(entry));
  return cloneJsonObject(isJsonObject(value) ? value : {});
}

function cloneJsonObject(value: JsonObject): ReadonlyJsonObject {
  const copy: { [key: string]: ReadonlyJsonValue } = {};
  for (const [key, child] of Object.entries(value)) copy[key] = cloneJsonValue(child);
  return copy;
}

function deepFreeze<T>(value: T): T {
  if (value === null || typeof value !== "object") return value;
  if (Array.isArray(value)) {
    for (const entry of value) deepFreeze(entry);
  } else {
    for (const entry of Object.values(value as Record<string, unknown>)) deepFreeze(entry);
  }
  return Object.freeze(value);
}

function compareCodeUnits(left: string, right: string): number {
  return left < right ? -1 : left > right ? 1 : 0;
}

function compareOptional(left: string | undefined, right: string | undefined): number {
  if (left === right) return 0;
  if (left === undefined) return -1;
  if (right === undefined) return 1;
  return compareCodeUnits(left, right);
}

function sortIssues(issues: readonly KxmEngineCompileIssue[]): KxmEngineCompileIssue[] {
  return [...issues].sort((left, right) => {
    const byStep = compareOptional(left.stepId, right.stepId);
    if (byStep !== 0) return byStep;
    const byOutcome = compareOptional(left.outcome, right.outcome);
    if (byOutcome !== 0) return byOutcome;
    return compareCodeUnits(left.code, right.code);
  });
}

function pushIssue(sink: IssueSink, issue: Omit<KxmEngineCompileIssue, "workflowId">): void {
  sink.issues.push({ workflowId: sink.workflowId, ...issue });
}
