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

import java.nio.file.Path;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiTooltipTexts;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.AiFailureMessageTranslator;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.ProcessingStatusPresentation;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt;
import de.gecheckt.pdf.umbenenner.application.port.out.history.DocumentHistoryRow;
import de.gecheckt.pdf.umbenenner.application.port.out.history.HistoryQuery;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultHistoryDetailsUseCase.HistoryDetailsResult;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultHistoryOverviewUseCase.HistoryOverviewResult;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
import de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.util.Duration;
import javafx.util.StringConverter;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SplitPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;

/**
 * Dritter Haupt-Tab des JavaFX-Editorfensters: der Historien-Tab „Verlauf".
 * <p>
 * Zeigt alle jemals verarbeiteten Dokumente aus der SQLite-Datenbank in einer
 * zweispaltigen Ansicht: links eine filterbare Dokumentenliste (~55%),
 * rechts ein Detailbereich mit Stammsatz, Versuchstabelle, KI-Begründung und
 * KI-Rohantwort (~45%).
 * Das Suchfeld ist mit einem 300-ms-Debounce ausgestattet (Live-Filter).
 *
 * <h2>Layout</h2>
 * <pre>
 *   ┌─────────────────────────────────────────────────────────────────┐
 *   │ [ Suchfeld                   ] [ Status ▾ ] [ Aktualisieren ]   │
 *   ├────────────────────────┬────────────────────────────────────────┤
 *   │ Dokumentenliste (~55%) │ Detailbereich (~45%)                   │
 *   │                        │   Dokument-Info                        │
 *   │                        │   Versuche-Tabelle                     │
 *   │                        │   KI-Begründung                        │
 *   ├────────────────────────┴────────────────────────────────────────┤
 *   │ [ Status zurücksetzen ] [ Eintrag löschen ]   Statuszeile       │
 *   └─────────────────────────────────────────────────────────────────┘
 * </pre>
 *
 * <h2>Threading</h2>
 * <p>Alle DB-Zugriffe laufen auf einem Hintergrund-Worker-Thread.
 * UI-Updates erfolgen ausschließlich via {@code Platform.runLater()}.
 * Destruktive Aktionen (Reset, Löschen) sind während eines aktiven
 * Verarbeitungslaufs deaktiviert.
 */
public final class GuiHistoryTab {
    private static final String BOLD_STYLE = "-fx-font-weight: bold;";
    private static final String NO_ERROR_DETAILS_MSG = "Keine Fehlerdetails gespeichert.";
    private static final String NO_CONFIG_LOADED_MSG = "Keine Konfiguration geladen.";



    private static final Logger LOG = LogManager.getLogger(GuiHistoryTab.class);

    private static final String TAB_TITLE = "Verlauf";
    private static final String EMPTY_DB_TEXT = "Noch keine Verarbeitungen vorhanden.";
    private static final String TOO_MANY_RESULTS_TEXT =
            "Weitere Einträge vorhanden – Filter verwenden um die Trefferliste einzuschränken.";
    private static final String DETAIL_PLACEHOLDER = "Dokument auswählen für Details";
    private static final String NO_REASONING_TEXT = "Kein KI-Reasoning für diesen Versuch vorhanden.";
    private static final String NO_RAW_RESPONSE_TEXT = "Keine KI-Rohantwort für diesen Versuch gespeichert.";
    private static final String LOADING_TEXT = "Wird geladen …";
    private static final String LAUF_AKTIV_HINWEIS = "Aktion während Verarbeitungslauf nicht möglich.";

    private static final DateTimeFormatter TIMESTAMP_FMT =
            DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm").withZone(ZoneId.systemDefault());

    // ---- Bridge-Ports ---------------------------------------------------
    private final GuiHistoryOverviewPort overviewPort;
    private final GuiHistoryDetailsPort detailsPort;
    private final GuiHistoryResetDocumentStatusPort resetPort;
    private final GuiDeleteDocumentHistoryPort deletePort;
    private final BooleanSupplier runningCheck;
    /** Liefert den Pfad zur aktuell geladenen Konfigurationsdatei, oder {@code null} wenn keine geladen. */
    private final Supplier<Path> configPathSupplier;

    // ---- JavaFX-Knoten --------------------------------------------------
    private final Tab tab = new Tab(TAB_TITLE);

    private final TextField searchField = new TextField();
    private final ComboBox<ProcessingStatus> statusFilterBox = new ComboBox<>();
    private final Button refreshButton = new Button("Aktualisieren");

    private final TableView<DocumentHistoryRow> overviewTable = new TableView<>();
    private final ObservableList<DocumentHistoryRow> overviewItems = FXCollections.observableArrayList();

    private final Label statusBarLabel = new Label();
    private final Label moreThanMaxLabel = new Label();

    // Detailbereich
    private final GridPane detailGrid = new GridPane();
    private final Label detailFingerprintLabel = new Label();
    private final Label detailSourceFileLabel = new Label();
    private final Label detailSourcePathLabel = new Label();
    private final Label detailStatusLabel = new Label();
    private final Label detailCreatedLabel = new Label();
    private final Label detailUpdatedLabel = new Label();

    private final TableView<ProcessingAttempt> attemptsTable = new TableView<>();
    private final ObservableList<ProcessingAttempt> attemptsItems = FXCollections.observableArrayList();
    private final TextArea failureArea = new TextArea();
    private final TextArea reasoningArea = new TextArea();
    private final TextArea rawResponseArea = new TextArea();

    private final Button resetButton = new Button("Status zurücksetzen");
    private final Button deleteButton = new Button("Eintrag löschen");

    // ---- Zustand --------------------------------------------------------
    private final ExecutorService workerPool;

    /**
     * Debounce-Timer für das Suchfeld: löst {@link #loadOverview()} aus, sobald
     * 300 ms nach der letzten Texteingabe vergangen sind.
     */
    private final PauseTransition searchDebounce = new PauseTransition(Duration.millis(300));

    /**
     * Sperre für DB-lesende und DB-schreibende Aktionen während einer
     * laufenden Datenbank-Anlage (vgl. „Neue Datenbank anlegen"). Wird auf {@code true}
     * gesetzt, solange die Anlage einer neuen SQLite-Datenbank läuft, und nach Erfolg
     * oder Fehler zuverlässig zurückgesetzt. Während dieser Zeit sind Suche, Filter,
     * Aktualisieren, Status-Reset und Löschen deaktiviert.
     */
    private boolean dbBusy = false;

