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

import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;

import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiTooltipTexts;
import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.util.Duration;

/**
 * Detailbereich-Komponente für die Bearbeitung des Zieldateinamens einer selektierten
 * Ergebnis-Zeile.
 * <p>
 * Die Komponente kapselt Eingabefeld, Validierungsanzeige sowie die
 * Schaltflächen „Dateiname übernehmen" und „Zurücksetzen auf KI-Vorschlag". Sie kennt
 * drei Zustände gemäß fachlicher Spezifikation:
 * <ul>
 *   <li><b>KI-Vorschlag</b> – der ursprünglich generierte Name; unveränderlich pro Zeile.</li>
 *   <li><b>Letzter gespeicherter Name</b> – der zuletzt bestätigte Name; entspricht dem
 *       aktuellen Stand in Dateisystem und Persistenz.</li>
 *   <li><b>Aktuelle Eingabe</b> – der im Textfeld sichtbare Wert; kann vom letzten
 *       gespeicherten Namen abweichen (Dirty-State).</li>
 * </ul>
 *
 * <h2>Threading</h2>
 * <p>
 * Alle öffentlichen Methoden müssen auf dem JavaFX Application Thread aufgerufen werden.
 * Die tatsächliche Speicher-Operation ist in der Verantwortung des aufrufenden Tabs und
 * läuft dort auf einem Hintergrund-Worker-Thread.
 */
public final class FileNameEditorPane {

    /** Feste PDF-Erweiterung für Zieldateien. */
    public static final String PDF_EXTENSION = ".pdf";

    /** Windows-Maximal-Pfadlänge (MAX_PATH = 260 inkl. Null-Terminator = 259 nutzbar). */
    public static final int MAX_WINDOWS_PATH_LENGTH = 259;

    private static final Set<String> RESERVED_WINDOWS_NAMES = buildReservedWindowsNames();
    private static final String FORBIDDEN_CHARS_REGEX = ".*[\\\\/:*?\"<>|].*";

    private final VBox root = new VBox(4);
    private final TextField textField = new TextField();
    private final Label validationLabel = new Label();
    private final Button saveButton = new Button("Dateiname übernehmen");
    private final Button resetButton = new Button("Zurücksetzen auf KI-Vorschlag");
    private final Label sectionTitle = new Label("Dateiname");

    private Optional<String> aiProposal = Optional.empty();
    private Optional<String> lastSavedName = Optional.empty();
    private String targetFolderPath = "";
    private boolean selectionEditable = false;
    private boolean globalEnabled = true;
    private boolean suppressValidation = false;

    private Consumer<String> onSaveRequested = name -> { };

    /**
     * Erstellt die Komponente mit leerem und deaktiviertem Zustand.
     */
    public FileNameEditorPane() {
        sectionTitle.setStyle("-fx-font-weight: bold;");

        textField.setId("filename-editor-text-field");
        Tooltip textFieldTooltip = new Tooltip(GuiTooltipTexts.DATEINAME_TEXTFELD);
        textFieldTooltip.setShowDelay(Duration.millis(300));
        textField.setTooltip(textFieldTooltip);
        HBox.setHgrow(textField, Priority.ALWAYS);

        HBox inputRow = new HBox(4, textField);
        inputRow.setAlignment(Pos.CENTER_LEFT);

        validationLabel.setId("filename-editor-validation-label");
        validationLabel.setStyle("-fx-font-size: 11px; -fx-text-fill: #c62828;");
        validationLabel.setVisible(false);
        validationLabel.setManaged(false);
        validationLabel.setWrapText(true);

        saveButton.setId("filename-editor-save-button");
        saveButton.setOnAction(e -> fireSaveRequest());
        Tooltip saveTooltip = new Tooltip(GuiTooltipTexts.DATEINAME_UEBERNEHMEN);
        saveTooltip.setShowDelay(Duration.millis(300));
        saveButton.setTooltip(saveTooltip);

        resetButton.setId("filename-editor-reset-button");
        resetButton.setOnAction(e -> resetToAiProposal());
        Tooltip resetTooltip = new Tooltip(GuiTooltipTexts.DATEINAME_ZURUECKSETZEN);
        resetTooltip.setShowDelay(Duration.millis(300));
        resetButton.setTooltip(resetTooltip);

        HBox buttonRow = new HBox(8, saveButton, resetButton);
        buttonRow.setAlignment(Pos.CENTER_LEFT);
        buttonRow.setPadding(new Insets(4, 0, 0, 0));

        root.getChildren().addAll(sectionTitle, inputRow, validationLabel, buttonRow);
        root.setPadding(new Insets(0, 0, 4, 0));

        // Live-Validierung auf jeden Tastendruck.
        textField.textProperty().addListener((obs, oldText, newText) -> {
            if (!suppressValidation) {
                refreshUiState();
            }
        });

        // Enter löst Speichern aus, Escape setzt auf lastSavedName zurück.
        textField.setOnKeyPressed(event -> {
            if (event.getCode() == KeyCode.ENTER) {
                if (!saveButton.isDisabled()) {
                    fireSaveRequest();
                    event.consume();
                }
            } else if (event.getCode() == KeyCode.ESCAPE) {
                discardChanges();
                event.consume();
            }
        });

        clearSelection();
    }

