/**
 * Persistenz der Nutzereinstellungen als JSON-Datei im `userData`-Verzeichnis.
 *
 * Bewusst ohne zusätzliche Abhängigkeit: es geht um wenige Bytes, und jede
 * eingelesene Datei wird streng validiert, bevor sie verwendet wird.
 */

import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';

import { app } from 'electron';

import { anzeigegroesseBereinigen } from '../shared/ansicht';
import { fensterlageBereinigen } from '../shared/fensterlage';
import { sprechtempoBereinigen } from '../shared/sprechtempo';
import { textabstandBereinigen } from '../shared/textabstand';
import { EINSTELLUNGEN_STANDARD, type Einstellungen } from '../shared/ipc';
import { istThemeAuswahl } from '../shared/theme';

function dateipfad(): string {
  return join(app.getPath('userData'), 'einstellungen.json');
}

/**
 * Nimmt beliebige Eingaben entgegen (Datei-Inhalt ODER IPC-Nutzlast aus dem
 * Renderer) und gibt garantiert ein gültiges Einstellungsobjekt zurück.
 */
export function einstellungenBereinigen(roh: unknown): Einstellungen {
  if (typeof roh !== 'object' || roh === null) {
    return EINSTELLUNGEN_STANDARD;
  }

  const kandidat = roh as Record<string, unknown>;

  const thema = kandidat['thema'];
  const profilId = kandidat['profilId'];
  const anzeigegroesse = kandidat['anzeigegroesse'];
  const optionenMischen = kandidat['optionenMischen'];
  const antwortzahlVerbergen = kandidat['antwortzahlVerbergen'];
  const vorlesen = kandidat['vorlesen'];
  const vorlesenAutomatisch = kandidat['vorlesenAutomatisch'];
  const vorlesenErklaerung = kandidat['vorlesenErklaerungBeiFehler'];
  const zuschnittGefragt = kandidat['zuschnittGefragt'];
  const tastenkuerzel = kandidat['tastenkuerzel'];
  const letzteSicherung = kandidat['letzteSicherung'];

  /* Ein Zeitpunkt, der sich nicht lesen lässt, ist kein Zeitpunkt. Er wird
     weggelassen statt auf „jetzt“ gesetzt: Die Karte sagt dann „noch nie
     gesichert“, und das ist die harmlosere der beiden Unwahrheiten. */
  const gueltigeSicherung =
    typeof letzteSicherung === 'string' && !Number.isNaN(Date.parse(letzteSicherung));

  const fensterlage = fensterlageBereinigen(kandidat['fenster']);

  const sitzungsumfang = kandidat['sitzungsumfang'];
  /* Dieselbe Spanne, die `Lernstand.sitzung` annimmt – eine Einstellung, die
     der Kern danach zurückweist, wäre eine Falle. */
  const gueltigerUmfang =
    typeof sitzungsumfang === 'number' &&
    Number.isInteger(sitzungsumfang) &&
    sitzungsumfang >= 1 &&
    sitzungsumfang <= 1000;

  const gueltigeProfilId =
    typeof profilId === 'number' && Number.isInteger(profilId) && profilId > 0;

  return {
    thema: istThemeAuswahl(thema) ? thema : EINSTELLUNGEN_STANDARD.thema,
    ...(gueltigerUmfang ? { sitzungsumfang } : {}),
    /* Beide Schwierigkeitsregler fallen auf „aus" zurück – dieselbe Aussage
       wie in EINSTELLUNGEN_STANDARD. Stünde hier ein anderer Rückfallwert,
       hebelte er den Standard aus, sobald das Feld dort einmal fehlt. */
    optionenMischen:
      typeof optionenMischen === 'boolean'
        ? optionenMischen
        : (EINSTELLUNGEN_STANDARD.optionenMischen ?? false),
    antwortzahlVerbergen:
      typeof antwortzahlVerbergen === 'boolean'
        ? antwortzahlVerbergen
        : (EINSTELLUNGEN_STANDARD.antwortzahlVerbergen ?? false),
    vorlesen: typeof vorlesen === 'boolean' ? vorlesen : (EINSTELLUNGEN_STANDARD.vorlesen ?? false),
    vorlesenAutomatisch:
      typeof vorlesenAutomatisch === 'boolean'
        ? vorlesenAutomatisch
        : (EINSTELLUNGEN_STANDARD.vorlesenAutomatisch ?? false),
    vorlesenErklaerungBeiFehler:
      typeof vorlesenErklaerung === 'boolean'
        ? vorlesenErklaerung
        : (EINSTELLUNGEN_STANDARD.vorlesenErklaerungBeiFehler ?? false),
    zuschnittGefragt:
      typeof zuschnittGefragt === 'boolean'
        ? zuschnittGefragt
        : (EINSTELLUNGEN_STANDARD.zuschnittGefragt ?? false),
    /* Bereinigt auf eine gültige Stufe: Ein Wert aus einer früheren Fassung
       oder von Hand in die Datei geschrieben soll die Anwendung nicht in
       eine unbedienbare Größe zwingen. */
    anzeigegroesse: anzeigegroesseBereinigen(anzeigegroesse),
    /* Wie die Anzeigegröße auf eine gültige Stufe gebracht: Ein Wert aus
       einer früheren Fassung oder von Hand in die Datei geschrieben soll
       die Darstellung nicht auf einen Zustand bringen, den es nicht gibt. */
    textabstand: textabstandBereinigen(kandidat['textabstand']),
    sprechtempo: sprechtempoBereinigen(kandidat['sprechtempo']),
    /* Wie `tastenkuerzel` nur durchgereicht, nicht aufgefüllt: Ohne
       gespeicherte Lage soll das Fenster die Vorgabe bekommen, nicht eine
       erfundene Größe. */
    ...(fensterlage === null ? {} : { fenster: fensterlage }),
    // `exactOptionalPropertyTypes`: das Feld darf nur gesetzt werden, wenn es
    // wirklich einen Wert hat.
    ...(gueltigeProfilId ? { profilId } : {}),
    /* Wie `profilId` nur durchgereicht, NICHT aufgefüllt: Das Fehlen des
       Feldes ist hier selbst eine Aussage – „noch nicht entschieden“ – und
       steuert die Voreinstellung nach erkanntem Hilfsmittel. Ein
       Rückfallwert machte daraus stillschweigend eine Entscheidung. */
    ...(typeof tastenkuerzel === 'boolean' ? { tastenkuerzel } : {}),
    /* Ebenfalls nicht aufgefüllt: Das Fehlen heißt „noch nie gesichert“ und
       ist eine Aussage, kein fehlender Wert. */
    ...(gueltigeSicherung ? { letzteSicherung } : {}),
  };
}

