import { useMemo, useState } from 'react';

import {
  bereiche as bereicheBauen,
  brueckeFinden,
  schuldigeWoerter,
  suchen,
  suchindexBauen,
  suchwoerter,
  zerlegung,
  type Bereich,
  type Brueckenangebot,
  type Suchergebnis,
  type Zerlegung,
} from '../../../shared/fragensuche';
import { nummernindexBauen, nummernTreffer } from '../../../shared/fragennummer';
import type { Katalog } from '../../../shared/katalog';
import type { Suchanfrage } from '../lernen/typen';
import { useVerzoegerteAnsage } from './useVerzoegerteAnsage';

interface Gebaut {
  readonly katalog: Katalog;
  readonly index: ReturnType<typeof suchindexBauen>;
  readonly nummern: ReturnType<typeof nummernindexBauen>;
  readonly bereiche: readonly Bereich[];
}

/**
 * Der zuletzt gebaute Index, gemerkt an der Katalog-Kennung.
 *
 * Der Aufbau kostet wenige Millisekunden – aber er kostet sie bei **jedem**
 * Öffnen der Ansicht, und der Katalog ändert sich zur Laufzeit nie. Der
 * Zwischenspeicher liegt bewusst außerhalb von React: `useMemo` gilt je
 * Komponente und wäre beim Verlassen der Ansicht weg.
 */
let gemerkt: Gebaut | null = null;

/** Für Tests: den Zwischenspeicher leeren, damit Läufe unabhängig bleiben. */
export function suchindexZwischenspeicherLeeren(): void {
  gemerkt = null;
}

function bauen(katalog: Katalog): Gebaut {
  if (gemerkt !== null && gemerkt.katalog === katalog) {
    return gemerkt;
  }
  gemerkt = {
    katalog,
    index: suchindexBauen(katalog),
    nummern: nummernindexBauen(katalog),
    bereiche: bereicheBauen(katalog),
  };
  return gemerkt;
}

export interface Suchlage {
  readonly text: string;
  readonly bereich: string | null;
  readonly bereiche: readonly Bereich[];
  readonly ergebnis: Suchergebnis;
  /** Fragen, die über ihre Nummer gefunden wurden – eigener Block. */
  readonly nummernIds: readonly string[];
  /** Trefferzahl ohne die Bereichseinschränkung – für den Leerzustand. */
  readonly ohneBereich: number;
  /**
   * Wörter, die im ganzen Katalog nicht vorkommen – **in der Schreibweise
   * des Suchenden**, nicht in der Kanonform.
   *
   * Sonst bekäme er ein Wort vorgehalten, das er nie getippt hat: Aus
   * „Schießstättenübung“ wurde „schiestatenubung“, aus „II-45“ ein „i-45“.
   * Die Faltung ist ein inneres Werkzeug.
   */
  readonly unbekannt: readonly string[];
  readonly zerlegung: Zerlegung | null;
  readonly bruecke: Brueckenangebot | null;
  /** Weder Text- noch Nummerntreffer – der Leerzustand ist berechtigt. */
  readonly leer: boolean;
  /** Der Satz für die Live-Region – erst nach einer Ruhepause. */
  readonly ansage: string;
  readonly textSetzen: (text: string) => void;
  readonly bereichSetzen: (bereich: string | null) => void;
}

/* Die Ruhepause und ihre Begründung stehen seit 0.22.0 in
   `useVerzoegerteAnsage` – dieselbe Mechanik braucht auch das Glossar, und
   zwei Zahlen an zwei Stellen laufen auseinander. */

/**
 * Der Zustand der Suchansicht.
 *
 * ## Warum der Suchtext hier liegt und nicht im Ansichtsobjekt
 *
 * Der Fokuseffekt in `App.tsx` hängt an `[ansicht, profiltor]`. Läge der
 * Suchtext im Ansichtsobjekt, risse jeder Tastenanschlag den Fokus aus dem
 * Eingabefeld auf die Überschrift – die Ansicht wäre unbenutzbar.
 *
 * ## Warum nur die Ansage entprellt wird
 *
 * Die Liste folgt sofort; das Suchen kostet 0,15 Millisekunden. Entprellt
 * wird ausschließlich der Text der Live-Region: Eine höfliche Region, die bei
 * jedem Anschlag eine neue Zahl spricht, ist genau das, wovor der Prüfplan zu
 * WCAG 4.1.3 warnt. Wer „Aufbewahrung“ tippt, soll einen Satz hören, nicht
 * zwölf.
 */
const LEERE_ANFRAGE: Suchanfrage = { text: '', bereich: null };

