/**
 * FSRS-6 – das Gedächtnismodell hinter der Wiedervorlage.
 *
 * FSRS (Free Spaced Repetition Scheduler) beschreibt den Gedächtnisstand einer
 * Frage durch zwei Zahlen:
 *
 * - **Stabilität** `S` (in Tagen): die Zeitspanne, nach der die
 *   Abrufwahrscheinlichkeit auf 90 % gefallen ist. Je größer, desto fester
 *   sitzt der Stoff.
 * - **Schwierigkeit** `D` (1 bis 10): wie mühsam sich die Frage merken lässt.
 *   Sie wächst bei Fehlern und sinkt bei mühelosen Treffern.
 *
 * Daraus folgt die **Abrufwahrscheinlichkeit** `R`: die geschätzte Chance, die
 * Frage jetzt richtig zu beantworten. Sie ist der eigentliche Nutzen des
 * Modells – aus ihr lassen sich sowohl der nächste Termin als auch eine
 * ehrliche Prognose für den Prüfungstag ableiten.
 *
 * ## Herkunft und Genauigkeit
 *
 * Die Formeln und Vorgabeparameter sind aus der Referenzimplementierung
 * py-fsrs (open-spaced-repetition, MIT) übernommen und in
 * `tests/fsrs.test.ts` gegen deren Kennwerte abgesichert. Bewusst nachgebaut
 * statt als Abhängigkeit eingebunden: Das Modell ist der didaktische Kern der
 * Anwendung und soll ohne Fremdcode nachvollziehbar bleiben. Jede Formel steht
 * deshalb einzeln und benannt hier.
 *
 * FSRS selbst ist ein Algorithmus, kein Programm – seine Verwendung ist an
 * keine Lizenz gebunden. Die Herkunft ist trotzdem im Bereich „Über diese
 * Software“ ausgewiesen.
 *
 * ## Was hier NICHT steht
 *
 * FSRS plant für dauerhaftes Behalten ohne Stichtag. Diese Anwendung lernt auf
 * einen Prüfungstermin hin. Die Anpassung daran – steigende Zielquote,
 * ausdrücklich **ohne** Deckelung am Termin – steht getrennt in
 * `lernplan.ts`, damit erkennbar bleibt, was Referenzverfahren ist und was
 * eigene Entscheidung.
 *
 * Dieser Satz nannte bis Fassung 0.16.0 eine „Deckelung am Termin“, die es
 * nie gab und die `lernplan.ts` zwei Dateien weiter ausdrücklich verneint.
 * Warum es sie nicht gibt, steht in `docs/entscheidung-lernphasen.md`.
 */

/** Bewertung einer Antwort – entspricht den FSRS-Graden 1 bis 4. */
export type FsrsGrad = 1 | 2 | 3 | 4;

export const GRAD_NOCHMAL: FsrsGrad = 1;
export const GRAD_SCHWER: FsrsGrad = 2;
export const GRAD_GUT: FsrsGrad = 3;
export const GRAD_LEICHT: FsrsGrad = 4;

/**
 * Die 21 Vorgabeparameter von FSRS-6.
 *
 * Sie stammen aus der Auswertung mehrerer Millionen echter Wiederholungen und
 * gelten für den Durchschnitt. FSRS kann sie aus der eigenen Historie
 * nachtrainieren; das setzt einige hundert Wiederholungen voraus und ist hier
 * noch nicht umgesetzt. Bis dahin sind die Vorgabewerte die beste verfügbare
 * Schätzung – und deutlich besser als ein festes Verdopplungsschema.
 */
export const FSRS_PARAMETER: readonly number[] = Object.freeze([
  /*  0 */ 0.212, // Anfangsstabilität nach „nochmal“
  /*  1 */ 1.2931, // Anfangsstabilität nach „schwer“
  /*  2 */ 2.3065, // Anfangsstabilität nach „gut“
  /*  3 */ 8.2956, // Anfangsstabilität nach „leicht“
  /*  4 */ 6.4133, // Anfangsschwierigkeit: Achsenabschnitt
  /*  5 */ 0.8334, // Anfangsschwierigkeit: Steigung über den Grad
  /*  6 */ 3.0194, // Schwierigkeitsänderung je Grad
  /*  7 */ 0.001, // Rückzug zur Mitte
  /*  8 */ 1.8722, // Stabilitätszuwachs: Grundfaktor
  /*  9 */ 0.1666, // Stabilitätszuwachs: Dämpfung durch bisherige Stabilität
  /* 10 */ 0.796, // Stabilitätszuwachs: Belohnung für späte Wiederholung
  /* 11 */ 1.4835, // Stabilität nach Vergessen: Grundfaktor
  /* 12 */ 0.0614, // Stabilität nach Vergessen: Dämpfung durch Schwierigkeit
  /* 13 */ 0.2629, // Stabilität nach Vergessen: Einfluss der alten Stabilität
  /* 14 */ 1.6483, // Stabilität nach Vergessen: Einfluss des Abrufs
  /* 15 */ 0.6014, // Abschlag für „schwer“
  /* 16 */ 1.8729, // Zuschlag für „leicht“
  /* 17 */ 0.5425, // Wiederholung am selben Tag: Grundfaktor
  /* 18 */ 0.0912, // Wiederholung am selben Tag: Verschiebung
  /* 19 */ 0.0658, // Wiederholung am selben Tag: Dämpfung
  /* 20 */ 0.1542, // Abfall der Vergessenskurve
]);

