const crypto = require('crypto');
const { sha256 } = require('./hash');
const { Rng } = require('./rng');
const {
  BASE1_ALGORITHM_VERSION,
  DYNAMIC_PARAMETER_POOL,
  OPERATOR_POOL,
  evaluateBase1Step,
  expressionForAxis,
} = require('./base1_algorithm');

const EXPRESSION_TREE_VERIFIER_VERSION = 'mazex-expression-tree-verifier-1.0';

const DEFAULT_VERIFIER_OPTIONS = {
  mode: 'auto',
  full_domain_max_cells: 2500,
  minimum_sample_points: 160,
  random_point_count: 96,
  adversarial_point_count: 64,
  max_operator_units: 256,
  max_tree_nodes: 1600,
  max_tree_depth: 256,
  max_expression_chars: 220000,
};

const OPERATOR_BY_ID = new Map(OPERATOR_POOL.map((operator) => [operator.id, operator]));
const DYNAMIC_BY_ID = new Map(DYNAMIC_PARAMETER_POOL.map((parameter) => [parameter.id, parameter]));

function normalizeOptions(options = {}) {
  return {
    ...DEFAULT_VERIFIER_OPTIONS,
    ...options,
  };
}

function clampInt(value, min, max) {
  const number = Math.trunc(Number(value) || 0);
  return Math.max(min, Math.min(max, number));
}

function pointCategoryCounts(points) {
  return points.reduce((acc, point) => {
    acc[point.category] = (acc[point.category] || 0) + 1;
    return acc;
  }, {});
}

function hashRows(kind, metadata, rows, rowWriter) {
  const hash = crypto.createHash('sha256');
  hash.update(`${kind}:${JSON.stringify(metadata)}\n`, 'utf8');
  for (let index = 0; index < rows.length; index += 1) {
    hash.update(`${rowWriter(rows[index], index)}\n`, 'utf8');
  }
  return `0x${hash.digest('hex')}`;
}

function shouldUseFullDomain(step, options) {
  const size = Math.max(2, Math.trunc(Number(step?.grid_size || 20)));
  if (options.mode === 'full_domain') return true;
  if (options.mode === 'sample') return false;
  return size * size <= Number(options.full_domain_max_cells || 0);
}

function makeVerificationPointRows(step, options = {}) {
  const merged = normalizeOptions(options);
  const N = Math.max(2, Math.trunc(Number(step?.grid_size || 20)));

  if (shouldUseFullDomain(step, merged)) {
    const points = [];
    for (let y = 0; y < N; y += 1) {
      for (let x = 0; x < N; x += 1) {
        points.push({ x, y, category: 'full_domain' });
      }
    }
    return points;
  }

  const seed = sha256({
    type: 'mazex.expression_tree.verification_points',
    version: EXPRESSION_TREE_VERIFIER_VERSION,
    formula_hash: step.formula_hash,
    layer_no: step.layer_no,
    step_no: step.step_no,
    seed: step.seed,
  });
  const rng = new Rng(seed);
  const points = [];
  const seen = new Set();
  const push = (x, y, category) => {
    const px = clampInt(x, 0, N - 1);
    const py = clampInt(y, 0, N - 1);
    const key = `${px},${py}`;
    if (seen.has(key)) return;
    seen.add(key);
    points.push({ x: px, y: py, category });
  };

  const mid = Math.floor(N / 2);
  const last = N - 1;
  push(0, 0, 'corner');
  push(last, 0, 'corner');
  push(0, last, 'corner');
  push(last, last, 'corner');
  push(mid, mid, 'center');
  push(mid - 1, mid, 'center');
  push(mid, mid - 1, 'center');
  push(mid + 1, mid, 'center');
  push(mid, mid + 1, 'center');
  push(mid, 0, 'edge_midpoint');
  push(0, mid, 'edge_midpoint');
  push(last, mid, 'edge_midpoint');
  push(mid, last, 'edge_midpoint');

  for (let i = 1; i <= 12; i += 1) {
    const t = Math.floor((i * last) / 13);
    push(t, t, 'diagonal');
    push(t, last - t, 'anti_diagonal');
  }

  for (let i = 0; i < 16; i += 1) {
    const x = rng.int(0, last);
    push(x, i % 2 === 0 ? 1 : Math.max(0, last - 1), 'near_boundary');
    const y = rng.int(0, last);
    push(i % 2 === 0 ? 1 : Math.max(0, last - 1), y, 'near_boundary');
  }

  for (let i = 0; i < Number(merged.adversarial_point_count || 0); i += 1) {
    const selector = i % 8;
    if (selector === 0) push(i % N, (i * i) % N, 'adversarial_square');
    if (selector === 1) push((i * 7 + 1) % N, (i * 11 + 3) % N, 'adversarial_prime_stride');
    if (selector === 2) push((mid + i) % N, (mid - i + N) % N, 'adversarial_center_ring');
    if (selector === 3) push((i * 2) % N, (i * 2 + 1) % N, 'adversarial_parity');
    if (selector === 4) push(last - (i % N), i % N, 'adversarial_mirror');
    if (selector === 5) push((i * i + mid) % N, (i * 3 + mid) % N, 'adversarial_polynomial');
    if (selector === 6) push(rng.int(0, Math.min(last, 3)), rng.int(0, last), 'adversarial_low_edge');
    if (selector === 7) push(rng.int(Math.max(0, last - 3), last), rng.int(0, last), 'adversarial_high_edge');
  }

  for (let i = 0; i < Number(merged.random_point_count || 0); i += 1) {
    push(rng.int(0, last), rng.int(0, last), 'random');
  }

  const minimum = Math.min(N * N, Math.max(Number(merged.minimum_sample_points || 0), points.length));
  while (points.length < minimum) {
    push(rng.int(0, last), rng.int(0, last), 'random_fill');
  }

  return points;
}

