package eu.mulk.aendggner.anwendung;

import eu.mulk.aendggner.aenderung.Aenderungsbefehl;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Aufhebung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Neufassung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturEinfuegung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturErsetzung;
import eu.mulk.aendggner.aenderung.Stelle;
import eu.mulk.aendggner.anwendung.BefehlAnwender.AngewandteAenderung;
import eu.mulk.aendggner.anwendung.BefehlAnwender.Status;
import eu.mulk.aendggner.gesetz.Norm;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import org.jspecify.annotations.Nullable;

/**
 * Wendet Angabe-Befehle auf die Inhaltsübersichts-Norm an. Deren Text besteht aus Tabellenzeilen
 * („§ 5 | Titel“) und Zwischenüberschriften („2. Abschnitt - …“); die Befehle ersetzen, entfernen
 * oder ergänzen einzelne solcher Zeilen.
 */
final class InhaltsuebersichtAnwender {

  private static final String ENBEZ = "Inhaltsübersicht";

  private InhaltsuebersichtAnwender() {}

  /**
   * Behandelt die Struktur-Befehle auf der Inhaltsübersicht (Angabe gefasst/ersetzt/gestrichen/
   * eingefügt). Liefert {@code null} für wortweise Operationen — die laufen über die normalen
   * Textzweige des {@link BefehlAnwender}, weil die Inhaltsübersicht eine gewöhnliche Norm ist.
   */
  static @Nullable AngewandteAenderung wendeAn(List<Norm> normen, Aenderungsbefehl befehl) {
    return switch (befehl) {
      case Neufassung n -> {
        var ziel = zielKette(n.stelle());
        if (ziel.isEmpty()) {
          yield ersetzeGesamteUebersicht(normen, n);
        }
        yield ersetzeZeilen(normen, befehl, ziel, ziel, n.neuerText());
      }
      case StrukturErsetzung s -> {
        var von = zielKette(s.stelle());
        var bis = s.bisStelle() != null ? zielKette(s.bisStelle()) : von;
        if (von.isEmpty() || bis.isEmpty()) {
          yield manuell(befehl, "Angabe-Bereich nennt kein auflösbares Ziel.");
        }
        yield ersetzeZeilen(normen, befehl, von, bis, s.text());
      }
      case Aufhebung a -> {
        var ziel = zielKette(a.stelle());
        if (ziel.isEmpty()) {
          yield manuell(befehl, "Zu streichende Angabe nennt kein auflösbares Ziel.");
        }
        yield ersetzeZeilen(normen, befehl, ziel, ziel, null);
      }
      case StrukturEinfuegung e -> {
        var anker = zielKette(e.stelle());
        if (anker.isEmpty()) {
          yield manuell(befehl, "Einfügeanker in der Inhaltsübersicht nennt kein Ziel.");
        }
        yield fuegeZeilenEin(normen, befehl, anker, e.vorher(), e.text());
      }
      case Aenderungsbefehl.Ersetzung ignoriert -> null;
      case Aenderungsbefehl.Streichung ignoriert -> null;
      case Aenderungsbefehl.WoerterEinfuegung ignoriert -> null;
      default ->
          manuell(befehl, "Änderungen an der Inhaltsübersicht werden nicht automatisch angewandt.");
    };
  }

  /** Die adressierte Angabe: die §-/Gliederungs-Komponenten hinter der Inhaltsübersichts-Marke. */
  private static List<Stelle.Komponente> zielKette(Stelle stelle) {
    return stelle.komponenten().stream()
        .filter(
            k -> k instanceof Stelle.Paragraph || k instanceof Stelle.Gliederungseinheit)
        .toList();
  }

