/**
 * Reifegrad und Prüfungsreife-Ampel.
 *
 * ## Was diese Datei beantwortet
 *
 * „Wie weit bin ich?“ – und zwar so, dass die Antwort einer Nachprüfung
 * standhält. Bis Fassung 0.11.0 hieß die Kennzahl `sicher` und bedeutete
 * *die letzte Antwort war richtig*. Das ist eine Ein-Antwort-Metrik und damit
 * flacher als die, die `PLAN.md` bei einem Mitbewerber kritisiert.
 *
 * ## Warum nicht einfach die FSRS-Abrufwahrscheinlichkeit
 *
 * Der naheliegende Ersatz wäre gewesen: für jede Frage `R` aus dem
 * Gedächtnismodell nehmen und mitteln. Nachgemessen an den Konstanten dieses
 * Projekts ergibt das eine Zahl, die **unehrlicher** ist als die abgelöste:
 *
 * | Katalog einmal durch, je Frage eine Antwort | Mittleres `R` | von 486 |
 * |---|---|---|
 * | 0 % richtig | 0,766 | 372 |
 * | 20 % richtig | 0,802 | 390 |
 * | 50 % richtig | 0,857 | 416 |
 *
 * Wer **jede** Frage genau einmal **falsch** beantwortet, stünde bei 372 von
 * 486. Der Grund ist kein Rechenfehler, sondern eine Verwechslung: `R` ist
 * eine **Planungsgröße**, keine Könnensmessung. Der Stabilitätswert nach der
 * ersten Antwort ist ein Vorgabewert aus der FSRS-Eichung – eine Annahme,
 * keine Messung. Und dieses Programm füttert das Modell mit einem Klick auf
 * eine von mehreren Antwortmöglichkeiten, den `bewertungAusErgebnis`
 * ausnahmslos als „gut“ wertet: Geraten und Gewusst sind für das Modell
 * dasselbe.
 *
 * Schlimmer noch: Die Zahl **stieg beim Falschantworten**. Eine überfällige
 * Frage mit `S = 2,31` und 20 Tagen Abstand steht bei `R = 0,707`; nach einem
 * „Nicht gewusst“ steht sie bei `R = 0,884`, weil der neue Stand wieder am
 * ersten Tag gemessen wird. In allen fünf nachgerechneten Fällen stieg sie.
 *
 * ## Die Regel: erst der belegte Abruf zählt
 *
 * Eine Frage geht erst in den Reifegrad ein, wenn sie **nach mindestens einem
 * Tag Abstand richtig beantwortet** wurde. Vorher zählt sie null.
 *
 * Diese Grenze ist nicht erfunden. `fsrs.ts` zieht sie bereits selbst: Unter
 * einem Tag Abstand greift dort die Kurzfristformel, weil „die
 * Vergessenskurve kein brauchbarer Maßstab“ ist. Was innerhalb eines Tages
 * geschieht, ist Wiedererkennen; Erinnern zeigt sich erst über Nacht.
 *
 * Der Beleg ist eine **Aussage über das Jetzt**, kein Orden für früher: Eine
 * falsche Antwort nimmt ihn wieder weg, gleich wie lange die Frage vorher
 * saß. Daraus folgt die Eigenschaft, an der der erste Entwurf gescheitert
 * war – **die Zahl kann durch eine falsche Antwort nie steigen.**
 *
 * ## Die Zahl ist eine Untergrenze, keine Prognose
 *
 * Sie sagt, was belegt sitzt, nicht was in der Prüfung herauskäme. Raten ist
 * nicht eingerechnet, Ungesehenes gilt als nicht gekonnt, und ein Beleg
 * verfällt mit der Zeit.
 *
 * Die Belegregel heilt allerdings nur die **Anzeige**, nicht die
 * Terminierung: Für die Wiedervorlage zählt die erratene Antwort weiter als
 * „gut“. Was das kostet und warum dort nichts geändert wird, ist in
 * `docs/entscheidung-ratewahrscheinlichkeit.md` nachgemessen – kurz: Ein
 * Rater mit Trefferchance 1/3 bekommt die Frage an 53 von 61 Tagen wieder
 * vorgelegt, und der Beleg steht dabei nur an 40 Prozent der Tage. Damit setzt sie dieselbe Haltung fort, die
 * `lernplan.ts` im Modulkopf festhält: eher zu niedrig als zu hoch. Wer diese
 * Grenze überschreitet, hat Luft – wie viel, sagt das Programm nicht, weil es
 * das nicht weiß.
 *
 * Die Begründung der Schwellen und die Messwerte stehen in
 * `docs/entscheidung-reifegrad.md`.
 */

