import type BetterSqlite3 from 'better-sqlite3';

/**
 * Datenbankschema des Lernstands.
 *
 * Bewusst als TypeScript-Konstante und nicht als `.sql`-Datei: der
 * Main-Prozess wird von electron-vite gebündelt, eine Textdatei müsste
 * zusätzlich als Ressource mitgepackt und zur Laufzeit gefunden werden.
 * Eine Konstante ist Teil des Bundles und kann nicht fehlen.
 *
 * Alle Anweisungen sind idempotent (`IF NOT EXISTS`) – das Schema wird bei
 * jedem Öffnen der Datenbank angewandt. Echte Migrationen (Spalten ändern,
 * Daten umschreiben) laufen später über {@link MIGRATIONEN}.
 */

/** Aktueller Stand des Schemas. Wird in `schema_version` festgehalten. */
export const SCHEMA_VERSION = 10;

/**
 * Ein Tag in Millisekunden.
 *
 * Steht hier und nicht in `lernstand.ts`, weil die Migration ihn braucht und
 * `schema.ts` nichts aus `lernstand.ts` importieren darf – die Abhängigkeit
 * läuft genau andersherum.
 */
export const TAG_MS = 86_400_000;

/**
 * Schema-Version 2: gespeicherte Läufe der Prüfungssimulation.
 *
 * Steht als eigene Konstante, weil dieselbe Anweisung an zwei Stellen
 * gebraucht wird – im Grundschema für neue Datenbanken und als
 * Migrationsschritt für bestehende. Ausschließlich `CREATE … IF NOT EXISTS`:
 * ein vorhandener Lernstand wird dabei nicht angefasst.
 *
 * Die Einzelantworten stehen weiterhin in `antwort_log`; hier liegt nur die
 * Zusammenfassung eines Laufs, die der Verlauf anzeigt.
 */
const PRUEFUNG_LAUF_SCHEMA = `
CREATE TABLE IF NOT EXISTS pruefung_lauf (
  id              INTEGER PRIMARY KEY AUTOINCREMENT,
  profil_id       INTEGER NOT NULL REFERENCES profil(id) ON DELETE CASCADE,
  -- ID des Prüfungsprofils aus src/shared/pruefung.ts, z. B. 'dsb'.
  -- Bewusst als Text und ohne Fremdschlüssel: die Profile stehen im Quelltext,
  -- nicht in der Datenbank, und ein alter Lauf soll auch dann lesbar bleiben,
  -- wenn ein Profil später umbenannt oder entfernt wird.
  pruefungsprofil TEXT    NOT NULL,
  zeitpunkt       TEXT    NOT NULL,
  gesamt          INTEGER NOT NULL CHECK (gesamt >= 0),
  richtig         INTEGER NOT NULL CHECK (richtig >= 0),
  quote           REAL    NOT NULL CHECK (quote >= 0 AND quote <= 1),
  urteil          TEXT    NOT NULL CHECK (
    urteil IN ('bestanden', 'nachpruefung', 'nicht_bestanden')
  ),
  dauer_ms        INTEGER NOT NULL CHECK (dauer_ms >= 0)
);

CREATE INDEX IF NOT EXISTS idx_pruefung_lauf_profil_zeit
  ON pruefung_lauf (profil_id, zeitpunkt);
`;