function pointSetHash(points, metadata = {}) {
  return hashRows('mazex.expression_tree.point_set', metadata, points, (point) => `${point.x},${point.y},${point.category}`);
}

function makeContextForPoint(step, point, index, options = {}) {
  const N = Math.max(2, Math.trunc(Number(step?.grid_size || 20)));
  const seed = sha256({
    type: 'mazex.expression_tree.verification_context',
    version: EXPRESSION_TREE_VERIFIER_VERSION,
    formula_hash: step.formula_hash,
    layer_no: step.layer_no,
    step_no: step.step_no,
    step_seed: step.seed,
    context_seed: options.context_seed || 'default',
    index,
    point,
  });
  const rng = new Rng(seed);
  const elapsed = rng.int(0, 86400);
  const lastX = rng.int(0, N - 1);
  const lastY = rng.int(0, N - 1);
  const lastU = rng.int(0, N - 1);
  const lastV = rng.int(0, N - 1);
  const lastDx = point.x - lastX;
  const lastDy = point.y - lastY;
  const historySeed = sha256({ seed, kind: 'history' });
  const chainSeed = sha256({ seed, kind: 'chain' });

  return {
    click_count: index + 1 + rng.int(0, 127),
    user_click_count: index + 1 + rng.int(0, 127),
    global_click_count: index + 1 + rng.int(0, 4095),
    last_x: lastX,
    last_y: lastY,
    last_u: lastU,
    last_v: lastV,
    last_dx: lastDx,
    last_dy: lastDy,
    last_distance: Math.floor(Math.sqrt(lastDx * lastDx + lastDy * lastDy)),
    elapsed_seconds: elapsed,
    time_bucket: Math.floor(elapsed / 30) % 256,
    time_mod: elapsed % N,

    history_hash: historySeed,
    history_sum_x: rng.int(0, N * 8),
    history_sum_y: rng.int(0, N * 8),
    history_sum_u: rng.int(0, N * 8),
    history_sum_v: rng.int(0, N * 8),
    history_unique_count: rng.int(0, 1023),
    history_repeat_count: rng.int(0, 1023),
    history_path_length: rng.int(0, N * 16),

    previous_step_hash: sha256({ seed, kind: 'previous_step' }),
    previous_solution_hash: sha256({ seed, kind: 'previous_solution' }),
    previous_block_hash: sha256({ seed, kind: 'previous_block' }),
    block_height: Math.max(0, Number(step.layer_no || 1) * 1000 + Number(step.step_no || 1) + index),
    chain_seed: chainSeed,
    difficulty_epoch: Math.floor(Number(step.layer_no || 1) / 100),
    hash_click_state: sha256({ seed, kind: 'click_state' }),
    hash_history_state: sha256({ seed, kind: 'history_state' }),
    hash_chain_state: sha256({ seed, kind: 'chain_state' }),
    nonlinear_jump: sha256({ seed, kind: 'nonlinear_jump' }),

    half_life_bucket: Math.floor(elapsed / 90) % 1024,
    decay_value: rng.int(0, N * 4),
    cooldown_value: rng.int(0, N * 4),
    age_factor: rng.int(0, N * 4),
    freshness_factor: rng.int(0, N * 4),
    cycle_phase: rng.int(0, 359),
    cycle_index: rng.int(0, 1023),
    pulse_state: rng.int(0, 1),
    wave_bucket: rng.int(0, N * 4),
    global_sample_count: rng.int(0, 1_000_000),
    global_submit_count: rng.int(0, 1_000_000),
    unique_sampler_count: rng.int(0, 100_000),
    solver_count: rng.int(0, 100_000),
    network_activity_bucket: rng.int(0, N * 4),
    multi_state_hash: sha256({ seed, kind: 'multi_state' }),
  };
}

