const { Rng } = require('./rng');

const COMBINER_RULES = [
  {
    mode: 'weighted_sum',
    unlock_layer: 1,
    min_components: 2,
    base_score: 140,
    per_extra_component_score: 45,
  },
  {
    mode: 'nested_chain',
    unlock_layer: 80,
    min_components: 2,
    base_score: 360,
    per_extra_component_score: 95,
  },
  {
    mode: 'conditional_gate',
    unlock_layer: 180,
    min_components: 2,
    base_score: 470,
    per_extra_component_score: 120,
  },
  {
    mode: 'cross_weighted_mix',
    unlock_layer: 320,
    min_components: 2,
    base_score: 560,
    per_extra_component_score: 150,
  },
  {
    mode: 'gated_weighted_sum',
    unlock_layer: 600,
    min_components: 3,
    base_score: 760,
    per_extra_component_score: 190,
  },
];

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

function safeLayer(layerNo) {
  return Math.max(1, Math.trunc(Number(layerNo || 1)));
}

function stepProgress(stepNo, stepCount) {
  const total = Math.max(1, Math.trunc(Number(stepCount || 1)));
  if (total <= 1) return 1;
  return clamp((Math.max(1, Number(stepNo || 1)) - 1) / (total - 1), 0, 1);
}

function combinerTargetScore({ layerNo = 1, stepNo = 1, stepCount = 1, componentCount = 1, seed = 'combiner-score' } = {}) {
  const n = safeLayer(layerNo);
  const progress = stepProgress(stepNo, stepCount);
  const rng = new Rng(`${seed}:${layerNo}:${stepNo}:${stepCount}:${componentCount}:score`);
  const layerPressure = 1 - Math.exp(-n / 620);
  const deepPressure = n > 1000 ? Math.min(240, Math.log2(n / 1000 + 1) * 90) : 0;
  const componentPressure = Math.max(0, Math.trunc(Number(componentCount || 1)) - 1) * 55;
  const progressPressure = progress * 160;
  const jitter = rng.pick([-40, -20, 0, 0, 20, 40]);
  return clamp(Math.round(120 + layerPressure * 720 + deepPressure + componentPressure + progressPressure + jitter), 0, 1000);
}

function scoreCombiner(rule, componentCount) {
  const extra = Math.max(0, Math.trunc(Number(componentCount || 1)) - 1);
  return clamp(rule.base_score + extra * rule.per_extra_component_score, 0, 1000);
}

function eligibleCombiners(layerNo, componentCount) {
  const n = safeLayer(layerNo);
  const count = Math.max(1, Math.trunc(Number(componentCount || 1)));
  if (count <= 1) return [];
  return COMBINER_RULES.filter((rule) => n >= rule.unlock_layer && count >= rule.min_components);
}

function weightedPick(rows, rng) {
  const total = rows.reduce((sum, row) => sum + row.weight, 0);
  let cursor = rng.next() * total;
  for (const row of rows) {
    cursor -= row.weight;
    if (cursor <= 0) return row;
  }
  return rows[rows.length - 1];
}

function selectCombiner({
  layerNo = 1,
  stepNo = 1,
  stepCount = 1,
  componentCount = 1,
  seed = 'combiner-selector',
} = {}) {
  const count = Math.max(1, Math.trunc(Number(componentCount || 1)));
  if (count <= 1) {
    return {
      combiner_mode: 'direct_formula',
      combiner_score_target: 0,
      combiner_score: 0,
      combiner_score_min: 0,
      combiner_score_max: 0,
      combiner_component_count: count,
      combiner_unlock_layer: 1,
      step_progress: stepProgress(stepNo, stepCount),
    };
  }

  const rng = new Rng(`${seed}:${layerNo}:${stepNo}:${stepCount}:${count}:mode`);
  const target = combinerTargetScore({ layerNo, stepNo, stepCount, componentCount: count, seed });
  const candidates = eligibleCombiners(layerNo, count);
  const tolerance = clamp(Math.round(90 + target * 0.12), 80, 220);
  const rows = candidates.map((rule) => {
    const score = scoreCombiner(rule, count);
    const distance = Math.abs(score - target);
    const fit = 1 / (1 + distance / 180);
    const maturity = clamp((safeLayer(layerNo) - rule.unlock_layer + 1) / 300, 0.25, 2.5);
    return {
      rule,
      score,
      weight: fit * fit * maturity,
    };
  });
  const lowerBound = clamp(target - tolerance, 0, 1000);
  const upperBound = clamp(target + tolerance, 0, 1000);
  const inRange = rows.filter((row) => row.score >= lowerBound && row.score <= upperBound);
  const pool = inRange.length ? inRange : rows.sort((a, b) => Math.abs(a.score - target) - Math.abs(b.score - target)).slice(0, 2);
  const picked = weightedPick(pool, rng);

  return {
    combiner_mode: picked.rule.mode,
    combiner_score_target: target,
    combiner_score: picked.score,
    combiner_score_min: lowerBound,
    combiner_score_max: upperBound,
    combiner_component_count: count,
    combiner_unlock_layer: picked.rule.unlock_layer,
    step_progress: Number(stepProgress(stepNo, stepCount).toFixed(4)),
  };
}

module.exports = {
  COMBINER_RULES,
  combinerTargetScore,
  selectCombiner,
};
