/**
 * Laden und Prüfen des Glossars.
 *
 * Wie die Erklärungen eigener redaktioneller Inhalt, wie diese in einer
 * eigenen Datei:
 *
 *  - Entwicklung: `<repo>/content/glossar.json`
 *  - Gepackt:     `<app>/resources/glossar.json`
 *
 * Fehlt es oder ist es unbrauchbar, läuft die Anwendung ohne. Der Lernbetrieb
 * hängt nicht daran.
 *
 * Die Prüfung hier betrifft nur die Form. Ob eine Fundstelle im Gesetz
 * existiert, klärt `data-pipeline/pruefe_glossar.py` vor der Auslieferung –
 * dort steht der Gesetzestext zur Verfügung, hier nicht.
 */

import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';

import { app } from 'electron';

import { GESETZE, type Fundstelle, type Gesetzeskuerzel } from '../shared/erklaerungen';
import {
  GLOSSAR_LEER,
  type Begriffsart,
  type Glossar,
  type Glossareintrag,
} from '../shared/glossar';
import { sucheAufwaerts } from './katalog';

const DATEI = 'glossar.json';

const NORM_MUSTER = /^(§ \d+[a-z]?|Anlage( [0-9IVX]+)?)$/u;
const GLIEDERUNG_MUSTER = /^[0-9]{1,3}[a-z]?$/u;
const BUCHSTABE_MUSTER = /^[a-z]{1,2}$/u;
const ARTEN: readonly Begriffsart[] = ['begriff', 'abkuerzung'];

function ungueltig(nachricht: string): never {
  throw new Error(`Glossar ungültig: ${nachricht}`);
}

function objekt(wert: unknown, pfad: string): Record<string, unknown> {
  if (typeof wert !== 'object' || wert === null || Array.isArray(wert)) {
    ungueltig(`${pfad} ist kein Objekt.`);
  }
  return wert as Record<string, unknown>;
}

function pflichttext(wert: unknown, pfad: string): string {
  if (typeof wert !== 'string' || wert.trim().length === 0) {
    ungueltig(`${pfad} fehlt oder ist leer.`);
  }
  return wert;
}

function kuerteText(wert: unknown, pfad: string): string | undefined {
  return wert === undefined ? undefined : pflichttext(wert, pfad);
}

function textliste(wert: unknown, pfad: string): string[] {
  if (!Array.isArray(wert)) {
    ungueltig(`${pfad} fehlt oder ist keine Liste.`);
  }
  return (wert as readonly unknown[]).map((eintrag, i) =>
    pflichttext(eintrag, `${pfad}[${String(i)}]`),
  );
}

function gliederung(wert: unknown, pfad: string, muster: RegExp): string | undefined {
  if (wert === undefined) {
    return undefined;
  }
  if (typeof wert !== 'string' || !muster.test(wert)) {
    ungueltig(`${pfad} hat eine unerwartete Form.`);
  }
  return wert;
}

/** Baut ein Feld nur ein, wenn es einen Wert hat (`exactOptionalPropertyTypes`). */
function optional<K extends string>(
  schluessel: K,
  wert: string | undefined,
): Record<K, string> | Record<string, never> {
  return wert === undefined ? {} : ({ [schluessel]: wert } as Record<K, string>);
}

function fundstelleLesen(roh: unknown, pfad: string): Fundstelle {
  const o = objekt(roh, pfad);

  const gesetz = o['gesetz'];
  if (typeof gesetz !== 'string' || !(GESETZE as readonly string[]).includes(gesetz)) {
    ungueltig(`${pfad}.gesetz ist kein bekanntes Gesetz.`);
  }

  const norm = pflichttext(o['norm'], `${pfad}.norm`);
  if (!NORM_MUSTER.test(norm)) {
    ungueltig(`${pfad}.norm muss „§ 12“ oder „Anlage 1“ lauten.`);
  }

  return {
    gesetz: gesetz as Gesetzeskuerzel,
    norm,
    ...optional('absatz', gliederung(o['absatz'], `${pfad}.absatz`, GLIEDERUNG_MUSTER)),
    ...optional('nummer', gliederung(o['nummer'], `${pfad}.nummer`, GLIEDERUNG_MUSTER)),
    ...optional('buchstabe', gliederung(o['buchstabe'], `${pfad}.buchstabe`, BUCHSTABE_MUSTER)),
    ...optional('satz', gliederung(o['satz'], `${pfad}.satz`, GLIEDERUNG_MUSTER)),
    ...optional('stelle', kuerteText(o['stelle'], `${pfad}.stelle`)),
  };
}