    /**
     * Liefert den Wurzel-Knoten der Komponente zum Einfügen in den Detailbereich.
     *
     * @return das Root-Control der Komponente; nie null
     */
    public Region getNode() {
        return root;
    }

    /**
     * Registriert einen Callback, der ausgelöst wird, wenn der Benutzer „Dateiname übernehmen"
     * anfordert. Parameter ist der gewünschte Basisname ohne {@code .pdf}-Erweiterung.
     *
     * @param callback Callback; darf nicht null sein (leerer Consumer als No-Op möglich)
     */
    public void setOnSaveRequested(Consumer<String> callback) {
        this.onSaveRequested = Objects.requireNonNull(callback, "callback must not be null");
    }

    /**
     * Aktualisiert den Zustand für die neu selektierte Zeile.
     * <p>
     * Der KI-Vorschlag wird aus {@link GuiBatchRunResultRow#finalFileName()} abgeleitet,
     * der letzte gespeicherte Name aus {@link GuiBatchRunResultRow#effectiveFileName()}.
     * Editierbarkeitsregeln:
     * <ul>
     *   <li>{@code resetPending} → nicht editierbar.</li>
     *   <li>{@code SUCCESS} und {@code SKIPPED_ALREADY_PROCESSED} → editierbar, sofern
     *       ein bisher gespeicherter Zieldateiname vorliegt (Umbenennen einer existierenden
     *       Zieldatei).</li>
     *   <li>{@code FAILED_RETRYABLE}, {@code FAILED_PERMANENT} und
     *       {@code SKIPPED_FINAL_FAILURE} → editierbar; das Eingabefeld erlaubt die
     *       Eingabe eines manuellen Zieldateinamens auch dann, wenn (noch) kein
     *       Vorschlag oder gespeicherter Name vorliegt (Kopieren der Quelldatei
     *       mit manuellem Namen).</li>
     * </ul>
     *
     * @param row              die neu selektierte Zeile; {@code null} führt zu {@link #clearSelection()}
     * @param targetFolderPath Zielordner-Pfad für die Pfadlängen-Validierung; darf
     *                         {@code null} sein (wird als leer behandelt)
     */
    public void loadSelection(GuiBatchRunResultRow row, String targetFolderPath) {
        this.targetFolderPath = targetFolderPath == null ? "" : targetFolderPath;
        if (row == null) {
            clearSelection();
            return;
        }
        this.aiProposal = stripPdfExtension(row.finalFileName());
        this.lastSavedName = stripPdfExtension(row.effectiveFileName());

        boolean editable;
        if (row.resetPending()) {
            editable = false;
        } else if (requiresExistingTargetForRename(row.status())) {
            // Umbenennen einer existierenden Zieldatei: nur sinnvoll, wenn ein
            // gespeicherter Name vorliegt.
            editable = lastSavedName.isPresent();
        } else {
            // Manuelle Kopie: das Feld ist auch ohne gespeicherten Namen editierbar.
            editable = isRowEditable(row);
        }
        this.selectionEditable = editable;

        suppressValidation = true;
        try {
            textField.setText(lastSavedName.orElse(""));
        } finally {
            suppressValidation = false;
        }
        refreshUiState();
    }

    /**
     * Liefert {@code true}, wenn die Zeile einen Status hat, bei dem die Editierung
     * eine bestehende Zieldatei umbenennt (im Gegensatz zur Kopie der Quelldatei).
     *
     * @param status der aggregierte Abschlussstatus der Zeile
     * @return {@code true} für SUCCESS und SKIPPED_ALREADY_PROCESSED; sonst {@code false}
     */
    private static boolean requiresExistingTargetForRename(DocumentCompletionStatus status) {
        return status == DocumentCompletionStatus.SUCCESS
                || status == DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED;
    }

    /**
     * Leert die Komponente und deaktiviert die Eingabe. Wird aufgerufen wenn keine Zeile
     * selektiert ist.
     */
    public void clearSelection() {
        this.aiProposal = Optional.empty();
        this.lastSavedName = Optional.empty();
        this.selectionEditable = false;
        suppressValidation = true;
        try {
            textField.setText("");
        } finally {
            suppressValidation = false;
        }
        refreshUiState();
    }

