import { createHash } from "node:crypto";

export const ENGINE_PRIVATE_EXTENSION_PLAN_REQUEST_SCHEMA_VERSION =
  "nodedc.engine.private-extension.plan-request/v1";
export const ENGINE_PRIVATE_EXTENSION_PLAN_SCHEMA_VERSION =
  "nodedc.engine.private-extension.plan/v1";
export const ENGINE_PRIVATE_EXTENSION_APPLY_REQUEST_SCHEMA_VERSION =
  "nodedc.engine.private-extension.apply-request/v1";
export const ENGINE_PRIVATE_EXTENSION_APPLY_RECEIPT_SCHEMA_VERSION =
  "nodedc.engine.private-extension.apply-receipt/v1";
export const ENGINE_PRIVATE_EXTENSION_OPERATION_SCHEMA_VERSION =
  "nodedc.engine.private-extension.operation/v1";
export const ENGINE_PRIVATE_EXTENSION_STATUS_SCHEMA_VERSION =
  "nodedc.engine.private-extension.status/v1";

export const ENGINE_PRIVATE_EXTENSION_MANAGE_CAPABILITY = "engine.private-extension.manage";
export const ENGINE_PRIVATE_EXTENSION_READ_CAPABILITY = "engine.private-extension.read";
export const ENGINE_PRIVATE_EXTENSION_PACKAGE_NAME = "n8n-nodes-ndc";
export const ENGINE_PRIVATE_EXTENSION_INACTIVE_BASELINE = "n8n-nodes-ndc.inactive/v1";

export const ENGINE_PRIVATE_EXTENSION_NODE_TYPES = Object.freeze([
  "n8n-nodes-ndc.ndcDataProductPublish",
  "n8n-nodes-ndc.ndcDataProductRead",
  "n8n-nodes-ndc.ndcFoundryBinding",
]);

export const ENGINE_PRIVATE_EXTENSION_CREDENTIAL_TYPES = Object.freeze([
  "ndcDataProductWriterApi",
  "ndcDataProductReaderApi",
  "ndcFoundryBindingApi",
]);
export const ENGINE_PRIVATE_EXTENSION_CREDENTIAL_SCHEMAS = ENGINE_PRIVATE_EXTENSION_CREDENTIAL_TYPES;

const PLAN_REQUEST_KEYS = new Set([
  "schemaVersion",
  "requestId",
  "idempotencyKey",
  "action",
  "requestedAt",
  "requestExpiresAt",
  "expectedCurrentGeneration",
  "target",
]);
const RELEASE_STATE_KEYS = new Set(["kind", "packageName", "releaseId", "packageSha256"]);
const INACTIVE_STATE_KEYS = new Set(["kind", "packageName", "baselineId"]);
const PREVIOUS_STATE_TARGET_KEYS = new Set(["kind", "packageName"]);
const PLAN_KEYS = new Set([
  "schemaVersion",
  "planId",
  "planHash",
  "requestId",
  "idempotencyKey",
  "action",
  "createdAt",
  "expiresAt",
  "singleUse",
  "requiredCapability",
  "expectedCurrentGeneration",
  "nextGeneration",
  "currentState",
  "targetState",
  "recoveryState",
  "actions",
  "transition",
  "acceptance",
  "failurePolicy",
]);
const TRANSITION_KEYS = new Set([
  "mountMode",
  "loaderMode",
  "loaderPath",
  "loaderEnvironment",
  "quiesceMode",
  "stateSwitch",
  "runtimeAction",
  "scope",
  "requireUniformGeneration",
  "hotReload",
  "preserveCredentials",
]);
const LOADER_ENVIRONMENT_KEYS = new Set([
  "N8N_COMMUNITY_PACKAGES_ENABLED",
  "N8N_COMMUNITY_PACKAGES_PREVENT_LOADING",
  "N8N_REINSTALL_MISSING_PACKAGES",
]);
const ACCEPTANCE_SPEC_KEYS = new Set([
  "mode",
  "nodeTypes",
  "credentialSchemas",
  "requireUniformGeneration",
]);
const FAILURE_POLICY_KEYS = new Set([
  "mode",
  "rollbackFailureOutcome",
  "preserveImmutableRelease",
  "preserveCredentials",
]);
const APPLY_REQUEST_KEYS = new Set([
  "schemaVersion",
  "planId",
  "planHash",
  "idempotencyKey",
  "confirmedAt",
]);
const APPLY_RECEIPT_KEYS = new Set([
  "schemaVersion",
  "operationId",
  "planId",
  "planHash",
  "action",
  "acceptedAt",
  "state",
]);
const OPERATION_KEYS = new Set([
  "schemaVersion",
  "operationId",
  "planId",
  "planHash",
  "action",
  "state",
  "outcome",
  "phase",
  "expectedCurrentGeneration",
  "nextGeneration",
  "targetState",
  "recoveryState",
  "effectiveState",
  "runtime",
  "acceptance",
  "updatedAt",
  "errorCode",
]);
const RUNTIME_KEYS = new Set(["mode", "expectedInstances", "readyInstances", "generation"]);
const ACCEPTANCE_REPORT_KEYS = new Set([
  "state",
  "nodeTypes",
  "credentialSchemas",
  "uniformGeneration",
]);
const OBSERVATION_KEYS = new Set(["expected", "observed"]);
const STATUS_KEYS = new Set([
  "schemaVersion",
  "packageName",
  "generation",
  "health",
  "currentState",
  "previousState",
  "activeOperationId",
  "runtime",
  "acceptance",
  "updatedAt",
  "errorCode",
]);