function makeVerificationContexts(step, points, options = {}) {
  return points.map((point, index) => makeContextForPoint(step, point, index, options));
}

function contextSetHash(contexts, metadata = {}) {
  return sha256({
    type: 'mazex.expression_tree.context_set',
    version: EXPRESSION_TREE_VERIFIER_VERSION,
    metadata,
    contexts,
  });
}

function treeStats(node, depth = 0) {
  if (!node || typeof node !== 'object') return { nodes: 0, operators: 0, max_depth: depth };
  const isOperator = node.type === 'op';
  const children = Array.isArray(node.children) ? node.children : [];
  return children.reduce(
    (acc, child) => {
      const next = treeStats(child, depth + 1);
      return {
        nodes: acc.nodes + next.nodes,
        operators: acc.operators + next.operators,
        max_depth: Math.max(acc.max_depth, next.max_depth),
      };
    },
    { nodes: 1, operators: isOperator ? 1 : 0, max_depth: depth },
  );
}

function collectTreeUnits(node, units = []) {
  if (!node || typeof node !== 'object') return units;
  if (node.type === 'op') units.push(node.unit);
  for (const child of node.children || []) collectTreeUnits(child, units);
  return units;
}

function validateTreeNode(node, errors, path, tier) {
  if (!node || typeof node !== 'object') {
    errors.push(`${path}: node must be an object`);
    return;
  }

  if (node.type === 'var') {
    if (!['x', 'y', 'N'].includes(node.name)) errors.push(`${path}: unsupported var ${node.name}`);
    return;
  }

  if (node.type === 'const') {
    if (!Number.isFinite(Number(node.value))) errors.push(`${path}: const must be finite`);
    return;
  }

  if (node.type !== 'op') {
    errors.push(`${path}: unsupported node type ${node.type}`);
    return;
  }

  if (!Array.isArray(node.children) || node.children.length !== 2) {
    errors.push(`${path}: operator node must have exactly two children`);
    return;
  }

  const unit = node.unit || {};
  const operator = OPERATOR_BY_ID.get(unit.operator_id);
  if (!operator) {
    errors.push(`${path}: unknown operator ${unit.operator_id}`);
  } else {
    if (operator.unlock_tier > tier) {
      errors.push(`${path}: operator ${operator.id} is locked for tier ${tier}`);
    }
    const dynamicId = unit.dynamic_param?.id;
    if (operator.requires_dynamic && !dynamicId) {
      errors.push(`${path}: operator ${operator.id} requires a dynamic parameter`);
    }
    if (dynamicId) {
      const dynamic = DYNAMIC_BY_ID.get(dynamicId);
      if (!dynamic) errors.push(`${path}: unknown dynamic parameter ${dynamicId}`);
      if (dynamic && dynamic.unlock_tier > tier) {
        errors.push(`${path}: dynamic parameter ${dynamicId} is locked for tier ${tier}`);
      }
      if (operator.dynamic_params && !operator.dynamic_params.includes(dynamicId)) {
        errors.push(`${path}: dynamic parameter ${dynamicId} is not allowed by ${operator.id}`);
      }
      if (!operator.dynamic_params && !operator.requires_dynamic) {
        errors.push(`${path}: operator ${operator.id} does not accept dynamic parameters`);
      }
    }
  }

  const staticValue = Number(unit.static_param?.value);
  if (!Number.isFinite(staticValue)) errors.push(`${path}: static parameter must be finite`);
  if (staticValue === 0) errors.push(`${path}: static parameter cannot be zero`);

  validateTreeNode(node.children[0], errors, `${path}.left`, tier);
  validateTreeNode(node.children[1], errors, `${path}.right`, tier);
}

