package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;

import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;

/**
 * Einzeilige Zusammenfassungsleiste, die nach Abschluss eines Verarbeitungslaufs
 * die aggregierten Ergebnisse anzeigt.
 *
 * <p>Das Banner erscheint nach Laufabschluss unterhalb des Fortschrittsbalkens und
 * oberhalb der Ergebnistabelle. Es zeigt nur Kategorien, deren Zähler größer als null
 * ist. Folgende Status werden nicht gezählt und tauchen nie im Banner auf:
 * {@code READY_FOR_AI}, {@code PROPOSAL_READY} und {@code PROCESSING} sind im
 * Enum {@link DocumentCompletionStatus} nicht enthalten – alle enthaltenen Werte
 * werden gezählt, außer Einträgen mit {@code resetPending=true}, da diese keinen
 * abgeschlossenen Zustand darstellen.
 *
 * <p>Farbe ist niemals das einzige Unterscheidungsmerkmal: Jedes Segment enthält
 * ein Icon und einen Text.
 *
 * <p>Die öffentlichen Methoden {@link #clear()} und {@link #update(Map)} sind
 * thread-agnostisch definiert, aber müssen auf dem JavaFX Application Thread aufgerufen
 * werden (oder das Banner muss via {@code Platform.runLater} aktualisiert werden).
 * Die Aggregations-Hilfsmethode {@link #aggregateCounts(Iterable)} ist vollständig
 * unabhängig von JavaFX und kann auf jedem Thread aufgerufen werden.
 */
public final class BatchRunSummaryBanner {

    /** Trennzeichen zwischen den Kategoriesegmenten. */
    private static final String SEGMENT_SEPARATOR = "  ·  ";

    /** Abstand zwischen den Label-Segmenten in Pixeln. */
    private static final int SPACING = 0;

    /** Innerer Abstand des Containers in Pixeln (oben/unten). */
    private static final double PADDING_V = 4.0;

    /** Standardfarbe für den Summentext. */
    private static final String STYLE_DEFAULT = "-fx-font-size: 12;";

    /**
     * Alle {@link DocumentCompletionStatus}-Werte, die im Banner angezeigt werden,
     * in der verbindlichen Anzeigereihenfolge gemäß Spezifikation.
     */
    private static final List<DocumentCompletionStatus> DISPLAYED_ORDER = List.of(
            DocumentCompletionStatus.SUCCESS,
            DocumentCompletionStatus.FAILED_RETRYABLE,
            DocumentCompletionStatus.FAILED_PERMANENT,
            DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED,
            DocumentCompletionStatus.SKIPPED_FINAL_FAILURE
    );

    /** Wurzel-Container des Banners – wird in das Tab-Layout eingebettet. */
    private final HBox container;

    /**
     * Vertikale Halterung fuer mehrzeilige Banner-Inhalte.
     *
     * <p>Enthaelt die Status-Zeile (bestehend), die Token-Zeile, die
     * Kosten-Zeile und optional eine Cache-only-Zeile. Die Token-/Kosten-
     * /Cache-Zeilen werden in V3.3 mit einem leeren {@link BatchRunTokenSummary}
     * vorbelegt; die echten Aggregat-Werte werden durch das nachfolgende
     * Arbeitspaket geliefert.
     */
    private final VBox lineContainer;

    /** Label, das den kompletten Bannertext als Inline-Segmente trägt. */
    private final Label contentLabel;

    /** Label fuer die Tokenzeile (Input/Output). */
    private final Label tokenLabel;

    /** Label fuer die Kosten-Zeile. */
    private final Label costLabel;

    /** Label fuer die optionale Cache-only-Hinweiszeile. */
    private final Label cacheOnlyLabel;

    /**
     * Erstellt ein neues, initial unsichtbares Summary-Banner.
     */
    public BatchRunSummaryBanner() {
        contentLabel = new Label();
        contentLabel.setStyle(STYLE_DEFAULT);
        contentLabel.setWrapText(false);

        tokenLabel = new Label();
        tokenLabel.setStyle(STYLE_DEFAULT);
        costLabel = new Label();
        costLabel.setStyle(STYLE_DEFAULT);
        cacheOnlyLabel = new Label();
        cacheOnlyLabel.setStyle(STYLE_DEFAULT);
        cacheOnlyLabel.setVisible(false);
        cacheOnlyLabel.setManaged(false);

        lineContainer = new VBox(2, contentLabel, tokenLabel, costLabel, cacheOnlyLabel);
        lineContainer.setAlignment(Pos.CENTER_LEFT);

        container = new HBox(SPACING, lineContainer);
        container.setAlignment(Pos.CENTER_LEFT);
        container.setStyle("-fx-padding: " + PADDING_V + " 0 " + PADDING_V + " 0;");

        // Initial unsichtbar, nimmt keinen Platz ein
        container.setVisible(false);
        container.setManaged(false);
    }