import { abrufwahrscheinlichkeit } from './fsrs';

/**
 * Kleinster Abstand, mit dem gerechnet wird.
 *
 * `abrufwahrscheinlichkeit(S, 0)` ist für **jedes** `S` exakt 1 – ohne diese
 * Untergrenze stünde eine gerade eben beantwortete Frage bei 100 Prozent.
 * `tageZwischen` im Anwendungskern liefert Bruchteile von Tagen, der Fall
 * tritt also bei jeder Sitzung ein.
 */
export const MINDESTABSTAND_TAGE = 1;

/**
 * Strengste Bestehensgrenze, die dieses Programm kennt.
 *
 * Aus `pruefung.ts`: Das Profil „Standard“ verlangt 80 Prozent, „nach Art
 * privater Lehrgangsträger“ höchstens 15 Fehler auf 75 Fragen – also
 * ebenfalls 60 von 75, das sind 80 Prozent. Die übrigen festen Profile liegen
 * darunter (75 und 70 Prozent). Das anpassbare Profil bleibt außen vor: Was
 * der Nutzer selbst einstellen kann, taugt nicht als Maßstab.
 */
export const SCHWELLE_KIPPE = 0.8;

/**
 * Ab hier gilt der Stand als prüfungsreif.
 *
 * Fünf Punkte über der strengsten Bestehensgrenze. Der Abstand ist kein
 * Sicherheitszuschlag aus dem Bauch, sondern deckt das, was zwischen einem
 * Katalog und einem Bogen liegt: Eine Prüfung zieht 75 bis 100 Fragen aus 575
 * und trifft dabei nicht den Durchschnitt. Wer genau auf der Grenze steht,
 * besteht bei günstiger Ziehung und fällt bei ungünstiger durch.
 *
 * Bewusst **keine** Wahrscheinlichkeitsangabe daneben. Eine Zahl wie „neun von
 * zehn Läufen“ setzte voraus, dass alle Fragen dieselbe Trefferchance haben –
 * das Gegenteil ist der Fall, die Verteilung ist zweigipflig. Die Rechnung
 * wäre exakt aussehend und falsch.
 */
export const SCHWELLE_REIF = 0.85;

/** Stand einer einzelnen Frage, wie ihn die Reiferechnung braucht. */
export interface ReifeFrage {
  /** Abschnitt, sonst Kapitel – die Ebene der Bereichsaufschlüsselung. */
  readonly bereich: string;
  /** FSRS-Stabilität in Tagen; `null`, solange nie beantwortet. */
  readonly stabilitaet: number | null;
  /** Tage seit der letzten Antwort, als Bruchzahl. */
  readonly tageSeitAntwort: number;
  /** Ob der Abruf belegt ist – siehe Modulkopf. */
  readonly bestaetigt: boolean;
}

/**
 * Beitrag einer einzelnen Frage, `zusatzTage` in der Zukunft.
 *
 * @returns Wert zwischen 0 und 1. Ohne Beleg immer 0.
 */
export function abrufFuer(frage: ReifeFrage, zusatzTage = 0): number {
  if (!frage.bestaetigt || frage.stabilitaet === null) {
    return 0;
  }
  const tage = Math.max(MINDESTABSTAND_TAGE, frage.tageSeitAntwort + zusatzTage);
  return abrufwahrscheinlichkeit(frage.stabilitaet, tage);
}

/**
 * Reifegrad über eine Fragenmenge: der Mittelwert der Einzelbeiträge.
 *
 * Ungesehene und unbelegte Fragen bleiben **im Nenner**. Sie herauszunehmen
 * ergäbe eine Zahl, die bei der ersten belegten Frage auf 100 Prozent
 * springt – die häufigste Art, eine Fortschrittsanzeige zu belügen.
 *
 * @param zusatzTage Blick in die Zukunft, für die Prognose zum Prüfungstermin.
 */