  /** Ersetzt die Zeilen von {@code von} bis {@code bis} durch die Angaben des Zitats (oder nichts). */
  private static AngewandteAenderung ersetzeZeilen(
      List<Norm> normen,
      Aenderungsbefehl befehl,
      List<Stelle.Komponente> von,
      List<Stelle.Komponente> bis,
      @Nullable String zitat) {
    int normIndex = StellenAufloeser.normIndex(gesetzAus(normen), ENBEZ);
    if (normIndex < 0) {
      return manuell(befehl, "Das Gesetz enthält keine Inhaltsübersicht.");
    }
    var norm = normen.get(normIndex);
    var vonFund = findeZeile(norm, von);
    if (vonFund.fehler() != null) {
      return manuell(befehl, vonFund.fehler());
    }
    var bisFund = von.equals(bis) ? vonFund : findeZeile(norm, bis);
    if (bisFund.fehler() != null) {
      return manuell(befehl, bisFund.fehler());
    }
    if (vonFund.absatzIndex() != bisFund.absatzIndex() || bisFund.bisZeile() < vonFund.vonZeile()) {
      return manuell(befehl, "Angabe-Bereich liegt nicht zusammenhängend in der Inhaltsübersicht.");
    }
    var zeilen = new ArrayList<>(zeilenVon(norm, vonFund.absatzIndex()));
    var einrueckung = einrueckungVon(zeilen.get(vonFund.vonZeile()));
    for (int i = bisFund.bisZeile(); i >= vonFund.vonZeile(); i--) {
      zeilen.remove(i);
    }
    if (zitat != null) {
      zeilen.addAll(vonFund.vonZeile(), angabenZeilen(zitat, einrueckung));
    }
    setzeZeilen(normen, normIndex, vonFund.absatzIndex(), zeilen);
    return angewandt(befehl);
  }

  private static AngewandteAenderung fuegeZeilenEin(
      List<Norm> normen,
      Aenderungsbefehl befehl,
      List<Stelle.Komponente> anker,
      boolean vorher,
      String zitat) {
    int normIndex = StellenAufloeser.normIndex(gesetzAus(normen), ENBEZ);
    if (normIndex < 0) {
      return manuell(befehl, "Das Gesetz enthält keine Inhaltsübersicht.");
    }
    var norm = normen.get(normIndex);
    var fund = findeZeile(norm, anker);
    if (fund.fehler() != null) {
      return manuell(befehl, fund.fehler());
    }
    var zeilen = new ArrayList<>(zeilenVon(norm, fund.absatzIndex()));
    var einrueckung = einrueckungVon(zeilen.get(fund.vonZeile()));
    int position = vorher ? fund.vonZeile() : fund.bisZeile() + 1;
    zeilen.addAll(position, angabenZeilen(zitat, einrueckung));
    setzeZeilen(normen, normIndex, fund.absatzIndex(), zeilen);
    return angewandt(befehl);
  }

  // --- Zeilenmodell ----------------------------------------------------------------------------

  private record Zeilenfund(
      int absatzIndex, int vonZeile, int bisZeile, @Nullable String fehler) {
    static Zeilenfund fehlgeschlagen(String begruendung) {
      return new Zeilenfund(-1, -1, -1, begruendung);
    }
  }

  /**
   * Findet die (norm-weit eindeutige) Zeile der adressierten Angabe. Die Kette wird verschachtelt
   * aufgelöst: „Teil 2 Abschnitt 4“ sucht die Abschnitt-Zeile erst hinter der Teil-2-Zeile (und
   * vor dem nächsten Teil), sodass gleichnamige Abschnitte anderer Teile nicht stören.
   */
  private static Zeilenfund findeZeile(Norm norm, List<Stelle.Komponente> kette) {
    var ziel = kette.get(kette.size() - 1);
    Zeilenfund gefunden = null;
    for (int a = 0; a < norm.absaetze().size(); a++) {
      var zeilen = zeilenVon(norm, a);
      int von = 0;
      int bis = zeilen.size();
      boolean fenstergueltig = true;
      for (int k = 0; k < kette.size() - 1 && fenstergueltig; k++) {
        int eltern = eindeutigeZeile(zeilen, zeilenMuster(kette.get(k)), von, bis);
        if (eltern < 0) {
          fenstergueltig = false;
          continue;
        }
        von = eltern + 1;
        bis = naechsteGleichrangige(zeilen, kette.get(k), von, zeilen.size());
      }
      if (!fenstergueltig) {
        continue;
      }
      int treffer = eindeutigeZeile(zeilen, zeilenMuster(ziel), von, bis);
      if (treffer == -2) {
        return Zeilenfund.fehlgeschlagen(
            "Die Angabe zu „" + anzeige(ziel) + "“ ist in der Inhaltsübersicht mehrdeutig.");
      }
      if (treffer >= 0) {
        if (gefunden != null) {
          return Zeilenfund.fehlgeschlagen(
              "Die Angabe zu „" + anzeige(ziel) + "“ ist in der Inhaltsübersicht mehrdeutig.");
        }
        gefunden = new Zeilenfund(a, treffer, treffer, null);
      }
    }
    if (gefunden == null) {
      return Zeilenfund.fehlgeschlagen(
          "Die Angabe zu „" + anzeige(ziel) + "“ ist in der Inhaltsübersicht nicht auffindbar.");
    }
    return gefunden;
  }