/**
 * Schema-Version 5: der laufende, noch nicht abgegebene Prüfungsbogen.
 *
 * **Warum das in der Datenbank steht.** Ein Bogen läuft bis zu zwei Stunden.
 * Er lag bisher ausschließlich im Arbeitsspeicher des Renderers; wer bei
 * Minute 90 das Fenster schloss, verlor alles ohne Rückfrage. Das trifft
 * gerade die Gruppe, die für denselben Bogen länger braucht, und widerspricht
 * der eigenen Zusage zu WCAG 2.2.6.
 *
 * **Warum der Kern die Zeile schreibt und nicht der Renderer.** `bogen`,
 * `auftrag` und `profil` stammen aus `Pruefung.starten` – der Renderer bekommt
 * sie, schickt sie aber nie zurück. Andernfalls wäre die einzige Prüfung der
 * eingereichten Antwortliste dahin: Wer den Bogen selbst mitbringt, kann ihn
 * auch erfinden. Der Renderer sichert nur, was ihm gehört: Eingaben,
 * Position, Phase und die verbrauchte Zeit.
 *
 * **Warum `lauf_id`.** Das Sichern ist entprellt. Ohne eine Kennung könnte
 * ein verspäteter Nachzügler eine Zeile wiederauferstehen lassen, die die
 * Auswertung gerade gelöscht hat – und dieselbe Prüfung ließe sich ein
 * zweites Mal abgeben. Gesichert wird deshalb ausschließlich per `UPDATE`
 * mit `lauf_id`; findet es keine Zeile, ist der Lauf vorbei und die Sicherung
 * verfällt still.
 *
 * **Eine Zeile je Profil.** Mehr als ein offener Bogen gleichzeitig wäre
 * keine Simulation, sondern eine Ablage.
 *
 * `profil` hält das *wirksame* Prüfungsprofil als JSON, also mit den
 * tatsächlich gewählten Werten für Zeit, Bestehensgrenze und Fehlergrenze.
 * Aus demselben Grund wie bei `pruefung_lauf.pruefungsprofil`: Die Profile
 * stehen im Quelltext, nicht in der Datenbank, und ein fortgesetzter Lauf
 * muss unter denselben Vorgaben zu Ende gehen, unter denen er begonnen hat.
 */
const PRUEFUNG_OFFEN_SCHEMA = `
CREATE TABLE IF NOT EXISTS pruefung_offen (
  profil_id       INTEGER PRIMARY KEY REFERENCES profil(id) ON DELETE CASCADE,
  -- Kennung dieses Laufs; schuetzt gegen verspaetete Sicherungen.
  lauf_id         TEXT    NOT NULL,
  -- JSON, vom Kern geschrieben: Auftrag, wirksames Profil, gezogener Bogen.
  auftrag         TEXT    NOT NULL,
  profil          TEXT    NOT NULL,
  bogen           TEXT    NOT NULL,
  -- JSON, vom Renderer gesichert: je Frage Auswahl, Freitext, Selbstbewertung.
  eingaben        TEXT    NOT NULL DEFAULT '{}',
  position        INTEGER NOT NULL DEFAULT 0 CHECK (position >= 0),
  phase           TEXT    NOT NULL DEFAULT 'bearbeiten' CHECK (
    phase IN ('bearbeiten', 'nachbewertung')
  ),
  -- Ob die Zeit bereits abgelaufen war. Ohne diese Spalte meldete eine
  -- wiederhergestellte Nachbewertung faelschlich einen Lauf ohne Zeitablauf.
  zeit_abgelaufen INTEGER NOT NULL DEFAULT 0 CHECK (zeit_abgelaufen IN (0, 1)),
  -- Verbrauchte Bearbeitungszeit. Waehrend das Programm zu ist, steht die
  -- Uhr still; beim Fortsetzen wird der Zeitursprung um diesen Wert
  -- zurueckdatiert, damit Restzeit und Bearbeitungsdauer aus derselben
  -- Rechnung stammen.
  verbraucht_ms   INTEGER NOT NULL DEFAULT 0 CHECK (verbraucht_ms >= 0),
  begonnen_am     TEXT    NOT NULL,
  gesichert_am    TEXT    NOT NULL
);
`;