    /**
     * Setzt den Textfeldinhalt auf den zuletzt gespeicherten Namen zurück. Äquivalent zum
     * Drücken der Escape-Taste im Textfeld.
     */
    public void discardChanges() {
        suppressValidation = true;
        try {
            textField.setText(lastSavedName.orElse(""));
        } finally {
            suppressValidation = false;
        }
        refreshUiState();
    }

    /**
     * Setzt den Textfeldinhalt auf den KI-Vorschlag zurück. Es erfolgt <em>kein</em>
     * Speichervorgang – der Benutzer kann anschließend über „Dateiname übernehmen"
     * bestätigen.
     */
    public void resetToAiProposal() {
        if (aiProposal.isEmpty()) {
            return;
        }
        suppressValidation = true;
        try {
            textField.setText(aiProposal.get());
        } finally {
            suppressValidation = false;
        }
        refreshUiState();
    }

    /**
     * Aktiviert oder deaktiviert die gesamte Komponente. Während eines laufenden Batch-Laufs
     * soll die Komponente deaktiviert sein.
     *
     * @param enabled {@code true} wenn Bedienung erlaubt ist
     */
    public void setEnabled(boolean enabled) {
        this.globalEnabled = enabled;
        refreshUiState();
    }

    /**
     * Liefert {@code true} wenn die aktuelle Texteingabe vom letzten gespeicherten Namen
     * abweicht.
     *
     * @return ob ungespeicherte Änderungen im Textfeld vorliegen
     */
    public boolean isDirty() {
        if (!selectionEditable) {
            return false;
        }
        String current = textField.getText() == null ? "" : textField.getText();
        String saved = lastSavedName.orElse("");
        return !current.equals(saved);
    }

    /**
     * Setzt den Dirty-State zurück, ohne das Textfeld neu zu laden. Wird aufgerufen,
     * nachdem eine Umbenennung erfolgreich abgeschlossen wurde, damit ein anschließendes
     * Ersetzen der Tabellenzeile keinen Verwerfen-Dialog auslöst. Der angezeigte Text
     * im Textfeld bleibt unverändert; {@code lastSavedName} wird auf den aktuellen
     * Textfeldinhalt gesetzt.
     */
    public void clearDirtyState() {
        String current = textField.getText() == null ? "" : textField.getText();
        this.lastSavedName = current.isBlank() ? Optional.empty() : Optional.of(current);
        refreshUiState();
    }

    /**
     * Liefert {@code true} wenn für die aktuelle Zeile ein KI-Vorschlag vorliegt.
     *
     * @return ob ein KI-Vorschlag existiert
     */
    public boolean hasAiProposal() {
        return aiProposal.isPresent();
    }

    /**
     * Liefert {@code true} wenn für die aktuelle Zeile ein zuletzt gespeicherter Name
     * existiert.
     *
     * @return ob ein letzter gespeicherter Name existiert
     */
    public boolean hasLastSaved() {
        return lastSavedName.isPresent();
    }

    /**
     * Aktualisiert intern den letzten gespeicherten Namen. Typisch nach erfolgreichem
     * Speichervorgang im Tab (ohne erneut {@link #loadSelection(GuiBatchRunResultRow, String)}
     * aufzurufen).
     *
     * @param newLastSavedName neuer letzter gespeicherter Name ohne {@code .pdf}; darf
     *                         {@code null} sein
     */
    public void updateLastSavedName(String newLastSavedName) {
        this.lastSavedName = newLastSavedName == null || newLastSavedName.isBlank()
                ? Optional.empty()
                : Optional.of(newLastSavedName);
        suppressValidation = true;
        try {
            textField.setText(lastSavedName.orElse(""));
        } finally {
            suppressValidation = false;
        }
        refreshUiState();
    }

    // --- Test-Accessoren ------------------------------------------------------

    /** Visible for tests. */
    TextField textField() {
        return textField;
    }

    /** Visible for tests. */
    Label validationLabel() {
        return validationLabel;
    }

    /** Visible for tests. */
    Button saveButton() {
        return saveButton;
    }

    /** Visible for tests. */
    Button resetButton() {
        return resetButton;
    }

    // --- Interne Helfer -------------------------------------------------------

    private void fireSaveRequest() {
        if (saveButton.isDisabled()) {
            return;
        }
        String current = textField.getText() == null ? "" : textField.getText();
        onSaveRequested.accept(current);
    }

