/**
 * Lernplan – FSRS auf einen Prüfungstermin hin.
 *
 * FSRS (siehe `fsrs.ts`) plant für dauerhaftes Behalten: Es hält die
 * Abrufwahrscheinlichkeit jeder Frage bei rund 90 % und dehnt die Abstände
 * dabei immer weiter. Für eine Prüfung an einem festen Tag ist das nicht ganz
 * das Richtige. Hier zählt ein anderes Ziel: **an genau diesem einen Tag soll
 * möglichst viel abrufbar sein.**
 *
 * Dieses Modul ist die Anpassung daran. Es steht bewusst getrennt von
 * `fsrs.ts`, damit erkennbar bleibt, was belegtes Referenzverfahren ist und
 * was eigene Entscheidung. Alles hier ist eigene Entscheidung.
 *
 * ## Die beiden Regeln
 *
 * **1. Die Zielquote steigt, je näher der Termin rückt.**
 * Weit vor der Prüfung genügen 90 % – wer zu früh zu oft wiederholt,
 * verschwendet Zeit, die für neue Fragen fehlt. In den letzten Wochen wird
 * die Zielquote schrittweise auf 97 % angehoben. Dadurch rücken die Termine
 * von selbst zusammen und der Stoff ist am Prüfungstag frisch.
 *
 * **2. Es wird nicht auf den Termin gedeckelt.**
 * Nachgerechnet und bewusst so gelassen – siehe
 * `docs/entscheidung-lernphasen.md`. Jede geprüfte Kappung schiebt alles, was
 * über den Termin hinausginge, auf den Prüfungstag selbst; gemessen 432 von
 * 486 Fragen an einem Tag.
 * Naheliegend wäre, jede Frage vor der Prüfung noch einmal vorzulegen. Das
 * wäre aber verschwendete Zeit: Eine Frage mit 60 Tagen Stabilität sitzt zehn
 * Tage vor der Prüfung noch zu 97,7 % – sie braucht keine Wiederholung. Genau
 * diese Fragen filtert Regel 1 bereits heraus, ohne dass eine zweite Regel
 * nötig ist. Wer wirklich alles noch einmal sehen will, hat dafür die
 * Prüfungssimulation.
 *
 * ## Ehrlichkeit der Prognose
 *
 * Die Prognose ist der Mittelwert der Abrufwahrscheinlichkeiten über den
 * **gesamten** Katalog. Eine nie beantwortete Frage geht mit 0 ein. Das ist
 * bewusst streng: Geraten wird nicht eingerechnet, und ungesehene Fragen
 * gelten als nicht gekonnt. Die Zahl ist damit eher zu niedrig als zu hoch –
 * in dieser Richtung schadet ein Fehler niemandem.
 */

import { intervallTage, naechsterStand, type FsrsGrad, type Gedaechtnisstand } from './fsrs';
import { reifegradVon, type ReifeFrage } from './reife';

/** Zielquote ohne Prüfungstermin – der Vorgabewert von FSRS. */
export const ZIELQUOTE_GRUND = 0.9;

/** Zielquote am Prüfungstag. */
export const ZIELQUOTE_TERMIN = 0.97;

/**
 * Ab dieser Zahl von Tagen vor dem Termin greift die Anhebung.
 *
 * Vier Wochen: kurz genug, dass die zusätzlichen Wiederholungen nicht die
 * ganze Vorbereitungszeit auffressen, lang genug, dass die Anhebung sanft
 * verläuft und nicht als plötzlicher Sprung erlebt wird.
 */
export const ANHEBUNG_AB_TAGEN = 28;

/**
 * Umfang einer Lernsitzung – zugleich der Tagesvorschlag ohne Prüfungstermin.
 *
 * Steht hier und nicht in der Oberfläche, damit der vorgeschlagene Tagesplan
 * und die Schaltfläche „Weiterlernen“ nicht auseinanderlaufen können.
 */
export const SITZUNGSUMFANG = 20;

/** Angenommene Bearbeitungsdauer, solange keine eigene Historie vorliegt. */
export const SEKUNDEN_PRO_FRAGE_VORGABE = 25;