const RELEASE_ID = /^\d+\.\d+\.\d+-[a-f0-9]{16}$/;
const SHA256 = /^[a-f0-9]{64}$/;
const HASH = /^sha256:[a-f0-9]{64}$/;
const OPAQUE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,159}$/;
const REASON_CODE = /^[a-z][a-z0-9_.:-]{2,127}$/;
const MAX_REQUEST_LIFETIME_MS = 15 * 60 * 1000;
const MAX_CLOCK_SKEW_MS = 60 * 1000;

const ACTIVATE_ACTIONS = Object.freeze([
  "verify_staged_immutable_release",
  "prepare_sealed_package_tree",
  "verify_community_package_loader_policy",
  "quiesce_deploy_run_and_drain_queue",
  "record_recovery_state",
  "atomic_switch_current",
  "force_recreate_main_workers_webhooks_as_version_barrier",
  "verify_exact_runtime_acceptance",
  "commit_active_state",
  "resume_deploy_run",
]);
const ROLLBACK_ACTIONS = Object.freeze([
  "verify_previous_activation_state",
  "verify_community_package_loader_policy",
  "quiesce_deploy_run_and_drain_queue",
  "record_recovery_state",
  "atomic_switch_current_to_previous",
  "force_recreate_main_workers_webhooks_as_version_barrier",
  "verify_exact_runtime_acceptance",
  "commit_rolled_back_state",
  "resume_deploy_run",
]);

const NON_TERMINAL_STATES = new Set([
  "queued",
  "preparing",
  "switching",
  "recreating",
  "accepting",
  "rolling-back",
]);
const TERMINAL_STATE_OUTCOMES = Object.freeze({
  active: "committed",
  rejected: "rejected",
  quarantined: "quarantined",
});

export function authorizeEnginePrivateExtensionOperation(operation, grantedCapabilities) {
  const capabilities = new Set(Array.isArray(grantedCapabilities) ? grantedCapabilities : []);
  const requiredCapability = operation === "status"
    ? ENGINE_PRIVATE_EXTENSION_READ_CAPABILITY
    : ENGINE_PRIVATE_EXTENSION_MANAGE_CAPABILITY;
  const authorized = operation === "status"
    ? capabilities.has(ENGINE_PRIVATE_EXTENSION_READ_CAPABILITY)
      || capabilities.has(ENGINE_PRIVATE_EXTENSION_MANAGE_CAPABILITY)
    : capabilities.has(ENGINE_PRIVATE_EXTENSION_MANAGE_CAPABILITY);
  return Object.freeze({
    ok: authorized,
    requiredCapability,
    errors: Object.freeze(authorized ? [] : ["engine_private_extension_capability_required"]),
  });
}

export function validateEnginePrivateExtensionPlanRequest(value, options = {}) {
  const errors = [];
  validatePlanRequestBody(value, options.now, errors);
  addAuthorizationErrors("plan", options.grantedCapabilities, errors);
  return result(errors);
}

export function computeEnginePrivateExtensionPlanHash(value) {
  if (!isPlainObject(value)) return "";
  const descriptor = { ...value };
  delete descriptor.planHash;
  return "sha256:" + createHash("sha256").update(stableJson(descriptor), "utf8").digest("hex");
}

