/**
 * Glossar der Fachbegriffe des Waffenrechts.
 *
 * Das Waffenrecht arbeitet mit Wörtern, die im Alltag etwas anderes bedeuten.
 * „Führen" heißt nicht lenken, sondern die tatsächliche Gewalt außerhalb der
 * eigenen Wohnung ausüben. „Erwerben" hat mit Kaufen nichts zu tun. Wer das
 * nicht weiß, versteht die Frage nicht – unabhängig davon, wie gut er den
 * Stoff kann.
 *
 * Genau darauf zielt WCAG 3.1.3 (Ungewöhnliche Wörter, Stufe AAA): Es muss
 * einen Weg geben, die Bedeutung nachzuschlagen. 3.1.4 verlangt dasselbe für
 * Abkürzungen; beide sind im Prüfplan dieses Projekts als Zusage aufgeführt.
 *
 * ## Herkunft der Definitionen
 *
 * Wo das Gesetz einen Begriff selbst bestimmt, wird diese Bestimmung
 * wiedergegeben – nicht eine eigene Umschreibung davon. Die
 * waffenrechtlichen Begriffe stehen in Anlage 1 Abschnitt 2 WaffG, die
 * technischen in Abschnitt 1. Gesetzestexte sind nach § 5 Abs. 1 UrhG
 * gemeinfrei.
 *
 * Jede Fundstelle wird wie bei den Erklärungen maschinell gegen den amtlichen
 * Gesetzestext geprüft (`data-pipeline/pruefe_glossar.py`).
 */

import type { Fundstelle } from './erklaerungen';

/**
 * Art des Eintrags.
 *
 * Abkürzungen werden getrennt geführt: Sie brauchen keine Erläuterung,
 * sondern die aufgelöste Form – und WCAG 3.1.4 verlangt genau das.
 */
export type Begriffsart = 'begriff' | 'abkuerzung';

export interface Glossareintrag {
  /** Anzeigeform, etwa „Führen" oder „WaffG". */
  readonly begriff: string;
  readonly art: Begriffsart;
  /**
   * Die Bedeutung in einem Satz. Bei Abkürzungen die aufgelöste Form.
   *
   * Steht für sich allein: Wer nur diesen Satz liest, muss die Frage
   * verstehen können.
   */
  readonly kurz: string;
  /** Ausführlichere Erläuterung, wo ein Satz nicht reicht. */
  readonly text?: string;
  /**
   * Wortformen, unter denen der Begriff im Katalog vorkommt.
   *
   * Nötig, weil Deutsch flektiert: „Führen" erscheint als „führt",
   * „geführt", „des Führens". Ohne die Formen bliebe der Begriff in genau
   * den Fragen unerkannt, in denen er gebraucht wird.
   */
  readonly varianten: readonly string[];
  readonly fundstellen: readonly Fundstelle[];
  /** Warum es zu diesem Begriff keine gesetzliche Bestimmung gibt. */
  readonly ohneFundstelleGrund?: string;
  /** Verwandte Begriffe, die beim Verständnis helfen. */
  readonly siehe?: readonly string[];
}

export interface GlossarMeta {
  readonly version: number;
  readonly stand: string;
  readonly gesetzesstand: Readonly<Record<string, string>>;
  readonly hinweis: string;
}

export interface Glossar {
  readonly meta: GlossarMeta;
  readonly eintraege: readonly Glossareintrag[];
}

/**
 * Zeichen, die in einem deutschen Wort vorkommen.
 *
 * `\b` aus der regulären Ausdruckssprache taugt hier nicht: Es kennt nur
 * ASCII-Wortzeichen. In „Schießstätte" gälte damit jedes ß und ä als
 * Wortgrenze, und „Stätte" wäre plötzlich ein eigenes Wort.
 */
const WORTZEICHEN = 'A-Za-zÄÖÜäöüßẞ0-9';

/** Escaped, was in einem regulären Ausdruck Bedeutung hätte. */
function maskiert(wert: string): string {
  return wert.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
}

function ausdruckFuer(eintrag: Glossareintrag): RegExp {
  // Längere Formen zuerst, damit „Kurzwaffe" nicht von „Waffe" verdeckt wird,
  // falls beide als Variante desselben Eintrags stehen.
  const alternativen = [...eintrag.varianten]
    .sort((a, b) => b.length - a.length)
    .map(maskiert)
    .join('|');

  return new RegExp(`(?<![${WORTZEICHEN}])(?:${alternativen})(?![${WORTZEICHEN}])`, 'giu');
}

