import { useCallback, useEffect, useId, useRef, useState } from 'react';

import type { Profil } from '../../../../shared/lernstand';
import { bridge } from '../../bridge';
import { fehlerMeldung, kanalFehltMeldung } from '../../lernen/bruecke';
import { useFokusBeimAufbau } from '../../hooks/useFokusBeimAufbau';
import { Hinweis } from '../lernen/Hinweis';

/** Muss zu `MAX_PROFILNAME_LAENGE` in `main/lernstand.ts` passen. */
const MAX_NAME = 60;

type Formular =
  | { readonly art: 'keins' }
  | { readonly art: 'anlegen' }
  | { readonly art: 'umbenennen'; readonly profilId: number }
  | { readonly art: 'loeschen'; readonly profilId: number };

interface Meldung {
  readonly art: 'hinweis' | 'fehler';
  readonly text: string;
}

export interface ProfilverwaltungProps {
  readonly profile: readonly Profil[];
  readonly profilId: number | null;
  readonly laedt: boolean;
  /** Wechselt das laufende Profil – nur vom Startbildschirm aus aufrufen. */
  readonly onWechseln: (profilId: number) => void;
  /** Die Profilliste hat sich geändert und muss neu geladen werden. */
  readonly onGeaendert: () => void;
}

/**
 * Profile anlegen, wechseln, umbenennen, löschen.
 *
 * **Warum es das braucht.** Der Anwendungskern konnte all das von Anfang an –
 * `profil:anlegen` und `profil:aktualisieren` sind geprüft und getestet. Die
 * Oberfläche hat die Kanäle nie aufgerufen: Es gab genau ein Profil, das beim
 * ersten Start entstand, und keinen Weg zu einem zweiten. Ein gemeinsam
 * genutzter Rechner – Verein, Familie, Schießstand – vermischte damit die
 * Lernstände aller.
 *
 * **Warum der Wechsel hier steht und nicht überall.** Ein Wechsel mitten in
 * einer Sitzung oder Prüfung wäre ein stiller Datenfehler: Die Frageliste
 * stammte noch vom alten Profil, die Antwort landete beim neuen. Der
 * Startbildschirm ist der einzige Ort, an dem nichts läuft – deshalb steht
 * der Wechsel nur hier.
 *
 * **Warum jede folgenreiche Aktion eine Rückfrage hat.** Ein Profil zu
 * löschen nimmt Wochen Arbeit mit. WCAG 3.3.4 verlangt für solche Schritte
 * Umkehr, Prüfung oder Bestätigung; umkehren lässt sich das nicht.
 */