function normalizeCandidateStep(officialStep, candidateStep) {
  const candidate = candidateStep || {};
  const trees = candidate.trees || {
    u: candidate.u_tree,
    v: candidate.v_tree,
  };
  return {
    ...officialStep,
    ...candidate,
    algorithm: candidate.algorithm || officialStep.algorithm || BASE1_ALGORITHM_VERSION,
    layer_no: officialStep.layer_no,
    step_no: officialStep.step_no,
    step_count: officialStep.step_count,
    growth_tier: officialStep.growth_tier,
    growth_units: officialStep.growth_units,
    grid_size: officialStep.grid_size,
    previous_hash: officialStep.previous_hash,
    seed: officialStep.seed,
    formula_hash: candidate.formula_hash || officialStep.formula_hash,
    trees,
    operator_units: candidate.operator_units || collectTreeUnits(trees?.u).concat(collectTreeUnits(trees?.v)),
  };
}

function measureStepBudget(step) {
  const uStats = treeStats(step?.trees?.u);
  const vStats = treeStats(step?.trees?.v);
  let expressionChars = 0;
  try {
    expressionChars = expressionForAxis(step, 'u').length + expressionForAxis(step, 'v').length;
  } catch (error) {
    expressionChars = Number.POSITIVE_INFINITY;
  }
  const units = collectTreeUnits(step?.trees?.u).concat(collectTreeUnits(step?.trees?.v));
  return {
    operator_units: units.length,
    tree_nodes: uStats.nodes + vStats.nodes,
    max_tree_depth: Math.max(uStats.max_depth, vStats.max_depth),
    expression_chars: expressionChars,
    u_tree_nodes: uStats.nodes,
    v_tree_nodes: vStats.nodes,
    u_operator_units: uStats.operators,
    v_operator_units: vStats.operators,
  };
}

function validateCandidateBudget(step, options = {}) {
  const merged = normalizeOptions(options);
  const metrics = measureStepBudget(step);
  const errors = [];
  if (metrics.operator_units > merged.max_operator_units) {
    errors.push(`operator_units ${metrics.operator_units} > ${merged.max_operator_units}`);
  }
  if (metrics.tree_nodes > merged.max_tree_nodes) {
    errors.push(`tree_nodes ${metrics.tree_nodes} > ${merged.max_tree_nodes}`);
  }
  if (metrics.max_tree_depth > merged.max_tree_depth) {
    errors.push(`max_tree_depth ${metrics.max_tree_depth} > ${merged.max_tree_depth}`);
  }
  if (metrics.expression_chars > merged.max_expression_chars) {
    errors.push(`expression_chars ${metrics.expression_chars} > ${merged.max_expression_chars}`);
  }
  return {
    ok: errors.length === 0,
    metrics,
    errors,
    limits: {
      max_operator_units: merged.max_operator_units,
      max_tree_nodes: merged.max_tree_nodes,
      max_tree_depth: merged.max_tree_depth,
      max_expression_chars: merged.max_expression_chars,
    },
  };
}

function validateCandidateTreeSchema(step) {
  const errors = [];
  if (!step?.trees?.u) errors.push('missing trees.u');
  if (!step?.trees?.v) errors.push('missing trees.v');
  if (step?.trees?.u) validateTreeNode(step.trees.u, errors, 'trees.u', Number(step.growth_tier || 0));
  if (step?.trees?.v) validateTreeNode(step.trees.v, errors, 'trees.v', Number(step.growth_tier || 0));
  return {
    ok: errors.length === 0,
    errors,
  };
}

function buildPointContextSet(step, options = {}) {
  const merged = normalizeOptions(options);
  const points = makeVerificationPointRows(step, merged);
  const mode = shouldUseFullDomain(step, merged) ? 'full_domain' : 'sample';
  const metadata = {
    verifier_version: EXPRESSION_TREE_VERIFIER_VERSION,
    algorithm: step.algorithm,
    layer_no: step.layer_no,
    step_no: step.step_no,
    grid_size: step.grid_size,
    mode,
    formula_hash: step.formula_hash,
  };
  const contexts = makeVerificationContexts(step, points, merged);
  return {
    mode,
    total: points.length,
    categories: pointCategoryCounts(points),
    points,
    contexts,
    point_set_hash: pointSetHash(points, metadata),
    context_set_hash: contextSetHash(contexts, metadata),
    metadata,
  };
}

