const fs = require('fs');
const path = require('path');
const { sha256 } = require('./hash');
const { Rng } = require('./rng');
const {
  DIMENSION_KEYS,
  assetDimensionProfile,
  buildLayerDimensionProfile,
  buildStepDimensionProfile,
  computeDifficultyProfile,
  dimensionMatchScore,
  gridSizeForLayer,
  topDimensions,
  weightedDimensionScore,
} = require('./difficulty');
const {
  estimateNetFormulaScore,
  selectStepFormulaCount,
  selectStepFormulaQuality,
} = require('./formula_selectors');
const { selectDynamicParametersForStep } = require('./dynamic_parameter_extractors');
const { buildParameterPlan } = require('./parameter_selectors');
const { selectCombiner } = require('./combiner_selectors');
const { buildOutputPipelinePlan, finalStepScore } = require('./output_pipeline');

const ROOT = path.resolve(__dirname, '..', '..');

function readJson(relativePath) {
  return JSON.parse(fs.readFileSync(path.join(ROOT, relativePath), 'utf8'));
}

function clamp(value, min, max) {
  return Math.max(min, Math.min(max, value));
}

function unique(values) {
  return Array.from(new Set(values.filter(Boolean)));
}

function getStepCount(layerNo, rule) {
  return Math.min(rule.base_steps + (layerNo - 1) * rule.increase_per_layer, rule.max_steps);
}

function getLayerRange(layerNo) {
  let min = 1;
  if (layerNo > 1500) min = 8;
  else if (layerNo > 800) min = 7;
  else if (layerNo > 300) min = 6;
  else if (layerNo > 100) min = 5;
  else if (layerNo > 50) min = 4;
  else if (layerNo > 20) min = 3;
  else if (layerNo > 5) min = 2;
  const max = clamp(min + 3, 4, 10);
  return { min, max };
}

function getBand(progress) {
  if (progress < 0.2) return 'warmup';
  if (progress < 0.7) return 'main';
  if (progress < 0.9) return 'advanced';
  return 'boss';
}

function getTargetDifficulty(layerMin, layerMax, band, rng, bossBonus) {
  if (band === 'warmup') {
    const high = Math.min(layerMin + 2, layerMax);
    return clamp(rng.int(layerMin, high) + rng.pick([-1, 0, 1]), layerMin, high);
  }
  if (band === 'main') {
    const low = Math.min(layerMin + 2, layerMax);
    const high = Math.min(layerMin + 4, layerMax);
    return clamp(rng.int(low, high) + rng.pick([-1, 0, 1]), low, high);
  }
  if (band === 'advanced') {
    const low = Math.max(layerMin, layerMax - 2);
    const high = Math.max(layerMin, layerMax - 1);
    return clamp(rng.int(low, high) + rng.pick([-1, 0, 1]), low, high);
  }
  return clamp(layerMax + bossBonus, 1, 10);
}

const HARD_STRUCTURES = [
  'S04_POLYNOMIAL',
  'S05_MOD_RESIDUE',
  'S06_PIECEWISE_GATE',
  'S07_DISTANCE_METRIC',
  'S08_BUCKET_FLOOR',
  'S09_LOOKUP_PERMUTATION',
  'S10_PERIODIC_WAVE',
  'S11_DECAY_TIME',
  'S12_GRAPH_RULE',
  'S13_BOARD_RULE',
  'S14_ENCODING_HASH',
  'S15_PROBABILITY_SEEDED',
  'S16_STATE_MEMORY',
  'S17_FSM_DYNAMIC',
  'S18_RECURRENCE_SEQUENCE',
  'S19_CONVOLUTION_LOCAL',
  'S20_OPTIMIZATION_OBJECTIVE',
  'S21_LANGUAGE_SYMBOL',
  'S22_BIO_REACTION_GROWTH',
  'S23_ECON_GAME_SYSTEM',
  'S24_COMPOSITE_BOSS',
  'S25_CHAIN_STATE_DEPENDENT',
  'S26_SPECIAL_CONSTANT_FIELD',
  'S27_PRIME_ARITHMETIC',
  'S28_SPECTRAL_TRANSFORM',
  'S29_GROUP_SYMMETRY',
  'S30_COMPLEX_ANALYTIC',
  'S31_TENSOR_GEOMETRY',
  'S32_VARIATIONAL_HAMILTONIAN',
  'S33_FIELD_EQUATION',
  'S34_QUANTUM_OPERATOR',
  'S35_FLUID_NAVIER_STOKES',
  'S36_STATISTICAL_MECHANICS',
  'S37_GAUGE_LATTICE_FIELD',
];