/**
 * Schema-Version 9: der Katalogstand, unter dem dieser Lernstand geführt wird.
 *
 * `frage_stand` und `antwort_log` verweisen mit Frage-IDs wie „I.1-01“ auf den
 * Katalog – bis Fassung 8 ohne Vermerk, gegen welchen BVA-Stand sie
 * entstanden. Veröffentlicht das Bundesverwaltungsamt eine Fassung mit
 * geänderter Nummerierung, zeigen Zeilen zu verschwundenen IDs still ins
 * Leere. Dieser Vermerk macht das erkennbar: Beim Öffnen vergleicht der
 * Lernstand den gespeicherten mit dem geladenen Stand und meldet eine
 * Abweichung, statt zu schweigen (`katalogstandAbgleichen` in `lernstand.ts`).
 *
 * Wie `schema_version` eine Fortschreibung: Jeder übernommene Stand bekommt
 * eine Zeile, maßgeblich ist die zuletzt eingetragene. Bewusst kein
 * Schlüssel auf `stand`: Wer zu einer älteren Programmfassung zurückkehrt,
 * kehrt zu einem schon dagewesenen Stand zurück – auch das ist ein Wechsel
 * und braucht eine neue Zeile.
 *
 * Die Migration legt die Tabelle nur an, und zwar **leer**. Den Ausgangswert
 * trägt der Lernstand beim Öffnen ein, denn die Migration kennt den geladenen
 * Katalog nicht – und ehrlich wäre ein anderer Wert ohnehin nicht zu haben:
 * Gegen welchen Stand die vorhandenen Zeilen wirklich entstanden, wurde nie
 * festgehalten und lässt sich nicht rekonstruieren. Der beim ersten Öffnen
 * geladene Stand ist die beste verfügbare Annahme; ab dann steht jeder
 * Wechsel schwarz auf weiß.
 */
const KATALOG_STAND_SCHEMA = `
CREATE TABLE IF NOT EXISTS katalog_stand (
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  -- ISO-Datum aus meta.stand des Katalogs, z. B. '2024-12-16'.
  stand       TEXT    NOT NULL,
  vermerkt_am TEXT    NOT NULL
);
`;

/**
 * Ergänzt eine Spalte, falls sie noch fehlt.
 *
 * SQLite kennt kein `ADD COLUMN IF NOT EXISTS`. Der Migrationslauf allein
 * würde genügen – er führt jeden Schritt genau einmal aus –, verlässt sich
 * dabei aber darauf, dass `schema_version` und die tatsächliche Tabelle
 * zusammenpassen. Tun sie das einmal nicht, scheitert der Schritt beim
 * Öffnen, und die Anwendung startet überhaupt nicht mehr.
 *
 * Der Blick in `PRAGMA table_info` kostet nichts und macht daraus einen
 * Schritt, der beliebig oft laufen darf.
 */
function spalteErgaenzen(
  db: BetterSqlite3.Database,
  tabelle: string,
  spalte: string,
  typ: string,
): void {
  /* Tabellen- und Spaltennamen lassen sich nicht als Parameter binden. Beide
     stammen hier ausschließlich aus Konstanten dieser Datei, nie aus einer
     Eingabe – die Prüfung hält das fest, damit es so bleibt. */
  if (!/^[a-z_][a-z0-9_]*$/u.test(tabelle) || !/^[a-z_][a-z0-9_]*$/u.test(spalte)) {
    throw new Error(`Ungültiger Bezeichner in der Migration: ${tabelle}.${spalte}`);
  }

  const vorhanden = db
    .prepare<[], { name: string }>(`PRAGMA table_info(${tabelle})`)
    .all()
    .some((zeile) => zeile.name === spalte);

  if (!vorhanden) {
    db.exec(`ALTER TABLE ${tabelle} ADD COLUMN ${spalte} ${typ}`);
  }
}

/**
 * Schema-Version 3: Gedächtnisstand nach FSRS.
 *
 * `frage_stand` bekommt zwei Spalten, die das Gedächtnismodell aus
 * `shared/fsrs.ts` fortschreibt: Stabilität in Tagen und Schwierigkeit
 * zwischen 1 und 10. Beide sind `NULL`, solange die Frage nie beantwortet
 * wurde – das ist die Unterscheidung zwischen „noch nie gesehen“ und
 * „gesehen, aber vergessen“.
 *
 * Als Migration und nicht im Grundschema: Eine frisch angelegte Datenbank
 * startet ebenfalls bei Version 0 und durchläuft alle Schritte. Stünden die
 * Spalten zusätzlich im Grundschema, liefe dieser Schritt in eine bereits
 * vorhandene Spalte.
 *
 * Bestehende Lernstände verlieren nichts. Ihre Fragen starten ohne
 * Gedächtnisstand und bekommen ihn bei der nächsten Antwort; bis dahin bleibt
 * `faellig_ab` unverändert gültig.
 */
function fsrsSpalten(db: BetterSqlite3.Database): void {
  spalteErgaenzen(db, 'frage_stand', 'stabilitaet', 'REAL');
  spalteErgaenzen(db, 'frage_stand', 'schwierigkeit', 'REAL');
}