/** Untergrenze der Stabilität. Verhindert Division durch null. */
export const MIN_STABILITAET = 0.001;

const MIN_SCHWIERIGKEIT = 1;
const MAX_SCHWIERIGKEIT = 10;

/** Gedächtnisstand einer einzelnen Frage bei einer Person. */
export interface Gedaechtnisstand {
  /** Stabilität in Tagen – nach dieser Zeit liegt der Abruf bei 90 %. */
  readonly stabilitaet: number;
  /** Schwierigkeit zwischen 1 und 10. */
  readonly schwierigkeit: number;
}

function p(index: number): number {
  const wert = FSRS_PARAMETER[index];
  /* Kann nur eintreten, wenn jemand FSRS_PARAMETER kürzt – dann ist ein
     klarer Fehler besser als stilles NaN, das sich bis in die Termine zieht. */
  if (wert === undefined) {
    throw new Error(`FSRS: Parameter ${String(index)} fehlt.`);
  }
  return wert;
}

function begrenzen(wert: number, min: number, max: number): number {
  return Math.min(Math.max(wert, min), max);
}

/** Abfall der Vergessenskurve. Negativ, weil die Wahrscheinlichkeit fällt. */
const ABFALL = -p(20);

/**
 * Streckfaktor der Vergessenskurve.
 *
 * So gewählt, dass `R(S, S) = 0,9` gilt – nach genau einer Stabilitätsdauer
 * liegt der Abruf definitionsgemäß bei 90 %. Das ist keine freie Konstante,
 * sondern folgt zwingend aus {@link ABFALL}.
 */
const STRECKUNG = Math.pow(0.9, 1 / ABFALL) - 1;

/**
 * Abrufwahrscheinlichkeit `R` nach `tage` Tagen ohne Wiederholung.
 *
 * Die Vergessenskurve ist eine Potenzfunktion, keine Exponentialfunktion:
 * Vergessen verlangsamt sich mit der Zeit, statt gleichmäßig weiterzulaufen.
 *
 * @returns Wert zwischen 0 und 1; 0 für eine nie beantwortete Frage.
 */
export function abrufwahrscheinlichkeit(stabilitaet: number, tage: number): number {
  if (!Number.isFinite(stabilitaet) || stabilitaet <= 0) {
    return 0;
  }
  const vergangen = Number.isFinite(tage) ? Math.max(0, tage) : 0;
  return Math.pow(1 + (STRECKUNG * vergangen) / stabilitaet, ABFALL);
}

/**
 * Abstand in Tagen, nach dem die Abrufwahrscheinlichkeit auf `zielquote`
 * gefallen ist – die Umkehrung von {@link abrufwahrscheinlichkeit}.
 *
 * Bei einer Zielquote von 0,9 ist das Ergebnis genau die Stabilität.
 */
export function intervallTage(stabilitaet: number, zielquote: number): number {
  if (!Number.isFinite(stabilitaet) || stabilitaet <= 0) {
    return 0;
  }
  const quote = begrenzen(zielquote, 0.5, 0.999);
  return (stabilitaet / STRECKUNG) * (Math.pow(quote, 1 / ABFALL) - 1);
}

/**
 * Anfangsschwierigkeit. Bewusst ohne Begrenzung, weil der ungekappte Wert für
 * „leicht“ als Zielpunkt des Rückzugs zur Mitte gebraucht wird.
 */
function ersteSchwierigkeit(grad: FsrsGrad): number {
  return p(4) - Math.exp(p(5) * (grad - 1)) + 1;
}

/** Gedächtnisstand nach der allerersten Beantwortung einer Frage. */
export function ersterStand(grad: FsrsGrad): Gedaechtnisstand {
  return {
    stabilitaet: Math.max(p(grad - 1), MIN_STABILITAET),
    schwierigkeit: begrenzen(ersteSchwierigkeit(grad), MIN_SCHWIERIGKEIT, MAX_SCHWIERIGKEIT),
  };
}

/**
 * Neue Schwierigkeit nach einer Antwort.
 *
 * Zwei Mechanismen greifen ineinander:
 *
 * 1. **Lineare Dämpfung** – je schwerer eine Frage schon ist, desto weniger
 *    verschiebt eine weitere Antwort sie noch. Ohne das würden ein paar
 *    Fehlversuche jede Frage dauerhaft an den Anschlag drücken.
 * 2. **Rückzug zur Mitte** – die Schwierigkeit driftet langsam zu dem Wert
 *    zurück, den eine mühelos beantwortete Frage hätte. Das verhindert, dass
 *    ein schlechter Tag eine Frage für immer als schwer abstempelt.
 */