const NONLINEAR_STRUCTURES = [
  'S04_POLYNOMIAL',
  'S06_PIECEWISE_GATE',
  'S08_BUCKET_FLOOR',
  'S10_PERIODIC_WAVE',
  'S11_DECAY_TIME',
  'S15_PROBABILITY_SEEDED',
  'S16_STATE_MEMORY',
  'S17_FSM_DYNAMIC',
  'S18_RECURRENCE_SEQUENCE',
  'S20_OPTIMIZATION_OBJECTIVE',
  'S22_BIO_REACTION_GROWTH',
  'S27_PRIME_ARITHMETIC',
  'S28_SPECTRAL_TRANSFORM',
  'S30_COMPLEX_ANALYTIC',
  'S31_TENSOR_GEOMETRY',
  'S32_VARIATIONAL_HAMILTONIAN',
  'S33_FIELD_EQUATION',
  'S34_QUANTUM_OPERATOR',
  'S35_FLUID_NAVIER_STOKES',
  'S36_STATISTICAL_MECHANICS',
  'S37_GAUGE_LATTICE_FIELD',
];

function buildLayerProfile({ layerNo, stepCount, gridSize, difficultyModel = {} }) {
  let stage = 'foundation';
  if (layerNo > 1500) stage = 'infinite_composition';
  else if (layerNo > 800) stage = 'advanced_composition';
  else if (layerNo > 300) stage = 'multi_composition';
  else if (layerNo > 100) stage = 'structured_composition';
  const dimensionProfile = buildLayerDimensionProfile({ layerNo, gridSize });

  const profiles = {
    foundation: {
      min_composition_by_band: { warmup: 1, main: 1, advanced: 1, boss: 2 },
      max_composition_by_band: { warmup: 1, main: 1, advanced: 2, boss: 2 },
      min_solver_complexity_by_band: { warmup: 0, main: 0, advanced: 500, boss: 900 },
      required_hard_structures_by_band: { warmup: 0, main: 0, advanced: 1, boss: 1 },
      banned_single_structures: [],
    },
    structured_composition: {
      min_composition_by_band: { warmup: 1, main: 2, advanced: 2, boss: 3 },
      max_composition_by_band: { warmup: 2, main: 2, advanced: 3, boss: 3 },
      min_solver_complexity_by_band: { warmup: 900, main: 1500, advanced: 2200, boss: 3200 },
      required_hard_structures_by_band: { warmup: 1, main: 1, advanced: 2, boss: 2 },
      banned_single_structures: ['S01_IDENTITY_SHIFT', 'S02_LINEAR_AFFINE', 'S03_MATRIX_TRANSFORM'],
    },
    multi_composition: {
      min_composition_by_band: { warmup: 2, main: 2, advanced: 3, boss: 4 },
      max_composition_by_band: { warmup: 2, main: 3, advanced: 4, boss: 4 },
      min_solver_complexity_by_band: { warmup: 1800, main: 2600, advanced: 3800, boss: 5200 },
      required_hard_structures_by_band: { warmup: 1, main: 2, advanced: 2, boss: 3 },
      banned_single_structures: ['S01_IDENTITY_SHIFT', 'S02_LINEAR_AFFINE', 'S03_MATRIX_TRANSFORM'],
    },
    advanced_composition: {
      min_composition_by_band: { warmup: 2, main: 3, advanced: 4, boss: 5 },
      max_composition_by_band: { warmup: 3, main: 4, advanced: 5, boss: 5 },
      min_solver_complexity_by_band: { warmup: 2800, main: 4200, advanced: 6200, boss: 8200 },
      required_hard_structures_by_band: { warmup: 2, main: 2, advanced: 3, boss: 4 },
      banned_single_structures: ['S01_IDENTITY_SHIFT', 'S02_LINEAR_AFFINE', 'S03_MATRIX_TRANSFORM'],
    },
    infinite_composition: {
      min_composition_by_band: { warmup: 3, main: 4, advanced: 5, boss: 5 },
      max_composition_by_band: { warmup: 4, main: 5, advanced: 5, boss: 5 },
      min_solver_complexity_by_band: { warmup: 4600, main: 6800, advanced: 9000, boss: 12000 },
      required_hard_structures_by_band: { warmup: 2, main: 3, advanced: 4, boss: 4 },
      banned_single_structures: ['S01_IDENTITY_SHIFT', 'S02_LINEAR_AFFINE', 'S03_MATRIX_TRANSFORM'],
    },
  };

  return {
    layer_no: layerNo,
    stage,
    step_count: stepCount,
    grid_size: gridSize,
    dimension_profile: dimensionProfile,
    dimension_score: weightedDimensionScore(dimensionProfile, difficultyModel),
    dominant_dimensions: topDimensions(dimensionProfile),
    allowed_structures: null,
    required_structure_pool: HARD_STRUCTURES,
    nonlinear_structure_pool: NONLINEAR_STRUCTURES,
    ...profiles[stage],
  };
}