export function validateEnginePrivateExtensionPlan(value, { request } = {}) {
  const errors = [];
  if (!isPlainObject(value)) return result(["enginePrivateExtensionPlan_must_be_object"]);
  if (value.schemaVersion !== ENGINE_PRIVATE_EXTENSION_PLAN_SCHEMA_VERSION) errors.push("schemaVersion_mismatch");
  rejectUnknownKeys(value, PLAN_KEYS, "enginePrivateExtensionPlan", errors);
  requiredOpaqueId(value.planId, "planId", errors);
  requiredHash(value.planHash, "planHash", errors);
  requiredOpaqueId(value.requestId, "requestId", errors);
  requiredOpaqueId(value.idempotencyKey, "idempotencyKey", errors);
  if (!new Set(["activate", "rollback"]).has(value.action)) errors.push("action_invalid");
  requiredTimestamp(value.createdAt, "createdAt", errors);
  requiredTimestamp(value.expiresAt, "expiresAt", errors);
  validateWindow(value.createdAt, value.expiresAt, Date.parse(value.createdAt), errors, "plan");
  if (value.singleUse !== true) errors.push("singleUse_must_be_true");
  if (value.requiredCapability !== ENGINE_PRIVATE_EXTENSION_MANAGE_CAPABILITY) {
    errors.push("requiredCapability_mismatch");
  }
  requiredGeneration(value.expectedCurrentGeneration, "expectedCurrentGeneration", errors);
  requiredGeneration(value.nextGeneration, "nextGeneration", errors);
  if (Number.isInteger(value.expectedCurrentGeneration)
      && value.nextGeneration !== value.expectedCurrentGeneration + 1) {
    errors.push("nextGeneration_must_increment_current_generation");
  }
  validateActivationState(value.currentState, "currentState", errors);
  validateActivationState(value.targetState, "targetState", errors);
  validateActivationState(value.recoveryState, "recoveryState", errors);
  if (!sameValue(value.currentState, value.recoveryState)) errors.push("recoveryState_must_equal_currentState");
  if (value.action === "activate" && value.targetState?.kind !== "release") {
    errors.push("activate_targetState_must_be_release");
  }
  if (sameValue(value.currentState, value.targetState)) errors.push("targetState_must_differ_from_currentState");
  validateExactArray(
    value.actions,
    value.action === "rollback" ? ROLLBACK_ACTIONS : ACTIVATE_ACTIONS,
    "actions",
    errors,
  );
  validateTransition(value.transition, errors);
  validateAcceptanceSpec(value.acceptance, value.targetState, errors);
  validateFailurePolicy(value.failurePolicy, errors);
  if (HASH.test(String(value.planHash || "")) && value.planHash !== computeEnginePrivateExtensionPlanHash(value)) {
    errors.push("planHash_mismatch");
  }
  if (request !== undefined) comparePlanToRequest(value, request, errors);
  return result(errors);
}

export function validateEnginePrivateExtensionApplyRequest(value, { plan, now = Date.now(), grantedCapabilities } = {}) {
  const errors = [];
  if (!isPlainObject(value)) return result(["enginePrivateExtensionApplyRequest_must_be_object"]);
  if (value.schemaVersion !== ENGINE_PRIVATE_EXTENSION_APPLY_REQUEST_SCHEMA_VERSION) errors.push("schemaVersion_mismatch");
  rejectUnknownKeys(value, APPLY_REQUEST_KEYS, "enginePrivateExtensionApplyRequest", errors);
  requiredOpaqueId(value.planId, "planId", errors);
  requiredHash(value.planHash, "planHash", errors);
  requiredOpaqueId(value.idempotencyKey, "idempotencyKey", errors);
  requiredTimestamp(value.confirmedAt, "confirmedAt", errors);
  addAuthorizationErrors("apply", grantedCapabilities, errors);
  const nowMs = normalizeNow(now, errors);
  if (isTimestamp(value.confirmedAt) && Date.parse(value.confirmedAt) > nowMs + MAX_CLOCK_SKEW_MS) {
    errors.push("confirmedAt_exceeds_clock_skew");
  }
  if (plan !== undefined) {
    const planValidation = validateEnginePrivateExtensionPlan(plan);
    if (!planValidation.ok) errors.push("plan_invalid_for_apply");
    if (value.planId !== plan?.planId) errors.push("planId_plan_mismatch");
    if (value.planHash !== plan?.planHash) errors.push("planHash_plan_mismatch");
    if (value.idempotencyKey !== plan?.idempotencyKey) errors.push("idempotencyKey_plan_mismatch");
    if (isTimestamp(plan?.expiresAt) && nowMs > Date.parse(plan.expiresAt)) errors.push("plan_expired");
    if (isTimestamp(value.confirmedAt) && isTimestamp(plan?.expiresAt)
        && Date.parse(value.confirmedAt) > Date.parse(plan.expiresAt)) {
      errors.push("confirmedAt_after_plan_expiresAt");
    }
    if (isTimestamp(value.confirmedAt) && isTimestamp(plan?.createdAt)
        && Date.parse(value.confirmedAt) < Date.parse(plan.createdAt)) {
      errors.push("confirmedAt_before_plan_createdAt");
    }
  }
  return result(errors);
}