/**
 * Schema-Version 4: die gewählte Zeitstufe eines Simulationslaufs.
 *
 * Ohne sie stehen ein Lauf ohne Uhr und ein Lauf unter Zeitdruck in der
 * Verlaufstabelle mit identischen Spalten nebeneinander – ausdrücklich zum
 * Vergleich eingeladen, obwohl sie nicht vergleichbar sind.
 *
 * Bestehende Läufe bekommen `NULL`: Ihre Zeitstufe ist nicht mehr zu
 * ermitteln, und sie zu raten wäre schlechter, als sie offen zu lassen.
 */
function zeitmodusSpalte(db: BetterSqlite3.Database): void {
  spalteErgaenzen(db, 'pruefung_lauf', 'zeitmodus', 'TEXT');
}

/**
 * Schema-Version 6: Welche Kapitel ein Profil dauerhaft abwählt.
 *
 * Kapitel IV („Not- und Seenotsignalmittel“) prüft nicht jede Prüfungsstelle.
 * Wer es nie braucht, schleppte bisher 89 der 575 Fragen durch jede Zahl der
 * Anwendung – Fortschritt, Tagespensum, Prognose, Prüfungsreife waren für ihn
 * dauerhaft falsch. Abwählen ging nur je Simulationslauf und wirkte nur auf
 * den gezogenen Bogen.
 *
 * **Warum die Spalte hier steht und nicht im Grundschema.** Weil sie sonst
 * auf bestehenden Lernständen niemals entstünde: `CREATE TABLE IF NOT EXISTS`
 * ist dort ein reiner Leerlauf, und das erste `SELECT` auf die Spalte
 * scheiterte mit „no such column“. Kein Test im Projekt bemerkte das – alle
 * legen frische Datenbanken an. `pruefungstermin` steht seit dem ersten
 * Commit im `CREATE TABLE` und ist deshalb **kein** Vorbild; das Vorbild ist
 * `zeitmodusSpalte` eine Ebene darüber.
 *
 * **Warum `TEXT NOT NULL DEFAULT '[]'`.** Der Vorgabewert füllt bestehende
 * Zeilen beim `ADD COLUMN` auf und bedient zugleich `profilEinfuegen`, das
 * seine Spalten einzeln nennt und die neue nicht kennt. `NOT NULL` ohne
 * Vorgabe verbietet SQLite bei `ADD COLUMN` ohnehin.
 *
 * **Warum eine Liste und kein Wahrheitswert.** Heute ist nur Kapitel IV
 * abwählbar. Ein `boolean` müsste beim nächsten Kapitel wieder migriert
 * werden; eine Liste von Kapitel-IDs trägt den Fall ohne Schemaschritt mit.
 */
function kapitelAusschlussSpalte(db: BetterSqlite3.Database): void {
  spalteErgaenzen(db, 'profil', 'kapitel_ausschluss', "TEXT NOT NULL DEFAULT '[]'");
}

/** Ein Migrationsschritt: SQL oder eine Funktion auf der Datenbank. */
export type Migration = string | ((db: BetterSqlite3.Database) => void);