  /** Die eindeutige Trefferzeile in [von,bis): Index, -1 (nicht gefunden) oder -2 (mehrdeutig). */
  private static int eindeutigeZeile(List<String> zeilen, Pattern muster, int von, int bis) {
    int treffer = -1;
    for (int z = von; z < bis; z++) {
      if (muster.matcher(zeilen.get(z).strip()).find()) {
        if (treffer >= 0) {
          return -2;
        }
        treffer = z;
      }
    }
    return treffer;
  }

  /** Die nächste Zeile derselben Gliederungsart („Teil <n>“) als Fenstergrenze. */
  private static int naechsteGleichrangige(
      List<String> zeilen, Stelle.Komponente eltern, int von, int bis) {
    if (!(eltern instanceof Stelle.Gliederungseinheit g)) {
      return bis;
    }
    var muster =
        Pattern.compile(
            "^(?:"
                + Pattern.quote(g.art())
                + "\\s+\\d|\\d+[a-z]?\\.\\s*"
                + Pattern.quote(g.art())
                + "\\b)");
    for (int z = von; z < bis; z++) {
      if (muster.matcher(zeilen.get(z).strip()).find()) {
        return z;
      }
    }
    return bis;
  }

  private static Pattern zeilenMuster(Stelle.Komponente ziel) {
    return switch (ziel) {
      case Stelle.Paragraph p ->
          Pattern.compile(
              "^" + Pattern.quote(p.sigel()) + "\\s*" + Pattern.quote(p.nummer()) + "(?![0-9a-z])");
      case Stelle.Gliederungseinheit g -> {
        if (g.nummer().isEmpty()) {
          yield Pattern.compile("^" + Pattern.quote(g.art()) + "\\b");
        }
        // Beide Schreibweisen: „Abschnitt 2“ und „2. Abschnitt“.
        yield Pattern.compile(
            "^(?:"
                + Pattern.quote(g.art())
                + "\\s+"
                + Pattern.quote(g.nummer())
                + "(?![0-9a-z])|"
                + Pattern.quote(g.nummer())
                + "\\.\\s*"
                + Pattern.quote(g.art())
                + "\\b)");
      }
      default -> Pattern.compile("(?!)");
    };
  }

  /**
   * „Die Inhaltsübersicht wird durch die folgende Inhaltsübersicht ersetzt: „…““ — der komplette
   * Zeilenbestand der Inhaltsübersichts-Norm wird aus dem Zitat neu aufgebaut.
   */
  private static AngewandteAenderung ersetzeGesamteUebersicht(
      List<Norm> normen, Neufassung befehl) {
    int normIndex = StellenAufloeser.normIndex(gesetzAus(normen), ENBEZ);
    if (normIndex < 0) {
      return manuell(befehl, "Das Gesetz enthält keine Inhaltsübersicht.");
    }
    var flach = befehl.neuerText().replaceAll("\\s+", " ").strip();
    flach = flach.replaceFirst("^Inhaltsübersicht\\s*", "");
    // Plausibilitätssperre: Enthält das Zitat Befehlssprache, hat vermutlich ein unbalanciertes
    // Anführungszeichen nachfolgende Befehle in das Zitat gezogen — dann keinesfalls anwenden.
    if (flach.contains("wie folgt geändert") || flach.contains(" wird wie folgt gefasst")) {
      return manuell(
          befehl,
          "Das Zitat der neuen Inhaltsübersicht enthält Befehlstext — vermutlich ist ein"
              + " Anführungszeichen unbalanciert; bitte manuell prüfen.");
    }
    var zeilen = new ArrayList<String>();
    for (var stueck : UEBERSICHT_MARKE.split(flach)) {
      var s = stueck.strip();
      if (s.isEmpty()) {
        continue;
      }
      var m =
          Pattern.compile(
                  "^((?:Teil|Abschnitt|Unterabschnitt|Kapitel|Buch)\\s+\\d+[a-z]?|Anhang"
                      + "|(?:§|Art\\.)\\s*\\d+[a-z]*)\\s*(.*)$")
              .matcher(s);
      if (m.matches() && !m.group(2).isEmpty()) {
        zeilen.add(m.group(1) + " | " + m.group(2));
      } else {
        zeilen.add(s);
      }
    }
    if (zeilen.size() < 2) {
      return manuell(befehl, "Das Zitat enthält keine erkennbare Inhaltsübersicht.");
    }
    var norm = normen.get(normIndex);
    normen.set(
        normIndex,
        norm.mitAbsaetzen(
            List.of(new eu.mulk.aendggner.gesetz.Absatz(null, String.join("\n", zeilen)))));
    return angewandt(befehl);
  }

