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

import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;

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

import de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingFailure;
import de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingSuccess;
import de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion;
import javafx.application.Platform;
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.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TextArea;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;

/**
 * Tab „Prompt" im Hauptfenster des GUI-Adapters.
 * <p>
 * Ermöglicht das Lesen, Bearbeiten und Speichern der konfigurierten KI-Prompt-Datei
 * direkt aus der Oberfläche heraus, ohne einen externen Editor öffnen zu müssen.
 * <p>
 * <strong>Verhalten:</strong>
 * <ul>
 *   <li>Beim Öffnen des Tabs wird der aktuelle Prompt-Inhalt auf einem Worker-Thread geladen.</li>
 *   <li>Bearbeitungen erzeugen einen Dirty-State; der Tab-Titel erhält einen Asterisk.</li>
 *   <li>„Speichern" schreibt den Inhalt atomar via {@link GuiPromptEditorPort}.</li>
 *   <li>„Auf Standard zurücksetzen" befüllt die TextArea mit dem Default-Template,
 *       ohne zu speichern.</li>
 *   <li>Bei fehlendem Prompt wird ein Hinweis und ein „Standard-Prompt erstellen"-Button
 *       angezeigt.</li>
 *   <li>Tab-Wechsel oder Schließen mit Dirty-State löst einen Bestätigungsdialog aus.</li>
 * </ul>
 * <p>
 * <strong>Threading:</strong> Alle blockierenden Operationen (Laden, Speichern,
 * Prompt-Datei anlegen) laufen auf einem Worker-Thread. UI-Aktualisierungen erfolgen
 * ausschließlich via {@code Platform.runLater}.
 */
public class GuiPromptEditorTab {

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

    private static final String TAB_TITLE = "Prompt";
    private static final String TAB_TITLE_DIRTY = "Prompt *";

    private GuiPromptEditorPort promptEditorPort;
    /** Konfigurierter Prompt-Dateipfad – wird für CreatePromptFile-Vorschläge benötigt. */
    private String configuredPromptPath;
    /** Konfigurierte maximale Titellänge – für den Default-Prompt-Inhalt. */
    private int maxTitleLength;

    // Thread-Strategie (injizierbar für Tests ohne JavaFX-Runtime)
    /** Erzeugt Worker-Threads für blockierende Operationen. */
    Function<Runnable, Thread> threadFactory;
    /** Übergibt UI-Updates an den JavaFX Application Thread. */
    Consumer<Runnable> fxDispatcher;

    private final Tab tab = new Tab(TAB_TITLE);
    private final TextArea textArea = new TextArea();
    private final Label statusLabel = new Label();
    private final Button saveButton = new Button("Speichern");
    private final Button resetButton = new Button("Auf Standard zurücksetzen");
    private final Button createDefaultButton = new Button("Standard-Prompt erstellen");

    /** Zeigt an, ob der aktuelle Inhalt der TextArea vom geladenen Stand abweicht. */
    private boolean dirty = false;
    /** Zuletzt aus der Datei geladener Inhalt (Baseline). */
    private String loadedContent = null;

    /**
     * Erstellt den Prompt-Editor-Tab.
     *
     * @param promptEditorPort    Bridge-Port zum Use-Case; darf nicht {@code null} sein
     * @param configuredPromptPath konfigurierter Pfad zur Prompt-Datei (für CreatePromptFile);
     *                            darf nicht {@code null} sein
     * @param maxTitleLength      konfigurierte maximale Titellänge für den Default-Prompt
     * @throws NullPointerException wenn {@code promptEditorPort} oder {@code configuredPromptPath} null ist
     */
    public GuiPromptEditorTab(GuiPromptEditorPort promptEditorPort,
                               String configuredPromptPath,
                               int maxTitleLength) {
        this.promptEditorPort = Objects.requireNonNull(promptEditorPort, "promptEditorPort must not be null");
        this.configuredPromptPath = Objects.requireNonNull(configuredPromptPath, "configuredPromptPath must not be null");
        this.maxTitleLength = maxTitleLength;
        // Standard-Implementierungen für den Produktionsbetrieb
        this.threadFactory = runnable -> {
            Thread t = new Thread(runnable, "gui-prompt-editor");
            t.setDaemon(true);
            return t;
        };
        this.fxDispatcher = Platform::runLater;
        buildTab();
    }

    /**
     * Liefert das JavaFX-Tab-Objekt, das dem TabPane hinzugefügt werden kann.
     *
     * @return das Tab; nie {@code null}
     */
    public Tab tab() {
        return tab;
    }