    /**
     * Erzeugt den Historien-Tab.
     *
     * @param overviewPort       Brücke zur Dokumentenübersicht; darf nicht {@code null} sein
     * @param detailsPort        Brücke zur Detailansicht; darf nicht {@code null} sein
     * @param resetPort          Brücke zum feldgenauen Status-Reset; darf nicht {@code null} sein
     * @param deletePort         Brücke zum vollständigen Löschen; darf nicht {@code null} sein
     * @param runningCheck       Liefert {@code true} wenn gerade ein Verarbeitungslauf aktiv ist;
     *                           darf nicht {@code null} sein
     * @param configPathSupplier Liefert den Pfad zur aktuell geladenen Konfigurationsdatei,
     *                           oder {@code null} wenn keine geladen ist; darf nicht {@code null} sein
     */
    public GuiHistoryTab(
            GuiHistoryOverviewPort overviewPort,
            GuiHistoryDetailsPort detailsPort,
            GuiHistoryResetDocumentStatusPort resetPort,
            GuiDeleteDocumentHistoryPort deletePort,
            BooleanSupplier runningCheck,
            Supplier<Path> configPathSupplier) {
        this.overviewPort = Objects.requireNonNull(overviewPort, "overviewPort darf nicht null sein");
        this.detailsPort = Objects.requireNonNull(detailsPort, "detailsPort darf nicht null sein");
        this.resetPort = Objects.requireNonNull(resetPort, "resetPort darf nicht null sein");
        this.deletePort = Objects.requireNonNull(deletePort, "deletePort darf nicht null sein");
        this.runningCheck = Objects.requireNonNull(runningCheck, "runningCheck darf nicht null sein");
        this.configPathSupplier = Objects.requireNonNull(configPathSupplier, "configPathSupplier darf nicht null sein");

        this.workerPool = Executors.newSingleThreadExecutor(r -> {
            Thread t = new Thread(r, "HistoryTabWorker");
            t.setDaemon(true);
            return t;
        });

        buildUi();
        wireEvents();
        tab.setClosable(false);
    }

    /**
     * Liefert den JavaFX-{@link Tab}, der in die TabPane eingefügt werden kann.
     *
     * @return der Tab; nie {@code null}
     */
    public Tab tab() {
        return tab;
    }

    /**
     * Lädt die Dokumentenübersicht neu – muss auf dem JavaFX Application Thread aufgerufen werden.
     * Wird vom Tab-Wechsel-Listener ausgelöst.
     */
    public void refresh() {
        loadOverview();
    }

    /**
     * Wird aufgerufen, wenn ein Verarbeitungslauf beendet wurde, damit Aktionsbuttons
     * wieder aktiviert werden können, falls ein Dokument in der Tabelle selektiert ist.
     * <p>
     * Muss auf dem JavaFX Application Thread aufgerufen werden.
     */
    public void notifyRunEnded() {
        if (!overviewTable.getSelectionModel().getSelectedItems().isEmpty()) {
            resetButton.setDisable(false);
            deleteButton.setDisable(false);
        }
    }

    /**
     * Schaltet die DB-Busy-Sperre des Verlauf-Tabs an oder aus.
     * <p>
     * Während der Sperre sind Suche, Statusfilter, Aktualisieren, Status-Reset und
     * Eintrag-Löschen deaktiviert. Wird typischerweise vom Workspace aufgerufen,
     * solange eine neue SQLite-Datenbank angelegt und aktiviert wird.
     * <p>
     * Muss auf dem JavaFX Application Thread aufgerufen werden.
     *
     * @param busy {@code true} aktiviert die Sperre, {@code false} hebt sie wieder auf
     */
    public void setDbBusy(boolean busy) {
        this.dbBusy = busy;
        searchField.setDisable(busy);
        statusFilterBox.setDisable(busy);
        refreshButton.setDisable(busy);
        if (busy) {
            resetButton.setDisable(true);
            deleteButton.setDisable(true);
        } else if (!overviewTable.getSelectionModel().getSelectedItems().isEmpty()
                && !runningCheck.getAsBoolean()) {
            resetButton.setDisable(false);
            deleteButton.setDisable(false);
        }
    }

    /**
     * Lädt die Übersicht erneut, sofern keine DB-Busy-Sperre aktiv ist.
     * <p>
     * Wird vom Workspace nach erfolgreichem Datenbank-Wechsel aufgerufen, damit der
     * Detailbereich und die Liste die neue (leere) Datenbank wiedergeben.
     * <p>
     * Muss auf dem JavaFX Application Thread aufgerufen werden.
     */
    public void reloadAfterDatabaseSwitch() {
        // Selektion aufheben, damit der Detailbereich nicht mit Stammdaten
        // aus der vorherigen Datenbank weiterzeigt.
        overviewTable.getSelectionModel().clearSelection();
        overviewItems.clear();
        clearDetailPane();
        loadOverview();
    }

    // =========================================================================
    // UI-Aufbau
    // =========================================================================

