package eu.mulk.aendggner;

import eu.mulk.aendggner.aenderung.DokumentArt;
import eu.mulk.aendggner.aenderung.DokumentKopf;
import eu.mulk.aendggner.aenderung.parse.AenderungsantragParser;
import eu.mulk.aendggner.aenderung.parse.AenderungsgesetzParser;
import eu.mulk.aendggner.aenderung.parse.DokumentErkenner;
import eu.mulk.aendggner.aenderung.parse.EntwurfsPatcher;
import eu.mulk.aendggner.aenderung.parse.PatchTextExtraktor;
import eu.mulk.aendggner.aenderung.parse.SuperskriptModus;
import eu.mulk.aendggner.aenderung.parse.TextBereiniger;
import eu.mulk.aendggner.anwendung.BefehlAnwender;
import eu.mulk.aendggner.gesetz.Gesetz;
import eu.mulk.aendggner.gesetz.Superskript;
import eu.mulk.aendggner.gesetz.gii.GiiXmlLoader;
import eu.mulk.aendggner.gesetz.land.LandesRechtLoader;
import eu.mulk.aendggner.synopse.HtmlRenderer;
import eu.mulk.aendggner.synopse.SynopseBuilder;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.jspecify.annotations.Nullable;

/**
 * Kernpipeline: Stammgesetz laden → Änderungsgesetze parsen und anwenden → Synopse rendern.
 *
 * <p>Wird sowohl von der Befehlszeile ({@link AendGgner}) als auch von der Browserfassung ({@code
 * eu.mulk.aendggner.wasm.BrowserMain}) verwendet, damit die Anwendungslogik nur an einer Stelle
 * existiert. Sie kennt kein Dateisystem: Eingaben kommen als {@link Quelle} (Name und Bytes).
 */
public final class Pipeline {

  private static final org.jboss.logging.Logger log =
      org.jboss.logging.Logger.getLogger(Pipeline.class);

  private Pipeline() {}

  public record Ergebnis(
      String html,
      long anzahlAngewandt,
      long anzahlManuell,
      int anzahlGeaenderteNormen,
      int anzahlProtokollEintraege) {}

  /**
   * Ein eingespeistes Änderungsdokument samt erkannter Art und aufbereitetem Text.
   *
   * @param eingearbeitet die Dokumente, die vor der Anwendung in {@code text} eingearbeitet wurden
   *     — bei einem Entwurf die Änderungsanträge, die ihn geändert haben. Sie gehen in die
   *     Quellenzeile ein, denn die gezeigte Fassung ist ohne sie nicht nachvollziehbar.
   * @param fassung welche von mehreren Fassungen des Dokuments gilt, sofern es mehrere trägt — die
   *     Beschlussempfehlung stellt Entwurf und Ausschussfassung nebeneinander. Sonst {@code null}.
   */
  record Quelldokument(
      Quelle quelle,
      DokumentKopf kopf,
      String text,
      List<String> eingearbeitet,
      @Nullable String fassung) {

    Quelldokument(Quelle quelle, DokumentKopf kopf, String text) {
      this(quelle, kopf, text, List.of(), null);
    }

    Quelldokument(Quelle quelle, DokumentKopf kopf, String text, List<String> eingearbeitet) {
      this(quelle, kopf, text, eingearbeitet, null);
    }

    String quellenAngabe(List<String> artikel) {
      var sb = new StringBuilder(quelle.name());
      sb.append(" [").append(kopf.anzeigeName());
      if (fassung != null) {
        sb.append(", ").append(fassung);
      }
      sb.append("]");
      for (var zusatz : eingearbeitet) {
        sb.append(" + ").append(zusatz);
      }
      return sb.append(" (Artikel ").append(String.join(", ", artikel)).append(")").toString();
    }
  }

  /** Bequemlichkeit für Befehlszeile und Tests; im Browser gibt es keine {@link Path}e. */
  public static Ergebnis erzeugeSynopse(
      Path baseFile, List<Path> patches, String artikel, boolean vollstaendig) throws Exception {
    var patchQuellen = new ArrayList<Quelle>();
    for (var patch : patches) {
      patchQuellen.add(Quelle.lies(patch));
    }
    return erzeugeSynopse(Quelle.lies(baseFile), patchQuellen, artikel, vollstaendig);
  }

