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

import java.awt.image.BufferedImage;
import java.io.File;
import java.nio.file.Path;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.encryption.InvalidPasswordException;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;

import javafx.application.Platform;
import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;

/**
 * Detailbereich-Komponente zur asynchronen Anzeige von Seiten einer Quelldatei.
 *
 * <p>Die Komponente rendert PDF-Seiten direkt mit Apache PDFBox und zeigt das Ergebnis
 * in einer {@link ImageView} an. Die Anzeige ist vollständig eingepasst (fit-to-view):
 * {@code fitWidth} und {@code fitHeight} der {@link ImageView} sind an die Größe des
 * umgebenden {@link StackPane} gebunden, {@code preserveRatio=true} erhält das
 * Seitenverhältnis. Es entstehen weder Scrollbalken noch Zoom-Artefakte.
 *
 * <p>Das Laden der PDF-Datei und das Rendering einzelner Seiten erfolgt auf einem
 * dedizierten Worker-Thread. UI-Updates laufen ausschließlich über den JavaFX
 * Application Thread. Bereits gerenderte Seiten werden in einem In-Memory-Cache
 * ({@code Map<Integer, Image>}) gehalten, sodass wiederholte Navigation kein
 * erneutes Rendering erfordert. Der Cache wird beim Wechsel der Quelldatei geleert.
 *
 * <p>Es gilt das Prinzip „Latest Preview Request Wins": Veraltete Lade- und
 * Rendering-Ergebnisse werden anhand einer Sequenznummer erkannt und verworfen,
 * sobald eine neue Anforderung eingeht.
 *
 * <h2>Fehlerfälle</h2>
 * <ul>
 *   <li>Quelldatei nicht vorhanden → Meldungstext im Vorschaubereich</li>
 *   <li>PDF nicht lesbar → Meldungstext im Vorschaubereich</li>
 *   <li>PDF passwortgeschützt → Meldungstext im Vorschaubereich</li>
 *   <li>Keine Selektion → neutraler Platzhaltertext</li>
 * </ul>
 *
 * <h2>Threading</h2>
 * <p>Alle öffentlichen Methoden müssen auf dem JavaFX Application Thread aufgerufen
 * werden. Das PDF-Öffnen, die Speicherhaltung des {@link PDDocument} und das
 * Rendering einzelner Seiten laufen ausschließlich auf dem Worker-Thread.
 */
public final class PdfPreviewPane {

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

    static final String PLACEHOLDER_TEXT = "Keine Datei ausgewählt";
    static final String FILE_NOT_FOUND_TEXT = "Quelldatei nicht gefunden";
    static final String PDF_UNREADABLE_TEXT = "PDF konnte nicht geöffnet werden";
    static final String PDF_PASSWORD_PROTECTED_TEXT =
            "PDF ist passwortgeschützt und kann nicht angezeigt werden";

    /** Render-Auflösung in DPI. 120 DPI ist ein guter Kompromiss aus Qualität und Geschwindigkeit. */
    private static final float RENDER_DPI = 120f;

    private final VBox root = new VBox(4);
    private final StackPane viewStack = new StackPane();
    private final ImageView imageView = new ImageView();
    private final Label overlayLabel = new Label(PLACEHOLDER_TEXT);
    private final ProgressIndicator progressIndicator = new ProgressIndicator();
    private final Label pageLabel = new Label();
    private final Button prevButton = new Button("◀ Vorherige");
    private final Button nextButton = new Button("Nächste ▶");
    private final Label sectionTitle = new Label("PDF-Vorschau");

    /**
     * Sequenznummer der aktuell angeforderten Vorschau. Jede neue Anforderung
     * (Laden oder Seitenwechsel) erhöht diesen Zähler. Lade-/Rendering-Ergebnisse
     * mit veralteter Sequenznummer werden verworfen.
     */
    private final AtomicLong currentRequestSequence = new AtomicLong(0);

    /**
     * Cache bereits gerenderter Seiten für die aktuell geladene Quelldatei.
     * Schlüssel ist die 1-basierte Seitennummer. Wird beim Wechsel der Quelldatei geleert.
     */
    private final Map<Integer, Image> pageCache = new ConcurrentHashMap<>();

    /** Hintergrund-Thread-Pool für Lade- und Rendering-Aufgaben. */
    private final ExecutorService executor =
            Executors.newSingleThreadExecutor(r -> {
                Thread t = new Thread(r, "pdf-preview-worker");
                t.setDaemon(true);
                return t;
            });