export const LERNSTAND_SCHEMA = `
-- Fortschreibung des Schemas. Jede angewandte Version bekommt eine Zeile,
-- sodass sich später nachvollziehen lässt, wann was migriert wurde.
CREATE TABLE IF NOT EXISTS schema_version (
  version       INTEGER NOT NULL PRIMARY KEY,
  angewendet_am TEXT    NOT NULL
);

-- Lernprofile. Mehrere Personen teilen sich ein Gerät, ohne dass die
-- Lernstände sich vermischen.
CREATE TABLE IF NOT EXISTS profil (
  id              INTEGER PRIMARY KEY AUTOINCREMENT,
  name            TEXT    NOT NULL UNIQUE,
  pruefungstermin TEXT,
  erstellt_am     TEXT    NOT NULL
);

-- Aktueller Stand je Frage und Profil. Diese Tabelle ist eine
-- Zusammenfassung; die Wahrheit steht in antwort_log.
CREATE TABLE IF NOT EXISTS frage_stand (
  profil_id           INTEGER NOT NULL REFERENCES profil(id) ON DELETE CASCADE,
  frage_id            TEXT    NOT NULL,
  versuche            INTEGER NOT NULL DEFAULT 0,
  richtige            INTEGER NOT NULL DEFAULT 0,
  zuletzt_beantwortet TEXT,
  faellig_ab          TEXT,
  gemerkt             INTEGER NOT NULL DEFAULT 0 CHECK (gemerkt IN (0, 1)),
  letzte_bewertung    TEXT CHECK (
    letzte_bewertung IS NULL
    OR letzte_bewertung IN ('nochmal', 'schwer', 'gut', 'leicht')
  ),
  -- Zuletzt vergebenes Wiedervorlage-Intervall in Tagen. Ergebnis der
  -- FSRS-Rechnung, nur zur Anzeige; maßgeblich ist faellig_ab.
  intervall_tage      REAL    NOT NULL DEFAULT 0,
  PRIMARY KEY (profil_id, frage_id)
);

-- Vollständige Historie. Wird niemals überschrieben und ist die Grundlage
-- für Statistik und für ein späteres Nachtrainieren der FSRS-Parameter.
CREATE TABLE IF NOT EXISTS antwort_log (
  id        INTEGER PRIMARY KEY AUTOINCREMENT,
  profil_id INTEGER NOT NULL REFERENCES profil(id) ON DELETE CASCADE,
  frage_id  TEXT    NOT NULL,
  zeitpunkt TEXT    NOT NULL,
  richtig   INTEGER NOT NULL CHECK (richtig IN (0, 1)),
  bewertung TEXT    NOT NULL CHECK (bewertung IN ('nochmal', 'schwer', 'gut', 'leicht')),
  dauer_ms  INTEGER NOT NULL CHECK (dauer_ms >= 0),
  -- JSON-Array der gewählten Antwortlabels, z. B. ["a","c"].
  auswahl   TEXT    NOT NULL DEFAULT '[]',
  freitext  TEXT
);

CREATE INDEX IF NOT EXISTS idx_antwort_log_profil_zeit
  ON antwort_log (profil_id, zeitpunkt);
CREATE INDEX IF NOT EXISTS idx_antwort_log_profil_frage
  ON antwort_log (profil_id, frage_id, id);
CREATE INDEX IF NOT EXISTS idx_frage_stand_faellig
  ON frage_stand (profil_id, faellig_ab);
CREATE INDEX IF NOT EXISTS idx_frage_stand_gemerkt
  ON frage_stand (profil_id, gemerkt);
${PRUEFUNG_LAUF_SCHEMA}${PRUEFUNG_OFFEN_SCHEMA}${KATALOG_STAND_SCHEMA}`;

/**
 * Migrationsschritte oberhalb von Version 1.
 *
 * Der Schlüssel ist die Zielversion, der Wert entweder auszuführendes SQL
 * oder eine Funktion – Letzteres für Schritte, die vorher etwas nachsehen
 * müssen. Beim Öffnen werden alle Schritte oberhalb der gespeicherten Version
 * der Reihe nach in einer Transaktion angewandt.
 *
 * Jeder Schritt muss so geschrieben sein, dass er auch dann durchläuft, wenn
 * sein Ergebnis bereits vorliegt. Sonst wäre eine einmal aus dem Tritt
 * geratene Datenbank dauerhaft nicht mehr zu öffnen.
 *
 * Version 2 legt `pruefung_lauf` an, Version 3 ergänzt `frage_stand` um den
 * Gedächtnisstand, Version 4 die Zeitstufe eines Laufs, Version 5 den
 * unterbrochenen Bogen, Version 6 die dauerhaft abgewählten Kapitel eines
 * Profils, Version 7 den belegten Abruf je Frage, Version 8 die Zeilen,
 * die nur in die Historie gehören, Version 9 den Vermerk des Katalogstands,
 * Version 10 die Kennzahlen, ohne die sich zwei Läufe nicht ehrlich
 * vergleichen lassen. Bestehende Lernstände behalten dabei alle Daten: es
 * kommen nur Tabellen und Spalten hinzu, nichts wird geändert oder gelöscht.
 *
 * **Tabellen dürfen doppelt stehen, Spalten nicht.** Version 2, 5 und 9 legen
 * Tabellen an und erscheinen deshalb auch im Grundschema – `CREATE TABLE IF
 * NOT EXISTS` tut auf einer Bestandsdatenbank noch etwas. Für eine Spalte
 * gilt das nicht: Sie gehört ausschließlich hierher.
 */