    private void refreshUiState() {
        boolean enabled = selectionEditable && globalEnabled;
        textField.setDisable(!enabled);
        // Button „Zurücksetzen auf KI-Vorschlag" ist nur aktiv, wenn Eingabe möglich
        // und ein KI-Vorschlag vorliegt.
        resetButton.setDisable(aiProposal.isEmpty() || !enabled);

        if (!enabled) {
            // Validierung und Speichern-Button unterdrücken, Rahmen neutral.
            validationLabel.setVisible(false);
            validationLabel.setManaged(false);
            textField.setStyle("");
            saveButton.setDisable(true);
            return;
        }

        String current = textField.getText() == null ? "" : textField.getText();
        Optional<String> error = validate(current);

        if (error.isPresent()) {
            validationLabel.setText(error.get());
            validationLabel.setVisible(true);
            validationLabel.setManaged(true);
            textField.setStyle("-fx-border-color: #c62828; -fx-border-width: 1.5;");
            saveButton.setDisable(true);
        } else {
            validationLabel.setVisible(false);
            validationLabel.setManaged(false);
            if (isDirty()) {
                // Dirty-Markierung: orangefarbener Rand.
                textField.setStyle("-fx-border-color: #e65100; -fx-border-width: 1.5;");
                saveButton.setDisable(false);
            } else {
                textField.setStyle("");
                saveButton.setDisable(true);
            }
        }
    }

    /**
     * Führt die vollständige Dateinamen-Validierung aus und liefert gegebenenfalls den
     * fachlichen Fehlertext. Paket-privat für Unit-Tests.
     *
     * @param input Eingabe aus dem Textfeld (ohne {@code .pdf})
     * @return der Fehlertext oder {@link Optional#empty()} wenn gültig
     */
    Optional<String> validate(String input) {
        if (input == null || input.isBlank()) {
            return Optional.of("Dateiname darf nicht leer sein");
        }
        if (!input.equals(input.strip())) {
            return Optional.of("Leerzeichen am Anfang oder Ende nicht erlaubt");
        }
        if (input.matches(FORBIDDEN_CHARS_REGEX)) {
            return Optional.of("Unerlaubtes Zeichen (nicht erlaubt: \\ / : * ? \" < > |)");
        }
        if (RESERVED_WINDOWS_NAMES.contains(input.toUpperCase(java.util.Locale.ROOT))) {
            return Optional.of("Reservierter Systemname");
        }
        if (input.endsWith(".")) {
            return Optional.of("Dateiname darf nicht auf einen Punkt enden");
        }
        int totalLength = pathLengthEstimate(input);
        if (totalLength > MAX_WINDOWS_PATH_LENGTH) {
            return Optional.of("Dateipfad zu lang (Windows-Limit " + MAX_WINDOWS_PATH_LENGTH
                    + " Zeichen, aktuell " + totalLength + ")");
        }
        return Optional.empty();
    }

    private int pathLengthEstimate(String baseName) {
        String folder = targetFolderPath == null ? "" : targetFolderPath;
        int folderLength = folder.length();
        int separatorLength = folderLength == 0 ? 0 : 1;
        return folderLength + separatorLength + baseName.length() + PDF_EXTENSION.length();
    }

    /**
     * Liefert {@code true}, wenn die Zeile fachlich für eine manuelle Dateinamens-Aktion
     * editierbar ist.
     * <p>
     * Editierbar sind alle nicht-resetpending-Zeilen unabhängig davon, ob die Aktion
     * eine Zieldatei umbenennt (SUCCESS, SKIPPED_ALREADY_PROCESSED) oder die Quelldatei
     * kopiert (FAILED_*, SKIPPED_FINAL_FAILURE). Die genaue Aktion wird vom Tab anhand
     * des Status entschieden.
     *
     * @param row die Zeile, deren Editierbarkeit geprüft werden soll
     * @return {@code true} wenn die Zeile editierbar ist; sonst {@code false}
     */
    private static boolean isRowEditable(GuiBatchRunResultRow row) {
        return !row.resetPending();
    }

    private static Optional<String> stripPdfExtension(Optional<String> fileNameWithExtension) {
        if (fileNameWithExtension.isEmpty()) {
            return Optional.empty();
        }
        String raw = fileNameWithExtension.get();
        if (raw.toLowerCase(java.util.Locale.ROOT).endsWith(PDF_EXTENSION)) {
            return Optional.of(raw.substring(0, raw.length() - PDF_EXTENSION.length()));
        }
        return Optional.of(raw);
    }

    private static Set<String> buildReservedWindowsNames() {
        Set<String> reserved = new HashSet<>();
        reserved.add("CON");
        reserved.add("PRN");
        reserved.add("AUX");
        reserved.add("NUL");
        for (int i = 1; i <= 9; i++) {
            reserved.add("COM" + i);
            reserved.add("LPT" + i);
        }
        return Set.copyOf(reserved);
    }
}
