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

const DYNAMIC_PARAMETER_POOL = [
  {
    id: 'chain_seed_mod',
    family: 'CHAIN_STATE',
    name: 'chain_seed_modulus',
    unlock_layer: 31,
    difficulty_score: 260,
  },
  {
    id: 'chain_previous_answer_mix',
    family: 'CHAIN_STATE',
    name: 'previous_answer_hash_mix',
    unlock_layer: 45,
    difficulty_score: 420,
  },
  {
    id: 'chain_layer_step_commit',
    family: 'CHAIN_STATE',
    name: 'layer_step_commit_mix',
    unlock_layer: 70,
    difficulty_score: 620,
  },
  {
    id: 'periodic_phase',
    family: 'PERIODIC_CYCLE',
    name: 'periodic_phase',
    unlock_layer: 81,
    difficulty_score: 520,
  },
  {
    id: 'periodic_wave_table',
    family: 'PERIODIC_CYCLE',
    name: 'periodic_wave_table',
    unlock_layer: 110,
    difficulty_score: 700,
  },
  {
    id: 'periodic_nested_cycle',
    family: 'PERIODIC_CYCLE',
    name: 'nested_periodic_cycle',
    unlock_layer: 140,
    difficulty_score: 860,
  },
  {
    id: 'time_bucket',
    family: 'TIME_CLOCK',
    name: 'time_bucket',
    unlock_layer: 151,
    difficulty_score: 640,
  },
  {
    id: 'time_window_gate',
    family: 'TIME_CLOCK',
    name: 'time_window_gate',
    unlock_layer: 210,
    difficulty_score: 780,
  },
  {
    id: 'time_elapsed_scale',
    family: 'TIME_CLOCK',
    name: 'elapsed_time_scale',
    unlock_layer: 260,
    difficulty_score: 920,
  },
  {
    id: 'half_life_bucket',
    family: 'HALF_LIFE_DECAY',
    name: 'half_life_bucket',
    unlock_layer: 301,
    difficulty_score: 760,
  },
  {
    id: 'radioactive_decay',
    family: 'HALF_LIFE_DECAY',
    name: 'radioactive_decay',
    unlock_layer: 420,
    difficulty_score: 900,
  },
  {
    id: 'double_half_life_decay',
    family: 'HALF_LIFE_DECAY',
    name: 'double_half_life_decay',
    unlock_layer: 600,
    difficulty_score: 980,
  },
];

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

function dynamicParameterCandidates({
  layerNo = 1,
  minScore = 1,
  maxScore = 1000,
  families = null,
} = {}) {
  const allowedFamilies = Array.isArray(families) && families.length ? new Set(families) : null;
  return DYNAMIC_PARAMETER_POOL.filter(
    (param) =>
      Number(layerNo || 1) >= param.unlock_layer &&
      param.difficulty_score >= minScore &&
      param.difficulty_score <= maxScore &&
      (!allowedFamilies || allowedFamilies.has(param.family)),
  );
}

function maxDynamicParameterCountForLayer(layerNo) {
  const n = Math.max(1, Math.trunc(Number(layerNo || 1)));
  if (n < 31) return 0;
  if (n < 151) return 1;
  if (n < 601) return 2;
  if (n < 1201) return 3;
  return clamp(3 + Math.floor(Math.log2(n / 1200 + 1)), 3, 8);
}

function dynamicPresenceChance(layerNo, band = 'main') {
  const n = Math.max(1, Math.trunc(Number(layerNo || 1)));
  if (n < 31) return 0;
  let chance = 0.22;
  if (n >= 81) chance = 0.38;
  if (n >= 151) chance = 0.55;
  if (n >= 301) chance = 0.72;
  if (n >= 601) chance = 0.86;
  if (n >= 1200) chance = 0.95;
  if (band === 'warmup') chance -= 0.18;
  if (band === 'advanced') chance += 0.08;
  if (band === 'boss') chance += 0.16;
  return clamp(chance, 0, 0.98);
}

function selectDynamicParameterCount({
  layerNo = 1,
  band = 'main',
  seed = 'dynamic-count',
  minCount = 0,
  maxCount = null,
} = {}) {
  const ceiling =
    maxCount !== null && maxCount !== undefined && Number.isFinite(Number(maxCount))
      ? Number(maxCount)
      : maxDynamicParameterCountForLayer(layerNo);
  if (ceiling <= 0) {
    return {
      dynamic_parameter_count: 0,
      max_dynamic_parameter_count: 0,
      presence_chance: 0,
      selector: 'dynamic_parameter_count_selector',
    };
  }

  const rng = new Rng(`${seed}:${layerNo}:${band}:dynamic-count`);
  const chance = dynamicPresenceChance(layerNo, band);
  if (rng.next() > chance) {
    return {
      dynamic_parameter_count: Math.max(0, Math.trunc(minCount)),
      max_dynamic_parameter_count: ceiling,
      presence_chance: chance,
      selector: 'dynamic_parameter_count_selector',
    };
  }

  const bandBonus = band === 'boss' ? 1 : band === 'advanced' ? rng.pick([0, 1]) : 0;
  const raw = 1 + bandBonus + (Number(layerNo || 1) >= 1200 ? rng.pick([0, 1]) : 0);
  return {
    dynamic_parameter_count: clamp(raw, Math.max(0, Math.trunc(minCount)), Math.max(0, Math.trunc(ceiling))),
    max_dynamic_parameter_count: ceiling,
    presence_chance: chance,
    selector: 'dynamic_parameter_count_selector',
  };
}