function behaviorRows(step, pointContextSet) {
  return pointContextSet.points.map((point, index) => {
    const context = pointContextSet.contexts[index];
    const output = evaluateBase1Step(step, point.x, point.y, context);
    return {
      x: point.x,
      y: point.y,
      category: point.category,
      context_hash: sha256(context),
      u: output.u,
      v: output.v,
    };
  });
}

function behaviorHashFromRows(rows, metadata = {}) {
  return hashRows(
    'mazex.expression_tree.behavior',
    metadata,
    rows,
    (row) => `${row.x},${row.y},${row.category},${row.context_hash}->${row.u},${row.v}`,
  );
}

function behaviorHashForStep(step, pointContextSet) {
  const metadata = {
    verifier_version: EXPRESSION_TREE_VERIFIER_VERSION,
    algorithm: step.algorithm,
    layer_no: step.layer_no,
    step_no: step.step_no,
    grid_size: step.grid_size,
    point_set_hash: pointContextSet.point_set_hash,
    context_set_hash: pointContextSet.context_set_hash,
  };
  const rows = behaviorRows(step, pointContextSet);
  return {
    behavior_hash: behaviorHashFromRows(rows, metadata),
    rows,
  };
}

function answerHash(candidateStep) {
  return sha256({
    algorithm: candidateStep.algorithm || BASE1_ALGORITHM_VERSION,
    trees: candidateStep.trees,
  });
}

function validationId({ officialStep, candidateAnswerHash, pointContextSet, candidateBehaviorHash }) {
  return sha256({
    type: 'mazex.expression_tree.validation',
    version: EXPRESSION_TREE_VERIFIER_VERSION,
    layer_no: officialStep.layer_no,
    step_no: officialStep.step_no,
    official_formula_hash: officialStep.formula_hash,
    answer_hash: candidateAnswerHash,
    point_set_hash: pointContextSet.point_set_hash,
    context_set_hash: pointContextSet.context_set_hash,
    candidate_behavior_hash: candidateBehaviorHash,
  });
}

function baseResult(officialStep, candidateStep) {
  return {
    ok: false,
    accepted: false,
    stage: 'init',
    verifier_version: EXPRESSION_TREE_VERIFIER_VERSION,
    algorithm: officialStep.algorithm || BASE1_ALGORITHM_VERSION,
    layer_no: officialStep.layer_no,
    step_no: officialStep.step_no,
    grid_size: officialStep.grid_size,
    answer_hash: answerHash(candidateStep),
    stages: [],
  };
}

function pushStage(result, name, ok, details = {}) {
  result.stages.push({ name, ok, ...details });
}

function fail(result, stage, code, message, extra = {}) {
  result.ok = false;
  result.accepted = false;
  result.stage = stage;
  result.code = code;
  result.message = message;
  result.failure = { stage, code, message, ...extra };
  return result;
}

function firstMismatch(officialRows, candidateRows) {
  const total = Math.min(officialRows.length, candidateRows.length);
  for (let index = 0; index < total; index += 1) {
    const expected = officialRows[index];
    const actual = candidateRows[index];
    if (expected.u !== actual.u || expected.v !== actual.v) {
      return {
        index,
        point: {
          x: expected.x,
          y: expected.y,
          category: expected.category,
          context_hash: expected.context_hash,
        },
        expected: { u: expected.u, v: expected.v },
        actual: { u: actual.u, v: actual.v },
      };
    }
  }
  if (officialRows.length !== candidateRows.length) {
    return {
      index: total,
      point: null,
      expected: { rows: officialRows.length },
      actual: { rows: candidateRows.length },
    };
  }
  return null;
}