    /**
     * Aktuell geöffnetes PDF-Dokument. Zugriff ausschließlich vom Worker-Thread.
     * {@code null} wenn kein Dokument geöffnet ist.
     */
    private PDDocument currentDocument = null;

    /**
     * Renderer für das aktuell geöffnete Dokument. Zugriff ausschließlich vom Worker-Thread.
     * {@code null} wenn kein Dokument geöffnet ist.
     */
    private PDFRenderer currentRenderer = null;

    /** Aktuell geladene Quelldatei; null wenn keine Selektion vorliegt. */
    private Path currentSourceFile = null;

    /** Aktuell angezeigte Seite (1-basiert; 0 wenn keine Datei geladen). */
    private int currentPage = 0;

    /** Anzahl der Seiten der aktuell geladenen PDF; -1 wenn nicht ermittelt. */
    private int totalPages = -1;

    /** Gibt an ob die Navigation bedienbar ist. */
    private boolean enabled = true;

    /**
     * Erstellt die Komponente im deaktivierten Platzhalter-Zustand.
     */
    public PdfPreviewPane() {
        sectionTitle.setStyle("-fx-font-weight: bold;");

        imageView.setId("pdf-preview-image-view");
        imageView.setPreserveRatio(true);
        imageView.setSmooth(true);
        // Fit-to-view: ImageView füllt den verfügbaren Bereich unter Wahrung des Seitenverhältnisses
        imageView.fitWidthProperty().bind(viewStack.widthProperty());
        imageView.fitHeightProperty().bind(viewStack.heightProperty());

        overlayLabel.setId("pdf-preview-overlay-label");
        overlayLabel.setStyle("-fx-text-fill: #555555;");
        overlayLabel.setWrapText(true);
        overlayLabel.setVisible(true);
        overlayLabel.setManaged(true);

        progressIndicator.setId("pdf-preview-progress");
        progressIndicator.setVisible(false);
        progressIndicator.setManaged(false);
        progressIndicator.setMaxWidth(60);
        progressIndicator.setMaxHeight(60);

        // Stack: ImageView hinter dem Overlay; Overlay überlagert das Bild bei Fehlern/Laden
        viewStack.getChildren().addAll(imageView, overlayLabel, progressIndicator);
        StackPane.setAlignment(imageView, Pos.CENTER);
        StackPane.setAlignment(overlayLabel, Pos.CENTER);
        StackPane.setAlignment(progressIndicator, Pos.CENTER);
        VBox.setVgrow(viewStack, Priority.ALWAYS);

        prevButton.setId("pdf-preview-prev-button");
        prevButton.setOnAction(e -> navigateToPreviousPage());

        nextButton.setId("pdf-preview-next-button");
        nextButton.setOnAction(e -> navigateToNextPage());

        pageLabel.setId("pdf-preview-page-label");
        pageLabel.setStyle("-fx-text-fill: #555555;");

        HBox navBar = new HBox(8, prevButton, pageLabel, nextButton);
        navBar.setAlignment(Pos.CENTER);
        navBar.setPadding(new Insets(4, 0, 4, 0));

        root.getChildren().addAll(sectionTitle, viewStack, navBar);
        root.setPadding(new Insets(4, 0, 0, 0));

        showPlaceholder();
        updateNavigationButtons();
    }

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

    /**
     * Lädt die angegebene Quelldatei asynchron und zeigt Seite 1 an.
     * Startet eine neue Vorschau-Anforderung und verwirft etwaige laufende Anforderungen.
     * Der Seiten-Cache wird geleert und ein etwaiges bereits geöffnetes PDF-Dokument
     * wird geschlossen.
     * <p>
     * Muss auf dem JavaFX Application Thread aufgerufen werden.
     *
     * @param sourceFile Pfad zur Quelldatei; null führt zu {@link #clear()}
     */
    public void loadSource(Path sourceFile) {
        if (sourceFile == null) {
            clear();
            return;
        }
        currentSourceFile = sourceFile;
        currentPage = 0;
        totalPages = -1;
        pageCache.clear();
        requestLoad(sourceFile);
    }

    /**
     * Leert die Komponente und zeigt den neutralen Platzhaltertext.
     * Das aktuell geöffnete PDF-Dokument wird asynchron auf dem Worker-Thread geschlossen.
     * <p>
     * Muss auf dem JavaFX Application Thread aufgerufen werden.
     */
    public void clear() {
        currentSourceFile = null;
        currentPage = 0;
        totalPages = -1;
        pageCache.clear();
        // Neue Sequenznummer: laufende Requests werden verworfen
        currentRequestSequence.incrementAndGet();
        // Dokument auf dem Worker-Thread schließen, da PDDocument ausschließlich dort genutzt wird
        executor.submit(this::closeCurrentDocumentOnWorker);
        imageView.setImage(null);
        showPlaceholder();
        updateNavigationButtons();
    }