    private void buildUi() {
        // --- Toolbar ---
        searchField.setPromptText("Suche nach Dateiname …");
        searchField.setPrefWidth(300);
        Tooltip.install(searchField, new Tooltip(
                "Freitextsuche über Quell- und Zieldateiname (Groß-/Kleinschreibung egal)."));

        statusFilterBox.setConverter(new StringConverter<>() {
            @Override public String toString(ProcessingStatus s) {
                return s == null ? "Alle Status" : ProcessingStatusPresentation.displayTextFor(s);
            }
            @Override public ProcessingStatus fromString(String text) { return null; }
        });
        statusFilterBox.getItems().add(null); // "Alle Status"
        statusFilterBox.getItems().addAll(ProcessingStatus.values());
        statusFilterBox.getSelectionModel().selectFirst();
        Tooltip.install(statusFilterBox, new Tooltip("Status-Filter: nur Einträge mit diesem Status anzeigen."));

        refreshButton.setTooltip(new Tooltip("Dokumentenliste neu aus der Datenbank laden."));

        Region spacer = new Region();
        HBox.setHgrow(spacer, Priority.ALWAYS);

        HBox toolbar = new HBox(8, searchField, statusFilterBox, spacer, refreshButton);
        toolbar.setAlignment(Pos.CENTER_LEFT);
        toolbar.setPadding(new Insets(6, 8, 6, 8));

        // --- Dokumentenliste (links) ---
        buildOverviewTable();

        moreThanMaxLabel.setStyle("-fx-text-fill: #d98200; -fx-font-style: italic;");
        moreThanMaxLabel.setVisible(false);
        moreThanMaxLabel.setManaged(false);

        VBox leftPane = new VBox(4, overviewTable, moreThanMaxLabel);
        VBox.setVgrow(overviewTable, Priority.ALWAYS);
        leftPane.setPadding(new Insets(0, 4, 0, 0));

        // --- Detailbereich (rechts) ---
        VBox rightPane = buildDetailPane();

        // --- SplitPane ---
        SplitPane splitPane = new SplitPane(leftPane, rightPane);
        splitPane.setDividerPositions(0.55);

        // --- Aktionsleiste unten ---
        resetButton.setTooltip(new Tooltip(
                "Setzt Status, Fehlerzähler und letzten Fehlerzeitpunkt zurück. "
                + "Versuche bleiben erhalten. Das Dokument wird beim nächsten Lauf erneut verarbeitet."));
        deleteButton.setTooltip(new Tooltip(
                "Löscht den Eintrag und alle Versuche vollständig. "
                + "Diese Aktion ist nicht rückgängig zu machen."));
        resetButton.setDisable(true);
        deleteButton.setDisable(true);

        statusBarLabel.setStyle("-fx-text-fill: #555555; -fx-font-style: italic;");

        HBox actionBar = new HBox(8, resetButton, deleteButton, spacerNew(), statusBarLabel);
        actionBar.setAlignment(Pos.CENTER_LEFT);
        actionBar.setPadding(new Insets(6, 8, 6, 8));

        // --- Gesamtlayout ---
        BorderPane content = new BorderPane();
        content.setTop(toolbar);
        content.setCenter(splitPane);
        content.setBottom(actionBar);
        BorderPane.setMargin(toolbar, Insets.EMPTY);

        tab.setContent(content);
    }

    private void buildOverviewTable() {
        overviewTable.setItems(overviewItems);
        overviewTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
        overviewTable.setPlaceholder(new Label(EMPTY_DB_TEXT));
        overviewTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);

        // Status-Icon-Spalte
        TableColumn<DocumentHistoryRow, String> statusCol = new TableColumn<>();
        statusCol.setGraphic(columnHeader("Status", GuiTooltipTexts.VERLAUF_COL_STATUS));
        statusCol.setCellValueFactory(cell ->
                new SimpleStringProperty(statusIcon(cell.getValue().overallStatus())));
        statusCol.setCellFactory(col -> new TableCell<>() {
            @Override
            protected void updateItem(String icon, boolean empty) {
                super.updateItem(icon, empty);
                if (empty || icon == null) {
                    setText(null);
                    setTooltip(null);
                } else {
                    setText(icon);
                    DocumentHistoryRow row = getTableView().getItems().get(getIndex());
                    setStyle("-fx-text-fill: " + statusColor(row.overallStatus()) + "; -fx-font-weight: bold;");
                    setTooltip(new Tooltip(statusTooltip(row.overallStatus())));
                }
            }
        });
        statusCol.setPrefWidth(60);
        statusCol.setMaxWidth(70);

        // Quelldateiname
        TableColumn<DocumentHistoryRow, String> sourceCol = new TableColumn<>();
        sourceCol.setGraphic(columnHeader("Quelldatei", GuiTooltipTexts.VERLAUF_COL_QUELLDATEI));
        sourceCol.setCellValueFactory(cell ->
                new SimpleStringProperty(cell.getValue().sourceFileName()));
        sourceCol.setCellFactory(col -> ellipsisCell());

        // Zieldateiname
        TableColumn<DocumentHistoryRow, String> targetCol = new TableColumn<>();
        targetCol.setGraphic(columnHeader("Zieldatei", GuiTooltipTexts.VERLAUF_COL_ZIELDATEI));
        targetCol.setCellValueFactory(cell ->
                new SimpleStringProperty(
                        cell.getValue().targetFileName() != null ? cell.getValue().targetFileName() : "—"));
        targetCol.setCellFactory(col -> ellipsisCell());

        // Letzter Versuch
        TableColumn<DocumentHistoryRow, String> updatedCol = new TableColumn<>();
        updatedCol.setGraphic(columnHeader("Letzter Versuch", GuiTooltipTexts.VERLAUF_COL_LETZTER_VERSUCH));
        updatedCol.setCellValueFactory(cell ->
                new SimpleStringProperty(formatInstant(cell.getValue().updatedAt())));
        updatedCol.setPrefWidth(140);
        updatedCol.setMaxWidth(160);

        // Anzahl Versuche
        TableColumn<DocumentHistoryRow, String> countCol = new TableColumn<>();
        countCol.setGraphic(columnHeader("Versuche", GuiTooltipTexts.VERLAUF_COL_VERSUCHE));
        countCol.setCellValueFactory(cell ->
                new SimpleStringProperty(String.valueOf(cell.getValue().attemptCount())));
        countCol.setPrefWidth(70);
        countCol.setMaxWidth(80);