function hardStructureCountForDimensions({ dimensionProfile, compositionCount, stageRequired }) {
  const hardSignal = Math.max(
    dimensionProfile.discrete_logic,
    dimensionProfile.number_theory,
    dimensionProfile.branching_piecewise,
    dimensionProfile.pseudo_3d_projection,
    dimensionProfile.time_dependency,
    dimensionProfile.semantic_abstraction,
  );
  return clamp(Math.max(stageRequired, Math.floor(hardSignal / 3)), 0, compositionCount);
}

function buildEffectiveDimensionProfile({ targetProfile, asset, gridSize }) {
  const assetProfile = assetDimensionProfile(asset, gridSize);
  const protocolDimensions = new Set([
    'layer_growth',
    'step_progress',
    'input_space',
    'verification_strength',
    'answer_budget_pressure',
  ]);
  return Object.fromEntries(
    DIMENSION_KEYS.map((key) => {
      const target = Number(targetProfile?.[key] || 0);
      if (protocolDimensions.has(key)) return [key, clamp(Math.round(target), 0, 10)];
      return [key, clamp(Math.round(Math.min(target, Number(assetProfile[key] || 0))), 0, 10)];
    }),
  );
}

function buildStepProfile({ layerProfile, stepNo, stepCount, band, targetDifficulty, rng, difficultyModel = {} }) {
  const dimensionProfile = buildStepDimensionProfile({
    layerDimensions: layerProfile.dimension_profile,
    stepNo,
    stepCount,
    band,
    targetDifficulty,
    rng,
  });
  const formulaCountDecision = selectStepFormulaCount({
    layerNo: layerProfile.layer_no,
    stepNo,
    stepCount,
    band,
    rng,
  });
  const compositionCount = formulaCountDecision.formula_count;
  const formulaQualityDecision = selectStepFormulaQuality({
    layerNo: layerProfile.layer_no,
    stepNo,
    stepCount,
    band,
    formulaCount: compositionCount,
    targetDifficulty,
    rng,
  });
  const dimensionScore = weightedDimensionScore(dimensionProfile, difficultyModel);
  const stageSolverTarget = layerProfile.min_solver_complexity_by_band[band] || 0;
  const targetSolverComplexity = Math.max(stageSolverTarget, Math.round(dimensionScore * 4.8));
  const stageRequiredHard = layerProfile.required_hard_structures_by_band[band] || 0;
  return {
    step_no: stepNo,
    progress_band: band,
    target_difficulty: targetDifficulty,
    composition_count: compositionCount,
    formula_count: compositionCount,
    formula_count_decision: formulaCountDecision,
    formula_quality_decision: formulaQualityDecision,
    net_formula_score_target: formulaQualityDecision.net_formula_score_target,
    net_formula_score_min: formulaQualityDecision.net_formula_score_min,
    net_formula_score_max: formulaQualityDecision.net_formula_score_max,
    average_net_formula_score_target: formulaQualityDecision.average_net_formula_score_target,
    target_dimension_profile: dimensionProfile,
    dimension_profile: dimensionProfile,
    dimension_score: dimensionScore,
    dominant_dimensions: topDimensions(dimensionProfile),
    target_solver_complexity: targetSolverComplexity,
    required_hard_structures: hardStructureCountForDimensions({
      dimensionProfile,
      compositionCount,
      stageRequired: stageRequiredHard,
    }),
    banned_single_structures: layerProfile.banned_single_structures,
  };
}

function assetStructures(asset) {
  return unique([asset.primary_structure, ...(asset.secondary_structure || [])]);
}