export function validateEnginePrivateExtensionApplyReceipt(value, { plan } = {}) {
  const errors = [];
  if (!isPlainObject(value)) return result(["enginePrivateExtensionApplyReceipt_must_be_object"]);
  if (value.schemaVersion !== ENGINE_PRIVATE_EXTENSION_APPLY_RECEIPT_SCHEMA_VERSION) errors.push("schemaVersion_mismatch");
  rejectUnknownKeys(value, APPLY_RECEIPT_KEYS, "enginePrivateExtensionApplyReceipt", errors);
  requiredOpaqueId(value.operationId, "operationId", errors);
  requiredOpaqueId(value.planId, "planId", errors);
  requiredHash(value.planHash, "planHash", errors);
  if (!new Set(["activate", "rollback"]).has(value.action)) errors.push("action_invalid");
  requiredTimestamp(value.acceptedAt, "acceptedAt", errors);
  if (value.state !== "queued") errors.push("apply_receipt_state_must_be_queued");
  if (plan !== undefined) {
    if (value.planId !== plan?.planId) errors.push("planId_plan_mismatch");
    if (value.planHash !== plan?.planHash) errors.push("planHash_plan_mismatch");
    if (value.action !== plan?.action) errors.push("action_plan_mismatch");
    if (isTimestamp(value.acceptedAt) && isTimestamp(plan?.expiresAt)
        && Date.parse(value.acceptedAt) > Date.parse(plan.expiresAt)) {
      errors.push("acceptedAt_after_plan_expiresAt");
    }
  }
  return result(errors);
}

export function validateEnginePrivateExtensionOperation(value) {
  const errors = [];
  if (!isPlainObject(value)) return result(["enginePrivateExtensionOperation_must_be_object"]);
  if (value.schemaVersion !== ENGINE_PRIVATE_EXTENSION_OPERATION_SCHEMA_VERSION) errors.push("schemaVersion_mismatch");
  rejectUnknownKeys(value, OPERATION_KEYS, "enginePrivateExtensionOperation", errors);
  requiredOpaqueId(value.operationId, "operationId", errors);
  requiredOpaqueId(value.planId, "planId", errors);
  requiredHash(value.planHash, "planHash", errors);
  if (!new Set(["activate", "rollback"]).has(value.action)) errors.push("action_invalid");
  if (!new Set([...NON_TERMINAL_STATES, "active", "rolled-back", "rejected", "quarantined"]).has(value.state)) {
    errors.push("state_invalid");
  }
  if (!new Set([
    "pending",
    "committed",
    "automatically-rolled-back",
    "explicitly-rolled-back",
    "rejected",
    "quarantined",
  ]).has(value.outcome)) errors.push("outcome_invalid");
  if (!new Set([
    "queued",
    "prepare",
    "switch",
    "force-recreate",
    "acceptance",
    "rollback",
    "complete",
  ]).has(value.phase)) errors.push("phase_invalid");
  requiredGeneration(value.expectedCurrentGeneration, "expectedCurrentGeneration", errors);
  requiredGeneration(value.nextGeneration, "nextGeneration", errors);
  if (Number.isInteger(value.expectedCurrentGeneration)
      && value.nextGeneration !== value.expectedCurrentGeneration + 1) {
    errors.push("nextGeneration_must_increment_current_generation");
  }
  validateActivationState(value.targetState, "targetState", errors);
  validateActivationState(value.recoveryState, "recoveryState", errors);
  validateActivationState(value.effectiveState, "effectiveState", errors);
  if (sameValue(value.targetState, value.recoveryState)) errors.push("targetState_must_differ_from_recoveryState");
  validateRuntimeReport(value.runtime, errors);
  validateAcceptanceReport(value.acceptance, value.effectiveState, errors);
  requiredTimestamp(value.updatedAt, "updatedAt", errors);
  validateOperationOutcome(value, errors);
  return result(errors);
}