function scoreDynamicParameter(param) {
  return clamp(Number(param?.difficulty_score || 1), 1, 1000);
}

function makeConfig(base, rng, seed) {
  const salt = sha256(`${seed}:${base.id}:dynamic-salt`).slice(0, 16);
  if (base.family === 'CHAIN_STATE') {
    return {
      modulus: rng.pick([17, 23, 31, 37, 41, 53, 61, 97]),
      salt,
    };
  }
  if (base.family === 'PERIODIC_CYCLE') {
    const period = rng.pick([3, 4, 5, 7, 8, 11, 13, 16]);
    return {
      period,
      tick_size_seconds: rng.pick([15, 30, 45, 60, 90, 120]),
      amplitude: rng.int(2, 19),
      phase_shift: rng.int(0, period - 1),
      table: Array.from({ length: period }, () => rng.int(0, 31)),
      salt,
    };
  }
  if (base.family === 'TIME_CLOCK') {
    return {
      tick_size_seconds: rng.pick([20, 30, 45, 60, 90, 120, 180]),
      scale: rng.int(1, 17),
      window_size: rng.pick([2, 3, 4, 5, 8]),
      salt,
    };
  }
  if (base.family === 'HALF_LIFE_DECAY') {
    return {
      initial_value: rng.int(64, 1000),
      half_life_seconds: rng.pick([30, 45, 60, 90, 120, 180, 300]),
      floor_value: rng.int(0, 8),
      salt,
    };
  }
  return { salt };
}

function extractDynamicParameter({
  seed = 'dynamic-parameter',
  layerNo = 1,
  minScore = 1,
  maxScore = 1000,
  families = null,
} = {}) {
  const candidates = dynamicParameterCandidates({ layerNo, minScore, maxScore, families });
  if (!candidates.length) throw new Error('No dynamic parameter candidates for the requested layer and score range.');
  const rng = new Rng(seed);
  const base = rng.pick(candidates);
  return {
    ...base,
    score_range: [1, 1000],
    config: makeConfig(base, rng, seed),
  };
}

function extractDynamicParameters({
  seed = 'dynamic-parameters',
  layerNo = 1,
  count = 1,
  minScore = 1,
  maxScore = 1000,
  families = null,
  allowRepeat = false,
} = {}) {
  const candidates = dynamicParameterCandidates({ layerNo, minScore, maxScore, families });
  if (!candidates.length) return [];
  const rng = new Rng(seed);
  const pool = [...candidates];
  const result = [];
  const total = Math.max(0, Math.trunc(count));

  for (let index = 0; index < total; index += 1) {
    if (!allowRepeat && !pool.length) break;
    const source = allowRepeat ? candidates : pool;
    const selectedIndex = rng.int(0, source.length - 1);
    const base = source[selectedIndex];
    result.push({
      ...base,
      score_range: [1, 1000],
      config: makeConfig(base, rng, `${seed}:${index}:${base.id}`),
    });
    if (!allowRepeat) pool.splice(selectedIndex, 1);
  }

  return result;
}

function selectDynamicParametersForStep({
  layerNo = 1,
  stepNo = 1,
  stepCount = 1,
  band = 'main',
  seed = 'dynamic-step',
  minScore = 1,
  maxScore = 1000,
  families = null,
} = {}) {
  const countDecision = selectDynamicParameterCount({
    layerNo,
    band,
    seed: `${seed}:${stepNo}:${stepCount}`,
  });
  const parameters = extractDynamicParameters({
    seed: `${seed}:${layerNo}:${stepNo}:dynamic-params`,
    layerNo,
    count: countDecision.dynamic_parameter_count,
    minScore,
    maxScore,
    families,
  });
  return {
    ...countDecision,
    dynamic_parameters: parameters,
    dynamic_parameter_score: parameters.reduce((sum, param) => sum + scoreDynamicParameter(param), 0),
  };
}

module.exports = {
  DYNAMIC_PARAMETER_POOL,
  dynamicParameterCandidates,
  dynamicPresenceChance,
  extractDynamicParameter,
  extractDynamicParameters,
  maxDynamicParameterCountForLayer,
  scoreDynamicParameter,
  selectDynamicParameterCount,
  selectDynamicParametersForStep,
};