function estimateAssetSolverComplexity(asset) {
  const structures = assetStructures(asset);
  const hardCount = structures.filter((structure) => HARD_STRUCTURES.includes(structure)).length;
  const nonlinearCount = structures.filter((structure) => NONLINEAR_STRUCTURES.includes(structure)).length;
  const variableCount = (asset.variables || []).filter((name) => !['x', 'y', 'N'].includes(name)).length;
  return 120 + hardCount * 320 + nonlinearCount * 380 + variableCount * 55 + Number(asset.difficulty_max || 1) * 60;
}

function weightedChoice(items, weights, rng) {
  const total = items.reduce((sum, item) => sum + Number(weights[item.primary_structure] || 1), 0);
  let cursor = rng.next() * total;
  for (const item of items) {
    cursor -= Number(weights[item.primary_structure] || 1);
    if (cursor <= 0) return item;
  }
  return items[items.length - 1];
}

function weightedComplexChoice(items, rng) {
  const total = items.reduce((sum, item) => sum + Math.max(1, estimateAssetSolverComplexity(item)), 0);
  let cursor = rng.next() * total;
  for (const item of items) {
    cursor -= Math.max(1, estimateAssetSolverComplexity(item));
    if (cursor <= 0) return item;
  }
  return items[items.length - 1];
}

function dimensionAwareChoice(items, rng, targetDimensions, difficultyModel, gridSize) {
  const weighted = items.map((item) => {
    const assetProfile = assetDimensionProfile(item, gridSize);
    const match = dimensionMatchScore(assetProfile, targetDimensions, difficultyModel);
    const complexity = Math.max(1, estimateAssetSolverComplexity(item));
    return { item, weight: complexity * (1 + match / 700) };
  });
  const total = weighted.reduce((sum, row) => sum + row.weight, 0);
  let cursor = rng.next() * total;
  for (const row of weighted) {
    cursor -= row.weight;
    if (cursor <= 0) return row.item;
  }
  return weighted[weighted.length - 1]?.item || items[items.length - 1];
}

function qualityAwareChoice(items, rng, { targetDimensions, difficultyModel, gridSize, targetNetScore }) {
  const safeTarget = Math.max(50, Number(targetNetScore || 50));
  const ranked = items.map((item) => {
    const assetProfile = assetDimensionProfile(item, gridSize);
    const match = dimensionMatchScore(assetProfile, targetDimensions, difficultyModel);
    const netScore = estimateNetFormulaScore(item, difficultyModel);
    const distance = Math.abs(netScore - safeTarget);
    const tolerance = Math.max(350, safeTarget * 0.32);
    const qualityFit = 1 / (1 + distance / tolerance);
    const dimensionFit = 1 + match / 1000;
    return {
      item,
      netScore,
      rankScore: qualityFit * qualityFit * 1000 * dimensionFit,
    };
  });
  ranked.sort((a, b) => b.rankScore - a.rankScore || Math.abs(a.netScore - safeTarget) - Math.abs(b.netScore - safeTarget));
  const shortlistSize = clamp(Math.ceil(ranked.length * 0.08), 3, 40);
  const weighted = ranked.slice(0, shortlistSize).map((row) => ({
    item: row.item,
    weight: Math.max(1, row.rankScore),
  }));
  const total = weighted.reduce((sum, row) => sum + row.weight, 0);
  let cursor = rng.next() * total;
  for (const row of weighted) {
    cursor -= row.weight;
    if (cursor <= 0) return row.item;
  }
  return weighted[weighted.length - 1]?.item || items[items.length - 1];
}

function findCost(difficulty, costRules) {
  return (
    costRules.find((rule) => difficulty >= rule.difficulty_min && difficulty <= rule.difficulty_max) || {
      sample_cost_gas: 0.01,
      submit_cost_gas: 1,
    }
  );
}

function assetUnlocked(asset, layerNo) {
  return Number(layerNo || 1) >= Number(asset.unlock_layer || 1);
}