export function Profilverwaltung({
  profile,
  profilId,
  laedt,
  onWechseln,
  onGeaendert,
}: ProfilverwaltungProps): React.JSX.Element {
  const [formular, setzeFormular] = useState<Formular>({ art: 'keins' });
  const [laeuft, setzeLaeuft] = useState(false);
  /* Derselbe Wert als Ref: `laeuft` steht erst nach dem nächsten Bildaufbau
     zur Verfügung, und zwei schnelle Klicks liegen davor. Siehe
     {@link ausfuehren}. */
  const laeuftRef = useRef(false);
  const [meldung, setzeMeldung] = useState<Meldung | null>(null);

  /*
    Wohin der Fokus geht, wenn ein Knopf verschwindet.

    Jedes Formular ersetzt die Schaltflächen, aus denen es hervorging, und
    holt den Fokus zu sich. Verschwindet es wieder, fiele er ohne Zutun auf
    `<body>`: Wer per Tastatur bedient, stünde am Seitenanfang und müsste
    sich durch die halben Einstellungen zurückarbeiten.

    Zwei Ziele kommen in Frage. Gibt es den auslösenden Knopf noch –
    Abbrechen, Umbenennen, Anlegen –, bekommt er den Fokus zurück. Ist er
    mitsamt seiner Zeile verschwunden – gelöschtes Profil – oder war er nur
    für das alte Profil da – „Wechseln" –, wandert der Fokus auf die
    Bestätigung: Sie sagt in Worten, was geschehen ist, und steht dicht bei
    der Liste.

    Umgesetzt über Rückruf-Referenzen statt über eine Merkliste von
    DOM-Knoten: Der Knopf meldet sich, sobald er wieder da ist, und nimmt
    den Fokus im selben Zug mit. Das kommt ohne einen Blick in eine
    Referenz während des Renderns aus.
  */
  const [ausloeser, setzeAusloeser] = useState<string | null>(null);
  const [fokusZiel, setzeFokusZiel] = useState<string | null>(null);
  const [fokusAufMeldung, setzeFokusAufMeldung] = useState(0);

  const meldungRef = useRef<HTMLDivElement | null>(null);
  useEffect(() => {
    if (fokusAufMeldung > 0) {
      meldungRef.current?.focus();
    }
  }, [fokusAufMeldung]);

  const knopfRef =
    (schluessel: string) =>
    (element: HTMLButtonElement | null): void => {
      if (element !== null && fokusZiel === schluessel) {
        element.focus();
        setzeFokusZiel(null);
      }
    };

  const schliessen = useCallback(() => {
    setzeFormular({ art: 'keins' });
    setzeFokusZiel(ausloeser);
    setzeAusloeser(null);
  }, [ausloeser]);

  /**
   * Ruft einen Profilkanal auf und räumt danach auf.
   *
   * Alle vier Aktionen laufen gleich ab: Kanal prüfen, aufrufen, Formular
   * schließen, Erfolg oder Fehler melden. Das einmal zu schreiben hält die
   * Fehlerbehandlung an einer Stelle – und damit gleich.
   */
  const ausfuehren = useCallback(
    (
      kanal: string,
      arbeit: (() => Promise<void>) | null,
      erfolgstext: string,
      fehlertext: string,
      /** Wohin nach dem Erfolg – zurück zum Knopf oder auf die Meldung. */
      fokusNach: 'ausloeser' | 'meldung' = 'ausloeser',
    ) => {
      if (arbeit === null) {
        setzeMeldung({ art: 'fehler', text: kanalFehltMeldung(kanal, fehlertext) });
        return;
      }

      /*
        Der Riegel gegen den zweiten Klick, und er sitzt hier statt an jeder
        Schaltfläche einzeln.

        Die Knöpfe tragen `aria-disabled` – bewusst, denn ein `disabled`
        verlöre den Fokus. Damit bleiben sie aber klickbar, und zwei schnelle
        Klicks riefen den Kanal zweimal. Bei genau zwei Profilen ging der
        erste Löschversuch durch und der zweite lief in „Das letzte Profil
        lässt sich nicht löschen“: Der Nutzer las eine rote Fehlermeldung,
        obwohl sein Profil weg war. Beim Anlegen dasselbe mit „Diesen Namen
        gibt es schon“.
      */
      if (laeuftRef.current) {
        return;
      }
      laeuftRef.current = true;
      setzeLaeuft(true);
      setzeMeldung(null);
      void arbeit()
        .then(() => {
          setzeMeldung({ art: 'hinweis', text: erfolgstext });
          if (fokusNach === 'meldung') {
            setzeFormular({ art: 'keins' });
            setzeAusloeser(null);
            setzeFokusAufMeldung((bisher) => bisher + 1);
          } else {
            schliessen();
          }
        })
        .catch((fehler: unknown) => {
          setzeMeldung({ art: 'fehler', text: fehlerMeldung(fehler, fehlertext) });
          /* Das Formular bleibt offen: Der eingegebene Name steht noch da
             und lässt sich verbessern, statt neu getippt zu werden. */
        })
        .finally(() => {
          laeuftRef.current = false;
          setzeLaeuft(false);
        });
    },
    [schliessen],
  );

  const anlegen = useCallback(
    (name: string) => {
      const kanal = bridge()?.profilAnlegen;
      ausfuehren(
        'profil:anlegen',
        kanal === undefined
          ? null
          : async () => {
              await kanal(name);
              onGeaendert();
            },
        `Das Profil „${name}“ ist angelegt. Sie lernen weiterhin unter Ihrem bisherigen Profil; zum Wechseln benutzen Sie die Schaltfläche in der Liste.`,
        'Das Profil konnte nicht angelegt werden.',
      );
    },
    [ausfuehren, onGeaendert],
  );

  const umbenennen = useCallback(
    (profil: Profil, name: string) => {
      if (name === profil.name) {
        schliessen();
        return;
      }
      const kanal = bridge()?.profilAktualisieren;
      ausfuehren(
        'profil:aktualisieren',
        kanal === undefined
          ? null
          : async () => {
              await kanal(profil.id, { name });
              onGeaendert();
            },
        `Das Profil heißt jetzt „${name}“.`,
        'Das Profil konnte nicht umbenannt werden.',
      );
    },
    [ausfuehren, onGeaendert, schliessen],
  );

  const loeschen = useCallback(
    (profil: Profil) => {
      const kanal = bridge()?.profilLoeschen;
      ausfuehren(
        'profil:loeschen',
        kanal === undefined
          ? null
          : async () => {
              const rest = await kanal(profil.id);
              /* Wer sich selbst gelöscht hat, braucht sofort ein neues
                 Zuhause – sonst zeigten Übersicht und Plan noch auf ein
                 Profil, das es nicht mehr gibt. */
              const nachfolger = rest[0];
              if (profil.id === profilId && nachfolger !== undefined) {
                onWechseln(nachfolger.id);
              } else {
                onGeaendert();
              }
            },
        `Das Profil „${profil.name}“ ist gelöscht.`,
        'Das Profil konnte nicht gelöscht werden.',
        /* Der auslösende Knopf ist mit seiner Zeile verschwunden. */
        'meldung',
      );
    },
    [ausfuehren, onGeaendert, onWechseln, profilId],
  );

  const oeffnen = (naechstes: Formular, schluessel: string): void => {
    setzeAusloeser(schluessel);
    setzeMeldung(null);
    setzeFormular(naechstes);
  };

  const einziges = profile.length <= 1;

  return (
    <div className="profile">
      <p className="profile__erklaerung">
        Jedes Profil führt seinen eigenen Lernstand: Antworten, Fortschritt je Frage, Merkliste,
        Prüfungstermin und Prüfungssimulationen. Nichts davon wird zwischen Profilen geteilt. Wer
        allein an diesem Rechner lernt, braucht nur das eine.
      </p>

      {laedt && profile.length === 0 ? <p>Die Profile werden geladen …</p> : null}

      <ul className="profile__liste">
        {profile.map((profil) => {
          const aktuell = profil.id === profilId;
          const umbenennenOffen = formular.art === 'umbenennen' && formular.profilId === profil.id;
          const loeschenOffen = formular.art === 'loeschen' && formular.profilId === profil.id;

          return (
            <li
              key={profil.id}
              className={`profile__zeile${aktuell ? ' profile__zeile--aktuell' : ''}`}
            >
              <p className="profile__name">
                <strong>{profil.name}</strong>
                {aktuell ? <span className="profile__marke"> – lernt gerade</span> : null}
              </p>

              {umbenennenOffen ? (
                <Namensformular
                  titel={`Profil „${profil.name}“ umbenennen`}
                  vorgabe={profil.name}
                  beschriftung="Neuer Name"
                  bestaetigen="Namen speichern"
                  laeuft={laeuft}
                  onAbbrechen={schliessen}
                  onBestaetigen={(name) => {
                    umbenennen(profil, name);
                  }}
                />
              ) : null}

              {loeschenOffen ? (
                <Loeschfrage
                  profil={profil}
                  aktuell={aktuell}
                  laeuft={laeuft}
                  onNein={schliessen}
                  onJa={() => {
                    loeschen(profil);
                  }}
                />
              ) : null}

              {umbenennenOffen || loeschenOffen ? null : (
                <div className="profile__aktionen">
                  {aktuell ? null : (
                    <button
                      type="button"
                      className="knopf"
                      ref={knopfRef(`wechseln:${String(profil.id)}`)}
                      onClick={() => {
                        setzeMeldung({
                          art: 'hinweis',
                          text: `Sie lernen jetzt als „${profil.name}“.`,
                        });
                        setzeFokusAufMeldung((bisher) => bisher + 1);
                        onWechseln(profil.id);
                      }}
                    >
                      {/* Der sichtbare Text steht am Anfang des zugänglichen
                          Namens – sonst wäre die Sprachsteuerung ausgesperrt
                          (WCAG 2.5.3). */}
                      Wechseln
                      <span className="nur-screenreader"> zu Profil {profil.name}</span>
                    </button>
                  )}

                  <button
                    type="button"
                    className="knopf"
                    ref={knopfRef(`umbenennen:${String(profil.id)}`)}
                    onClick={() => {
                      oeffnen(
                        { art: 'umbenennen', profilId: profil.id },
                        `umbenennen:${String(profil.id)}`,
                      );
                    }}
                  >
                    Umbenennen
                    <span className="nur-screenreader"> – Profil {profil.name}</span>
                  </button>

                  {einziges ? null : (
                    <button
                      type="button"
                      className="knopf"
                      ref={knopfRef(`loeschen:${String(profil.id)}`)}
                      onClick={() => {
                        oeffnen(
                          { art: 'loeschen', profilId: profil.id },
                          `loeschen:${String(profil.id)}`,
                        );
                      }}
                    >
                      Löschen
                      <span className="nur-screenreader"> – Profil {profil.name}</span>
                    </button>
                  )}
                </div>
              )}
            </li>
          );
        })}
      </ul>

      {einziges ? (
        <p className="profile__hinweis">
          Das letzte Profil lässt sich nicht löschen – ohne Profil hätte die Anwendung keinen Ort
          für Ihre Antworten. Wenn Sie von vorn anfangen wollen, benutzen Sie weiter unten „Neu
          anfangen“.
        </p>
      ) : null}

      {formular.art === 'anlegen' ? (
        <Namensformular
          titel="Neues Profil anlegen"
          vorgabe=""
          beschriftung="Name des neuen Profils"
          bestaetigen="Profil anlegen"
          laeuft={laeuft}
          onAbbrechen={schliessen}
          onBestaetigen={anlegen}
        />
      ) : (
        <div className="profile__anlegen">
          <button
            type="button"
            className="knopf"
            ref={knopfRef('anlegen')}
            onClick={() => {
              oeffnen({ art: 'anlegen' }, 'anlegen');
            }}
          >
            Neues Profil anlegen
          </button>
        </div>
      )}

      <div
        role="status"
        aria-live="polite"
        className="profile__meldung"
        ref={meldungRef}
        tabIndex={-1}
      >
        {meldung === null ? null : <Hinweis art={meldung.art}>{meldung.text}</Hinweis>}
      </div>
    </div>
  );
}