/**
 * Wie viele Tage die Einführung neuer Fragen nach vorn gezogen wird.
 *
 * Eine Frage, die zum ersten Mal am Vorabend auftaucht, ist am Prüfungstag
 * nicht gefestigt. Der Puffer zieht die Einführung deshalb nach vorn, damit
 * möglichst jede Frage wenigstens eine Wiederholung erlebt. Er wächst nicht
 * über drei Tage hinaus – bei kurzer Vorbereitungszeit wäre das sonst der
 * größte Teil davon.
 *
 * **Er stoppt die Einführung nicht.** Der Kommentar behauptete das bis
 * Fassung 0.16.0 („Tage, an denen keine neuen Fragen mehr eingeführt
 * werden“), und der Code leistete es nie: Der Puffer ist ein kleinerer
 * Divisor, ab vier Tagen Rest ist er null, und die Rate wird täglich neu
 * gebildet. Wer zurückliegt, bekommt bis zum Vortag neue Fragen angetragen –
 * und dazu von {@link machbarkeitFuer} die Auskunft, dass das nicht mehr zu
 * schaffen ist. Am Prüfungstag selbst kommen keine mehr dazu, weil ein Beleg
 * an diesem Tag nicht mehr entstehen kann.
 */
export const PUFFER_TAGE_MAX = 3;

/** Einschätzung, ob das Pensum in der verbleibenden Zeit zu schaffen ist. */
export type Machbarkeit =
  'kein_termin' | 'termin_vorbei' | 'entspannt' | 'machbar' | 'knapp' | 'zu_wenig_zeit';

/** Empfohlenes Tagespensum. */
export interface Tagespensum {
  /** Neue, noch nie beantwortete Fragen. */
  readonly neu: number;
  /** Heute zur Wiederholung fällige Fragen. */
  readonly wiederholung: number;
  /** Summe aus beidem. */
  readonly gesamt: number;
  /** Geschätzte Dauer in Minuten, auf Basis des eigenen bisherigen Tempos. */
  readonly minuten: number;
}

/** Der vollständige Lernplan, wie ihn die Oberfläche anzeigt. */
export interface Lernplan {
  /** Prüfungstermin als ISO-Datum, oder `null`, wenn keiner gesetzt ist. */
  readonly termin: string | null;
  /** Volle Tage bis zum Termin. 0 = heute, negativ = vorbei. */
  readonly tageBisTermin: number | null;
  readonly gesamtFragen: number;
  readonly nieBeantwortet: number;
  readonly faellig: number;
  /** Aktuell angesetzte Zielquote zwischen {@link ZIELQUOTE_GRUND} und {@link ZIELQUOTE_TERMIN}. */
  readonly zielquote: number;
  /** Geschätzte Trefferquote, wenn heute geprüft würde. */
  readonly prognoseHeute: number;
  /**
   * Geschätzte Trefferquote am Prüfungstag, **wenn ab jetzt nicht mehr
   * gelernt wird**. Die Differenz zu {@link prognoseHeute} zeigt, was
   * Nichtstun kostet.
   */
  readonly prognoseAmTermin: number | null;
  readonly pensum: Tagespensum;
  readonly machbarkeit: Machbarkeit;
  /** Sekunden je Frage, die der Schätzung zugrunde liegen. */
  readonly sekundenProFrage: number;
  /**
   * Bereits angesetzte Wiederholungen je Tag, beginnend mit heute.
   *
   * Eine **Untergrenze**, keine Prognose: Was hier steht, ist der heutige
   * Kalenderstand; jede Antwort von heute setzt neue Termine. Die neuen
   * Fragen bleiben draußen – sie kommen mit der Einführungsrate, die als
   * eigene, planbare Größe daneben steht.
   *
   * Optional geführt wie die übrigen jüngeren Erweiterungen, damit die
   * Attrappen der Oberflächentests mit dem bisherigen Zuschnitt gültig
   * bleiben.
   */
  readonly vorschau?: readonly number[];
}

/**
 * Stand einer einzelnen Frage, wie ihn die Planung braucht.
 *
 * Erbt bewusst von {@link ReifeFrage}: Planung und Reifegrad rechnen auf
 * **derselben** Zeilenform, damit die Prognose des Lernplans und die Zahl der
 * Ampel nicht nur zufällig übereinstimmen, sondern dieselbe Rechnung sind.
 * Genau das Auseinanderlaufen war der Befund in `docs/stand.md` 7.1.
 */