function filterCandidates(assets, difficulty, rules, previousSteps, layerNo = 1) {
  const filters = rules.selection_filters;
  const recentStructureLimit = rules.anti_duplication.recent_primary_structure_block;
  const recentFamilyLimit = rules.anti_duplication.recent_family_block;
  const maxSameStructure = rules.anti_duplication.max_same_primary_structure_per_layer;
  const recentStructures = new Set(previousSteps.slice(-recentStructureLimit).map((step) => step.primary_structure));
  const recentFamilies = new Set(previousSteps.slice(-recentFamilyLimit).map((step) => step.family_id));
  const structureCounts = previousSteps.reduce((acc, step) => {
    acc[step.primary_structure] = (acc[step.primary_structure] || 0) + 1;
    return acc;
  }, {});

  let fallback = [];
  for (let radius = 0; radius <= 2; radius += 1) {
    const base = assets.filter((asset) => {
      return (
        asset.enabled === filters.enabled_required &&
        assetUnlocked(asset, layerNo) &&
        filters.state_dependency_allowed.includes(asset.state_dependency) &&
        filters.verification_mode_allowed.includes(asset.verification_mode) &&
        asset.difficulty_min <= difficulty + radius &&
        asset.difficulty_max >= difficulty - radius &&
        (structureCounts[asset.primary_structure] || 0) < maxSameStructure
      );
    });
    if (!base.length) continue;
    if (!fallback.length) fallback = base;
    const noStructureRepeat = base.filter((asset) => !recentStructures.has(asset.primary_structure));
    const noFamilyRepeat = noStructureRepeat.filter((asset) => !recentFamilies.has(asset.family_id));
    if (noFamilyRepeat.length) return noFamilyRepeat;
    if (noStructureRepeat.length) return noStructureRepeat;
  }

  return fallback.length
    ? fallback
    : assets.filter((asset) => asset.enabled && assetUnlocked(asset, layerNo) && filters.state_dependency_allowed.includes(asset.state_dependency));
}

function componentCandidates(assets, targetDifficulty, rules, layerNo = 1) {
  const filters = rules.selection_filters;
  const floor = Math.max(1, targetDifficulty - 5);
  return assets.filter(
    (asset) =>
      asset.enabled === filters.enabled_required &&
      assetUnlocked(asset, layerNo) &&
      filters.state_dependency_allowed.includes(asset.state_dependency) &&
      filters.verification_mode_allowed.includes(asset.verification_mode) &&
      asset.difficulty_min <= targetDifficulty + 2 &&
      asset.difficulty_max >= floor,
  );
}

function remainingNetFormulaTarget(stepProfile, selected, difficultyModel) {
  const selectedScore = selected.reduce((sum, asset) => sum + estimateNetFormulaScore(asset, difficultyModel), 0);
  const remainingSlots = Math.max(1, Number(stepProfile.composition_count || 1) - selected.length);
  const remainingScore = Math.max(50, Number(stepProfile.net_formula_score_target || 0) - selectedScore);
  return Math.round(remainingScore / remainingSlots);
}

function pickDistinctAsset(
  pool,
  selected,
  rng,
  requiredStructures = null,
  targetDimensions = null,
  difficultyModel = null,
  gridSize = 40,
  targetNetScore = null,
) {
  const selectedFamilies = new Set(selected.map((asset) => asset.family_id));
  const selectedIds = new Set(selected.map((asset) => asset.asset_id));
  let candidates = pool.filter((asset) => !selectedIds.has(asset.asset_id));
  if (requiredStructures) {
    candidates = candidates.filter((asset) => requiredStructures.includes(asset.primary_structure));
  }
  const noFamilyRepeat = candidates.filter((asset) => !selectedFamilies.has(asset.family_id));
  if (noFamilyRepeat.length) candidates = noFamilyRepeat;
  if (!candidates.length) return null;
  if (targetDimensions && difficultyModel && Number.isFinite(Number(targetNetScore))) {
    return qualityAwareChoice(candidates, rng, { targetDimensions, difficultyModel, gridSize, targetNetScore });
  }
  if (targetDimensions && difficultyModel) {
    return dimensionAwareChoice(candidates, rng, targetDimensions, difficultyModel, gridSize);
  }
  return weightedComplexChoice(candidates, rng);
}