export function naechsteSchwierigkeit(schwierigkeit: number, grad: FsrsGrad): number {
  const aenderung = -(p(6) * (grad - 3));
  const gedaempft = schwierigkeit + ((10 - schwierigkeit) * aenderung) / 9;
  const zurueckgezogen = p(7) * ersteSchwierigkeit(GRAD_LEICHT) + (1 - p(7)) * gedaempft;
  return begrenzen(zurueckgezogen, MIN_SCHWIERIGKEIT, MAX_SCHWIERIGKEIT);
}

/**
 * Stabilität nach einer richtigen Antwort.
 *
 * Der Zuwachs ist umso größer, je *unwahrscheinlicher* der Treffer war – wer
 * eine Frage kurz vor dem Vergessen noch hinbekommt, festigt sie am stärksten.
 * Genau darauf zielt verteiltes Lernen.
 */
function stabilitaetNachTreffer(
  schwierigkeit: number,
  stabilitaet: number,
  abruf: number,
  grad: FsrsGrad,
): number {
  const abschlagSchwer = grad === GRAD_SCHWER ? p(15) : 1;
  const zuschlagLeicht = grad === GRAD_LEICHT ? p(16) : 1;

  return (
    stabilitaet *
    (1 +
      Math.exp(p(8)) *
        (11 - schwierigkeit) *
        Math.pow(stabilitaet, -p(9)) *
        (Math.exp((1 - abruf) * p(10)) - 1) *
        abschlagSchwer *
        zuschlagLeicht)
  );
}

/**
 * Stabilität nach einer falschen Antwort.
 *
 * Der Fortschritt wird zurückgesetzt, aber nicht gelöscht: Wer eine Frage
 * schon einmal konnte, lernt sie beim zweiten Mal schneller wieder. Die
 * Obergrenze stellt sicher, dass ein Fehler die Stabilität niemals erhöht.
 */
function stabilitaetNachFehler(schwierigkeit: number, stabilitaet: number, abruf: number): number {
  const langfristig =
    p(11) *
    Math.pow(schwierigkeit, -p(12)) *
    (Math.pow(stabilitaet + 1, p(13)) - 1) *
    Math.exp((1 - abruf) * p(14));

  const obergrenze = stabilitaet / Math.exp(p(17) * p(18));

  return Math.min(langfristig, obergrenze);
}

/**
 * Stabilität bei einer Wiederholung am selben Tag.
 *
 * Ohne verstrichene Zeit sagt die Vergessenskurve nichts aus – deshalb eine
 * eigene Formel. „schwer“, „gut“ und „leicht“ dürfen die Stabilität dabei nie
 * senken; eine nicht-falsche Antwort soll nicht bestrafen.
 */
function stabilitaetAmSelbenTag(stabilitaet: number, grad: FsrsGrad): number {
  const zuwachs = Math.exp(p(17) * (grad - 3 + p(18))) * Math.pow(stabilitaet, -p(19));
  const wirksam = grad >= GRAD_SCHWER ? Math.max(zuwachs, 1) : zuwachs;
  return Math.max(stabilitaet * wirksam, MIN_STABILITAET);
}

/**
 * Schreibt den Gedächtnisstand nach einer Antwort fort.
 *
 * @param stand        Bisheriger Stand, oder `null` bei der ersten Antwort.
 * @param grad         Selbsteinschätzung von 1 (nochmal) bis 4 (leicht).
 * @param abstandTage  Tage seit der letzten Antwort auf diese Frage.
 */
export function naechsterStand(
  stand: Gedaechtnisstand | null,
  grad: FsrsGrad,
  abstandTage: number,
): Gedaechtnisstand {
  if (stand === null) {
    return ersterStand(grad);
  }

  const abstand = Number.isFinite(abstandTage) ? Math.max(0, abstandTage) : 0;
  const schwierigkeit = naechsteSchwierigkeit(stand.schwierigkeit, grad);

  /* Unter einem Tag Abstand ist die Vergessenskurve kein brauchbarer Maßstab –
     dann greift die Kurzfristformel. */
  if (abstand < 1) {
    return {
      stabilitaet: stabilitaetAmSelbenTag(stand.stabilitaet, grad),
      schwierigkeit,
    };
  }

  const abruf = abrufwahrscheinlichkeit(stand.stabilitaet, abstand);
  const stabilitaet =
    grad === GRAD_NOCHMAL
      ? stabilitaetNachFehler(stand.schwierigkeit, stand.stabilitaet, abruf)
      : stabilitaetNachTreffer(stand.schwierigkeit, stand.stabilitaet, abruf, grad);

  return { stabilitaet: Math.max(stabilitaet, MIN_STABILITAET), schwierigkeit };
}