/** Anlegen und Umbenennen unterscheiden sich nur in den Wörtern. */
function Namensformular({
  titel,
  vorgabe,
  beschriftung,
  bestaetigen,
  laeuft,
  onAbbrechen,
  onBestaetigen,
}: {
  readonly titel: string;
  readonly vorgabe: string;
  readonly beschriftung: string;
  readonly bestaetigen: string;
  readonly laeuft: boolean;
  readonly onAbbrechen: () => void;
  readonly onBestaetigen: (name: string) => void;
}): React.JSX.Element {
  const feldRef = useFokusBeimAufbau<HTMLInputElement>();
  const feldId = useId();
  const [name, setzeName] = useState(vorgabe);
  const [fehler, setzeFehler] = useState<string | null>(null);

  const gekuerzt = name.trim();

  return (
    <form
      className="profile__formular"
      aria-labelledby={`${feldId}-titel`}
      onSubmit={(ereignis) => {
        ereignis.preventDefault();
        /* Vorprüfung im Haus, damit der leere Name nicht erst über den
           Anwendungskern und zurück reisen muss (WCAG 3.3.1). */
        if (gekuerzt.length === 0) {
          setzeFehler('Bitte geben Sie einen Namen ein.');
          feldRef.current?.focus();
          return;
        }
        setzeFehler(null);
        onBestaetigen(gekuerzt);
      }}
    >
      <h4 className="profile__formular-titel" id={`${feldId}-titel`}>
        {titel}
      </h4>

      <label htmlFor={feldId}>{beschriftung}</label>
      <input
        id={feldId}
        ref={feldRef}
        type="text"
        className="profile__feld"
        value={name}
        maxLength={MAX_NAME}
        autoComplete="off"
        spellCheck={false}
        /*
          Der Fehlertext hängt im Fehlerfall zusätzlich an `aria-describedby`.

          `aria-errormessage` allein genügte nicht: VoiceOver und ältere
          NVDA- und JAWS-Stände werten es nicht oder unvollständig aus. Dort
          hörte man beim erneuten Fokussieren des Feldes nur „ungültig“, aber
          nicht warum – die `role="alert"`-Ansage beim Erscheinen fängt nur
          den ersten Augenblick ab. Die Prüfungswahl hält es seit jeher so
          (`Pruefungswahl.tsx`, Zahlenfelder); diese Stelle war die einzige
          im Baum, die davon abwich.
        */
        aria-describedby={fehler === null ? `${feldId}-hilfe` : `${feldId}-hilfe ${feldId}-fehler`}
        {...(fehler === null
          ? {}
          : { 'aria-invalid': true, 'aria-errormessage': `${feldId}-fehler` })}
        onChange={(ereignis) => {
          setzeName(ereignis.target.value);
          setzeFehler(null);
        }}
      />
      <p id={`${feldId}-hilfe`} className="profile__feldhilfe">
        Höchstens {MAX_NAME} Zeichen. Der Name steht nur auf diesem Rechner; er wird nirgends
        übertragen.
      </p>

      {fehler === null ? null : (
        <div id={`${feldId}-fehler`}>
          <Hinweis art="fehler" alsMeldung>
            {fehler}
          </Hinweis>
        </div>
      )}

      <div className="profile__formular-aktionen">
        <button type="submit" className="knopf knopf--haupt" aria-disabled={laeuft}>
          {laeuft ? 'Wird gespeichert …' : bestaetigen}
        </button>
        <button type="button" className="knopf" onClick={onAbbrechen}>
          Abbrechen
        </button>
      </div>
    </form>
  );
}