function selectComponentAssets({ assets, targetDifficulty, rules, previousSteps, stepProfile, layerProfile, difficultyModel, gridSize, rng }) {
  if (stepProfile.composition_count <= 1) {
    const candidates = filterCandidates(assets, targetDifficulty, rules, previousSteps, layerProfile.layer_no).filter(
      (asset) => !stepProfile.banned_single_structures.includes(asset.primary_structure),
    );
    const pool = candidates.length ? candidates : filterCandidates(assets, targetDifficulty, rules, previousSteps, layerProfile.layer_no);
    return [
      qualityAwareChoice(pool, rng, {
        targetDimensions: stepProfile.dimension_profile,
        difficultyModel,
        gridSize,
        targetNetScore: stepProfile.net_formula_score_target,
      }),
    ];
  }

  const pool = componentCandidates(assets, targetDifficulty, rules, layerProfile.layer_no);
  const hardPool = pool.filter((asset) => layerProfile.required_structure_pool.includes(asset.primary_structure));
  const selected = [];
  const requiredHard = Math.min(stepProfile.required_hard_structures, stepProfile.composition_count);

  while (selected.length < requiredHard) {
    const next = pickDistinctAsset(
      hardPool,
      selected,
      rng,
      layerProfile.required_structure_pool,
      stepProfile.dimension_profile,
      difficultyModel,
      gridSize,
      remainingNetFormulaTarget(stepProfile, selected, difficultyModel),
    );
    if (!next) break;
    selected.push(next);
  }

  while (selected.length < stepProfile.composition_count) {
    const next = pickDistinctAsset(
      pool,
      selected,
      rng,
      null,
      stepProfile.dimension_profile,
      difficultyModel,
      gridSize,
      remainingNetFormulaTarget(stepProfile, selected, difficultyModel),
    );
    if (!next) break;
    selected.push(next);
  }

  return selected.length ? selected : [weightedComplexChoice(pool.length ? pool : assets.filter((asset) => asset.enabled), rng)];
}

function makeCompositeAsset({ selectedAssets, allocatorSeed, formulaSeed, layerNo, stepNo, combinerPlan }) {
  if (selectedAssets.length === 1) return selectedAssets[0];
  const compositeId = `MXC-${sha256({ allocatorSeed, selected: selectedAssets.map((asset) => asset.asset_id) }).slice(0, 12)}`;
  const structures = unique(selectedAssets.flatMap((asset) => assetStructures(asset)));
  const variables = unique(selectedAssets.flatMap((asset) => asset.variables || []));
  const mode = combinerPlan?.combiner_mode || 'weighted_sum';
  return {
    asset_id: compositeId,
    pool_id: 'COMPOSITE',
    source_category: 'Composite',
    primary_structure: selectedAssets[0].primary_structure,
    secondary_structure: structures.filter((structure) => structure !== selectedAssets[0].primary_structure),
    family_id: `composite.layer_${layerNo}.step_${stepNo}.${compositeId}`,
    formula_template: {
      composition: mode,
      components: selectedAssets.map((asset) => asset.asset_id),
    },
    combiner_mode: mode,
    combiner_score: combinerPlan?.combiner_score || 0,
    combiner_plan: combinerPlan || null,
    variables,
    state_dependency: 'none',
    difficulty_min: Math.min(...selectedAssets.map((asset) => asset.difficulty_min || 1)),
    difficulty_max: Math.max(...selectedAssets.map((asset) => asset.difficulty_max || 1)),
    boss_enabled: true,
    duplicate_policy: 'composite',
    theme_candidates: unique(selectedAssets.flatMap((asset) => asset.theme_candidates || [])).slice(0, 12),
    verification_mode: 'exact_integer',
    enabled: true,
    components: selectedAssets.map((asset, index) => ({
      component_id: `C${index + 1}`,
      asset_id: asset.asset_id,
      pool_id: asset.pool_id,
      primary_structure: asset.primary_structure,
      secondary_structure: asset.secondary_structure || [],
      family_id: asset.family_id,
      formula_template: asset.formula_template,
      variables: asset.variables,
      state_dependency: asset.state_dependency,
      verification_mode: asset.verification_mode,
      parameter_seed: sha256(`${formulaSeed}:component:${index + 1}:${asset.asset_id}:parameters`),
    })),
  };
}

function netFormulaScoreStatus(actualScore, minScore, maxScore) {
  if (actualScore < minScore) return 'under_target';
  if (actualScore > maxScore) return 'over_target';
  return 'matched';
}