export interface PlanFrage extends ReifeFrage {
  /** Ob die Frage heute zur Wiederholung ansteht. */
  readonly faellig: boolean;
  /**
   * In wie vielen Tagen die Frage ansteht; 0 heißt heute oder überfällig.
   *
   * `null` für nie beantwortete Fragen: Sie haben keinen Termin, sondern
   * warten auf die Einführungsrate. Gebraucht wird der Wert für die
   * Arbeitslast-Vorschau (siehe {@link Lernplan.vorschau}).
   */
  readonly faelligInTagen?: number | null;
}

/** Alles, was die Planung an Fakten braucht. Kommt aus der Datenbank. */
export interface Planungsdaten {
  readonly termin: string | null;
  readonly tageBisTermin: number | null;
  readonly fragen: readonly PlanFrage[];
  /** Bisheriges Tempo; `null`, solange zu wenig Historie vorliegt. */
  readonly sekundenProFrage: number | null;
}

/**
 * Untergrenze der Zielquote in einem Bereich mit K.-o.-Kriterium.
 *
 * **Warum es sie gibt.** Manche Prüfungsordnungen lassen in Notwehr und
 * Notstand höchstens zwei Fehler zu, gleich wie gut der Rest sitzt. Die
 * Reifeampel weiß das seit 0.22.0 und deckelt daran ihr Gesamturteil
 * ({@link gesamtstufeMitDeckel}) – der Planer wusste es nicht und behandelte
 * die 43 Fragen aus I.5 wie jede andere. Die Ampel warnte also, und die
 * Wiedervorlage handelte nicht danach.
 *
 * **Warum 0,95 und nicht 0,97.** Nachgerechnet mit den Parametern dieses
 * Projekts (Abfall 0,1542): Bei einer Stabilität von 60 Tagen ergibt 0,90 ein
 * Intervall von 60 Tagen, 0,95 eines von 24 und 0,97 eines von 13. Die
 * Terminquote dauerhaft anzulegen hieße, diese Fragen viereinhalbmal so oft
 * vorzulegen wie alle anderen – auch ein Jahr vor der Prüfung. 0,95 hält sie
 * spürbar enger, ohne die Sitzung zu füllen; und sobald der Termin naht,
 * steigt die allgemeine Quote ohnehin darüber und die Untergrenze greift
 * nicht mehr.
 */
export const ZIELQUOTE_KO = 0.95;

/**
 * Zielquote für den heutigen Tag.
 *
 * Ohne Termin bleibt es bei {@link ZIELQUOTE_GRUND}. Sonst steigt sie linear
 * an, sobald weniger als {@link ANHEBUNG_AB_TAGEN} Tage verbleiben, und
 * erreicht am Termin selbst {@link ZIELQUOTE_TERMIN}.
 */
export function zielquoteFuer(tageBisTermin: number | null, istKoBereich = false): number {
  /* Die Untergrenze steht am Ende und nicht als eigener Zweig: Sie soll die
     Anhebung zum Termin nicht ersetzen, sondern nur verhindern, dass ein
     K.-o.-Bereich fern vom Termin auf die Grundquote zurückfällt. */
  const untergrenze = istKoBereich ? ZIELQUOTE_KO : 0;
  if (tageBisTermin === null || !Number.isFinite(tageBisTermin)) {
    return Math.max(ZIELQUOTE_GRUND, untergrenze);
  }
  if (tageBisTermin >= ANHEBUNG_AB_TAGEN) {
    return Math.max(ZIELQUOTE_GRUND, untergrenze);
  }
  /* Nach dem Termin bleibt die Zielquote oben: Wer den Termin verstreichen
     lässt, ohne ihn zu ändern, lernt vermutlich für einen Nachholtermin. */
  const naehe = 1 - Math.max(0, tageBisTermin) / ANHEBUNG_AB_TAGEN;
  return Math.max(ZIELQUOTE_GRUND + (ZIELQUOTE_TERMIN - ZIELQUOTE_GRUND) * naehe, untergrenze);
}

/**
 * Tage, an denen noch neue Fragen eingeführt werden können.
 *
 * @see PUFFER_TAGE_MAX zur Begründung des Abzugs.
 */