export function useFragensuche(katalog: Katalog, anfang: Suchanfrage = LEERE_ANFRAGE): Suchlage {
  const [text, textSetzen] = useState(anfang.text);
  const [rohBereich, bereichSetzen] = useState<string | null>(anfang.bereich);

  const gebaut = useMemo(() => bauen(katalog), [katalog]);

  /*
    Ein mitgebrachter Bereich, den es nicht mehr gibt, filterte still auf
    nichts: `imBereich` wäre dann für jeden Eintrag falsch, und der
    Bereichsname fiele ausgerechnet auf „Alle Bereiche“ zurück – ein
    Leersatz, der sich selbst widerspricht.

    Geprüft wird beim **Lesen**, nicht durch ein Zurücksetzen: Ein Effekt,
    der hier Zustand setzte, wäre `react-hooks/set-state-in-effect` –
    dieselbe Falle wie in Befund H28. Ableiten statt setzen.

    Heute ist der Fall nicht erreichbar (der Katalog wird nur aus dem
    Fehlerzustand des Startbildschirms neu geladen, und dort gibt es keine
    Anfrage mehr). Die zwei Zeilen kosten nichts und decken den
    Katalogwechsel ab, den `docs/entscheidung-katalog-aktualitaet.md` als
    offene Aufgabe führt.
  */
  const bereich =
    rohBereich === null || gebaut.bereiche.some((b) => b.id === rohBereich) ? rohBereich : null;

  const lage = useMemo(() => {
    const ergebnis = suchen(gebaut.index, text, bereich);
    const woerter = suchwoerter(text);
    const fehlende = new Set(schuldigeWoerter(gebaut.index, woerter));
    const nummernIds = nummernTreffer(gebaut.nummern, text);
    /* Ein Nummerntreffer ist ein Treffer. Ohne diese Zeile stand über der
       gefundenen Frage „Keine Frage gefunden.“ – und genau dieser Satz ging
       auch in die Live-Region. */
    const leer =
      !ergebnis.stoebern &&
      !ergebnis.zuKurz &&
      ergebnis.treffer.length === 0 &&
      nummernIds.length === 0;

    return {
      ergebnis,
      nummernIds,
      /* Die Gegenrechnung kostet einen zweiten Lauf – also 0,15 ms. Ohne sie
         wüsste niemand, dass die Einschränkung schuld ist. */
      ohneBereich:
        bereich === null ? ergebnis.treffer.length : suchen(gebaut.index, text).treffer.length,
      /* Zurückübersetzt auf das, was der Nutzer wirklich getippt hat. */
      unbekannt: text
        .split(/\s+/u)
        .filter((roh) => roh.length > 0)
        .filter((roh) => suchwoerter(roh).some((gefaltet) => fehlende.has(gefaltet))),
      zerlegung:
        leer && woerter.length === 1 ? zerlegung(gebaut.index, text.trim(), bereich) : null,
      bruecke: brueckeFinden(gebaut.index, text, bereich),
      leer,
    };
  }, [gebaut, text, bereich]);

  const meldung = useMemo(() => {
    /* Ein Nummerntreffer geht vor: Bei der Eingabe „5“ standen drei Fragen
       auf dem Schirm, während die Ansage „Bitte noch ein Zeichen eingeben“
       lautete. Wer hört statt sieht, hielt die Suche für nicht ausgeführt. */
    if (lage.nummernIds.length > 0 && lage.ergebnis.treffer.length === 0) {
      return lage.nummernIds.length === 1
        ? 'Eine Frage über die Nummer gefunden.'
        : `${String(lage.nummernIds.length)} Fragen über die Nummer gefunden.`;
    }
    if (lage.ergebnis.zuKurz) {
      return 'Bitte noch ein Zeichen eingeben.';
    }
    const anzahl = lage.ergebnis.treffer.length;
    const bereichsname = gebaut.bereiche.find((b) => b.id === bereich)?.titel ?? 'Alle Bereiche';
    if (lage.ergebnis.stoebern) {
      return `${String(anzahl)} Fragen in ${bereichsname}.`;
    }
    /* Der Bereich wird auch bei Treffern genannt. Bis Fassung 0.26.0 hieß
       es schlicht „7 Fragen gefunden.“ – wer auf ein Kapitel eingeschränkt
       hatte, erfuhr an dieser Stelle nicht, worauf sich die Zahl bezieht.
       Seit der Rückweg die Einschränkung mitbringt, wiegt das schwerer:
       Sie kann gesetzt sein, ohne dass man sie selbst gerade gesetzt hat. */
    const wo = bereich === null ? '' : ` in ${bereichsname}`;
    if (anzahl === 0) {
      return `Keine Frage gefunden${wo}.`;
    }
    return anzahl === 1 ? `Eine Frage gefunden${wo}.` : `${String(anzahl)} Fragen gefunden${wo}.`;
  }, [lage, gebaut, bereich]);

  const ansage = useVerzoegerteAnsage(meldung);

  return {
    text,
    bereich,
    bereiche: gebaut.bereiche,
    ...lage,
    ansage,
    textSetzen,
    bereichSetzen,
  };
}