  public static Ergebnis erzeugeSynopse(
      Quelle baseFile, List<Quelle> patches, String artikel, boolean vollstaendig)
      throws Exception {
    var altesGesetz = ladeStammgesetz(baseFile);
    var extraktor = new PatchTextExtraktor(superskriptModus(altesGesetz));
    var parser = new AenderungsgesetzParser();

    var warnungen = new ArrayList<String>();
    var dokumente = wendeAntraegeAn(leseDokumente(patches, extraktor, warnungen), warnungen);

    var gesetz = altesGesetz;
    var protokoll = new ArrayList<BefehlAnwender.AngewandteAenderung>();
    var quellen = new ArrayList<String>();
    boolean entwurfsfassung = false;

    for (var dokument : dokumente) {
      var parseErgebnis = parser.parse(dokument.text(), gesetz, artikel, entwurfsGrenzen(dokument));
      if (parseErgebnis.befehle().isEmpty()) {
        warnungen.add(
            "In %s (%s) wurde kein auf %s anwendbarer Artikel gefunden."
                .formatted(
                    dokument.quelle().name(), dokument.kopf().anzeigeName(), gesetz.jurabk()));
      }
      var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
      gesetz = anwendung.neu();
      protokoll.addAll(anwendung.protokoll());
      warnungen.addAll(parseErgebnis.warnungen());
      entwurfsfassung |= dokument.kopf().art().istEntwurfsfassung();
      quellen.add(dokument.quellenAngabe(parseErgebnis.artikel()));
    }

    var gesamtErgebnis = new BefehlAnwender.AnwendungsErgebnis(gesetz, protokoll);
    var synopse = SynopseBuilder.baue(altesGesetz, gesamtErgebnis, warnungen, vollstaendig);
    var quellenZeile = baseFile.name() + " + " + String.join(" + ", quellen);
    var html = HtmlRenderer.rendere(synopse, quellenZeile, entwurfsfassung);

    return new Ergebnis(
        html,
        gesamtErgebnis.anzahlAngewandt(),
        gesamtErgebnis.anzahlManuell(),
        synopse.eintraege().size(),
        protokoll.size());
  }

  /**
   * Liest die Änderungsdokumente ein, bestimmt ihre Art und sortiert die aus, aus denen keine
   * Synopse zu gewinnen ist. Verworfen wird nichts stillschweigend: Jedes ausgesonderte Dokument
   * hinterlässt eine Warnung, die in der Synopse erscheint.
   */
  private static List<Quelldokument> leseDokumente(
      List<Quelle> patches, PatchTextExtraktor extraktor, List<String> warnungen) throws Exception {
    var dokumente = new ArrayList<Quelldokument>();
    for (var datei : patches) {
      var rohText = extraktor.extrahiere(datei);
      // Die Erkennung arbeitet auf dem Rohtext: Der Bereiniger entfernt genau die
      // Drucksachenköpfe, aus denen Art und Nummer hervorgehen.
      var kopf = DokumentErkenner.erkenne(rohText);
      log.infof("Datei %s erkannt als %s.", datei.name(), kopf.anzeigeName());
      if (kopf.art() == DokumentArt.OHNE_BEFEHLE) {
        warnungen.add(
            "%s ist ein %s und enthält keine Änderungsbefehle; die Datei wurde übergangen."
                .formatted(datei.name(), kopf.art().anzeigeName()));
        continue;
      }
      if (kopf.art() == DokumentArt.BESCHLUSSEMPFEHLUNG) {
        // Die maßgebliche Fassung steht nicht im Fließtext, sondern in der zweispaltigen
        // Zusammenstellung: links der Entwurf, rechts die Beschlüsse des Ausschusses.
        var fassung = extraktor.leseZusammenstellung(datei);
        warnungen.addAll(fassung.warnungen());
        if (fassung.text() == null) {
          // Eine halb aufgelöste Fassung auszugeben wäre schlimmer als keine.
          warnungen.add(
              ("%s ist eine Beschlussempfehlung, deren Zusammenstellung sich nicht auflösen ließ;"
                      + " die Datei wurde übergangen. Für eine Synopse eignet sich der zugrunde"
                      + " liegende Gesetzentwurf%s.")
                  .formatted(
                      datei.name(),
                      kopf.bezugsDrucksachen().isEmpty()
                          ? ""
                          : " (Drs. " + kopf.bezugsDrucksachen().get(0) + ")"));
          continue;
        }
        dokumente.add(
            new Quelldokument(
                datei,
                kopf,
                TextBereiniger.bereinige(fassung.text()),
                List.of(),
                "Ausschussfassung"));
        continue;
      }
      dokumente.add(new Quelldokument(datei, kopf, TextBereiniger.bereinige(rohText)));
    }
    return dokumente;
  }