        overviewTable.getColumns().setAll(statusCol, sourceCol, targetCol, updatedCol, countCol);
    }

    private VBox buildDetailPane() {
        // Dokument-Info
        detailGrid.setHgap(8);
        detailGrid.setVgap(4);
        detailGrid.setPadding(new Insets(8));

        addDetailRow(0, "Fingerprint:", detailFingerprintLabel);
        addDetailRow(1, "Quelldatei:", detailSourceFileLabel);
        addDetailRow(2, "Quellpfad:", detailSourcePathLabel);
        addDetailRow(3, "Status:", detailStatusLabel);
        addDetailRow(4, "Erstellt:", detailCreatedLabel);
        addDetailRow(5, "Aktualisiert:", detailUpdatedLabel);

        Label detailTitle = new Label("Dokument-Details");
        detailTitle.setStyle(BOLD_STYLE);

        // Versuche-Tabelle
        buildAttemptsTable();
        Label attemptsTitle = new Label("Verarbeitungsversuche");
        attemptsTitle.setStyle(BOLD_STYLE);

        // Fehlerursache (aus letztem Fehler-Versuch)
        failureArea.setEditable(false);
        failureArea.setWrapText(true);
        failureArea.setPrefRowCount(3);
        failureArea.setPromptText(NO_ERROR_DETAILS_MSG);
        Label failureTitle = new Label("Fehlerursache (letzter Fehler-Versuch)");
        failureTitle.setStyle(BOLD_STYLE);

        failureArea.setTooltip(new Tooltip(GuiTooltipTexts.VERLAUF_FAILURE_AREA));

        // KI-Begründung
        reasoningArea.setEditable(false);
        reasoningArea.setWrapText(true);
        reasoningArea.setPrefRowCount(4);
        reasoningArea.setText(DETAIL_PLACEHOLDER);
        reasoningArea.setTooltip(new Tooltip(GuiTooltipTexts.VERLAUF_REASONING_AREA));
        Label reasoningTitle = new Label("KI-Begründung (ausgewählter Versuch)");
        reasoningTitle.setStyle(BOLD_STYLE);

        // KI-Rohantwort (vollständige, unveränderte Antwort der KI für den Versuch)
        rawResponseArea.setEditable(false);
        rawResponseArea.setWrapText(true);
        rawResponseArea.setPrefRowCount(6);
        rawResponseArea.setPromptText(NO_RAW_RESPONSE_TEXT);
        rawResponseArea.setTooltip(new Tooltip(
                "Die vollständige, unveränderte Antwort der KI für diesen Versuch – "
                        + "inklusive eventuellem Text um das JSON herum. Nützlich zur Fehlersuche."));
        Label rawResponseTitle = new Label("KI-Rohantwort (ausgewählter Versuch)");
        rawResponseTitle.setStyle(BOLD_STYLE);

        VBox rightPane = new VBox(8,
                detailTitle, detailGrid,
                attemptsTitle, attemptsTable,
                failureTitle, failureArea,
                reasoningTitle, reasoningArea,
                rawResponseTitle, rawResponseArea);
        rightPane.setPadding(new Insets(4, 8, 4, 4));
        VBox.setVgrow(attemptsTable, Priority.ALWAYS);

        ScrollPane scroll = new ScrollPane(rightPane);
        scroll.setFitToWidth(true);
        scroll.setFitToHeight(true);

        VBox wrapper = new VBox(scroll);
        VBox.setVgrow(scroll, Priority.ALWAYS);
        return wrapper;
    }

    private void buildAttemptsTable() {
        attemptsTable.setItems(attemptsItems);
        attemptsTable.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
        attemptsTable.setPlaceholder(new Label("Keine Versuche vorhanden."));
        attemptsTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);
        attemptsTable.setPrefHeight(150);

        TableColumn<ProcessingAttempt, String> numCol = new TableColumn<>();
        numCol.setGraphic(columnHeader("#", GuiTooltipTexts.VERLAUF_VERSUCHE_COL_NR));
        numCol.setCellValueFactory(c ->
                new SimpleStringProperty(String.valueOf(c.getValue().attemptNumber())));
        numCol.setPrefWidth(40);
        numCol.setMaxWidth(50);

        TableColumn<ProcessingAttempt, String> dateCol = new TableColumn<>();
        dateCol.setGraphic(columnHeader("Datum", GuiTooltipTexts.VERLAUF_VERSUCHE_COL_DATUM));
        dateCol.setCellValueFactory(c ->
                new SimpleStringProperty(formatInstant(c.getValue().endedAt())));
        dateCol.setPrefWidth(130);
        dateCol.setMaxWidth(150);

        TableColumn<ProcessingAttempt, String> statusCol = new TableColumn<>();
        statusCol.setGraphic(columnHeader("Status", GuiTooltipTexts.VERLAUF_VERSUCHE_COL_STATUS));
        statusCol.setCellValueFactory(c ->
                new SimpleStringProperty(
                        ProcessingStatusPresentation.displayTextFor(c.getValue().status())));
        statusCol.setPrefWidth(160);

        TableColumn<ProcessingAttempt, String> providerCol = new TableColumn<>();
        providerCol.setGraphic(columnHeader("Provider", GuiTooltipTexts.VERLAUF_VERSUCHE_COL_PROVIDER));
        providerCol.setCellValueFactory(c ->
                new SimpleStringProperty(
                        c.getValue().aiProvider() != null ? c.getValue().aiProvider() : "—"));
        providerCol.setPrefWidth(90);

        TableColumn<ProcessingAttempt, String> modelCol = new TableColumn<>();
        modelCol.setGraphic(columnHeader("Modell", GuiTooltipTexts.VERLAUF_VERSUCHE_COL_MODELL));
        modelCol.setCellValueFactory(c ->
                new SimpleStringProperty(
                        c.getValue().modelName() != null ? c.getValue().modelName() : "—"));
        modelCol.setCellFactory(col -> ellipsisCell());

        TableColumn<ProcessingAttempt, String> fileNameCol = new TableColumn<>();
        fileNameCol.setGraphic(columnHeader("Vorgeschlagener Name", GuiTooltipTexts.VERLAUF_VERSUCHE_COL_VORGESCHLAGENER_NAME));
        fileNameCol.setCellValueFactory(c ->
                new SimpleStringProperty(
                        c.getValue().finalTargetFileName() != null
                                ? c.getValue().finalTargetFileName() : "—"));
        fileNameCol.setCellFactory(col -> ellipsisCell());

        TableColumn<ProcessingAttempt, String> inputTokensCol = new TableColumn<>("Input-Tokens");
        inputTokensCol.setCellValueFactory(c ->
                new SimpleStringProperty(formatTokenCount(c.getValue().inputTokens())));
        inputTokensCol.setPrefWidth(110);

        TableColumn<ProcessingAttempt, String> outputTokensCol = new TableColumn<>("Output-Tokens");
        outputTokensCol.setCellValueFactory(c ->
                new SimpleStringProperty(formatTokenCount(c.getValue().outputTokens())));
        outputTokensCol.setPrefWidth(110);

        TableColumn<ProcessingAttempt, String> costCol = new TableColumn<>("Kosten");
        costCol.setCellValueFactory(c -> new SimpleStringProperty(formatAttemptCost(c.getValue())));
        costCol.setPrefWidth(140);

        attemptsTable.getColumns().setAll(numCol, dateCol, statusCol, providerCol, modelCol,
                fileNameCol, inputTokensCol, outputTokensCol, costCol);
    }

    /**
     * Formatiert eine Token-Anzahl mit deutscher Tausenderfassung.
     *
     * @param value Wert oder {@code null}
     * @return Anzeigetext, "—" bei {@code null}
     */
    private static String formatTokenCount(Long value) {
        if (value == null) {
            return "—";
        }
        return String.format(java.util.Locale.GERMAN, "%,d", value);
    }

    /**
     * Bestimmt den Anzeigetext fuer die Kosten-Spalte eines Versuchs.
     *
     * <p>Cache-only-Versuche zeigen den Hinweis &quot;nur Cache-Tokens, keine
     * Standardkosten&quot;. Bei fehlendem Preis-Snapshot erscheint
     * &quot;Preis fehlt&quot;. Mikrobetraege werden als &quot;&lt;
     * $0.0001&quot; dargestellt.
     *
     * @param attempt Versuchseintrag
     * @return Anzeigetext fuer die Kosten-Spalte
     */
    private static String formatAttemptCost(ProcessingAttempt attempt) {
        boolean hasStandardTokens = attempt.inputTokens() != null || attempt.outputTokens() != null;
        boolean hasCacheTokens =
                (attempt.cacheCreationInputTokens() != null && attempt.cacheCreationInputTokens() > 0)
                || (attempt.cacheReadInputTokens() != null && attempt.cacheReadInputTokens() > 0);
        if (!hasStandardTokens) {
            if (hasCacheTokens) {
                return "nur Cache-Tokens, keine Standardkosten";
            }
            return "—";
        }
        de.gecheckt.pdf.umbenenner.application.cost.CostCalculator calc =
                new de.gecheckt.pdf.umbenenner.application.cost.CostCalculator();
        de.gecheckt.pdf.umbenenner.application.cost.CostResult result = calc.calculateAttempt(
                attempt.inputTokens(),
                attempt.outputTokens(),
                attempt.priceInputPerTokenNanoUsd(),
                attempt.priceOutputPerTokenNanoUsd(),
                hasCacheTokens);
        if (result.amountUsd() == null) {
            if (result.missingPriceSnapshot()) {
                return "Preis fehlt";
            }
            return "—";
        }
        java.math.BigDecimal amount = result.amountUsd();
        java.math.BigDecimal threshold = new java.math.BigDecimal("0.0001");
        if (amount.signum() > 0 && amount.compareTo(threshold) < 0) {
            return "< $0.0001";
        }
        java.math.BigDecimal rounded = amount.setScale(4, java.math.RoundingMode.HALF_UP);
        String suffix = result.cacheTokensIgnored() ? " (ohne Cache-Anteil)" : "";
        if (result.partialTokens()) {
            return "~$" + rounded.toPlainString() + suffix;
        }
        return "$" + rounded.toPlainString() + suffix;
    }

    // =========================================================================
    // Event-Verdrahtung
    // =========================================================================

    private void wireEvents() {
        refreshButton.setOnAction(e -> loadOverview());

        // Live-Filter: 300-ms-Debounce bei jeder Texteingabe
        searchDebounce.setOnFinished(e -> loadOverview());
        searchField.textProperty().addListener((obs, old, val) -> searchDebounce.playFromStart());
        // Enter-Taste: sofort suchen, Debounce-Timer stoppen
        searchField.setOnAction(e -> { searchDebounce.stop(); loadOverview(); });

        statusFilterBox.setOnAction(e -> loadOverview());

        // Detailbereich und Buttons bei Selektionsänderung aktualisieren
        overviewTable.getSelectionModel().getSelectedItems().addListener(
                (ListChangeListener<DocumentHistoryRow>) change -> {
                    List<DocumentHistoryRow> sel =
                            List.copyOf(overviewTable.getSelectionModel().getSelectedItems());
                    boolean running = runningCheck.getAsBoolean();
                    if (sel.isEmpty()) {
                        clearDetailPane();
                        resetButton.setDisable(true);
                        deleteButton.setDisable(true);
                    } else if (sel.size() == 1) {
                        resetButton.setDisable(running);
                        deleteButton.setDisable(running);
                        loadDetails(sel.get(0).fingerprint());
                    } else {
                        // Mehrfachauswahl: Detail-Bereich löschen, Buttons aktivieren
                        clearDetailPane();
                        resetButton.setDisable(running);
                        deleteButton.setDisable(running);
                        statusBarLabel.setText(sel.size() + " Einträge ausgewählt.");
                    }
                });

        resetButton.setOnAction(e -> handleResetAction());
        deleteButton.setOnAction(e -> handleDeleteAction());

        // Tab soll beim ersten Betreten automatisch laden
        tab.selectedProperty().addListener((obs, oldVal, selected) -> {
            if (Boolean.TRUE.equals(selected)) {
                loadOverview();
            }
        });
    }

    // =========================================================================
    // Daten laden (Worker-Thread)
    // =========================================================================

    private void loadOverview() {
        statusBarLabel.setText(LOADING_TEXT);
        overviewItems.clear();
        moreThanMaxLabel.setVisible(false);
        moreThanMaxLabel.setManaged(false);

        Path configPath = configPathSupplier.get();
        if (configPath == null) {
            statusBarLabel.setText("Keine Konfiguration geladen – bitte zuerst eine Konfigurationsdatei öffnen.");
            overviewTable.setPlaceholder(new Label(NO_CONFIG_LOADED_MSG));
            return;
        }

        String searchText = searchField.getText();
        ProcessingStatus selectedStatus = statusFilterBox.getSelectionModel().getSelectedItem();
        String statusFilter = selectedStatus == null ? null : selectedStatus.name();

        HistoryQuery query = new HistoryQuery(searchText, statusFilter, HistoryQuery.DEFAULT_LIMIT);

        workerPool.submit(() -> {
            try {
                HistoryOverviewResult result = overviewPort.loadOverview(configPath, query);
                Platform.runLater(() -> {
                    overviewItems.setAll(result.rows());
                    if (result.hasMore()) {
                        moreThanMaxLabel.setText(TOO_MANY_RESULTS_TEXT);
                        moreThanMaxLabel.setVisible(true);
                        moreThanMaxLabel.setManaged(true);
                    } else {
                        moreThanMaxLabel.setVisible(false);
                        moreThanMaxLabel.setManaged(false);
                    }
                    if (result.rows().isEmpty()) {
                        overviewTable.setPlaceholder(new Label(EMPTY_DB_TEXT));
                        statusBarLabel.setText("Keine Einträge gefunden.");
                    } else {
                        statusBarLabel.setText(result.rows().size() + " Einträge geladen.");
                    }
                });
            } catch (Exception ex) {
                LOG.error("Fehler beim Laden der Historienübersicht: {}", ex.getMessage(), ex);
                Platform.runLater(() ->
                        statusBarLabel.setText("Fehler beim Laden: " + ex.getMessage()));
            }
        });
    }

    private void loadDetails(DocumentFingerprint fingerprint) {
        reasoningArea.setText(LOADING_TEXT);
        attemptsItems.clear();
        clearDetailFields();

        Path configPath = configPathSupplier.get();
        if (configPath == null) {
            reasoningArea.setText(DETAIL_PLACEHOLDER);
            return;
        }

        workerPool.submit(() -> {
            try {
                Optional<HistoryDetailsResult> result = detailsPort.loadDetails(configPath, fingerprint);
                Platform.runLater(() -> {
                    if (result.isEmpty()) {
                        clearDetailPane();
                        statusBarLabel.setText("Eintrag nicht mehr vorhanden.");
                    } else {
                        populateDetailPane(result.get());
                    }
                });
            } catch (Exception ex) {
                LOG.error("Fehler beim Laden der Dokumentdetails für {}: {}",
                        fingerprint.sha256Hex(), ex.getMessage(), ex);
                Platform.runLater(() -> {
                    reasoningArea.setText("Fehler beim Laden der Details: " + ex.getMessage());
                    statusBarLabel.setText("Fehler beim Laden der Details.");
                });
            }
        });
    }

    // =========================================================================
    // Aktionen
    // =========================================================================

    private void handleResetAction() {
        if (runningCheck.getAsBoolean()) {
            showInfo(LAUF_AKTIV_HINWEIS);
            return;
        }

        List<DocumentHistoryRow> selectedItems =
                List.copyOf(overviewTable.getSelectionModel().getSelectedItems());
        if (selectedItems.isEmpty()) return;

        Path configPath = configPathSupplier.get();
        if (configPath == null) {
            showInfo(NO_CONFIG_LOADED_MSG);
            return;
        }

        long successCount = selectedItems.stream()
                .filter(r -> r.overallStatus() == ProcessingStatus.SUCCESS)
                .count();

        Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
        confirm.setTitle("Status zurücksetzen");
        confirm.setHeaderText("Status zurücksetzen?");
        confirm.setContentText(buildResetConfirmationText(selectedItems, successCount));
        Optional<ButtonType> choice = confirm.showAndWait();
        if (choice.isEmpty() || choice.get() != ButtonType.OK) return;

        resetButton.setDisable(true);
        deleteButton.setDisable(true);
        statusBarLabel.setText("Status wird zurückgesetzt …");

        workerPool.submit(() -> {
            int okCount = 0;
            int errCount = 0;
            for (DocumentHistoryRow row : selectedItems) {
                try {
                    resetPort.resetStatus(configPath, row.fingerprint());
                    LOG.info("Status-Reset durchgeführt für Fingerprint: {}", row.fingerprint().sha256Hex());
                    okCount++;
                } catch (Exception ex) {
                    LOG.error("Status-Reset fehlgeschlagen für {}: {}",
                            row.fingerprint().sha256Hex(), ex.getMessage(), ex);
                    errCount++;
                }
            }
            final int ok = okCount, err = errCount;
            Platform.runLater(() -> {
                if (err == 0) {
                    statusBarLabel.setText("Status erfolgreich zurückgesetzt: " + ok + " Eintrag/Einträge.");
                } else {
                    statusBarLabel.setText("Status zurückgesetzt: " + ok + " OK, " + err + " Fehler.");
                }
                loadOverview();
            });
        });
    }

    private static String buildResetConfirmationText(List<DocumentHistoryRow> selectedItems, long successCount) {
        StringBuilder sb = new StringBuilder();
        sb.append("Setzt den Status auf READY_FOR_AI zurück.\n");
        sb.append("Fehlerzähler und letzter Fehlerzeitpunkt werden gelöscht.\n");
        sb.append("Die Versuchshistorie bleibt vollständig erhalten.\n\n");
        if (selectedItems.size() == 1) {
            sb.append("Quelldatei: ").append(selectedItems.get(0).sourceFileName());
        } else {
            sb.append(selectedItems.size()).append(" Einträge werden zurückgesetzt.");
        }
        if (successCount > 0) {
            sb.append("\n\nHinweis: ").append(successCount)
              .append(" der ausgewählten Einträge ")
              .append(successCount == 1 ? "hat" : "haben")
              .append(" Status \"Erfolgreich\". ")
              .append(successCount == 1 ? "Dieser Eintrag wird" : "Diese Einträge werden")
              .append(" erneut verarbeitet.");
        }
        return sb.toString();
    }

    private void handleDeleteAction() {
        if (runningCheck.getAsBoolean()) {
            showInfo(LAUF_AKTIV_HINWEIS);
            return;
        }

        List<DocumentHistoryRow> selectedItems =
                List.copyOf(overviewTable.getSelectionModel().getSelectedItems());
        if (selectedItems.isEmpty()) return;

        Path configPath = configPathSupplier.get();
        if (configPath == null) {
            showInfo(NO_CONFIG_LOADED_MSG);
            return;
        }

        String contentText;
        if (selectedItems.size() == 1) {
            contentText = "Der Stammsatz und ALLE Verarbeitungsversuche werden unwiderruflich gelöscht.\n"
                    + "Diese Aktion kann nicht rückgängig gemacht werden.\n\n"
                    + "Quelldatei: " + selectedItems.get(0).sourceFileName();
        } else {
            contentText = selectedItems.size() + " Einträge werden mit allen Versuchen unwiderruflich gelöscht.\n"
                    + "Diese Aktion kann nicht rückgängig gemacht werden.";
        }

        Alert confirm = new Alert(Alert.AlertType.WARNING);
        confirm.setTitle("Eintrag löschen");
        confirm.setHeaderText(selectedItems.size() == 1 ? "Eintrag vollständig löschen?"
                : selectedItems.size() + " Einträge vollständig löschen?");
        confirm.setContentText(contentText);
        confirm.getButtonTypes().setAll(ButtonType.OK, ButtonType.CANCEL);
        Optional<ButtonType> choice = confirm.showAndWait();
        if (choice.isEmpty() || choice.get() != ButtonType.OK) return;

        resetButton.setDisable(true);
        deleteButton.setDisable(true);
        statusBarLabel.setText("Einträge werden gelöscht …");

        workerPool.submit(() -> {
            int okCount = 0;
            int errCount = 0;
            for (DocumentHistoryRow row : selectedItems) {
                try {
                    deletePort.deleteHistory(configPath, row.fingerprint());
                    LOG.info("Dokumenteintrag gelöscht für Fingerprint: {}", row.fingerprint().sha256Hex());
                    okCount++;
                } catch (Exception ex) {
                    LOG.error("Löschen fehlgeschlagen für {}: {}",
                            row.fingerprint().sha256Hex(), ex.getMessage(), ex);
                    errCount++;
                }
            }
            final int ok = okCount, err = errCount;
            Platform.runLater(() -> {
                if (err == 0) {
                    statusBarLabel.setText("Gelöscht: " + ok + " Eintrag/Einträge.");
                } else {
                    statusBarLabel.setText("Gelöscht: " + ok + " OK, " + err + " Fehler.");
                }
                clearDetailPane();
                loadOverview();
            });
        });
    }

    // =========================================================================
    // Detail-Bereich befüllen / leeren
    // =========================================================================

    private void populateDetailPane(HistoryDetailsResult result) {
        DocumentRecord record = result.record();
        String fpFull = record.fingerprint().sha256Hex();
        detailFingerprintLabel.setText(fpFull.substring(0, Math.min(12, fpFull.length())) + " …");
        detailFingerprintLabel.setTooltip(new Tooltip(fpFull));
        detailSourceFileLabel.setText(record.lastKnownSourceFileName());
        detailSourcePathLabel.setText(record.lastKnownSourceLocator().value());
        detailSourcePathLabel.setTooltip(new Tooltip(record.lastKnownSourceLocator().value()));
        detailStatusLabel.setText(ProcessingStatusPresentation.displayTextFor(record.overallStatus()));
        detailStatusLabel.setStyle("-fx-text-fill: " + statusColor(record.overallStatus()) + ";");
        detailStatusLabel.setTooltip(new Tooltip(statusTooltip(record.overallStatus())));
        detailCreatedLabel.setText(formatInstant(record.createdAt()));
        detailUpdatedLabel.setText(formatInstant(record.updatedAt()));

        attemptsItems.setAll(result.attempts());

        // Fehlerursache aus letztem Fehler-Versuch anzeigen
        showLastFailureMessage(result.attempts(), record.overallStatus());

        selectLatestAttemptAndShowReasoning(result.attempts());
        attemptsTable.getSelectionModel().selectedItemProperty().addListener(
                (obs, old, attempt) -> onAttemptSelected(attempt));
    }

    private void selectLatestAttemptAndShowReasoning(java.util.List<ProcessingAttempt> attempts) {
        if (!attempts.isEmpty()) {
            ProcessingAttempt last = attempts.get(attempts.size() - 1);
            attemptsTable.getSelectionModel().select(last);
            showReasoning(last);
            showRawResponse(last);
        } else {
            reasoningArea.setText("");
            reasoningArea.setPromptText(NO_REASONING_TEXT);
            rawResponseArea.setText("");
            rawResponseArea.setPromptText(NO_RAW_RESPONSE_TEXT);
        }
    }

    private void onAttemptSelected(ProcessingAttempt attempt) {
        if (attempt != null) {
            showReasoning(attempt);
            showRawResponse(attempt);
        }
    }

    /**
     * Zeigt die Fehlerursache des letzten Fehlschlags im Fehlerursache-Bereich an.
     * Relevant bei Status FAILED_FINAL, FAILED_RETRYABLE und SKIPPED_FINAL_FAILURE.
     * Bei fehlendem Eintrag oder leerem Feld wird ein Platzhalter-Text gesetzt.
     */
    private void showLastFailureMessage(List<ProcessingAttempt> attempts, ProcessingStatus overallStatus) {
        boolean failureRelevant = overallStatus == ProcessingStatus.FAILED_FINAL
                || overallStatus == ProcessingStatus.FAILED_RETRYABLE
                || overallStatus == ProcessingStatus.SKIPPED_FINAL_FAILURE;

        if (!failureRelevant || attempts.isEmpty()) {
            failureArea.setText("");
            failureArea.setPromptText("Keine Fehlerdetails für diesen Status.");
            return;
        }

        // Letzten Versuch mit nicht-leerem failure_message suchen (absteigend nach attempt_number)
        String failureMessage = null;
        for (int i = attempts.size() - 1; i >= 0; i--) {
            String msg = attempts.get(i).failureMessage();
            if (msg != null && !msg.isBlank()) {
                failureMessage = msg;
                break;
            }
        }

        failureArea.setText(failureMessage != null
                ? AiFailureMessageTranslator.translate(failureMessage) : "");
        failureArea.setPromptText(NO_ERROR_DETAILS_MSG);
        // Die technische Originalmeldung als Tooltip bereitstellen, damit die exakte
        // Ursache jederzeit einsehbar ist, ohne den lesbaren Anzeigetext zu überladen.
        failureArea.setTooltip(new Tooltip(failureMessage != null
                ? failureMessage
                : GuiTooltipTexts.VERLAUF_FAILURE_AREA));
    }

    private void showReasoning(ProcessingAttempt attempt) {
        String reasoning = attempt.aiReasoning();
        if (reasoning != null && !reasoning.isBlank()) {
            reasoningArea.setText(reasoning);
            reasoningArea.setPromptText("");
        } else {
            reasoningArea.setText("");
            reasoningArea.setPromptText(NO_REASONING_TEXT);
        }
    }

    /**
     * Zeigt die vollständige KI-Rohantwort des ausgewählten Versuchs an.
     * <p>
     * Enthält der Versuch keine gespeicherte Rohantwort (z. B. Skip-/Pre-Check-Versuche
     * oder Netzwerkfehler vor der Antwort), wird ein Platzhaltertext angezeigt.
     *
     * @param attempt der ausgewählte Verarbeitungsversuch; darf nicht {@code null} sein
     */
    private void showRawResponse(ProcessingAttempt attempt) {
        String raw = attempt.aiRawResponse();
        if (raw != null && !raw.isBlank()) {
            rawResponseArea.setText(raw);
            rawResponseArea.setPromptText("");
        } else {
            rawResponseArea.setText("");
            rawResponseArea.setPromptText(NO_RAW_RESPONSE_TEXT);
        }
    }

    private void clearDetailPane() {
        clearDetailFields();
        attemptsItems.clear();
        failureArea.setText("");
        failureArea.setPromptText(NO_ERROR_DETAILS_MSG);
        failureArea.setTooltip(new Tooltip(GuiTooltipTexts.VERLAUF_FAILURE_AREA));
        reasoningArea.setText(DETAIL_PLACEHOLDER);
        rawResponseArea.setText("");
        rawResponseArea.setPromptText(NO_RAW_RESPONSE_TEXT);
    }

    private void clearDetailFields() {
        detailFingerprintLabel.setText("");
        detailFingerprintLabel.setTooltip(null);
        detailSourceFileLabel.setText("");
        detailSourcePathLabel.setText("");
        detailSourcePathLabel.setTooltip(null);
        detailStatusLabel.setText("");
        detailStatusLabel.setStyle("");
        detailStatusLabel.setTooltip(null);
        detailCreatedLabel.setText("");
        detailUpdatedLabel.setText("");
    }

    // =========================================================================
    // Hilfsmethoden
    // =========================================================================

    private void addDetailRow(int row, String labelText, Label valueLabel) {
        Label label = new Label(labelText);
        label.setStyle(BOLD_STYLE);
        valueLabel.setMaxWidth(Double.MAX_VALUE);
        GridPane.setHgrow(valueLabel, Priority.ALWAYS);
        detailGrid.add(label, 0, row);
        detailGrid.add(valueLabel, 1, row);
    }

    private String formatInstant(Instant instant) {
        if (instant == null) return "—";
        return TIMESTAMP_FMT.format(instant);
    }

    private static String statusIcon(ProcessingStatus status) {
        if (status == null) return "?";
        return switch (status) {
            case SUCCESS -> "✓";
            case FAILED_RETRYABLE -> "↻";
            case FAILED_FINAL -> "×";
            case SKIPPED_ALREADY_PROCESSED -> "≡";
            case SKIPPED_FINAL_FAILURE -> "⊘";
            case READY_FOR_AI -> "⟳";
            case PROPOSAL_READY -> "◇";
            case PROCESSING -> "▶";
        };
    }

    private static String statusColor(ProcessingStatus status) {
        if (status == null) return "#000000";
        return switch (status) {
            case SUCCESS -> "#2e7d32";
            case FAILED_RETRYABLE -> "#d98200";
            case FAILED_FINAL -> "#c62828";
            case SKIPPED_ALREADY_PROCESSED -> "#757575";
            case SKIPPED_FINAL_FAILURE -> "#424242";
            case READY_FOR_AI -> "#1565c0";
            case PROPOSAL_READY -> "#0288d1";
            case PROCESSING -> "#9e9e9e";
        };
    }

    private static String statusTooltip(ProcessingStatus status) {
        if (status == null) return "";
        return switch (status) {
            case SUCCESS -> "Erfolgreich verarbeitet und umbenannt.";
            case FAILED_RETRYABLE -> "Temporärer Fehler – wird beim nächsten Lauf automatisch erneut versucht.";
            case FAILED_FINAL -> "Dauerhaft nicht verarbeitbar – z. B. kein Textinhalt (Foto-PDF), "
                    + "Passwortschutz oder beschädigte Datei. Kein weiterer automatischer Versuch.";
            case SKIPPED_ALREADY_PROCESSED -> "Übersprungen – wurde bereits in einem früheren Lauf erfolgreich verarbeitet.";
            case SKIPPED_FINAL_FAILURE -> "Endgültig übersprungen nach wiederholten Fehlern.";
            case READY_FOR_AI -> "Wartet auf Verarbeitung.";
            case PROPOSAL_READY -> "KI-Vorschlag liegt vor, wartet auf Bestätigung.";
            case PROCESSING -> "Wird gerade verarbeitet.";
        };
    }

    /**
     * Erzeugt ein Label für den Spaltenkopf einer TableColumn mit Tooltip.
     * Wird anstelle von {@code column.setText()} verwendet, da TableColumn
     * kein direktes {@code setTooltip()} unterstützt.
     *
     * @param title   sichtbarer Spaltentext
     * @param tooltip Tooltip-Text
     * @return ein Label mit gesetztem Tooltip
     */
    private static Label columnHeader(String title, String tooltip) {
        Label label = new Label(title);
        label.setTooltip(new Tooltip(tooltip));
        return label;
    }

    private static <T> TableCell<T, String> ellipsisCell() {
        return new TableCell<>() {
            @Override
            protected void updateItem(String item, boolean empty) {
                super.updateItem(item, empty);
                if (empty || item == null) {
                    setText(null);
                    setTooltip(null);
                } else {
                    setText(item);
                    setTooltip(new Tooltip(item));
                }
            }
        };
    }

    private static Region spacerNew() {
        Region r = new Region();
        HBox.setHgrow(r, Priority.ALWAYS);
        return r;
    }

    private void showInfo(String message) {
        Alert alert = new Alert(Alert.AlertType.INFORMATION);
        alert.setTitle("Hinweis");
        alert.setHeaderText(null);
        alert.setContentText(message);
        alert.showAndWait();
    }
}
