// adaptive.jsx — pure functions, no React.
// Owns three responsibilities:
//   1. seed initial difficulty from the kid's age (and per-game offset)
//   2. nudge difficulty after a session based on recent round results
//   3. translate a 0..1 difficulty into game-specific parameters
//
// Difficulty is always a number in [0, 1]. Each runner reads its own
// parameters via mapDifficultyToParams so this file is the only place that
// knows how a game scales with skill.

// Per-game offset added to the age-seeded base. Negative = easier start.
const GAME_OFFSETS = {
  addisjon: 0.0,
  subtraksjon: 0.05,
  multiplikasjon: 0.10,
  divisjon: 0.15,
  klokka: 0.05,
  former: -0.05,
  telling: -0.15,
  tallinje: 0.0,
};

const MIN_DIFF = 0.0;
const MAX_DIFF = 1.0;

function clamp(n, lo, hi) {
  return Math.min(hi, Math.max(lo, n));
}

// Age 5 → 0.05, age 12 → 0.75, linear in between. Capped at the ends.
function seedDifficulty(age, gameId) {
  const a = typeof age === 'number' ? age : 8;
  const base = clamp((a - 5) / 7, 0, 1) * 0.7 + 0.05;
  const offset = GAME_OFFSETS[gameId] ?? 0;
  return clamp(base + offset, MIN_DIFF, MAX_DIFF);
}

// Recent results are { correct, msToAnswer, difficulty }. We weight more
// heavily toward fewer mistakes than fast solves so we never push a kid
// into frustration just because they happen to be quick.
function nextDifficulty(prev, recentResults) {
  if (!Array.isArray(recentResults) || recentResults.length === 0) return prev;
  const n = recentResults.length;
  const correct = recentResults.filter(r => r.correct).length;
  const accuracy = correct / n;
  const avgMs = recentResults.reduce((s, r) => s + (r.msToAnswer || 0), 0) / n;

  // Speed score: under 2s/round is fast for arithmetic, over 8s is slow.
  const speedScore = clamp(1 - (avgMs - 2000) / 6000, 0, 1);

  // Default behavior: easier on misses (asymmetric), harder only on streaks.
  if (accuracy < 0.6) return clamp(prev - 0.10, MIN_DIFF, MAX_DIFF);
  if (accuracy < 0.8) return clamp(prev - 0.03, MIN_DIFF, MAX_DIFF);
  // Solid accuracy: nudge up a little, more if also fast.
  const bump = 0.04 + 0.06 * speedScore;
  return clamp(prev + bump, MIN_DIFF, MAX_DIFF);
}

// Helper for per-game runners: linearly interpolate between [easy, hard]
// at the given difficulty. Returns an integer.
function lerpInt(diff, easy, hard) {
  return Math.round(easy + (hard - easy) * clamp(diff, 0, 1));
}

// Returns game-specific parameter object. Each runner picks what it needs.
function mapDifficultyToParams(gameId, difficulty) {
  const d = clamp(difficulty, 0, 1);
  switch (gameId) {
    case 'addisjon':
      return {
        // Operand magnitude: 1..5 at d=0, 1..99 at d=1.
        max: lerpInt(d, 5, 99),
        min: 1,
        rounds: 5,
      };
    case 'subtraksjon':
      return {
        max: lerpInt(d, 5, 99),
        min: 1,
        allowNegative: d > 0.7,
        rounds: 5,
      };
    case 'multiplikasjon':
      return {
        // Factor range from 1..5 up through 1..12 (gangetabellen).
        maxFactor: lerpInt(d, 5, 12),
        minFactor: 1,
        rounds: 5,
      };
    case 'divisjon':
      return {
        // Always integer results. Quotient up to maxQ, divisor up to maxDiv.
        maxQ: lerpInt(d, 5, 12),
        maxDiv: lerpInt(d, 5, 12),
        rounds: 5,
      };
    case 'klokka':
      // Granularity in minutes: 60 (whole hours) → 1 (every minute).
      return {
        granularity: [60, 30, 15, 5, 1][Math.min(4, Math.floor(d * 5))],
        rounds: 5,
      };
    case 'former':
      // How many shape names are in play. More shapes = harder.
      return {
        shapeCount: lerpInt(d, 4, 10),
        rounds: 5,
      };
    case 'telling':
      return {
        maxCount: lerpInt(d, 5, 30),
        // Visual noise: at high difficulty add distractor objects.
        addNoise: d > 0.5,
        rounds: 5,
      };
    case 'tallinje':
      return {
        // Number line range: 0..10 at d=0, 0..1000 at d=1.
        maxValue: [10, 20, 50, 100, 1000][Math.min(4, Math.floor(d * 5))],
        // Tick density relative to range: more ticks = easier.
        ticks: lerpInt(d, 11, 5),
        rounds: 5,
      };
    default:
      return { rounds: 5 };
  }
}

// Compute a 0..1 performance score for the lottery to weight win odds.
function performanceScore(results) {
  if (!Array.isArray(results) || results.length === 0) return 0.5;
  const accuracy = results.filter(r => r.correct).length / results.length;
  const avgMs = results.reduce((s, r) => s + (r.msToAnswer || 0), 0) / results.length;
  const speed = clamp(1 - (avgMs - 2000) / 6000, 0, 1);
  return clamp(0.6 * accuracy + 0.4 * speed, 0, 1);
}

Object.assign(window, {
  seedDifficulty,
  nextDifficulty,
  mapDifficultyToParams,
  performanceScore,
  clamp,
  lerpInt,
});