export function einfuehrungstage(tageBisTermin: number): number {
  const puffer = Math.min(PUFFER_TAGE_MAX, Math.floor(tageBisTermin / 5));
  return Math.max(1, tageBisTermin - puffer);
}

/**
 * Neue Fragen für heute – das „neu“ des Tagespensums.
 *
 * Eine Funktion, zwei Verwender, und genau das ist der Punkt:
 * {@link lernplanBerechnen} zeigt die Zahl an, `Lernstand.sitzung()` deckelt
 * die neuen Fragen einer Sitzung damit. Anzeige und Sitzung sind dieselbe
 * Rechnung. Bis Fassung 0.20.0 war die Rate reine Anzeige – die Sitzung
 * füllte unabhängig davon mit neuen Fragen auf (`docs/stand.md` 7.11).
 *
 * Mit Termin werden die ungesehenen Fragen gleichmäßig über die
 * verbleibenden {@link einfuehrungstage} verteilt. Die Rate ist keine Menge,
 * die sich abarbeitet, sondern wird täglich neu gebildet – wer Tage
 * auslässt, bekommt danach von selbst mehr.
 *
 * Ohne Termin gibt es nichts zu verteilen – dann gilt {@link SITZUNGSUMFANG},
 * damit der Plan nicht „nichts zu tun“ meldet, während der halbe Katalog
 * ungesehen ist. Für die übliche Sitzung von 20 Fragen ist das zugleich das
 * bisherige Verhalten: Der Deckel liegt auf der Sitzungsgröße und greift
 * nicht. Nach dem Termin bleibt es bei 0: Wer den Termin nicht nachträgt,
 * bekommt keinen erfundenen Plan.
 *
 * **Am Prüfungstag selbst ebenfalls 0**, und der Grund ist keine Schonung,
 * sondern eine Ableitung aus der Belegregel: Eine Frage, die heute zum ersten
 * Mal auftaucht, kann nicht mehr belegt werden. Der Beleg verlangt eine
 * richtige Antwort nach mindestens einem Tag Abstand
 * (`MINDESTABSTAND_TAGE` in `reife.ts`), und diesen Tag gibt es nicht mehr.
 * Neue Fragen sind heute nicht knapp, sondern wirkungslos.
 *
 * Vorher stand hier `< 0`. Da `einfuehrungstage(0)` auf 1 zurückfällt, ergab
 * das am Prüfungstag `ceil(n / 1)` – der ganze Rest. Nachgerechnet: 300
 * ungesehene Fragen und 20 fällige ergaben am Morgen der Prüfung „Heute 320
 * Fragen, etwa 133 Minuten“.
 *
 * Der Vortag bleibt ausdrücklich, wie er ist: Dort ist der Beleg noch
 * erreichbar, mehr Tage gibt es nicht, und `machbarkeitFuer` sagt von selbst,
 * wenn das nicht mehr zu schaffen ist. Ein harter Einführungsstopp über
 * mehrere Tage wäre schlechter – er verstecke dreizehn ungesehene Fragen bei
 * drei Tagen Rest ganz.
 */
export function einfuehrungsrate(nieBeantwortet: number, tageBisTermin: number | null): number {
  if (tageBisTermin === null) {
    return Math.min(nieBeantwortet, SITZUNGSUMFANG);
  }
  if (tageBisTermin <= 0) {
    return 0;
  }
  return Math.min(nieBeantwortet, Math.ceil(nieBeantwortet / einfuehrungstage(tageBisTermin)));
}

/**
 * Einschätzung des Tagespensums.
 *
 * Bewertet wird die **Zeit**, nicht die Anzahl: Eine Stunde täglich ist eine
 * Aussage, die jeder einordnen kann; „62 Fragen“ ist es nicht.
 */
export function machbarkeitFuer(minuten: number, tageBisTermin: number | null): Machbarkeit {
  if (tageBisTermin === null) {
    return 'kein_termin';
  }
  if (tageBisTermin < 0) {
    return 'termin_vorbei';
  }
  if (minuten <= 20) {
    return 'entspannt';
  }
  if (minuten <= 45) {
    return 'machbar';
  }
  if (minuten <= 90) {
    return 'knapp';
  }
  return 'zu_wenig_zeit';
}

