// Spaced repetition scheduling — a faithful SM-2 variant adapted to a 4-button
// grading UI (Again / Hard / Good / Easy). Pure & deterministic.
//
// The classic SM-2 algorithm grades recall on 0..5 and recomputes an ease
// factor (EF) plus an interval. We collapse the UI to four buttons and map them
// to behaviors that match learner expectations on Anki-style apps:
//
//   grade 0 = Again : failed recall. Reset the rep streak, count a lapse, drop
//                     ease by 0.2, and re-show soon (a short sub-day interval).
//   grade 1 = Hard  : recalled with difficulty. Grow the interval gently (x1.2)
//                     and drop ease by 0.15.
//   grade 2 = Good  : normal recall. Use the SM-2 schedule:
//                     rep 1 -> 1 day, rep 2 -> 6 days, then interval *= ease.
//   grade 3 = Easy  : effortless recall. Like Good but with an easy bonus
//                     (~x1.3) and ease bumped up by 0.15.
//
// Invariants:
//   - ease is floored at 1.3 (SM-2's classic minimum).
//   - reps increments only on a passing grade (>= 1); Again resets it to 0.
//   - due = now + intervalDays * MS_PER_DAY.
//   - lastReviewed is set to `now` on every review.
//
// IMPORTANT: relative imports only inside src/lib (vitest has no '@/*' alias).

import type { SrsState } from '../db/schema';

const MS_PER_DAY = 86_400_000;
const MIN_EASE = 1.3;
/** Sub-day interval (in days) used when a card is failed and must re-show soon. */
const AGAIN_INTERVAL_DAYS = 1 / 1440; // ~1 minute, expressed in days
const HARD_MULTIPLIER = 1.2;
const EASY_BONUS = 1.3;

/** A brand-new card: due immediately, neutral ease, nothing learned yet. */
export function initialSrs(now: number): SrsState {
  return {
    ease: 2.5,
    intervalDays: 0,
    reps: 0,
    lapses: 0,
    due: now,
  };
}

/**
 * Apply a review `grade` to `srs` at time `now`, returning the next state.
 * Does not mutate the input.
 */
export function review(
  srs: SrsState,
  grade: 0 | 1 | 2 | 3,
  now: number,
): SrsState {
  let { ease, intervalDays, reps, lapses } = srs;

  if (grade === 0) {
    // Again: the card was forgotten.
    ease = floorEase(ease - 0.2);
    reps = 0;
    lapses += 1;
    intervalDays = AGAIN_INTERVAL_DAYS;
  } else if (grade === 1) {
    // Hard: passed, but gently grow the interval and lower ease.
    ease = floorEase(ease - 0.15);
    reps += 1;
    intervalDays = nextInterval(srs, ease, HARD_MULTIPLIER);
  } else if (grade === 2) {
    // Good: standard SM-2 progression, ease unchanged.
    reps += 1;
    intervalDays = nextInterval(srs, ease, 1);
  } else {
    // Easy: like Good with an easy bonus and an ease bump.
    ease = floorEase(ease + 0.15);
    reps += 1;
    intervalDays = nextInterval(srs, ease, EASY_BONUS);
  }

  return {
    ease,
    intervalDays,
    reps,
    lapses,
    due: now + intervalDays * MS_PER_DAY,
    lastReviewed: now,
  };
}

/**
 * SM-2 interval progression for a passing grade.
 *  - first successful rep (the card had reps 0) -> 1 day
 *  - second successful rep (had reps 1)         -> 6 days
 *  - thereafter                                 -> previous interval * ease
 * The result is scaled by `multiplier` (Hard < 1-ish via lower ease, Easy bonus).
 */
function nextInterval(prev: SrsState, ease: number, multiplier: number): number {
  let base: number;
  if (prev.reps <= 0) {
    base = 1;
  } else if (prev.reps === 1) {
    base = 6;
  } else {
    base = prev.intervalDays * ease;
  }
  return base * multiplier;
}

/** Clamp an ease factor to SM-2's minimum of 1.3. */
function floorEase(ease: number): number {
  return ease < MIN_EASE ? MIN_EASE : ease;
}