/**
 * Löst auf, wenn zwei Einträge dieselbe Fundstelle beanspruchen.
 *
 * ## Der Fall, um den es geht
 *
 * Deutsch schreibt Substantive groß, und daran hängt hier eine Bedeutung:
 * „Geschossen" ist der Dativ Plural von „Geschoss", „geschossen" das Partizip
 * von „schießen". Beide Wortformen stehen als Variante in **verschiedenen**
 * Einträgen, und die Suche arbeitet ohne Rücksicht auf Groß- und
 * Kleinschreibung — sie muss das, weil ein klein notiertes Verb am Satzanfang
 * groß steht und ein nominalisiertes Verb („das Führen") ohnehin.
 *
 * Bis Fassung 0.24.1 bekam der Lernende deshalb beide Einträge angeboten. Am
 * Katalog nachgezählt: zwölf Fragen mit dem Verb („Darf unter
 * Alkoholeinfluss geschossen werden?") zeigten den Substantiveintrag
 * „Geschoss", sieben mit dem Substantiv („Gefährdungsbereich von Geschossen")
 * den Verbeintrag „Schießen".
 *
 * ## Die Regel
 *
 * Beanspruchen mehrere Einträge dieselbe Schreibweise im Text, gewinnt der,
 * der sie **zeichengenau** als Variante führt. Führt keiner sie genau, bleibt
 * es bei allen — dann ist die Groß-/Kleinschreibung kein Unterscheidungs-
 * merkmal, und weniger zu zeigen wäre geraten.
 *
 * Ein Eintrag bleibt, sobald er **eine** seiner Fundstellen behält. „Das
 * Führen ist erlaubnispflichtig" trifft weiterhin, weil dort niemand sonst
 * Anspruch erhebt.
 */
function eindeutigMachen(
  treffer: readonly { readonly eintrag: Glossareintrag; readonly formen: readonly string[] }[],
): Glossareintrag[] {
  const genau = new Map<string, Set<string>>();
  for (const { eintrag, formen } of treffer) {
    for (const form of formen) {
      if (eintrag.varianten.includes(form)) {
        const bisher = genau.get(form) ?? new Set<string>();
        bisher.add(eintrag.begriff);
        genau.set(form, bisher);
      }
    }
  }

  return treffer
    .filter(({ eintrag, formen }) =>
      formen.some((form) => {
        const beansprucht = genau.get(form);
        return beansprucht === undefined || beansprucht.has(eintrag.begriff);
      }),
    )
    .map(({ eintrag }) => eintrag);
}

/**
 * Vorbereitetes Glossar – die Ausdrücke werden einmal gebaut, nicht je Frage.
 */
export interface Begriffssuche {
  readonly eintraege: readonly Glossareintrag[];
  /** Findet die Begriffe, die in einem Text tatsächlich vorkommen. */
  readonly imText: (text: string) => Glossareintrag[];
}

/**
 * Bereitet die Suche vor.
 *
 * Einträge ohne Varianten werden übergangen: Ein Ausdruck aus einer leeren
 * Alternativenliste würde auf jede Stelle passen und das ganze Glossar an
 * jede Frage hängen.
 */
export function begriffssucheBauen(glossar: Glossar): Begriffssuche {
  const vorbereitet = glossar.eintraege
    .filter((eintrag) => eintrag.varianten.length > 0)
    .map((eintrag) => ({ eintrag, ausdruck: ausdruckFuer(eintrag) }));

  return {
    eintraege: glossar.eintraege,
    imText: (text: string): Glossareintrag[] => {
      const treffer = vorbereitet
        .map(({ eintrag, ausdruck }) => {
          /* `lastIndex` zurücksetzen: Der Ausdruck ist global und wird über
             alle Aufrufe hinweg wiederverwendet. */
          ausdruck.lastIndex = 0;
          return { eintrag, formen: [...text.matchAll(ausdruck)].map((m) => m[0]) };
        })
        .filter(({ formen }) => formen.length > 0);

      return eindeutigMachen(treffer).sort((a, b) => a.begriff.localeCompare(b.begriff, 'de'));
    },
  };
}

/** Leeres Glossar – die Anwendung läuft auch ohne. */
export const GLOSSAR_LEER: Glossar = Object.freeze({
  meta: Object.freeze({
    version: 0,
    stand: '',
    gesetzesstand: Object.freeze({}),
    hinweis: '',
  }),
  eintraege: Object.freeze([]),
});