/**
 * Obergrenze der Wiedervorlage in Tagen.
 *
 * FSRS kann bei gut sitzendem Stoff Intervalle von Jahren berechnen. Für eine
 * Prüfungsvorbereitung ist das ohne Nutzen: Ein halbes Jahr ist länger als
 * jede realistische Vorbereitungszeit, und eine Frage nach zwei Jahren wieder
 * vorzulegen hilft niemandem, der in acht Wochen geprüft wird.
 */
export const MAX_INTERVALL_TAGE = 180;

/** Ergebnis der Terminberechnung nach einer Antwort. */
export interface Wiedervorlage extends Gedaechtnisstand {
  /**
   * Abstand bis zur nächsten Vorlage in ganzen Tagen.
   *
   * 0 bedeutet: sofort wieder fällig – spätestens die nächste Sitzung legt
   * die Frage wieder vorn vor.
   */
  readonly intervallTage: number;
}

/**
 * Schreibt den Gedächtnisstand fort und bestimmt daraus den nächsten Termin.
 *
 * Die einzige Stelle, an der FSRS und Prüfungstermin zusammenkommen.
 *
 * Ein „nochmal“ führt bewusst zu Intervall 0 statt zu dem, was FSRS
 * ausrechnen würde (rund fünf Stunden): Die Frage ist sofort wieder fällig
 * und steht in der nächsten Sitzung in der Vorranggruppe vorn. Dass sie
 * schon in der **laufenden** Sitzung wiederkommt, leistet nicht dieses
 * Intervall, sondern die Oberfläche: `useSitzung` reiht eine falsch
 * beantwortete Frage bis zu zweimal ans Sitzungsende, bis sie einmal richtig
 * beantwortet ist. (Bis Fassung 0.20.0 behauptete dieser Kommentar das
 * Wiedersehen in derselben Sitzung, und niemand leistete es.) Der
 * Gedächtnisstand wird davon nicht berührt: Er wird ganz normal nach FSRS
 * fortgeschrieben und bestimmt weiterhin, wie es danach weitergeht.
 */
/**
 * Streut ein Wiedervorlage-Intervall, damit nicht ganze Tagesjahrgänge im
 * Gleichschritt marschieren.
 *
 * **Das Problem.** Ohne Streuung ist der Termin punktgenau `jetzt + n Tage`.
 * Wer an einem Abend zwanzig neue Fragen mit „gut“ beantwortet, bekommt für
 * alle zwanzig dasselbe Intervall — und damit denselben Termin, und zwei
 * Termine später wieder. Die Arbeitslastvorschau macht diese Berge sichtbar;
 * geglättet hat sie sie nie.
 *
 * **Warum aus der Frage-ID und nicht aus einer Zufallsquelle.** Der erste
 * Entwurf nahm `this.zufall` des Lernstands. Er fiel sofort auf: Jeder Test,
 * der einen Abstand nachrechnet, ohne die Quelle festzunageln, wurde damit
 * zufällig — zwei Zusicherungen wurden auf der Stelle rot, eine davon ohne
 * jede Berührung. Ein Verfahren, das reproduzierbare Zahlen liefert, darf
 * nicht an einer Quelle hängen, die es nicht kennt. Derselbe Versatz für
 * dieselbe Frage ist zudem kein Nachteil: Verschieden sind die **Fragen**
 * untereinander, und genau darum geht es.
 *
 * **Die Regeln.** Höchstens 15 Prozent, gedeckelt auf sieben Tage, und erst
 * ab drei Tagen: Darunter ist ein Tag Versatz keine Streuung mehr, sondern
 * eine andere Antwort — bei Intervall 1 hieße „ein Tag früher“ noch heute.
 */
export function gestreutesIntervall(tage: number, frageId: string): number {
  if (tage < 3) {
    return tage;
  }
  const spanne = Math.min(7, Math.max(1, Math.round(tage * 0.15)));
  /* Ein einfacher, stabiler Streuwert aus der Kennung. Keine Kryptographie –
     gebraucht wird nur, dass benachbarte Fragen verschieden herauskommen. */
  let summe = 0;
  for (let i = 0; i < frageId.length; i += 1) {
    summe = (summe * 31 + frageId.charCodeAt(i)) % 100_003;
  }
  const versatz = (summe % (2 * spanne + 1)) - spanne;
  return Math.min(MAX_INTERVALL_TAGE, Math.max(1, tage + versatz));
}