    // -------------------------------------------------------------------------
    // Öffentliche API
    // -------------------------------------------------------------------------

    /**
     * Versteckt das Banner und leert seinen Inhalt.
     *
     * <p>Muss auf dem JavaFX Application Thread aufgerufen werden.
     */
    public void clear() {
        contentLabel.setText("");
        tokenLabel.setText("");
        costLabel.setText("");
        cacheOnlyLabel.setText("");
        cacheOnlyLabel.setVisible(false);
        cacheOnlyLabel.setManaged(false);
        container.setVisible(false);
        container.setManaged(false);
    }

    /**
     * Aktualisiert das Banner mit den aggregierten Zählern und macht es sichtbar.
     *
     * <p>Zeigt nur Kategorien mit Anzahl &gt; 0. Wenn alle Zähler null sind (leerer Lauf),
     * wird das Banner versteckt.
     *
     * <p>Muss auf dem JavaFX Application Thread aufgerufen werden.
     *
     * @param counts Zuordnung von Verarbeitungsstatus zu Anzahl;
     *               fehlende Status werden als 0 interpretiert; darf nicht null sein
     */
    public void update(Map<DocumentCompletionStatus, Integer> counts) {
        update(counts, BatchRunTokenSummary.empty());
    }

    /**
     * Aktualisiert das Banner mit Status-Zaehlern und Token-/Kosten-Aggregaten.
     *
     * <p>Zeigt die Status-Zeile (wenn nicht leer) sowie die Token- und Kosten-
     * Zeilen. Die Cache-only-Zeile erscheint nur, wenn {@link
     * BatchRunTokenSummary#cacheOnlyAttemptCount()} groesser als 0 ist.
     *
     * <p>Muss auf dem JavaFX Application Thread aufgerufen werden.
     *
     * @param counts        Zaehler je Status; nicht {@code null}
     * @param tokenSummary  Aggregat-Werte fuer Tokens und Kosten; nicht {@code null}
     */
    public void update(Map<DocumentCompletionStatus, Integer> counts,
                       BatchRunTokenSummary tokenSummary) {
        Objects.requireNonNull(counts, "counts darf nicht null sein");
        Objects.requireNonNull(tokenSummary, "tokenSummary darf nicht null sein");

        String text = buildBannerText(counts);
        contentLabel.setText(text);

        tokenLabel.setText(buildTokenLine(tokenSummary));
        costLabel.setText(buildCostLine(tokenSummary));

        if (tokenSummary.cacheOnlyAttemptCount() > 0) {
            cacheOnlyLabel.setText(buildCacheOnlyLine(tokenSummary.cacheOnlyAttemptCount()));
            cacheOnlyLabel.setVisible(true);
            cacheOnlyLabel.setManaged(true);
        } else {
            cacheOnlyLabel.setText("");
            cacheOnlyLabel.setVisible(false);
            cacheOnlyLabel.setManaged(false);
        }

        if (text.isEmpty() && !tokenSummary.hasAnyData()) {
            clear();
            return;
        }
        container.setVisible(true);
        container.setManaged(true);
    }

    /**
     * Token-/Kosten-Aggregat fuer einen Banner-Eintrag.
     *
     * <p>Ein {@link #empty()}-Default reicht, solange das Read-Model fuer
     * Aggregate noch nicht implementiert ist. AP-B liefert spaeter die echten
     * Werte ueber den {@code TokenStatisticsReadModelPort}.
     *
     * @param totalInputTokens        Summe Input-Tokens; ggf. {@code 0}
     * @param totalOutputTokens       Summe Output-Tokens; ggf. {@code 0}
     * @param totalCostUsd            Summe der Kosten in USD; ggf. {@code BigDecimal.ZERO}
     * @param hasMissingPriceSnapshot {@code true}, wenn mind. ein Versuch ohne Preis-Snapshot vorlag
     * @param hasCacheTokensIgnored   {@code true}, wenn Cache-Tokens vorkamen
     * @param cacheOnlyAttemptCount   Anzahl Cache-only-Versuche im Lauf
     */
    public record BatchRunTokenSummary(
            long totalInputTokens,
            long totalOutputTokens,
            BigDecimal totalCostUsd,
            boolean hasMissingPriceSnapshot,
            boolean hasCacheTokensIgnored,
            long cacheOnlyAttemptCount) {

        /**
         * Liefert ein leeres Aggregat (alle Zaehler null, Kosten 0).
         *
         * @return leeres Aggregat
         */
        public static BatchRunTokenSummary empty() {
            return new BatchRunTokenSummary(0L, 0L, BigDecimal.ZERO, false, false, 0L);
        }

        /**
         * Pruefung, ob ueberhaupt Daten zum Anzeigen vorliegen.
         *
         * @return {@code true} bei Werten ungleich 0
         */
        public boolean hasAnyData() {
            return totalInputTokens > 0 || totalOutputTokens > 0
                    || (totalCostUsd != null && totalCostUsd.signum() != 0)
                    || cacheOnlyAttemptCount > 0;
        }
    }

