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

const BAND_COUNT_MULTIPLIER = {
  warmup: 0.9,
  main: 1,
  advanced: 1.25,
  boss: 1.65,
};

const BAND_QUALITY_MULTIPLIER = {
  warmup: 0.82,
  main: 1,
  advanced: 1.18,
  boss: 1.42,
};

const BAND_TOLERANCE = {
  warmup: 0.2,
  main: 0.16,
  advanced: 0.13,
  boss: 0.1,
};

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 progressForStep(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 bandForProgress(progress) {
  if (progress < 0.2) return 'warmup';
  if (progress < 0.7) return 'main';
  if (progress < 0.9) return 'advanced';
  return 'boss';
}

function jitter(rng, size = 1) {
  if (!rng || size <= 0) return 0;
  return rng.pick([-size, 0, 0, size]);
}

function layerFormulaBaseCount(layerNo) {
  const n = safeLayer(layerNo);
  const logPressure = Math.floor(Math.log2(n + 1) / 2.2);
  const sqrtPressure = Math.floor(Math.sqrt(n) / 38);
  const latePressure = n > 300 ? Math.floor(Math.log2(n / 300 + 1)) : 0;
  const deepPressure = n > 2000 ? Math.floor(Math.pow(n / 2000, 0.35)) : 0;
  return 1 + logPressure + sqrtPressure + latePressure + deepPressure;
}

function countMaxForLayer(layerNo) {
  const n = safeLayer(layerNo);
  return clamp(4 + Math.ceil(Math.log2(n + 1) * 3 + Math.sqrt(n) / 4), 4, 120);
}

function selectStepFormulaCount({
  layerNo = 1,
  stepNo = 1,
  stepCount = 1,
  band = null,
  seed = 'formula-count',
  rng = null,
  minCount = 1,
  maxCount = null,
} = {}) {
  const progress = progressForStep(stepNo, stepCount);
  const progressBand = band || bandForProgress(progress);
  const localRng = rng || new Rng(`${seed}:${layerNo}:${stepNo}:count`);
  const base = layerFormulaBaseCount(layerNo);
  const multiplier = BAND_COUNT_MULTIPLIER[progressBand] || 1;
  const progressBonus = Math.floor(progress * Math.max(1, base / 2));
  const raw = Math.round(base * multiplier + progressBonus + jitter(localRng, 1));
  const ceiling =
    maxCount !== null && maxCount !== undefined && Number.isFinite(Number(maxCount))
      ? Number(maxCount)
      : countMaxForLayer(layerNo);
  const formulaCount = clamp(raw, Math.max(1, Math.trunc(minCount)), Math.max(1, Math.trunc(ceiling)));

  return {
    formula_count: formulaCount,
    base_formula_count: base,
    progress_band: progressBand,
    progress,
    min_formula_count: Math.max(1, Math.trunc(minCount)),
    max_formula_count: Math.max(1, Math.trunc(ceiling)),
    selector: 'formula_count_selector',
  };
}

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

function plannedDifficultyForBand(layerNo, band) {
  const range = layerDifficultyRange(layerNo);
  if (band === 'warmup') return range.min;
  if (band === 'main') return clamp(range.min + 2, range.min, range.max);
  if (band === 'advanced') return clamp(range.max - 1, range.min, range.max);
  return range.max;
}

function perFormulaQualityTarget({ layerNo = 1, band = 'main', targetDifficulty = null, rng = null }) {
  const n = safeLayer(layerNo);
  const difficulty = Number(targetDifficulty || plannedDifficultyForBand(n, band));
  const saturation = 1 - Math.exp(-n / 520);
  const base = 160 + 7600 * saturation;
  const late =
    n > 100
      ? Math.min(2200, 180 * (Math.pow(2, Math.min(1800, n - 100) / 600) - 1))
      : 0;
  const difficultyPressure = difficulty * 140;
  const multiplier = BAND_QUALITY_MULTIPLIER[band] || 1;
  const raw = Math.round((base + late + difficultyPressure) * multiplier + jitter(rng, 60));
  const reachableCap = n < 600 ? 5200 : n < 800 ? 5800 : 6200;
  return clamp(raw, 800, reachableCap);
}

function selectStepFormulaQuality({
  layerNo = 1,
  stepNo = 1,
  stepCount = 1,
  band = null,
  formulaCount = 1,
  targetDifficulty = null,
  seed = 'formula-quality',
  rng = null,
} = {}) {
  const progress = progressForStep(stepNo, stepCount);
  const progressBand = band || bandForProgress(progress);
  const localRng = rng || new Rng(`${seed}:${layerNo}:${stepNo}:quality`);
  const count = Math.max(1, Math.trunc(Number(formulaCount || 1)));
  const average = perFormulaQualityTarget({
    layerNo,
    band: progressBand,
    targetDifficulty,
    rng: localRng,
  });
  const total = average * count;
  const tolerance = BAND_TOLERANCE[progressBand] || 0.16;

  return {
    net_formula_score_target: total,
    net_formula_score_min: Math.round(total * (1 - tolerance)),
    net_formula_score_max: Math.round(total * (1 + tolerance)),
    average_net_formula_score_target: average,
    formula_count: count,
    progress_band: progressBand,
    tolerance,
    score_scope: 'net_formula_only_excludes_static_parameters',
    selector: 'formula_quality_selector',
  };
}

function templateText(asset) {
  const template = asset?.formula_template || {};
  const helpers = Array.isArray(template.helpers) ? template.helpers.join(' ') : '';
  return [helpers, template.u, template.v].filter(Boolean).join(' ');
}

function countMatches(text, regex) {
  return (String(text || '').match(regex) || []).length;
}

function estimateSingleAssetNetFormulaScore(asset, difficultyModel = {}) {
  const structureScores = difficultyModel.structure_base_score || {};
  const primaryScore = Number(structureScores[asset.primary_structure] || 100);
  const secondaryScore = (asset.secondary_structure || asset.secondary_structures || []).reduce(
    (sum, structure) => sum + Number(structureScores[structure] || 80) * 0.18,
    0,
  );
  const text = templateText(asset);
  const expressionScore = Math.min(1800, text.length * 4);
  const branchScore = countMatches(text, /\bif\s*\(/gi) * 180 + countMatches(text, /\btable\s*\(/gi) * 140;
  const operatorScore = countMatches(text, /(\+|-|\*|\/|\^|\bmod\b|==|>=|<=|>|<|&&|\|\|)/gi) * 22;
  const functionScore = countMatches(text, /\b(abs|floor|ceil|round|sqrt|sin|cos|tan|log|min|max)\s*\(/gi) * 60;
  const variableScore = Math.max(0, (asset.variables || []).filter((name) => !['x', 'y', 'N'].includes(name)).length) * 35;
  const semanticScore = Math.min(420, (asset.theme_candidates || []).length * 28);
  const stateScore = asset.state_dependency && asset.state_dependency !== 'none' ? 420 : 0;
  const difficultyRangeScore = ((Number(asset.difficulty_min || 1) + Number(asset.difficulty_max || 1)) / 2) * 120;

  return clamp(
    Math.round(
      primaryScore * 0.75 +
        secondaryScore +
        expressionScore +
        branchScore +
        operatorScore +
        functionScore +
        variableScore +
        semanticScore +
        stateScore +
        difficultyRangeScore,
    ),
    50,
    10000,
  );
}

function estimateNetFormulaScore(asset, difficultyModel = {}) {
  if (Array.isArray(asset?.components) && asset.components.length) {
    return asset.components.reduce((sum, component) => sum + estimateSingleAssetNetFormulaScore(component, difficultyModel), 0);
  }
  return estimateSingleAssetNetFormulaScore(asset, difficultyModel);
}

function selectLayerFormulaPlan({
  layerNo = 1,
  stepCount = 30,
  seasonSeed = 'mazex-season',
} = {}) {
  const steps = [];
  for (let stepNo = 1; stepNo <= stepCount; stepNo += 1) {
    const progress = progressForStep(stepNo, stepCount);
    const band = bandForProgress(progress);
    const count = selectStepFormulaCount({
      layerNo,
      stepNo,
      stepCount,
      band,
      seed: `${seasonSeed}:layer:${layerNo}:step:${stepNo}`,
    });
    const quality = selectStepFormulaQuality({
      layerNo,
      stepNo,
      stepCount,
      band,
      formulaCount: count.formula_count,
      targetDifficulty: plannedDifficultyForBand(layerNo, band),
      seed: `${seasonSeed}:layer:${layerNo}:step:${stepNo}`,
    });
    steps.push({
      step_no: stepNo,
      progress_band: band,
      formula_count: count.formula_count,
      net_formula_score_target: quality.net_formula_score_target,
      average_net_formula_score_target: quality.average_net_formula_score_target,
    });
  }

  return {
    layer_no: safeLayer(layerNo),
    step_count: stepCount,
    total_formula_count_target: steps.reduce((sum, step) => sum + step.formula_count, 0),
    total_net_formula_score_target: steps.reduce((sum, step) => sum + step.net_formula_score_target, 0),
    average_formula_count_per_step:
      steps.length > 0 ? Number((steps.reduce((sum, step) => sum + step.formula_count, 0) / steps.length).toFixed(2)) : 0,
    steps,
  };
}

module.exports = {
  bandForProgress,
  estimateNetFormulaScore,
  layerDifficultyRange,
  selectLayerFormulaPlan,
  selectStepFormulaCount,
  selectStepFormulaQuality,
};