export function wiedervorlageBerechnen(
  bisher: Gedaechtnisstand | null,
  grad: FsrsGrad,
  abstandTage: number,
  tageBisTermin: number | null,
  istKoBereich = false,
): Wiedervorlage {
  const stand = naechsterStand(bisher, grad, abstandTage);

  if (grad === 1) {
    return { ...stand, intervallTage: 0 };
  }

  const roh = intervallTage(stand.stabilitaet, zielquoteFuer(tageBisTermin, istKoBereich));
  const tage = Math.min(MAX_INTERVALL_TAGE, Math.max(1, Math.round(roh)));

  return { ...stand, intervallTage: tage };
}

/** So viele Tage weit reicht die Arbeitslast-Vorschau höchstens. */
export const VORSCHAU_TAGE = 14;

/**
 * Die bereits angesetzten Wiederholungen der nächsten Tage.
 *
 * **Wozu.** Der Plan kannte bis 0.22.0 nur „heute“. Weil die Zielquote ab
 * {@link ANHEBUNG_AB_TAGEN} Tagen vor dem Termin steigt und die Intervalle
 * dadurch zusammenrücken, türmt sich vor der Prüfung ein Wiederholungsberg,
 * den der Lernende erst am jeweiligen Morgen erfährt. Wer Schichtdienst,
 * Familie oder einen vollen Kalender hat, kann Lerntage aber nur planen, wenn
 * er die kommende Last kennt.
 *
 * **Was das ist und was nicht.** Eine reine Bestandsauskunft: So viele Fragen
 * stehen an diesem Tag **jetzt schon** im Kalender. Keine Prognose — und
 * ausdrücklich eine **Untergrenze**, denn jede Antwort von heute setzt neue
 * Termine. Mit wachsendem Abstand wird sie systematisch leerer; die Anzeige
 * sagt das, statt eine leere Woche zu suggerieren.
 *
 * Die neuen Fragen bleiben draußen: Sie haben keinen Termin, sondern kommen
 * mit der Einführungsrate — eine planbare Größe, die daneben steht.
 */
export function vorschauBauen(
  fragen: readonly PlanFrage[],
  tageBisTermin: number | null,
): readonly number[] {
  const weite =
    tageBisTermin === null || tageBisTermin < 0
      ? VORSCHAU_TAGE
      : Math.min(VORSCHAU_TAGE, tageBisTermin);

  const tage = new Array<number>(weite + 1).fill(0);
  for (const frage of fragen) {
    const wann = frage.faelligInTagen;
    if (wann === null || wann === undefined || wann > weite) {
      continue;
    }
    tage[wann] = (tage[wann] ?? 0) + 1;
  }
  return tage;
}

/** Stellt aus den Rohdaten den Lernplan zusammen. */
export function lernplanBerechnen(daten: Planungsdaten): Lernplan {
  const { termin, tageBisTermin, fragen } = daten;

  const gesamtFragen = fragen.length;
  const nieBeantwortet = fragen.filter((f) => f.stabilitaet === null).length;
  const faellig = fragen.filter((f) => f.faellig).length;

  const sekundenProFrage =
    daten.sekundenProFrage !== null && daten.sekundenProFrage > 0
      ? daten.sekundenProFrage
      : SEKUNDEN_PRO_FRAGE_VORGABE;

  /* Dieselbe Rate, mit der `Lernstand.sitzung()` die neuen Fragen deckelt –
     Begründung und Randfälle stehen an der Funktion selbst. */
  const neu = einfuehrungsrate(nieBeantwortet, tageBisTermin);

  const gesamt = neu + faellig;
  const minuten = Math.round((gesamt * sekundenProFrage) / 60);

  return {
    termin,
    tageBisTermin,
    gesamtFragen,
    nieBeantwortet,
    faellig,
    zielquote: zielquoteFuer(tageBisTermin),
    prognoseHeute: reifegradVon(fragen, 0),
    prognoseAmTermin:
      tageBisTermin === null || tageBisTermin < 0 ? null : reifegradVon(fragen, tageBisTermin),
    pensum: { neu, wiederholung: faellig, gesamt, minuten },
    machbarkeit: machbarkeitFuer(minuten, tageBisTermin),
    sekundenProFrage,
    vorschau: vorschauBauen(fragen, tageBisTermin),
  };
}