export function reifegradVon(fragen: readonly ReifeFrage[], zusatzTage = 0): number {
  if (fragen.length === 0) {
    return 0;
  }
  let summe = 0;
  for (const frage of fragen) {
    summe += abrufFuer(frage, zusatzTage);
  }
  return summe / fragen.length;
}

/**
 * Die Zahl, die in der Oberfläche steht.
 *
 * Gerundet, nicht abgeschnitten: Abschneiden wäre um bis zu eine ganze Frage
 * zu pessimistisch, und die Zahl ist bereits eine Untergrenze – ein zweiter
 * Abschlag darauf wäre keine Vorsicht mehr, sondern eine zweite Verzerrung.
 */
export function belegteFragen(reifegrad: number, fragenGesamt: number): number {
  return Math.round(reifegrad * fragenGesamt);
}

/**
 * Wie viele Fragen eine Schwelle bei dieser Menge verlangt.
 *
 * Aufgerundet: 0,85 von 486 sind 413,1 Fragen, und 413 erreichen die Quote
 * nicht. Dieselbe Rechnung wie `benoetigteTreffer` in `pruefung.ts`, aus
 * demselben Grund.
 */
export function benoetigteFragen(schwelle: number, fragenGesamt: number): number {
  return Math.ceil(schwelle * fragenGesamt);
}

/**
 * Die Stufen der Ampel.
 *
 * `ohne_beleg` ist keine vierte Farbe, sondern der ehrliche Sonderfall: Wer
 * noch keine Frage ein zweites Mal wiedergesehen hat, bekommt keine Einstufung
 * vorgegaukelt, sondern die Auskunft, woran das liegt.
 */
export type Reifestufe = 'ohne_beleg' | 'zurueck' | 'kippe' | 'reif';

/** Das Wort, das in der Oberfläche steht. */
export const STUFE_WORT: Readonly<Record<Reifestufe, string>> = Object.freeze({
  ohne_beleg: 'Noch kein belegter Stand',
  zurueck: 'Noch nicht so weit',
  kippe: 'Auf der Kippe',
  reif: 'Prüfungsreif',
});

/**
 * Einstufung – **an der angezeigten Zahl, nicht am Bruchwert**.
 *
 * Der Unterschied ist keine Feinheit. Entschiede die Stufe am ungerundeten
 * Reifegrad und nennte der Satz die gerundete Zahl, gäbe es ein Band, in dem
 * „Prüfungsreif“ neben einer Zahl steht, die eine Zeile tiefer als noch nicht
 * ausreichend ausgewiesen ist. So gilt: Die Stufe ist genau dann erreicht,
 * wenn die genannte Zahl die genannte Zielzahl erreicht.
 */
export function stufeFuer(belegt: number, fragenGesamt: number): Reifestufe {
  if (fragenGesamt === 0 || belegt === 0) {
    return 'ohne_beleg';
  }
  if (belegt >= benoetigteFragen(SCHWELLE_REIF, fragenGesamt)) {
    return 'reif';
  }
  if (belegt >= benoetigteFragen(SCHWELLE_KIPPE, fragenGesamt)) {
    return 'kippe';
  }
  return 'zurueck';
}

/**
 * Die Gesamtstufe, gedeckelt durch einen zurückliegenden K.-o.-Bereich.
 *
 * Manche Prüfungsordnungen lassen in Notwehr und Notstand höchstens zwei
 * Fehler zu, gleich wie gut der Rest ist (`pruefung.ts`, `koKriterien`). Wer
 * insgesamt bei 88 Prozent steht und dort bei 60, fällt sicher durch. Eine
 * Ampel namens „Prüfungsreife“, die das verschweigt, wäre gefährlicher als
 * gar keine – deshalb deckelt der schwächste K.-o.-Bereich das Gesamturteil.
 *
 * Gedeckelt wird auf `kippe`, nicht auf `zurueck`: Der übrige Stand ist ja
 * vorhanden, es fehlt eine benannte Stelle. Wer ohnehin schon zurückliegt,
 * bleibt dort.
 */