    /**
     * Gibt an, ob der Prompt-Editor ungespeicherte Änderungen enthält.
     *
     * @return {@code true}, wenn Dirty-State aktiv ist
     */
    public boolean hasDirtyContent() {
        return dirty;
    }

    /**
     * Aktualisiert den Tab auf eine neue Konfiguration.
     * <p>
     * Setzt Port, Prompt-Dateipfad und maximale Titellänge auf die neuen Werte.
     * Der bisherige Lade-Baseline wird verworfen und der Dirty-State zurückgesetzt.
     * Ist der Tab zum Zeitpunkt des Aufrufs sichtbar, wird ein erneutes Laden sofort
     * ausgelöst; andernfalls erfolgt das Laden beim nächsten Öffnen des Tabs.
     * <p>
     * Muss auf dem JavaFX Application Thread aufgerufen werden.
     *
     * @param newPort           neuer Port für Prompt-Operationen; darf nicht {@code null} sein
     * @param newPromptPath     neuer konfigurierter Prompt-Dateipfad; darf nicht {@code null} sein
     * @param newMaxTitleLength neue konfigurierte maximale Titellänge
     */
    public void notifyConfigurationChanged(GuiPromptEditorPort newPort,
                                            String newPromptPath,
                                            int newMaxTitleLength) {
        this.promptEditorPort = Objects.requireNonNull(newPort, "newPort must not be null");
        this.configuredPromptPath = Objects.requireNonNull(newPromptPath, "newPromptPath must not be null");
        this.maxTitleLength = newMaxTitleLength;
        this.loadedContent = null;
        this.dirty = false;
        this.tab.setText(TAB_TITLE);
        this.saveButton.setDisable(true);
        if (tab.isSelected()) {
            loadPromptAsync();
        }
    }

    /**
     * Verwirft alle ungespeicherten Änderungen und setzt den Tab in den Lade-Bereitschaftszustand.
     * <p>
     * Setzt Dirty-State und Tab-Titel zurück. Ist der Tab zum Zeitpunkt des Aufrufs sichtbar,
     * wird der Prompt-Inhalt sofort neu geladen; andernfalls erfolgt das Laden beim nächsten
     * Öffnen des Tabs (gesteuert durch den Tab-Selektions-Listener).
     * <p>
     * Muss auf dem JavaFX Application Thread aufgerufen werden.
     */
    public void discardChanges() {
        this.loadedContent = null;
        this.dirty = false;
        this.tab.setText(TAB_TITLE);
        this.saveButton.setDisable(true);
        if (tab.isSelected()) {
            loadPromptAsync();
        }
    }

    /**
     * Zeigt einen Bestätigungsdialog, wenn ungespeicherte Änderungen vorhanden sind.
     * Gibt {@code true} zurück, wenn die Änderungen verworfen werden dürfen.
     *
     * @return {@code true} zum Verwerfen, {@code false} zum Abbrechen
     */
    public boolean confirmDiscardIfDirty() {
        if (!dirty) {
            return true;
        }
        Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
        alert.setTitle("Ungespeicherte Änderungen");
        alert.setHeaderText("Der Prompt-Editor enthält ungespeicherte Änderungen.");
        alert.setContentText("Möchten Sie die Änderungen verwerfen?");
        alert.getButtonTypes().setAll(
                new ButtonType("Verwerfen"),
                ButtonType.CANCEL);
        Optional<ButtonType> result = alert.showAndWait();
        return result.isPresent() && result.get().getText().equals("Verwerfen");
    }

    /**
     * Lädt den aktuellen Prompt-Inhalt auf einem Worker-Thread und zeigt ihn in der TextArea an.
     * <p>
     * Muss vom JavaFX Application Thread aufgerufen werden. Die eigentliche I/O-Operation
     * läuft auf einem Hintergrund-Thread; UI-Updates erfolgen via {@code fxDispatcher}.
     */
    public void loadPromptAsync() {
        setStatus("Lade Prompt-Datei ...");
        saveButton.setDisable(true);
        resetButton.setDisable(true);
        createDefaultButton.setVisible(false);
        createDefaultButton.setManaged(false);

        Thread worker = threadFactory.apply(() -> {
            var result = promptEditorPort.loadCurrentPrompt();
            fxDispatcher.accept(() -> applyLoadResult(result));
        });
        worker.start();
    }

    // -------------------------------------------------------------------------
    // Private Aufbau
    // -------------------------------------------------------------------------