    /**
     * Aktiviert oder deaktiviert die Navigations-Buttons.
     * Während eines laufenden Batch-Laufs soll die Navigation deaktiviert sein.
     * Die Vorschau-Anzeige bleibt sichtbar.
     *
     * @param enabled {@code true} wenn Navigation erlaubt ist
     */
    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
        updateNavigationButtons();
    }

    /**
     * Beendet den internen Executor sauber und schließt das eventuell noch offene
     * PDF-Dokument. Muss beim Schließen der Anwendung aufgerufen werden.
     */
    public void shutdown() {
        try {
            executor.submit(this::closeCurrentDocumentOnWorker);
        } catch (RuntimeException ignored) {
            // Executor wurde bereits beendet – keine Aktion erforderlich
        }
        executor.shutdown();
    }

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

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

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

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

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

    /** Visible for tests. */
    ProgressIndicator progressIndicator() {
        return progressIndicator;
    }

    // --- Navigation -----------------------------------------------------------

    private void navigateToPreviousPage() {
        if (!enabled || currentPage <= 1) {
            return;
        }
        goToPage(currentPage - 1);
    }

    private void navigateToNextPage() {
        if (!enabled || totalPages <= 0 || currentPage >= totalPages) {
            return;
        }
        goToPage(currentPage + 1);
    }

    /**
     * Wechselt zur angegebenen Seite. Bereits gerenderte Seiten werden direkt aus dem
     * Cache angezeigt; ansonsten wird ein Rendering-Auftrag auf den Worker-Thread gelegt.
     *
     * @param targetPage Ziel-Seite (1-basiert, muss im gültigen Bereich liegen)
     */
    private void goToPage(int targetPage) {
        currentPage = targetPage;
        updatePageLabel();
        updateNavigationButtons();

        Image cached = pageCache.get(targetPage);
        if (cached != null) {
            imageView.setImage(cached);
            showContent();
            return;
        }

        long seq = currentRequestSequence.incrementAndGet();
        showLoading();
        executor.submit(() -> renderPageOnWorker(targetPage, seq));
    }

    // --- Asynchrones Laden und Rendering --------------------------------------

    /**
     * Startet eine asynchrone Lade-Anforderung für die angegebene Datei.
     * Erhöht die Sequenznummer, damit veraltete Ergebnisse erkannt und verworfen werden.
     *
     * @param file die zu ladende Quelldatei
     */
    private void requestLoad(Path file) {
        long seq = currentRequestSequence.incrementAndGet();
        LOG.debug("PDF-Vorschau: Lade {} (Anforderung #{})", file, seq);

        showLoading();
        updateNavigationButtons();

        executor.submit(() -> loadAndRenderFirstPageOnWorker(file, seq));
    }

    /**
     * Öffnet die PDF-Datei, ermittelt die Seitenzahl und rendert die erste Seite.
     * Läuft ausschließlich auf dem Worker-Thread.
     *
     * @param file die zu ladende Datei
     * @param seq  die Sequenznummer dieser Anforderung
     */
    private void loadAndRenderFirstPageOnWorker(Path file, long seq) {
        File ioFile = file.toFile();

        if (!ioFile.exists()) {
            LOG.warn("PDF-Vorschau: Rendering fehlgeschlagen – Datei nicht gefunden: {}", file);
            publishError(seq, FILE_NOT_FOUND_TEXT);
            return;
        }

        // Vorheriges Dokument schließen bevor ein neues geöffnet wird
        closeCurrentDocumentOnWorker();

        try {
            PDDocument doc = Loader.loadPDF(ioFile);
            currentDocument = doc;
            currentRenderer = new PDFRenderer(doc);

            int pages = Math.max(1, doc.getNumberOfPages());
            BufferedImage buffered =
                    currentRenderer.renderImageWithDPI(0, RENDER_DPI, ImageType.RGB);
            Image fxImage = SwingFXUtils.toFXImage(buffered, null);

            final int totalPagesFinal = pages;
            Platform.runLater(() -> {
                if (currentRequestSequence.get() != seq) {
                    return; // Veraltet – verwerfen
                }
                totalPages = totalPagesFinal;
                currentPage = 1;
                pageCache.put(1, fxImage);
                imageView.setImage(fxImage);
                showContent();
                updateNavigationButtons();
                updatePageLabel();
                LOG.debug("PDF-Vorschau: Rendering abgeschlossen – {} Seite(n)", totalPagesFinal);
            });
        } catch (InvalidPasswordException ipe) {
            LOG.warn("PDF-Vorschau: PDF ist passwortgeschützt: {}", file, ipe);
            closeCurrentDocumentOnWorker();
            publishError(seq, PDF_PASSWORD_PROTECTED_TEXT);
        } catch (Exception e) {
            LOG.warn("PDF-Vorschau: Rendering fehlgeschlagen: {}", file, e);
            closeCurrentDocumentOnWorker();
            publishError(seq, PDF_UNREADABLE_TEXT);
        }
    }

    /**
     * Rendert eine einzelne Seite des aktuell geöffneten Dokuments.
     * Läuft ausschließlich auf dem Worker-Thread.
     *
     * @param page 1-basierte Seitennummer
     * @param seq  die Sequenznummer dieser Anforderung
     */
    private void renderPageOnWorker(int page, long seq) {
        PDFRenderer renderer = currentRenderer;
        if (renderer == null) {
            // Dokument wurde zwischenzeitlich geschlossen – nichts zu tun
            return;
        }
        try {
            BufferedImage buffered = renderer.renderImageWithDPI(page - 1, RENDER_DPI, ImageType.RGB);
            Image fxImage = SwingFXUtils.toFXImage(buffered, null);
            Platform.runLater(() -> {
                if (currentRequestSequence.get() != seq) {
                    return; // Veraltet – verwerfen
                }
                pageCache.put(page, fxImage);
                if (currentPage == page) {
                    imageView.setImage(fxImage);
                    showContent();
                }
            });
        } catch (Exception e) {
            LOG.warn("PDF-Vorschau: Rendering von Seite {} fehlgeschlagen", page, e);
            publishError(seq, PDF_UNREADABLE_TEXT);
        }
    }

    /**
     * Schließt das aktuell geöffnete PDF-Dokument, falls vorhanden. Läuft ausschließlich
     * auf dem Worker-Thread und ist idempotent.
     */
    private void closeCurrentDocumentOnWorker() {
        PDDocument doc = currentDocument;
        currentDocument = null;
        currentRenderer = null;
        if (doc != null) {
            try {
                doc.close();
            } catch (Exception e) {
                LOG.debug("PDF-Vorschau: Schließen des Dokuments schlug fehl", e);
            }
        }
    }

    /**
     * Übergibt eine Fehlermeldung auf den FX-Thread. Veraltete Meldungen werden verworfen.
     *
     * @param seq     Sequenznummer der Anforderung, zu der die Meldung gehört
     * @param message anzuzeigende Fehlermeldung
     */
    private void publishError(long seq, String message) {
        Platform.runLater(() -> {
            if (currentRequestSequence.get() != seq) {
                return;
            }
            showError(message);
            updateNavigationButtons();
        });
    }

    // --- UI-Zustandshelfer ---------------------------------------------------

    private void showPlaceholder() {
        overlayLabel.setText(PLACEHOLDER_TEXT);
        overlayLabel.setVisible(true);
        overlayLabel.setManaged(true);
        imageView.setVisible(false);
        imageView.setManaged(false);
        progressIndicator.setVisible(false);
        progressIndicator.setManaged(false);
        pageLabel.setText("");
    }

    private void showLoading() {
        progressIndicator.setVisible(true);
        progressIndicator.setManaged(true);
        overlayLabel.setVisible(false);
        overlayLabel.setManaged(false);
        imageView.setVisible(false);
        imageView.setManaged(false);
    }

    private void showContent() {
        progressIndicator.setVisible(false);
        progressIndicator.setManaged(false);
        overlayLabel.setVisible(false);
        overlayLabel.setManaged(false);
        imageView.setVisible(true);
        imageView.setManaged(true);
    }

    private void showError(String message) {
        overlayLabel.setText(message);
        overlayLabel.setVisible(true);
        overlayLabel.setManaged(true);
        imageView.setVisible(false);
        imageView.setManaged(false);
        progressIndicator.setVisible(false);
        progressIndicator.setManaged(false);
        pageLabel.setText("");
    }

    private void updateNavigationButtons() {
        boolean canNavigate = enabled && currentSourceFile != null && totalPages > 0;
        prevButton.setDisable(!canNavigate || currentPage <= 1);
        nextButton.setDisable(!canNavigate || currentPage >= totalPages);
    }

    private void updatePageLabel() {
        if (totalPages > 0 && currentPage > 0) {
            pageLabel.setText("Seite " + currentPage + " / " + totalPages);
        } else {
            pageLabel.setText("");
        }
    }
}