/**
 * Schema-Version 7: der belegte Abruf je Frage.
 *
 * Siehe `shared/reife.ts` zur Regel selbst. Hier zählt nur, dass sie sich für
 * bestehende Lernstände **exakt nachbilden** lässt: `antwort_log` ist die
 * vollständige Historie und wird nie überschrieben – der Kommentar über
 * `frage_stand` sagt es selbst, dort stehe nur die Zusammenfassung, „die
 * Wahrheit steht in antwort_log“.
 *
 * Nachgespielt wird je Frage in Antwortreihenfolge, mit derselben Regel, die
 * künftig beim Antworten gilt: Eine richtige Antwort mit mindestens einem Tag
 * Abstand setzt den Beleg, eine falsche nimmt ihn weg, eine richtige am
 * selben Tag lässt ihn stehen. Kein Schätzen, kein Vorgabewert – wer seinen
 * Stand ehrlich erarbeitet hat, behält ihn auf die Frage genau.
 *
 * Die Schleife läuft über alle Zeilen aller Profile. Bei 575 Fragen und
 * einigen tausend Antworten ist das eine Sache von Millisekunden, und sie
 * geschieht genau einmal.
 */
function belegSpalte(db: BetterSqlite3.Database): void {
  spalteErgaenzen(db, 'frage_stand', 'bestaetigt', 'INTEGER NOT NULL DEFAULT 0');

  const antworten = db
    .prepare<[], { profil_id: number; frage_id: string; zeitpunkt: string; richtig: number }>(
      `SELECT profil_id, frage_id, zeitpunkt, richtig
         FROM antwort_log
        ORDER BY profil_id, frage_id, id`,
    )
    .all();

  /* Verschachtelt statt zusammengesetzter Schluessel: Ein Trennzeichen
     muesste garantiert in keiner Frage-ID vorkommen, und die naheliegende
     Wahl - das Nullzeichen - hat in diesem Projekt schon einmal eine
     Quelldatei fuer git zur Binaerdatei gemacht. */
  const beleg = new Map<number, Map<string, boolean>>();
  let laufendesProfil = -1;
  let laufendeFrage = '';
  let vorigerZeitpunkt = Number.NaN;

  for (const zeile of antworten) {
    if (zeile.profil_id !== laufendesProfil || zeile.frage_id !== laufendeFrage) {
      laufendesProfil = zeile.profil_id;
      laufendeFrage = zeile.frage_id;
      vorigerZeitpunkt = Number.NaN;
    }

    const jetzt = Date.parse(zeile.zeitpunkt);
    const abstandTage = Number.isNaN(vorigerZeitpunkt) ? 0 : (jetzt - vorigerZeitpunkt) / TAG_MS;

    let jeProfil = beleg.get(zeile.profil_id);
    if (jeProfil === undefined) {
      jeProfil = new Map<string, boolean>();
      beleg.set(zeile.profil_id, jeProfil);
    }

    if (zeile.richtig === 0) {
      jeProfil.set(zeile.frage_id, false);
    } else if (abstandTage >= 1) {
      jeProfil.set(zeile.frage_id, true);
    }

    if (!Number.isNaN(jetzt)) {
      vorigerZeitpunkt = jetzt;
    }
  }

  const setzen = db.prepare<[number, string]>(
    'UPDATE frage_stand SET bestaetigt = 1 WHERE profil_id = ? AND frage_id = ?',
  );
  for (const [profilId, jeProfil] of beleg) {
    for (const [frageId, belegt] of jeProfil) {
      if (belegt) {
        setzen.run(profilId, frageId);
      }
    }
  }
}