  // Zeilenanfänge einer Inhaltsübersicht: §-Angaben (nicht Querverweise) und Gliederungsmarken.
  private static final Pattern UEBERSICHT_MARKE =
      Pattern.compile(
          "(?=(?:§|Art\\.)\\s*\\d+[a-z]?\\s+"
              + "(?!Absatz|Absätze|Abs|Satz|Sätze|Nummer|Nummern|Nr|Buchstabe|Buchstaben"
              + "|und|bis|oder|sowie|des|der|dieses)"
              + "(?:\\(|\\p{Lu})"
              + "|(?<!\\S)(?:Teil|Abschnitt|Unterabschnitt|Kapitel|Buch) \\d+[a-z]?(?!\\S)"
              + "|(?<!\\S)Anhang(?!\\S))");

  /**
   * Zerlegt das Zitat in Angabe-Zeilen: bei §-Angaben eine Zeile je Paragraph (im Zeilenformat der
   * Inhaltsübersicht, „§ N | Titel“), sonst eine einzelne Zeile.
   */
  private static List<String> angabenZeilen(String zitat, String einrueckung) {
    var flach = zitat.strip().replaceAll("\\s+", " ");
    var zeilen = new ArrayList<String>();
    if (flach.startsWith("§") || flach.startsWith("Art.")) {
      for (var stueck : PARAGRAPH_ANGABE.split(flach)) {
        var s = stueck.strip();
        if (s.isEmpty()) {
          continue;
        }
        var m = Pattern.compile("^((?:§|Art\\.)\\s*\\d+[a-z]*)\\s*(.*)$").matcher(s);
        if (m.matches() && !m.group(2).isEmpty()) {
          zeilen.add(einrueckung + m.group(1) + " | " + m.group(2));
        } else {
          zeilen.add(einrueckung + s);
        }
      }
    }
    if (zeilen.isEmpty()) {
      zeilen.add(einrueckung + flach);
    }
    return zeilen;
  }

  // Trennt einen Block mehrerer §-Angaben an den §-Anfängen; Querverweise („… zu § 3 Absatz 3“)
  // trennen nicht (nach ihnen folgt ein Kleinwort oder eine Strukturangabe statt eines Titels).
  private static final Pattern PARAGRAPH_ANGABE =
      Pattern.compile(
          "(?=(?:§|Art\\.)\\s*\\d+[a-z]?\\s+"
              + "(?!Absatz|Absätze|Abs|Satz|Sätze|Nummer|Nummern|Nr|Buchstabe|Buchstaben"
              + "|und|bis|oder|sowie|des|der|dieses)"
              + "(?:\\(|\\p{Lu}))");

  private static List<String> zeilenVon(Norm norm, int absatzIndex) {
    return norm.absaetze().get(absatzIndex).text().lines().toList();
  }

  private static void setzeZeilen(
      List<Norm> normen, int normIndex, int absatzIndex, List<String> zeilen) {
    var norm = normen.get(normIndex);
    var absaetze = new ArrayList<>(norm.absaetze());
    absaetze.set(absatzIndex, absaetze.get(absatzIndex).mitText(String.join("\n", zeilen)));
    normen.set(normIndex, norm.mitAbsaetzen(absaetze));
  }

  private static String einrueckungVon(String zeile) {
    return zeile.substring(0, zeile.length() - zeile.stripLeading().length());
  }

  private static String anzeige(Stelle.Komponente ziel) {
    return switch (ziel) {
      case Stelle.Paragraph p -> p.enbez();
      case Stelle.Gliederungseinheit g -> g.bezeichnung();
      default -> ziel.toString();
    };
  }

  private static AngewandteAenderung angewandt(Aenderungsbefehl befehl) {
    return new AngewandteAenderung(
        befehl, Status.ANGEWANDT, "", new LinkedHashSet<>(List.of(ENBEZ)));
  }

  private static AngewandteAenderung manuell(Aenderungsbefehl befehl, String begruendung) {
    return new AngewandteAenderung(befehl, Status.MANUELL_PRUEFEN, begruendung, Set.of());
  }

  private static eu.mulk.aendggner.gesetz.Gesetz gesetzAus(List<Norm> normen) {
    return new eu.mulk.aendggner.gesetz.Gesetz("", null, null, normen);
  }
}