    private void buildTab() {
        tab.setClosable(false);

        // TextArea – monospace Font für bessere Lesbarkeit
        textArea.setWrapText(true);
        textArea.setFont(Font.font("Monospace", 13));
        textArea.setPrefRowCount(20);
        textArea.setTooltip(new Tooltip(GuiTooltipTexts.PROMPT_TEXTAREA));
        VBox.setVgrow(textArea, Priority.ALWAYS);

        // Dirty-State-Tracking
        textArea.textProperty().addListener((obs, oldVal, newVal) -> {
            if (loadedContent != null) {
                boolean nowDirty = !newVal.equals(loadedContent);
                if (nowDirty != dirty) {
                    dirty = nowDirty;
                    tab.setText(dirty ? TAB_TITLE_DIRTY : TAB_TITLE);
                }
            }
        });

        // Status-Label
        statusLabel.setWrapText(true);
        statusLabel.setStyle("-fx-text-fill: #555555;");

        // Buttons verdrahten
        saveButton.setTooltip(new Tooltip(GuiTooltipTexts.PROMPT_SPEICHERN));
        saveButton.setOnAction(e -> requestSave());

        resetButton.setTooltip(new Tooltip(GuiTooltipTexts.PROMPT_ZURUECKSETZEN));
        resetButton.setOnAction(e -> resetToDefault());

        createDefaultButton.setTooltip(new Tooltip(GuiTooltipTexts.PROMPT_STANDARD_ANLEGEN));
        createDefaultButton.setOnAction(e -> requestCreateDefault());
        createDefaultButton.setVisible(false);
        createDefaultButton.setManaged(false);

        HBox buttonBar = new HBox(8, saveButton, resetButton, createDefaultButton);
        buttonBar.setAlignment(Pos.CENTER_LEFT);
        buttonBar.setPadding(new Insets(6, 0, 0, 0));

        VBox content = new VBox(6, textArea, statusLabel, buttonBar);
        content.setPadding(new Insets(12));
        VBox.setVgrow(textArea, Priority.ALWAYS);

        BorderPane root = new BorderPane(content);
        tab.setContent(root);

        // Beim Öffnen des Tabs laden (falls Konfiguration bereits vorhanden)
        tab.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
            if (Boolean.TRUE.equals(isSelected) && loadedContent == null) {
                loadPromptAsync();
            }
        });
    }

    private void applyLoadResult(de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingResult result) {
        if (result instanceof PromptLoadingSuccess success) {
            loadedContent = success.promptContent();
            textArea.setText(loadedContent);
            textArea.setEditable(true);
            saveButton.setDisable(false);
            resetButton.setDisable(false);
            createDefaultButton.setVisible(false);
            createDefaultButton.setManaged(false);
            setStatus("Prompt-Datei geladen. Identifikator: " + success.promptIdentifier().identifier());
            dirty = false;
            tab.setText(TAB_TITLE);
            LOG.info("Prompt-Editor: Prompt-Datei erfolgreich geladen (Identifikator: {}).",
                    success.promptIdentifier().identifier());
        } else if (result instanceof PromptLoadingFailure failure) {
            boolean fileNotFound = "FILE_NOT_FOUND".equals(failure.failureReason());
            if (fileNotFound) {
                // Datei fehlt – Hinweis und Anlegen-Button anzeigen
                loadedContent = null;
                textArea.setEditable(false);
                textArea.clear();
                saveButton.setDisable(true);
                resetButton.setDisable(false);
                createDefaultButton.setVisible(true);
                createDefaultButton.setManaged(true);
                setStatus("Keine Prompt-Datei vorhanden. Legen Sie eine Standard-Datei an oder "
                        + "konfigurieren Sie den Pfad im Konfigurationstab.");
                LOG.info("Prompt-Editor: Keine Prompt-Datei am konfigurierten Pfad vorhanden.");
            } else {
                // Anderer Fehler (I/O, leer usw.)
                loadedContent = null;
                textArea.setEditable(false);
                textArea.clear();
                saveButton.setDisable(true);
                resetButton.setDisable(false);
                createDefaultButton.setVisible(false);
                createDefaultButton.setManaged(false);
                setStatus("Fehler beim Laden der Prompt-Datei: " + failure.failureMessage());
                LOG.warn("Prompt-Editor: Laden fehlgeschlagen ({}): {}",
                        failure.failureReason(), failure.failureMessage());
            }
        }
    }

    private void requestSave() {
        String currentText = textArea.getText();

        // Leerer Prompt: Bestätigungsdialog
        if (currentText.trim().isEmpty()) {
            Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
            confirm.setTitle("Leerer Prompt");
            confirm.setHeaderText("Der Prompt ist leer.");
            confirm.setContentText("Wirklich eine leere Prompt-Datei speichern?");
            confirm.getButtonTypes().setAll(ButtonType.OK, ButtonType.CANCEL);
            Optional<ButtonType> choice = confirm.showAndWait();
            if (choice.isEmpty() || choice.get() != ButtonType.OK) {
                return;
            }
        }

        setStatus("Speichere ...");
        saveButton.setDisable(true);

        Thread worker = threadFactory.apply(() -> {
            PromptSaveResult result = promptEditorPort.save(currentText);
            fxDispatcher.accept(() -> applySaveResult(result, currentText));
        });
        worker.start();
    }

    private void applySaveResult(PromptSaveResult result, String savedContent) {
        saveButton.setDisable(false);
        if (result instanceof PromptSaveResult.Saved saved) {
            loadedContent = savedContent;
            dirty = false;
            tab.setText(TAB_TITLE);
            setStatus("Prompt-Datei gespeichert: " + saved.absolutePath());
            textArea.setEditable(true);
            LOG.info("Prompt-Editor: Prompt-Datei gespeichert unter {}.", saved.absolutePath());
        } else if (result instanceof PromptSaveResult.TargetDirectoryMissing missing) {
            setStatus("Fehler: " + missing.message());
            LOG.warn("Prompt-Editor: Speichern fehlgeschlagen – Ordner fehlt: {}", missing.message());
        } else if (result instanceof PromptSaveResult.WriteFailed failed) {
            setStatus("Fehler beim Schreiben: " + failed.message());
            LOG.warn("Prompt-Editor: Speichern fehlgeschlagen – Schreibfehler: {}", failed.message());
        } else if (result instanceof PromptSaveResult.AtomicMoveFailed atomicFailed) {
            setStatus("Fehler: Atomares Speichern fehlgeschlagen (kein Fallback). " + atomicFailed.message());
            LOG.warn("Prompt-Editor: Atomares Verschieben fehlgeschlagen: {}", atomicFailed.message());
        }
    }

    void resetToDefault() {
        String defaultContent = de.gecheckt.pdf.umbenenner.application.validation
                .technicaltest.DefaultPromptTemplate.defaultContent(maxTitleLength);
        textArea.setText(defaultContent);
        textArea.setEditable(true);
        saveButton.setDisable(false);
        setStatus("Standard-Prompt-Inhalt in den Editor geladen (noch nicht gespeichert).");
        LOG.info("Prompt-Editor: Standard-Prompt-Inhalt in TextArea geladen (nicht gespeichert).");
    }

    private void requestCreateDefault() {
        createDefaultButton.setDisable(true);
        setStatus("Lege Standard-Prompt-Datei an ...");

        CorrectionSuggestion.CreatePromptFile suggestion = new CorrectionSuggestion.CreatePromptFile(
                configuredPromptPath,
                "Standard-Prompt-Datei anlegen",
                maxTitleLength);

        Thread worker = threadFactory.apply(() -> {
            CorrectionOutcome outcome = promptEditorPort.createDefaultPromptIfMissing(suggestion);
            fxDispatcher.accept(() -> applyCreateDefaultResult(outcome));
        });
        worker.start();
    }

    private void applyCreateDefaultResult(CorrectionOutcome outcome) {
        createDefaultButton.setDisable(false);
        if (outcome instanceof CorrectionOutcome.Applied applied) {
            setStatus(applied.message() + " Lade Inhalt ...");
            LOG.info("Prompt-Editor: Standard-Prompt-Datei angelegt. Lade neu.");
            // Inhalt sofort neu laden
            loadPromptAsync();
        } else if (outcome instanceof CorrectionOutcome.Failed failed) {
            setStatus("Fehler beim Anlegen der Standard-Prompt-Datei: " + failed.errorMessage());
            LOG.warn("Prompt-Editor: Anlegen der Standard-Prompt-Datei fehlgeschlagen: {}", failed.errorMessage());
        } else if (outcome instanceof CorrectionOutcome.NotAttempted notAttempted) {
            setStatus("Aktion nicht verfügbar: " + notAttempted.reason());
            LOG.warn("Prompt-Editor: Anlegen nicht versucht: {}", notAttempted.reason());
        }
    }

    private void setStatus(String message) {
        statusLabel.setText(message);
    }
}