export function validateEnginePrivateExtensionStatus(value) {
  const errors = [];
  if (!isPlainObject(value)) return result(["enginePrivateExtensionStatus_must_be_object"]);
  if (value.schemaVersion !== ENGINE_PRIVATE_EXTENSION_STATUS_SCHEMA_VERSION) errors.push("schemaVersion_mismatch");
  rejectUnknownKeys(value, STATUS_KEYS, "enginePrivateExtensionStatus", errors);
  if (value.packageName !== ENGINE_PRIVATE_EXTENSION_PACKAGE_NAME) errors.push("packageName_mismatch");
  requiredGeneration(value.generation, "generation", errors);
  if (!new Set(["ready", "transitioning", "quarantined"]).has(value.health)) errors.push("health_invalid");
  validateActivationState(value.currentState, "currentState", errors);
  validateActivationState(value.previousState, "previousState", errors);
  if (value.activeOperationId !== undefined) requiredOpaqueId(value.activeOperationId, "activeOperationId", errors);
  validateRuntimeReport(value.runtime, errors);
  validateAcceptanceReport(value.acceptance, value.currentState, errors);
  requiredTimestamp(value.updatedAt, "updatedAt", errors);
  if (value.runtime?.generation !== value.generation) errors.push("runtime_generation_mismatch");
  if (value.health === "ready") {
    if (value.activeOperationId !== undefined) errors.push("ready_status_must_not_have_active_operation");
    if (value.acceptance?.state !== "accepted") errors.push("ready_status_requires_acceptance");
    if (value.errorCode !== undefined) errors.push("ready_status_must_not_have_errorCode");
  } else if (value.health === "transitioning") {
    if (value.activeOperationId === undefined) errors.push("transitioning_status_requires_active_operation");
  } else {
    requiredReason(value.errorCode, "errorCode", errors);
  }
  return result(errors);
}

function validatePlanRequestBody(value, now, errors) {
  if (!isPlainObject(value)) {
    errors.push("enginePrivateExtensionPlanRequest_must_be_object");
    return;
  }
  if (value.schemaVersion !== ENGINE_PRIVATE_EXTENSION_PLAN_REQUEST_SCHEMA_VERSION) errors.push("schemaVersion_mismatch");
  rejectUnknownKeys(value, PLAN_REQUEST_KEYS, "enginePrivateExtensionPlanRequest", errors);
  requiredOpaqueId(value.requestId, "requestId", errors);
  requiredOpaqueId(value.idempotencyKey, "idempotencyKey", errors);
  if (!new Set(["activate", "rollback"]).has(value.action)) errors.push("action_invalid");
  requiredTimestamp(value.requestedAt, "requestedAt", errors);
  requiredTimestamp(value.requestExpiresAt, "requestExpiresAt", errors);
  requiredGeneration(value.expectedCurrentGeneration, "expectedCurrentGeneration", errors);
  validateWindow(value.requestedAt, value.requestExpiresAt, normalizeNow(now, errors), errors, "request");
  validateRequestTarget(value.target, value.action, errors);
}

function validateRequestTarget(value, action, errors) {
  if (!isPlainObject(value)) {
    errors.push("target_must_be_object");
    return;
  }
  if (action === "activate") {
    validateActivationState(value, "target", errors);
    if (value.kind !== "release") errors.push("activate_target_must_be_release");
    return;
  }
  rejectUnknownKeys(value, PREVIOUS_STATE_TARGET_KEYS, "target", errors);
  if (value.kind !== "previous-state") errors.push("rollback_target_must_be_previous-state");
  if (value.packageName !== ENGINE_PRIVATE_EXTENSION_PACKAGE_NAME) errors.push("target.packageName_mismatch");
}