function allocateLayer({ layerNo = 1, seasonSeed = 'mazex-season-1', previousBlockHash = 'genesis' } = {}) {
  const rules = readJson('MazeX_formula_allocator_rules.json');
  const difficultyModel = readJson('MazeX_difficulty_model.json');
  const assetDoc = readJson(rules.asset_index);
  const assets = assetDoc.assets;
  const stepCount = getStepCount(layerNo, rules.step_count_rule);
  const { min: layerMin, max: layerMax } = getLayerRange(layerNo);
  const gridSize = gridSizeForLayer(layerNo, difficultyModel);
  const layerProfile = buildLayerProfile({ layerNo, stepCount, gridSize, difficultyModel });
  const steps = [];

  for (let stepNo = 1; stepNo <= stepCount; stepNo += 1) {
    const progress = stepCount === 1 ? 0 : (stepNo - 1) / (stepCount - 1);
    const band = getBand(progress);
    const rng = new Rng(`${seasonSeed}:${layerNo}:${stepNo}:${previousBlockHash}`);
    const targetDifficulty = getTargetDifficulty(
      layerMin,
      layerMax,
      band,
      rng,
      rules.layer_difficulty_rule.boss_bonus,
    );
    const allocatorSeed = sha256(`${seasonSeed}:${layerNo}:${stepNo}:${previousBlockHash}`);
    const formulaSeedBase = sha256(`${allocatorSeed}:formula`);
    const stepProfile = buildStepProfile({ layerProfile, stepNo, stepCount, band, targetDifficulty, rng, difficultyModel });
    const parameterPlan = buildParameterPlan({
      layerNo,
      stepNo,
      stepCount,
      seed: `${allocatorSeed}:parameter-plan`,
    });
    const dynamicParameterPlan = selectDynamicParametersForStep({
      layerNo,
      stepNo,
      stepCount,
      band,
      seed: `${allocatorSeed}:dynamic`,
    });
    const selectedAssets = selectComponentAssets({
      assets,
      targetDifficulty,
      rules,
      previousSteps: steps,
      stepProfile,
      layerProfile,
      difficultyModel,
      gridSize,
      rng,
    });
    const actualNetFormulaScore = selectedAssets.reduce(
      (sum, selected) => sum + estimateNetFormulaScore(selected, difficultyModel),
      0,
    );
    const netFormulaScoreRatio = Number(
      (actualNetFormulaScore / Math.max(1, stepProfile.net_formula_score_target)).toFixed(4),
    );
    const assetSeedId = selectedAssets.map((selected) => selected.asset_id).join(':');
    const formulaSeed = sha256(`${formulaSeedBase}:${assetSeedId}`);
    const combinerPlan = selectCombiner({
      layerNo,
      stepNo,
      stepCount,
      componentCount: selectedAssets.length,
      seed: `${allocatorSeed}:combiner`,
    });
    const asset = makeCompositeAsset({ selectedAssets, allocatorSeed, formulaSeed, layerNo, stepNo, combinerPlan });
    const outputPipeline = buildOutputPipelinePlan({
      parameterPlan,
      dynamicParameterPlan,
      seed: `${allocatorSeed}:output-pipeline`,
    });
    const effectiveDimensionProfile = buildEffectiveDimensionProfile({
      targetProfile: stepProfile.target_dimension_profile,
      asset,
      gridSize,
    });
    asset.step_dimension_profile = effectiveDimensionProfile;
    const parameterSeed = sha256(`${formulaSeed}:parameters`);
    const verificationSeed = sha256(`${formulaSeed}:verification`);
    const themeHint = asset.theme_candidates?.length ? rng.pick(asset.theme_candidates) : '';
    const cost = findCost(targetDifficulty, rules.cost_rules);
    const difficulty = computeDifficultyProfile({
      layerNo,
      stepNo,
      stepCount,
      targetDifficulty,
      band,
      asset,
      gridSize,
      model: difficultyModel,
    });
    const finalScore = finalStepScore({
      baseDifficultyScore: difficulty.difficulty_score,
      formulaScore: actualNetFormulaScore,
      combinerScore: combinerPlan.combiner_score,
      outputPipelineScore: outputPipeline.output_pipeline_score,
    });

    steps.push({
      layer_no: layerNo,
      step_no: stepNo,
      step_count: stepCount,
      progress_band: band,
      target_difficulty: targetDifficulty,
      layer_profile: layerProfile,
      step_profile: stepProfile,
      target_dimension_profile: stepProfile.target_dimension_profile,
      dimension_profile: effectiveDimensionProfile,
      dimension_score: difficulty.dimension_score,
      dominant_dimensions: difficulty.dominant_dimensions,
      composition_count: selectedAssets.length,
      formula_count: selectedAssets.length,
      formula_count_target: stepProfile.formula_count,
      formula_count_decision: stepProfile.formula_count_decision,
      net_formula_score_target: stepProfile.net_formula_score_target,
      net_formula_score_min: stepProfile.net_formula_score_min,
      net_formula_score_max: stepProfile.net_formula_score_max,
      average_net_formula_score_target: stepProfile.average_net_formula_score_target,
      actual_net_formula_score: actualNetFormulaScore,
      net_formula_score_gap: actualNetFormulaScore - stepProfile.net_formula_score_target,
      net_formula_score_ratio: netFormulaScoreRatio,
      net_formula_score_status: netFormulaScoreStatus(
        actualNetFormulaScore,
        stepProfile.net_formula_score_min,
        stepProfile.net_formula_score_max,
      ),
      formula_quality_decision: stepProfile.formula_quality_decision,
      combiner_mode: combinerPlan.combiner_mode,
      combiner_score_target: combinerPlan.combiner_score_target,
      combiner_score: combinerPlan.combiner_score,
      combiner_score_min: combinerPlan.combiner_score_min,
      combiner_score_max: combinerPlan.combiner_score_max,
      combiner_component_count: combinerPlan.combiner_component_count,
      combiner_plan: combinerPlan,
      output_pipeline: outputPipeline,
      static_parameter_score: outputPipeline.static_parameter_score,
      output_pipeline_score: outputPipeline.output_pipeline_score,
      parameter_count: parameterPlan.parameter_count,
      parameter_score_target: parameterPlan.parameter_score_target,
      single_parameter_score_target: parameterPlan.single_parameter_score_target,
      estimated_static_parameter_injection_score_target: parameterPlan.estimated_injection_score_target,
      operator_score_min: parameterPlan.operator_score_min,
      operator_score_max: parameterPlan.operator_score_max,
      number_score_min: parameterPlan.number_score_min,
      number_score_max: parameterPlan.number_score_max,
      parameter_plan: parameterPlan,
      dynamic_parameter_count: dynamicParameterPlan.dynamic_parameter_count,
      dynamic_parameter_score: outputPipeline.dynamic_parameter_score,
      dynamic_parameters: dynamicParameterPlan.dynamic_parameters,
      dynamic_parameter_plan: dynamicParameterPlan,
      asset_id: asset.asset_id,
      pool_id: asset.pool_id,
      primary_structure: asset.primary_structure,
      family_id: asset.family_id,
      formula_template: asset.formula_template,
      variables: asset.variables,
      secondary_structure: asset.secondary_structure || [],
      theme_hint: themeHint,
      allocator_seed: allocatorSeed,
      formula_seed: formulaSeed,
      parameter_seed: parameterSeed,
      verification_seed: verificationSeed,
      sample_cost_gas: cost.sample_cost_gas,
      submit_cost_gas: cost.submit_cost_gas,
      reward_weight: difficulty.reward_weight,
      difficulty_model_version: difficultyModel.version,
      difficulty_score: difficulty.difficulty_score,
      final_step_score: finalScore,
      work_score: finalScore,
      solver_complexity_score: difficulty.solver_complexity_score,
      target_solver_complexity: stepProfile.target_solver_complexity,
      readable_difficulty: difficulty.readable_difficulty,
      grid_size: difficulty.grid_size,
      verification_mode: difficulty.verification_mode,
      verification_point_count: difficulty.verification_point_count,
      verification_strength: difficulty.verification_strength,
      answer_budget_pressure: difficulty.answer_budget_pressure,
      layer_pressure: difficulty.layer_pressure,
      difficulty_components: {
        ...difficulty.difficulty_components,
        actual_formula_score: actualNetFormulaScore,
        combiner_score: combinerPlan.combiner_score,
        static_parameter_score: outputPipeline.static_parameter_score,
        dynamic_parameter_score: outputPipeline.dynamic_parameter_score,
        output_pipeline_score: outputPipeline.output_pipeline_score,
        final_step_score: finalScore,
      },
      components: asset.components || [],
      structure_bundle: unique([asset.primary_structure, ...(asset.secondary_structure || [])]),
      boss_zone: band === 'boss',
    });
  }

  return {
    layer_no: layerNo,
    step_count: stepCount,
    layer_difficulty_min: layerMin,
    layer_difficulty_max: layerMax,
    layer_profile: layerProfile,
    difficulty_model_version: difficultyModel.version,
    grid_size: gridSize,
    season_seed: seasonSeed,
    previous_block_hash: previousBlockHash,
    steps,
  };
}

module.exports = { allocateLayer, readJson, buildLayerProfile, buildStepProfile };