  /**
   * Wendet jeden Änderungsantrag auf den Entwurf an, den er ändern will, und nimmt ihn aus der
   * Liste: Was danach bleibt, sind lauter Dokumente, die unmittelbar das Stammgesetz ändern.
   *
   * <p>Zugeordnet wird über die Drucksachennummer, die der Antrag selbst nennt („(Drs. 19/9707)“).
   * Nur wo die fehlt, entscheidet die Reihenfolge der Argumente — der zuletzt genannte Entwurf
   * davor. Findet sich gar kein Entwurf, bleibt der Antrag unangewandt und wird gemeldet; ihn
   * ersatzweise auf das Stammgesetz loszulassen wäre falsch, denn seine Stellenangaben zielen auf
   * die Drucksache.
   */
  private static List<Quelldokument> wendeAntraegeAn(
      List<Quelldokument> dokumente, List<String> warnungen) {
    if (dokumente.stream().noneMatch(d -> d.kopf().art() == DokumentArt.AENDERUNGSANTRAG)) {
      return dokumente;
    }
    var ergebnis = new ArrayList<>(dokumente);
    for (var antrag : dokumente) {
      if (antrag.kopf().art() != DokumentArt.AENDERUNGSANTRAG) {
        continue;
      }
      ergebnis.remove(antrag);
      int zielIndex = findeEntwurf(ergebnis, antrag, dokumente.indexOf(antrag));
      if (zielIndex < 0) {
        warnungen.add(
            ("%s ist ein Änderungsantrag zu %s; der zugehörige Gesetzentwurf wurde nicht"
                    + " mitgegeben, der Antrag blieb daher unberücksichtigt.")
                .formatted(
                    antrag.quelle().name(),
                    antrag.kopf().bezugsDrucksachen().isEmpty()
                        ? "einer Drucksache"
                        : "Drs. " + String.join(", ", antrag.kopf().bezugsDrucksachen())));
        continue;
      }
      var ziel = ergebnis.get(zielIndex);
      var parseErgebnis = AenderungsantragParser.parse(antrag.text());
      warnungen.addAll(parseErgebnis.warnungen());
      var patch = EntwurfsPatcher.wendeAn(ziel.text(), parseErgebnis.befehle());
      warnungen.addAll(patch.warnungen());
      log.infof(
          "%s: %d von %d Antragsbefehlen auf %s angewandt.",
          antrag.quelle().name(),
          patch.angewandt(),
          parseErgebnis.befehle().size(),
          ziel.quelle().name());
      var eingearbeitet = new ArrayList<>(ziel.eingearbeitet());
      eingearbeitet.add(antrag.quelle().name() + " [" + antrag.kopf().anzeigeName() + "]");
      ergebnis.set(
          zielIndex,
          new Quelldokument(
              ziel.quelle(),
              ziel.kopf(),
              patch.text(),
              List.copyOf(eingearbeitet),
              ziel.fassung()));
    }
    return ergebnis;
  }

  /** Der Index des Entwurfs, den {@code antrag} ändert; {@code -1}, wenn keiner dabei ist. */
  private static int findeEntwurf(
      List<Quelldokument> dokumente, Quelldokument antrag, int antragsPosition) {
    for (int i = 0; i < dokumente.size(); i++) {
      var kopf = dokumente.get(i).kopf();
      if (kopf.eigeneDrucksache() != null
          && antrag.kopf().bezugsDrucksachen().contains(kopf.eigeneDrucksache())) {
        return i;
      }
    }
    int letzter = -1;
    for (int i = 0; i < dokumente.size() && i < antragsPosition; i++) {
      if (dokumente.get(i).kopf().art() == DokumentArt.GESETZENTWURF) {
        letzter = i;
      }
    }
    return letzter;
  }

  /**
   * Entwürfe tragen hinter dem Regelungstext einen Begründungsteil, dessen Freitext keine Befehle
   * enthält und den letzten Artikel nicht verunreinigen darf. Verkündete Gesetze haben ihn nicht —
   * dort bliebe die Suche nach Begründungsmarken folgenlos, aber sie unterbleibt trotzdem, damit
   * ein Gesetzblatt nicht an einem gleichlautenden Wort abbricht.
   */
  private static boolean entwurfsGrenzen(Quelldokument dokument) {
    return dokument.kopf().art().istEntwurfsfassung();
  }

  /** Bequemlichkeit für Befehlszeile und Tests; im Browser gibt es keine {@link Path}e. */
  static Gesetz ladeStammgesetz(Path baseFile) throws Exception {
    return ladeStammgesetz(Quelle.lies(baseFile));
  }

  /**
   * Gii-XML → {@link GiiXmlLoader}; PDF/Klartext (Landesrecht) → {@link LandesRechtLoader}.
   *
   * <p>Vorweg wird ausgepackt: gesetze-im-internet.de gibt das Norm-XML nur als {@code xml.zip}
   * aus, und das soll unentpackt taugen (siehe {@link ZipAuspacker}). Die Änderungsdokumente
   * bleiben davon unberührt — Gesetzblätter und Drucksachen kommen nirgends als Archiv.
   */
  static Gesetz ladeStammgesetz(Quelle baseFile) throws Exception {
    var quelle = ZipAuspacker.auspacken(baseFile);
    return DateiTyp.erkenne(quelle.inhalt()) == DateiTyp.XML
        ? new GiiXmlLoader().load(quelle)
        : new LandesRechtLoader().load(quelle);
  }

  /**
   * Der Superskriptmodus folgt der Schreibweise des Stammgesetzes: Trägt es amtliche Satznummern
   * (bayerisches Landesrecht, Niedersachsen u.a.), werden auch die Änderungsgesetze mit
   * Superskript-Erhalt extrahiert, damit Zitate und Stammtext dieselbe Schreibweise tragen; sonst
   * (Bundesrecht, Länder ohne amtliche Satzzählung) sind hochgestellte Ziffern bloße Fußnotenmarker
   * und werden verworfen.
   */
  static SuperskriptModus superskriptModus(Gesetz gesetz) {
    for (var norm : gesetz.normen()) {
      for (var absatz : norm.absaetze()) {
        if (Superskript.traegtSatznummern(absatz.text())) {
          return SuperskriptModus.BEHALTEN;
        }
      }
    }
    return SuperskriptModus.ENTFERNEN;
  }
}
