/**
 * Die selbsttätige Sicherheitskopie des Lernstands.
 *
 * **Wozu.** Eine Sicherung entstand bis 0.22.0 ausschließlich, wenn jemand
 * die Karte „Lernstand sichern und übertragen“ aufschlug und bediente. Die
 * einzige selbsttätige Kopie entstand vor dem **Einspielen** einer fremden
 * Sicherung – also genau dann, wenn ohnehin jemand mit Sicherungen hantiert.
 * Wer die Karte nie öffnete, hatte nichts: Ein Datenträgerdefekt, ein
 * versehentliches Löschen oder eine beschädigte Datei nahmen alles mit, was
 * über Wochen gelernt worden war.
 *
 * Diese Kopie liegt auf demselben Datenträger und ersetzt deshalb **keine**
 * richtige Sicherung – das sagt die Karte auch. Sie deckt die häufigen Fälle:
 * die eine beschädigte Datei, den Fehlgriff, den missglückten Einspielvorgang.
 * Gegen einen Plattendefekt hilft nur eine Kopie anderswo, und dazu rät die
 * Anwendung weiterhin.
 *
 * **Warum beim Beenden.** Beim Start wäre die Kopie die des vorigen Standes
 * und ließe die Arbeit des letzten Tages aus. Beim Beenden ist sie so frisch
 * wie möglich. `VACUUM INTO` braucht dafür die offene Verbindung – der Aufruf
 * gehört also **vor** `lernstandSchliessen()`.
 *
 * **Warum nicht bei jedem Beenden.** Eine Kopie je Programmstart füllte den
 * Datenträger mit Fassungen, die sich um Minuten unterscheiden. Der Abstand
 * von einer Woche ist der Kompromiss: alt genug, dass sich etwas geändert
 * hat, jung genug, dass der Verlust überschaubar bleibt.
 */

import { existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
import { join } from 'node:path';

import type BetterSqlite3 from 'better-sqlite3';

import {
  aufraeumen,
  sicherungsDateiname,
  sicherungSchreiben,
  type DatenbankKonstruktor,
} from './sicherung';

/** Unterordner im `userData`-Verzeichnis. */
export const SELBSTSICHERUNG_ORDNER = 'sicherungen';

/** Vorsatz im Dateinamen – hält die Kopien von allem anderen getrennt. */
export const SELBSTSICHERUNG_VORSATZ = 'Lernstand-selbsttaetig';

/** So lange gilt die letzte Kopie als frisch genug. */
export const ABSTAND_MS = 7 * 24 * 60 * 60 * 1000;

/** So viele Kopien bleiben liegen. */
export const KOPIEN_BEHALTEN = 3;

/** Voller Pfad des Kopienordners. */
export function selbstsicherungsordner(userData: string): string {
  return join(userData, SELBSTSICHERUNG_ORDNER);
}

/** Alle vorhandenen Kopien, jüngste zuerst. */
function vorhandene(ordner: string): { name: string; zeit: number }[] {
  if (!existsSync(ordner)) {
    return [];
  }
  return readdirSync(ordner)
    .filter(
      (name) => name.startsWith(`${SELBSTSICHERUNG_VORSATZ}-`) && name.endsWith('.wsklernstand'),
    )
    .map((name) => ({ name, zeit: statSync(join(ordner, name)).mtimeMs }))
    .sort((a, b) => b.zeit - a.zeit);
}

/**
 * Ob eine neue Kopie fällig ist.
 *
 * Als eigene Funktion, weil daran die Entscheidung hängt und sie sich sonst
 * nur über das Vorstellen der Systemuhr prüfen ließe.
 */
export function istFaellig(letzteMs: number | null, jetztMs: number): boolean {
  if (letzteMs === null) {
    return true;
  }
  /* Eine Kopie mit einem Zeitstempel aus der Zukunft – Uhrsprung, kopierter
     Ordner – gilt als überfällig, nicht als frisch. Sonst unterbliebe die
     Sicherung bis zu dem Tag, den ihr Stempel behauptet. */
  return jetztMs - letzteMs >= ABSTAND_MS || letzteMs > jetztMs;
}

/**
 * Legt eine Kopie an, wenn eine fällig ist.
 *
 * @returns Pfad der geschriebenen Kopie, oder `null`, wenn keine fällig war.
 */
export function selbstsicherungAnlegen(
  datenbank: BetterSqlite3.Database,
  userData: string,
  Datenbank: DatenbankKonstruktor,
  jetzt: Date = new Date(),
): string | null {
  const ordner = selbstsicherungsordner(userData);
  const bisher = vorhandene(ordner);
  const letzte = bisher[0]?.zeit ?? null;

  if (!istFaellig(letzte, jetzt.getTime())) {
    return null;
  }

  mkdirSync(ordner, { recursive: true });

  /* Sekundengenauer Name: Zwei Kopien in derselben Sekunde kann es nicht
     geben, weil eine je Woche entsteht – der Stempel ist hier nur der
     Ordnung wegen so genau. */
  const stempel = `${String(jetzt.getFullYear())}-${zwei(jetzt.getMonth() + 1)}-${zwei(jetzt.getDate())}-${zwei(jetzt.getHours())}${zwei(jetzt.getMinutes())}${zwei(jetzt.getSeconds())}`;
  const ziel = join(ordner, `${SELBSTSICHERUNG_VORSATZ}-${stempel}.wsklernstand`);
  aufraeumen(ziel);

  sicherungSchreiben(datenbank, ziel, Datenbank);
  aufraeumenAlte(ordner);
  return ziel;
}

function zwei(wert: number): string {
  return String(wert).padStart(2, '0');
}

/** Nur die jüngsten Kopien bleiben; ausschließlich dieses Muster. */
function aufraeumenAlte(ordner: string): void {
  try {
    for (const alt of vorhandene(ordner).slice(KOPIEN_BEHALTEN)) {
      aufraeumen(join(ordner, alt.name));
    }
  } catch {
    /* Aufräumen ist Komfort. Misslingt es, bleiben ein paar Dateien mehr
       liegen – kein Grund, eine gelungene Sicherung zu verwerfen. */
  }
}

/** Vorsatz der Kopie vor einem zerstörenden Schritt – wieder ein eigener. */
export const VOR_DEM_VERWERFEN = 'Lernstand-vor-dem-Verwerfen';

/**
 * Eine Sicherheitskopie vor „Neu anfangen“ und vor dem Löschen eines Profils.
 *
 * **Die Lücke, die sie schließt.** Beide Einspielwege legen selbstverständlich
 * vorher eine Kopie an – das Einspielen einer fremden Sicherung
 * (`VOR_DEM_EINSPIELEN`) und das Übernehmen eines Profils
 * (`VOR_DEM_UEBERNEHMEN`). Die beiden Wege, die tatsächlich etwas vernichten,
 * taten es nicht. Wer „Neu anfangen“ drückte oder ein Profil löschte, war den
 * Lernstand los, und die selbsttätige Wochenkopie war je nach Tag bis zu
 * sieben Tage alt.
 *
 * **Drei Unterschiede zur Wochenkopie.**
 *
 * 1. **Ungedrosselt.** {@link selbstsicherungAnlegen} schweigt, wenn die
 *    letzte Kopie noch frisch ist – hier wäre genau das der Ausfall.
 * 2. **Eigener Vorsatz und eigenes Kontingent.** Sonst räumten die Wege sich
 *    gegenseitig ab, und die Kopie vor dem Verwerfen fiele der nächsten
 *    Wochenkopie zum Opfer.
 * 3. **Sie wirft.** Die Wochenkopie ist eine Zugabe; misslingt sie, schließt
 *    das Fenster trotzdem. Diese hier steht vor einem Schritt, der Daten
 *    vernichtet: Lässt sie sich nicht schreiben, wird nichts vernichtet.
 *
 * @returns Pfad der geschriebenen Kopie.
 */
export function kopieVorDemVerwerfen(
  datenbank: BetterSqlite3.Database,
  userData: string,
  Datenbank: DatenbankKonstruktor,
  jetzt: Date = new Date(),
): string {
  const ordner = selbstsicherungsordner(userData);
  mkdirSync(ordner, { recursive: true });

  /* Sekundengenau, und trotzdem mit Zähler: Zwei zerstörende Schritte in
     derselben Sekunde sind unwahrscheinlich – aber „unwahrscheinlich“ ist
     bei einer Sicherheitskopie das falsche Wort. */
  let ziel = join(ordner, sicherungsDateiname(jetzt, VOR_DEM_VERWERFEN));
  for (let zaehler = 2; existsSync(ziel) && zaehler < 100; zaehler += 1) {
    ziel = join(
      ordner,
      sicherungsDateiname(jetzt, VOR_DEM_VERWERFEN).replace(
        /\.wsklernstand$/u,
        `-${String(zaehler)}.wsklernstand`,
      ),
    );
  }

  sicherungSchreiben(datenbank, ziel, Datenbank);
  eigeneAufraeumen(ordner, VOR_DEM_VERWERFEN);
  return ziel;
}

/** Nur die jüngsten Kopien eines Vorsatzes bleiben liegen. */
function eigeneAufraeumen(ordner: string, vorsatz: string): void {
  try {
    const kopien = readdirSync(ordner)
      .filter((name) => name.startsWith(`${vorsatz}-`) && name.endsWith('.wsklernstand'))
      .map((name) => ({ name, zeit: statSync(join(ordner, name)).mtimeMs }))
      .sort((a, b) => b.zeit - a.zeit);
    for (const alt of kopien.slice(KOPIEN_BEHALTEN)) {
      aufraeumen(join(ordner, alt.name));
    }
  } catch {
    /* Aufräumen ist Komfort – anders als das Schreiben selbst. */
  }
}