    private static String buildTokenLine(BatchRunTokenSummary s) {
        return String.format(Locale.GERMAN, "Tokens: Input %,d   Output %,d",
                s.totalInputTokens(), s.totalOutputTokens());
    }

    private static String buildCostLine(BatchRunTokenSummary s) {
        BigDecimal cost = s.totalCostUsd() != null ? s.totalCostUsd() : BigDecimal.ZERO;
        BigDecimal rounded = cost.setScale(4, RoundingMode.HALF_UP);
        StringBuilder sb = new StringBuilder("Kosten: $").append(rounded.toPlainString());
        if (s.hasCacheTokensIgnored()) {
            sb.append(" (ohne Cache-Anteil)");
        }
        if (s.hasMissingPriceSnapshot()) {
            sb.append(" (unvollstaendig)");
        }
        return sb.toString();
    }

    private static String buildCacheOnlyLine(long count) {
        return "ℹ " + count + " Cache-only Versuche (in Kosten nicht enthalten)";
    }

    /**
     * Liefert den JavaFX-Container-Knoten zum Einbetten in das Tab-Layout.
     *
     * @return der Container-Knoten; nie null
     */
    public HBox getNode() {
        return container;
    }

    // -------------------------------------------------------------------------
    // Aggregations-Hilfe (thread-agnostisch, testbar ohne JavaFX)
    // -------------------------------------------------------------------------

    /**
     * Zählt die Anzahl jedes {@link DocumentCompletionStatus} in der übergebenen
     * Iterable. Einträge mit {@code resetPending=true} werden ignoriert, da sie
     * keinen abgeschlossenen Verarbeitungszustand darstellen.
     *
     * <p>Diese Methode ist vollständig unabhängig von JavaFX und kann auf jedem
     * Thread aufgerufen werden.
     *
     * @param rows die Ergebniszeilen des Laufs; darf nicht null sein;
     *             null-Elemente werden übersprungen
     * @return eine Map mit der Anzahl je Status; enthält alle anzuzeigenden
     *         Status (fehlende haben Wert 0); nie null
     */
    public static Map<DocumentCompletionStatus, Integer> aggregateCounts(
            Iterable<? extends GuiBatchRunResultRow> rows) {
        Objects.requireNonNull(rows, "rows darf nicht null sein");

        Map<DocumentCompletionStatus, Integer> counts = new EnumMap<>(DocumentCompletionStatus.class);
        // Alle anzuzeigenden Status mit 0 vorbelegen
        for (DocumentCompletionStatus status : DISPLAYED_ORDER) {
            counts.put(status, 0);
        }

        for (GuiBatchRunResultRow row : rows) {
            if (row == null) {
                continue;
            }
            // Reset-Pending-Zeilen zählen nicht – sie haben noch keinen abgeschlossenen Status
            if (row.resetPending()) {
                continue;
            }
            DocumentCompletionStatus status = row.status();
            // Nur anzuzeigende Status zählen (entspricht dem Ausschluss von
            // Übergangszuständen wie READY_FOR_AI, PROPOSAL_READY, PROCESSING)
            if (counts.containsKey(status)) {
                counts.merge(status, 1, Integer::sum);
            }
        }
        return counts;
    }

    // -------------------------------------------------------------------------
    // Interne Hilfsmethoden
    // -------------------------------------------------------------------------

    /**
     * Erzeugt den angezeigten Bannertext aus den Zählern.
     * Liefert einen leeren String wenn alle Zähler null sind.
     *
     * @param counts die Zähler je Status; darf nicht null sein
     * @return der fertige Bannertext oder ein leerer String
     */
    static String buildBannerText(Map<DocumentCompletionStatus, Integer> counts) {
        List<String> segments = new ArrayList<>();
        for (DocumentCompletionStatus status : DISPLAYED_ORDER) {
            int count = counts.getOrDefault(status, 0);
            if (count > 0) {
                String icon = ProcessingStatusPresentation.iconFor(status);
                String category = ProcessingStatusPresentation.summaryCategoryFor(status);
                segments.add(icon + " " + count + " " + category);
            }
        }
        return String.join(SEGMENT_SEPARATOR, segments);
    }
}