/**
 * Schema-Version 8: Zeilen, die nur in die Historie gehören.
 *
 * Läuft in der Prüfungssimulation die Zeit ab, kommen Fragen im Bogen vor, die
 * nie aufgeschlagen wurden. Sie gehören in die Historie – der Bogen enthielt
 * sie ja –, dürfen den Lernstand aber nicht zurückstufen. Deshalb schreibt
 * `protokollieren()` sie in `antwort_log`, ohne `frage_stand` anzufassen.
 *
 * Die Tagesbilanz zählte sie trotzdem mit, und zwar als falsch beantwortet:
 * „Heute beantwortet: 16 richtig, 40 falsch“ nach einem abgelaufenen Bogen,
 * von dem jemand 16 Fragen gesehen hatte. Unterscheiden liess sich das an
 * nichts – eine falsch beantwortete offene Frage sieht in jeder Spalte
 * genauso aus.
 *
 * Bestehende Zeilen bekommen 0 und zählen weiter mit. Rückwirkend liesse es
 * sich nicht ermitteln, und zu raten wäre schlechter, als es stehen zu lassen
 * und zu sagen.
 */
function nurHistorieSpalte(db: BetterSqlite3.Database): void {
  spalteErgaenzen(db, 'antwort_log', 'nur_historie', 'INTEGER NOT NULL DEFAULT 0');
}

/**
 * Schema-Version 10: was ein Simulationslauf über sein Ergebnis hinaus sagt.
 *
 * Ein gespeicherter Lauf trug bis Fassung 9 nur Zahl, Treffer, Quote und
 * Urteil. Das genügt für eine Liste, aber nicht für einen **ehrlichen**
 * Vergleich zweier Läufe:
 *
 * - `quote` zählt unbeantwortete Fragen wie falsch beantwortete. Ein Lauf, in
 *   dem die Zeit ablief, sieht darin wie ein Wissenseinbruch aus. Ohne
 *   `unbeantwortet` und `zeit_abgelaufen` kann die Anwendung das nicht sagen –
 *   und ein Vergleichssatz, der es verschweigt, ist schlimmer als keiner.
 * - Die Themenanalyse gab es nur je Einzellauf und nur im Arbeitsspeicher.
 *   Dass ein Bereich in drei von vier Simulationen unter der Grenze lag, war
 *   nirgends zu sehen. `bereiche` hält sie als JSON fest, so wie sie in der
 *   Auswertung stand.
 * - `bestehens_quote` ist der Maßstab, an dem ein Bereich gemessen wird. Er
 *   muss mitgespeichert werden, weil er sich später nicht mehr ermitteln
 *   lässt: Beim frei eingestellten Profil sind die gewählten Werte nach dem
 *   Lauf fort, und ein Fehlerpunkte-Profil hat gar keine Quote, sondern eine
 *   umgerechnete (siehe `grenzquote` in `shared/pruefung.ts`).
 *
 * **Alle vier Spalten sind bewusst `NULL`-fähig.** Bestehende Läufe bekommen
 * keinen Ersatzwert. `unbeantwortet = 0` hieße „es blieb nichts offen“ – eine
 * Behauptung über Läufe, von denen niemand weiß, wie sie endeten. `NULL` heißt
 * „nicht festgehalten“, und die Oberfläche sagt genau das. Aus `antwort_log`
 * ließe es sich auch nicht zurückrechnen: Dort steht nicht, welcher Lauf
 * welche Zeile geschrieben hat.
 */
function laufKennzahlenSpalten(db: BetterSqlite3.Database): void {
  spalteErgaenzen(db, 'pruefung_lauf', 'unbeantwortet', 'INTEGER');
  spalteErgaenzen(db, 'pruefung_lauf', 'zeit_abgelaufen', 'INTEGER');
  spalteErgaenzen(db, 'pruefung_lauf', 'bereiche', 'TEXT');
  spalteErgaenzen(db, 'pruefung_lauf', 'bestehens_quote', 'REAL');
}

export const MIGRATIONEN: Readonly<Record<number, Migration>> = Object.freeze({
  2: PRUEFUNG_LAUF_SCHEMA,
  3: fsrsSpalten,
  4: zeitmodusSpalte,
  5: PRUEFUNG_OFFEN_SCHEMA,
  6: kapitelAusschlussSpalte,
  7: belegSpalte,
  8: nurHistorieSpalte,
  9: KATALOG_STAND_SCHEMA,
  10: laufKennzahlenSpalten,
});