/** Die Rückfrage vor dem Löschen – eigener Baustein wegen des Fokus. */
function Loeschfrage({
  profil,
  aktuell,
  laeuft,
  onJa,
  onNein,
}: {
  readonly profil: Profil;
  readonly aktuell: boolean;
  readonly laeuft: boolean;
  readonly onJa: () => void;
  readonly onNein: () => void;
}): React.JSX.Element {
  const bereichRef = useFokusBeimAufbau<HTMLDivElement>();
  const titelId = useId();

  return (
    <div
      className="profile__loeschfrage"
      ref={bereichRef}
      tabIndex={-1}
      role="group"
      aria-labelledby={titelId}
      aria-describedby={`${titelId}-text`}
    >
      <h4 className="profile__formular-titel" id={titelId}>
        Profil „{profil.name}“ wirklich löschen?
      </h4>
      <p id={`${titelId}-text`}>
        Damit verschwinden alle Antworten, der Fortschritt je Frage, die Merkliste, der
        Prüfungstermin und alle Prüfungssimulationen dieses Profils. Das lässt sich nicht rückgängig
        machen.
        {aktuell
          ? ' Sie lernen gerade unter diesem Profil; danach wechselt die Anwendung auf ein anderes.'
          : ''}
      </p>

      <div className="profile__formular-aktionen">
        {/* Das Harmlose zuerst und als Hauptschaltfläche: Wer versehentlich
            hier gelandet ist, trifft mit der Eingabetaste nichts Schlimmes. */}
        <button type="button" className="knopf knopf--haupt" onClick={onNein}>
          Nein, behalten
        </button>
        <button
          type="button"
          className="knopf knopf--warnung"
          aria-disabled={laeuft}
          onClick={onJa}
        >
          {laeuft ? 'Wird gelöscht …' : 'Ja, Profil löschen'}
        </button>
      </div>
    </div>
  );
}