export function gesamtstufeMitDeckel(
  gesamt: Reifestufe,
  kokriterien: readonly Reifestufe[],
): Reifestufe {
  if (gesamt !== 'reif') {
    return gesamt;
  }
  return kokriterien.every((stufe) => stufe === 'reif') ? 'reif' : 'kippe';
}

/**
 * Der Kernsatz zu einem Stand – **einmal formuliert, überall derselbe**.
 *
 * Steht hier und nicht in der Komponente, weil ihn drei Stellen brauchen:
 * der Startbildschirm, der PDF-Lernbericht und das Handbuch. Drei
 * Formulierungen desselben Sachverhalts driften auseinander, sobald eine
 * Schwelle sich ändert – und dann behauptet der ausgedruckte Bericht etwas
 * anderes als der Bildschirm, von dem er stammt.
 *
 * Die Zahl steht **vorn**, das Urteil dahinter: „413 von 486 Fragen sitzen
 * belegt“ ist überprüfbar, „prüfungsreif“ ist eine Auslegung davon. Wer nur
 * den Anfang liest, hat die Tatsache; wer weiterliest, bekommt die Einordnung.
 */
export function reifesatz(
  belegt: number,
  fragenGesamt: number,
  stufe: Reifestufe,
  deckelnd: readonly string[] = [],
): string {
  if (stufe === 'ohne_beleg') {
    return (
      'Noch keine Frage ist belegt: Dafür muss eine Frage nach mindestens einem Tag Abstand ' +
      'noch einmal richtig beantwortet werden. Beim ersten Mal zählt sie nicht mit – ' +
      'Wiedererkennen ist kein Erinnern.'
    );
  }

  const kern = `${String(belegt)} von ${String(fragenGesamt)} Fragen sitzen belegt.`;

  /*
    Die Einordnung des Gesamtstands – **ohne** den Deckel eines
    K.-o.-Bereichs. Genau darauf kam es an: `stufe` ist bereits gedeckelt,
    und aus ihr allein liesse sich nicht ablesen, ob der Gesamtstand für
    sich genommen reicht.
  */
  const ohneDeckel = stufeFuer(belegt, fragenGesamt);
  const einordnung =
    ohneDeckel === 'reif'
      ? 'Das liegt über der strengsten Bestehensgrenze dieses Programms.'
      : ohneDeckel === 'kippe'
        ? `Das ist genau die strengste Bestehensgrenze dieses Programms – ohne jeden ` +
          `Abstand. Für „${STUFE_WORT.reif}“ wären ` +
          `${String(benoetigteFragen(SCHWELLE_REIF, fragenGesamt))} nötig.`
        : `Die strengste Bestehensgrenze dieses Programms verlangt ` +
          `${String(benoetigteFragen(SCHWELLE_KIPPE, fragenGesamt))}.`;

  if (deckelnd.length > 0) {
    const bereiche = deckelnd.join(' und ');
    const nachsatz =
      'Manche Prüfungsstellen lassen dort nur zwei Fehler zu, gleich wie gut der Rest ist.';

    /*
      „Insgesamt reicht das“ nur, wenn es das auch tut.

      Bis Fassung 0.19.1 stand dieser Satz unbedingt, sobald ein K.-o.-Bereich
      zurücklag – gemeldet mit „2 von 486 Fragen sitzen belegt. Insgesamt
      reicht das – aber I.5 liegt zurück.“ Bei 2 von 486 reicht überhaupt
      nichts. Der Deckel ist nur dann die Nachricht, wenn der Gesamtstand
      allein genügen würde; sonst ist er ein Zusatz zu einer Einordnung, die
      ohnehin nicht trägt.

      Der Grund steht in beiden Fällen vor der Ampelfarbe, nicht dahinter:
      Wer „prüfungsreif“ läse und den Nachsatz überginge, ginge mit einer
      Lücke in die Prüfung, die ihn unabhängig vom Rest durchfallen lässt.
    */
    return ohneDeckel === 'reif'
      ? `${kern} Insgesamt reicht das – aber ${bereiche} liegt zurück. ${nachsatz}`
      : `${kern} ${einordnung} Dazu liegt ${bereiche} zurück. ${nachsatz}`;
  }

  return `${kern} ${einordnung}`;
}