function eintragLesen(roh: unknown, pfad: string): Glossareintrag {
  const o = objekt(roh, pfad);

  const art = o['art'];
  if (typeof art !== 'string' || !(ARTEN as readonly string[]).includes(art)) {
    ungueltig(`${pfad}.art muss „begriff“ oder „abkuerzung“ sein.`);
  }

  /* Leer ist zulässig: Der Eintrag erscheint dann nur in der Glossaransicht
     und wird an keiner Frage angezeigt. Bei Abkürzungen, die nur in den
     Erklärungen vorkommen, ist genau das richtig – eine erfundene Wortform
     wäre schlechter. Die Begriffssuche übergeht solche Einträge. */
  const varianten = textliste(o['varianten'], `${pfad}.varianten`);

  const fundstellenRoh = o['fundstellen'];
  if (!Array.isArray(fundstellenRoh)) {
    ungueltig(`${pfad}.fundstellen fehlt oder ist keine Liste.`);
  }
  const fundstellen = (fundstellenRoh as readonly unknown[]).map((eintrag, i) =>
    fundstelleLesen(eintrag, `${pfad}.fundstellen[${String(i)}]`),
  );

  const grund = kuerteText(o['ohneFundstelleGrund'], `${pfad}.ohneFundstelleGrund`);
  if (fundstellen.length === 0 && grund === undefined) {
    ungueltig(`${pfad}: ohne Fundstelle muss ein Grund angegeben sein.`);
  }

  const text = kuerteText(o['text'], `${pfad}.text`);
  const siehe = o['siehe'] === undefined ? undefined : textliste(o['siehe'], `${pfad}.siehe`);

  return {
    begriff: pflichttext(o['begriff'], `${pfad}.begriff`),
    art: art as Begriffsart,
    kurz: pflichttext(o['kurz'], `${pfad}.kurz`),
    varianten,
    fundstellen,
    ...(text === undefined ? {} : { text }),
    ...(grund === undefined ? {} : { ohneFundstelleGrund: grund }),
    ...(siehe === undefined ? {} : { siehe }),
  };
}

function glossarValidieren(roh: unknown): Glossar {
  const o = objekt(roh, 'Die Datei');
  const meta = objekt(o['meta'], 'meta');

  const version = meta['version'];
  if (typeof version !== 'number' || !Number.isInteger(version) || version < 1) {
    ungueltig('meta.version fehlt oder ist keine ganze Zahl ab 1.');
  }

  const gesetzesstandRoh = objekt(meta['gesetzesstand'], 'meta.gesetzesstand');
  const gesetzesstand: Record<string, string> = {};
  for (const [kuerzel, stand] of Object.entries(gesetzesstandRoh)) {
    gesetzesstand[kuerzel] = pflichttext(stand, `meta.gesetzesstand.${kuerzel}`);
  }

  const eintraegeRoh = o['eintraege'];
  if (!Array.isArray(eintraegeRoh)) {
    ungueltig('eintraege fehlt oder ist keine Liste.');
  }

  const eintraege = (eintraegeRoh as readonly unknown[]).map((eintrag, i) =>
    eintragLesen(eintrag, `eintraege[${String(i)}]`),
  );

  /* Doppelte Begriffe wären in der Anzeige verwirrend und in „siehe“
     mehrdeutig. */
  const gesehen = new Set<string>();
  for (const eintrag of eintraege) {
    if (gesehen.has(eintrag.begriff)) {
      ungueltig(`„${eintrag.begriff}“ kommt mehrfach vor.`);
    }
    gesehen.add(eintrag.begriff);
  }

  return {
    meta: {
      version,
      stand: pflichttext(meta['stand'], 'meta.stand'),
      gesetzesstand,
      hinweis: pflichttext(meta['hinweis'], 'meta.hinweis'),
    },
    eintraege,
  };
}

/** Pfad der Glossardatei – gepackt wie ungepackt. */
export function glossarPfad(): string {
  if (app.isPackaged) {
    return join(process.resourcesPath, DATEI);
  }

  const relativ = join('content', DATEI);
  for (const start of [app.getAppPath(), __dirname, process.cwd()]) {
    const treffer = sucheAufwaerts(start, relativ);
    if (treffer) {
      return treffer;
    }
  }
  return join(app.getAppPath(), '..', relativ);
}

let speicher: Glossar | null = null;

export function glossarLaden(): Glossar {
  if (speicher !== null) {
    return speicher;
  }

  const pfad = glossarPfad();
  if (!existsSync(pfad)) {
    speicher = GLOSSAR_LEER;
    return speicher;
  }

  try {
    speicher = glossarValidieren(JSON.parse(readFileSync(pfad, 'utf8')));
  } catch (fehler: unknown) {
    console.error('Glossar konnte nicht gelesen werden:', fehler);
    speicher = GLOSSAR_LEER;
  }

  return speicher;
}