function verifyCandidateStep({ officialStep, candidateStep, options = {} }) {
  const runtimeCandidate = normalizeCandidateStep(officialStep, candidateStep);
  const result = baseResult(officialStep, runtimeCandidate);

  const budget = validateCandidateBudget(runtimeCandidate, options);
  result.budget = budget;
  pushStage(result, 'budget', budget.ok, { metrics: budget.metrics, errors: budget.errors });
  if (!budget.ok) {
    return fail(result, 'budget', 'ANSWER_BUDGET_REJECTED', 'Candidate answer exceeds verifier budget', {
      errors: budget.errors,
    });
  }

  const schema = validateCandidateTreeSchema(runtimeCandidate);
  result.schema = schema;
  pushStage(result, 'tree_schema', schema.ok, { errors: schema.errors });
  if (!schema.ok) {
    return fail(result, 'tree_schema', 'ANSWER_TREE_REJECTED', 'Candidate tree schema is invalid', {
      errors: schema.errors,
    });
  }

  const pointContextSet = buildPointContextSet(officialStep, options);
  result.point_set = {
    mode: pointContextSet.mode,
    total: pointContextSet.total,
    categories: pointContextSet.categories,
    hash: pointContextSet.point_set_hash,
  };
  result.context_set = {
    hash: pointContextSet.context_set_hash,
  };
  pushStage(result, 'point_context', true, {
    mode: pointContextSet.mode,
    total: pointContextSet.total,
    point_set_hash: pointContextSet.point_set_hash,
    context_set_hash: pointContextSet.context_set_hash,
  });

  let officialBehavior;
  let candidateBehavior;
  try {
    officialBehavior = behaviorHashForStep(officialStep, pointContextSet);
  } catch (error) {
    pushStage(result, 'behavior', false);
    return fail(result, 'behavior', 'OFFICIAL_EVALUATION_ERROR', error.message || 'Official evaluation failed');
  }

  try {
    candidateBehavior = behaviorHashForStep(runtimeCandidate, pointContextSet);
  } catch (error) {
    pushStage(result, 'behavior', false);
    return fail(result, 'behavior', 'CANDIDATE_EVALUATION_ERROR', error.message || 'Candidate evaluation failed');
  }

  const mismatch = firstMismatch(officialBehavior.rows, candidateBehavior.rows);
  result.behavior = {
    official_hash: officialBehavior.behavior_hash,
    candidate_hash: candidateBehavior.behavior_hash,
    checked_points: pointContextSet.total,
    mismatch,
  };
  result.behavior_hash = officialBehavior.behavior_hash;

  if (mismatch) {
    pushStage(result, 'behavior', false, {
      checked_points: pointContextSet.total,
      failed_index: mismatch.index,
    });
    return fail(result, 'behavior', 'BEHAVIOR_MISMATCH', 'Candidate behavior does not match official behavior', {
      mismatch,
    });
  }

  result.validation_id = validationId({
    officialStep,
    candidateAnswerHash: result.answer_hash,
    pointContextSet,
    candidateBehaviorHash: candidateBehavior.behavior_hash,
  });
  pushStage(result, 'behavior', true, {
    checked_points: pointContextSet.total,
    behavior_hash: officialBehavior.behavior_hash,
  });
  pushStage(result, 'finality', true, {
    validation_id: result.validation_id,
    behavior_hash: officialBehavior.behavior_hash,
  });

  return {
    ...result,
    ok: true,
    accepted: true,
    stage: 'accepted',
    code: 'ACCEPTED',
    message: 'Candidate accepted by expression-tree verifier',
  };
}

function verifyOfficialStep(step, options = {}) {
  const candidate = {
    algorithm: step.algorithm,
    trees: step.trees,
    operator_units: step.operator_units,
  };
  return verifyCandidateStep({
    officialStep: step,
    candidateStep: candidate,
    options,
  });
}

function verifierInfo(step, options = {}) {
  const pointContextSet = buildPointContextSet(step, options);
  return {
    verifier_version: EXPRESSION_TREE_VERIFIER_VERSION,
    algorithm: BASE1_ALGORITHM_VERSION,
    stages: ['budget', 'tree_schema', 'point_context', 'behavior', 'finality'],
    point_set: {
      mode: pointContextSet.mode,
      total: pointContextSet.total,
      categories: pointContextSet.categories,
      hash: pointContextSet.point_set_hash,
    },
    context_set: {
      hash: pointContextSet.context_set_hash,
    },
    budget_limits: validateCandidateBudget(step, options).limits,
  };
}

module.exports = {
  EXPRESSION_TREE_VERIFIER_VERSION,
  DEFAULT_VERIFIER_OPTIONS,
  makeVerificationPointRows,
  makeVerificationContexts,
  pointCategoryCounts,
  pointSetHash,
  contextSetHash,
  measureStepBudget,
  validateCandidateBudget,
  validateCandidateTreeSchema,
  buildPointContextSet,
  behaviorHashForStep,
  verifyCandidateStep,
  verifyOfficialStep,
  verifierInfo,
};