function validateActivationState(value, path, errors) {
  if (!isPlainObject(value)) {
    errors.push(path + "_must_be_object");
    return;
  }
  if (value.kind === "release") {
    rejectUnknownKeys(value, RELEASE_STATE_KEYS, path, errors);
    if (value.packageName !== ENGINE_PRIVATE_EXTENSION_PACKAGE_NAME) errors.push(path + ".packageName_mismatch");
    if (typeof value.releaseId !== "string" || !RELEASE_ID.test(value.releaseId)) {
      errors.push(path + ".releaseId_invalid");
    }
    if (typeof value.packageSha256 !== "string" || !SHA256.test(value.packageSha256)) {
      errors.push(path + ".packageSha256_invalid");
    } else if (typeof value.releaseId === "string"
        && RELEASE_ID.test(value.releaseId)
        && !value.releaseId.endsWith("-" + value.packageSha256.slice(0, 16))) {
      errors.push(path + ".releaseId_digest_mismatch");
    }
    return;
  }
  if (value.kind === "inactive-baseline") {
    rejectUnknownKeys(value, INACTIVE_STATE_KEYS, path, errors);
    if (value.packageName !== ENGINE_PRIVATE_EXTENSION_PACKAGE_NAME) errors.push(path + ".packageName_mismatch");
    if (value.baselineId !== ENGINE_PRIVATE_EXTENSION_INACTIVE_BASELINE) errors.push(path + ".baselineId_mismatch");
    return;
  }
  errors.push(path + ".kind_invalid");
}

function validateTransition(value, errors) {
  if (!isPlainObject(value)) {
    errors.push("transition_must_be_object");
    return;
  }
  rejectUnknownKeys(value, TRANSITION_KEYS, "transition", errors);
  const expected = {
    mountMode: "read-only",
    loaderMode: "community-package",
    loaderPath: "/home/node/.n8n/nodes/node_modules/n8n-nodes-ndc",
    loaderEnvironment: {
      N8N_COMMUNITY_PACKAGES_ENABLED: "true",
      N8N_COMMUNITY_PACKAGES_PREVENT_LOADING: "false",
      N8N_REINSTALL_MISSING_PACKAGES: "false",
    },
    quiesceMode: "block-deploy-run-and-drain-queue",
    stateSwitch: "atomic-current-previous",
    runtimeAction: "force-recreate",
    scope: "main-workers-webhooks",
    requireUniformGeneration: true,
    hotReload: false,
    preserveCredentials: true,
  };
  if (isPlainObject(value.loaderEnvironment)) {
    rejectUnknownKeys(value.loaderEnvironment, LOADER_ENVIRONMENT_KEYS, "transition.loaderEnvironment", errors);
  }
  if (!sameValue(value, expected)) errors.push("transition_policy_mismatch");
}

function validateAcceptanceSpec(value, targetState, errors) {
  if (!isPlainObject(value)) {
    errors.push("acceptance_must_be_object");
    return;
  }
  rejectUnknownKeys(value, ACCEPTANCE_SPEC_KEYS, "acceptance", errors);
  if (value.mode !== "exact") errors.push("acceptance.mode_must_be_exact");
  const expectedNodes = expectedTypes(targetState, ENGINE_PRIVATE_EXTENSION_NODE_TYPES);
  const expectedCredentials = expectedTypes(targetState, ENGINE_PRIVATE_EXTENSION_CREDENTIAL_TYPES);
  validateExactArray(value.nodeTypes, expectedNodes, "acceptance.nodeTypes", errors);
  validateExactArray(value.credentialSchemas, expectedCredentials, "acceptance.credentialSchemas", errors);
  if (value.requireUniformGeneration !== true) errors.push("acceptance.requireUniformGeneration_must_be_true");
}

function validateFailurePolicy(value, errors) {
  if (!isPlainObject(value)) {
    errors.push("failurePolicy_must_be_object");
    return;
  }
  rejectUnknownKeys(value, FAILURE_POLICY_KEYS, "failurePolicy", errors);
  const expected = {
    mode: "automatic-rollback",
    rollbackFailureOutcome: "quarantined",
    preserveImmutableRelease: true,
    preserveCredentials: true,
  };
  if (!sameValue(value, expected)) errors.push("failurePolicy_mismatch");
}

function validateRuntimeReport(value, errors) {
  if (!isPlainObject(value)) {
    errors.push("runtime_must_be_object");
    return;
  }
  rejectUnknownKeys(value, RUNTIME_KEYS, "runtime", errors);
  if (value.mode !== "force-recreate") errors.push("runtime.mode_must_be_force-recreate");
  requiredPositiveInteger(value.expectedInstances, "runtime.expectedInstances", errors);
  requiredNonnegativeInteger(value.readyInstances, "runtime.readyInstances", errors);
  if (Number.isInteger(value.expectedInstances) && Number.isInteger(value.readyInstances)
      && value.readyInstances > value.expectedInstances) errors.push("runtime.readyInstances_exceeds_expectedInstances");
  requiredGeneration(value.generation, "runtime.generation", errors);
}