export function einstellungenLesen(): Einstellungen {
  const pfad = dateipfad();
  if (!existsSync(pfad)) {
    return EINSTELLUNGEN_STANDARD;
  }

  try {
    return einstellungenBereinigen(JSON.parse(readFileSync(pfad, 'utf8')));
  } catch (fehler) {
    console.warn('[einstellungen] Datei nicht lesbar, verwende Standardwerte:', fehler);
    return EINSTELLUNGEN_STANDARD;
  }
}

/**
 * Schreibt Einstellungen – als Änderung, nicht als Ersatz.
 *
 * Der Renderer schickt nur die Felder, die er ändern will; sie werden über den
 * gespeicherten Stand gelegt. Ohne dieses Zusammenlegen würde jede
 * Themenumschaltung alle übrigen Einstellungen auf die Standardwerte
 * zurücksetzen, weil {@link einstellungenBereinigen} fehlende Felder auffüllt.
 * Heute folgenlos – sobald aber eine zweite Einstellung wirklich benutzt wird,
 * ein stiller Datenverlust.
 *
 * Wirft, wenn nicht geschrieben werden konnte. Das ist der Unterschied zu
 * vorher: Ein voller Datenträger oder ein schreibgeschütztes Verzeichnis blieb
 * unbemerkt – die Umstellung griff sofort und war nach dem Neustart wieder
 * weg, ohne dass je etwas gemeldet wurde.
 *
 * **Geschrieben wird über eine Nebendatei.** Ginge es geradewegs in die
 * Zieldatei, hinterließe ein Abbruch mitten im Schreiben – Stromausfall,
 * voller Datenträger nach dem halben Puffer – eine verstümmelte Datei.
 * {@link einstellungenLesen} fiele dann sauber auf die Werksvorgaben zurück,
 * aber der Nutzer verlöre wortlos Farbschema, Anzeigegröße, Vorlesen,
 * Tastenkürzel und das zuletzt benutzte Profil. `rename` innerhalb desselben
 * Verzeichnisses ist unter Windows wie unter POSIX unteilbar: Es liegt
 * entweder der alte oder der neue Stand da, nie ein halber. Das war der
 * einzige nicht unteilbare Schreibweg der Anwendung – der Lernstand ist
 * transaktional, die Sicherung nutzt `VACUUM INTO`.
 */
export function einstellungenSchreiben(roh: unknown): Einstellungen {
  const aenderung = typeof roh === 'object' && roh !== null ? roh : {};
  const bereinigt = einstellungenBereinigen({ ...einstellungenLesen(), ...aenderung });
  const pfad = dateipfad();
  const neben = `${pfad}.neu`;

  try {
    mkdirSync(dirname(pfad), { recursive: true });
    writeFileSync(neben, `${JSON.stringify(bereinigt, null, 2)}\n`, 'utf8');
    renameSync(neben, pfad);
  } catch (fehler: unknown) {
    /* Die Nebendatei nicht liegen lassen: Ein halb geschriebener Rest neben
       der gültigen Datei verwirrt beim Nachsehen und belegt Platz, den es
       gerade nicht gab. Scheitert auch das, bleibt es dabei – der
       ursprüngliche Fehler ist der, der gemeldet gehört. */
    try {
      rmSync(neben, { force: true });
    } catch {
      /* bewusst still */
    }
    const grund = fehler instanceof Error ? fehler.message : String(fehler);
    /* `cause` hängt den ursprünglichen Fehler an, statt ihn wegzuwerfen. Die
       lesbare Meldung geht an den Nutzer, der Systemfehlercode (ENOSPC,
       EACCES) und die Aufrufliste bleiben für die Fehlersuche erhalten –
       genau die Angaben, die bei „konnte nicht gespeichert werden“ zählen. */
    throw new Error(`Die Einstellungen konnten nicht gespeichert werden: ${grund}`, {
      cause: fehler,
    });
  }

  return bereinigt;
}