function validateAcceptanceReport(value, effectiveState, errors) {
  if (!isPlainObject(value)) {
    errors.push("acceptance_must_be_object");
    return;
  }
  rejectUnknownKeys(value, ACCEPTANCE_REPORT_KEYS, "acceptance", errors);
  if (!new Set(["pending", "accepted", "rejected"]).has(value.state)) errors.push("acceptance.state_invalid");
  const expectedNodes = expectedTypes(effectiveState, ENGINE_PRIVATE_EXTENSION_NODE_TYPES);
  const expectedCredentials = expectedTypes(effectiveState, ENGINE_PRIVATE_EXTENSION_CREDENTIAL_TYPES);
  validateObservation(value.nodeTypes, expectedNodes, "acceptance.nodeTypes", value.state, errors);
  validateObservation(value.credentialSchemas, expectedCredentials, "acceptance.credentialSchemas", value.state, errors);
  if (typeof value.uniformGeneration !== "boolean") errors.push("acceptance.uniformGeneration_must_be_boolean");
  if (value.state === "accepted" && value.uniformGeneration !== true) {
    errors.push("accepted_runtime_requires_uniform_generation");
  }
}

function validateObservation(value, expected, path, state, errors) {
  if (!isPlainObject(value)) {
    errors.push(path + "_must_be_object");
    return;
  }
  rejectUnknownKeys(value, OBSERVATION_KEYS, path, errors);
  validateExactArray(value.expected, expected, path + ".expected", errors);
  if (!Array.isArray(value.observed) || value.observed.some((item) => typeof item !== "string")) {
    errors.push(path + ".observed_must_be_string_array");
    return;
  }
  if (new Set(value.observed).size !== value.observed.length) errors.push(path + ".observed_must_be_unique");
  if (state === "accepted" && !sameValue(value.observed, expected)) errors.push(path + ".observed_exact_set_required");
}

function validateOperationOutcome(value, errors) {
  if (NON_TERMINAL_STATES.has(value.state)) {
    if (value.outcome !== "pending") errors.push("nonterminal_operation_outcome_must_be_pending");
    return;
  }
  if (value.state === "rolled-back") {
    const expected = value.action === "rollback" ? "explicitly-rolled-back" : "automatically-rolled-back";
    if (value.outcome !== expected) errors.push("rolled_back_outcome_mismatch");
  } else if (TERMINAL_STATE_OUTCOMES[value.state] !== value.outcome) {
    errors.push("terminal_operation_outcome_mismatch");
  }
  if (value.state === "active" || value.state === "rolled-back") {
    if (value.acceptance?.state !== "accepted") errors.push("successful_terminal_state_requires_acceptance");
    if (value.runtime?.readyInstances !== value.runtime?.expectedInstances) {
      errors.push("successful_terminal_state_requires_all_instances_ready");
    }
    if (value.runtime?.generation !== value.nextGeneration) {
      errors.push("successful_terminal_state_runtime_generation_mismatch");
    }
    if (value.errorCode !== undefined && value.state === "active") errors.push("active_state_must_not_have_errorCode");
    if (value.state === "active" && value.action !== "activate") errors.push("active_state_action_mismatch");
    const expectedEffective = value.state === "active"
      ? value.targetState
      : value.action === "rollback" ? value.targetState : value.recoveryState;
    if (!sameValue(value.effectiveState, expectedEffective)) errors.push("effectiveState_terminal_mismatch");
    if (value.state === "rolled-back" && value.action === "activate") {
      requiredReason(value.errorCode, "errorCode", errors);
    }
    if (value.state === "rolled-back" && value.action === "rollback" && value.errorCode !== undefined) {
      errors.push("explicit_rollback_must_not_have_errorCode");
    }
  } else if (value.state === "rejected" || value.state === "quarantined") {
    requiredReason(value.errorCode, "errorCode", errors);
  }
}

function comparePlanToRequest(plan, request, errors) {
  const requestErrors = [];
  validatePlanRequestBody(request, plan.createdAt, requestErrors);
  if (requestErrors.length) errors.push("request_invalid_for_plan_comparison");
  if (plan.requestId !== request?.requestId) errors.push("requestId_request_mismatch");
  if (plan.idempotencyKey !== request?.idempotencyKey) errors.push("idempotencyKey_request_mismatch");
  if (plan.action !== request?.action) errors.push("action_request_mismatch");
  if (plan.expectedCurrentGeneration !== request?.expectedCurrentGeneration) {
    errors.push("expectedCurrentGeneration_request_mismatch");
  }
  if (request?.action === "activate" && !sameValue(plan.targetState, request?.target)) {
    errors.push("targetState_request_mismatch");
  }
  if (isTimestamp(request?.requestExpiresAt) && isTimestamp(plan.expiresAt)
      && Date.parse(plan.expiresAt) > Date.parse(request.requestExpiresAt)) {
    errors.push("plan_expiresAt_exceeds_request");
  }
}

function addAuthorizationErrors(operation, grantedCapabilities, errors) {
  errors.push(...authorizeEnginePrivateExtensionOperation(operation, grantedCapabilities).errors);
}

function validateWindow(start, end, nowMs, errors, label) {
  if (!isTimestamp(start) || !isTimestamp(end) || !Number.isFinite(nowMs)) return;
  const startMs = Date.parse(start);
  const endMs = Date.parse(end);
  if (endMs <= startMs) errors.push(label + "_expiresAt_must_be_after_start");
  if (endMs - startMs > MAX_REQUEST_LIFETIME_MS) errors.push(label + "_lifetime_exceeds_15_minutes");
  if (startMs > nowMs + MAX_CLOCK_SKEW_MS) errors.push(label + "_start_exceeds_clock_skew");
  if (endMs < nowMs) errors.push(label + "_expired");
}

function normalizeNow(value, errors) {
  const candidate = value === undefined ? Date.now() : value;
  const milliseconds = typeof candidate === "number" ? candidate : Date.parse(candidate);
  if (!Number.isFinite(milliseconds)) {
    errors.push("now_invalid");
    return Number.NaN;
  }
  return milliseconds;
}

function requiredGeneration(value, path, errors) {
  requiredNonnegativeInteger(value, path, errors);
}

function requiredPositiveInteger(value, path, errors) {
  if (!Number.isInteger(value) || value < 1) errors.push(path + "_must_be_positive_integer");
}

function requiredNonnegativeInteger(value, path, errors) {
  if (!Number.isInteger(value) || value < 0) errors.push(path + "_must_be_nonnegative_integer");
}

function requiredOpaqueId(value, path, errors) {
  if (typeof value !== "string" || !OPAQUE_ID.test(value)) errors.push(path + "_invalid");
}

function requiredReason(value, path, errors) {
  if (typeof value !== "string" || !REASON_CODE.test(value)) errors.push(path + "_invalid");
}

function requiredHash(value, path, errors) {
  if (typeof value !== "string" || !HASH.test(value)) errors.push(path + "_invalid");
}

function requiredTimestamp(value, path, errors) {
  if (!isTimestamp(value)) errors.push(path + "_invalid_timestamp");
}

function isTimestamp(value) {
  return typeof value === "string" && Number.isFinite(Date.parse(value));
}

function expectedTypes(state, releaseTypes) {
  return state?.kind === "release" ? [...releaseTypes] : [];
}

function validateExactArray(actual, expected, path, errors) {
  if (!Array.isArray(actual)) {
    errors.push(path + "_must_be_array");
    return;
  }
  if (!sameValue(actual, [...expected])) errors.push(path + "_mismatch");
}

function rejectUnknownKeys(value, allowedKeys, path, errors) {
  if (!isPlainObject(value)) return;
  for (const key of Object.keys(value)) {
    if (!allowedKeys.has(key)) errors.push(path + "." + key + "_not_allowed");
  }
}

function isPlainObject(value) {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

function sameValue(left, right) {
  return stableJson(left) === stableJson(right);
}

function stableJson(value) {
  if (Array.isArray(value)) return "[" + value.map(stableJson).join(",") + "]";
  if (isPlainObject(value)) {
    return "{" + Object.keys(value).sort().map((key) => JSON.stringify(key) + ":" + stableJson(value[key])).join(",") + "}";
  }
  return JSON.stringify(value);
}

function result(errors) {
  const unique = [...new Set(errors)];
  return Object.freeze({ ok: unique.length === 0, errors: Object.freeze(unique) });
}
