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

import java.util.List;
import java.util.Objects;
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.adapter.in.gui.editor.ConfirmationDialogContent;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiMessageEntry;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiMessageSeverity;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionReport;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionPlan;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestReport;
import javafx.application.Platform;

/**
 * Koordiniert den gesammelten Bestätigungsdialog und die anschließende Ausführung
 * schreibender Korrekturmaßnahmen nach einem technischen Gesamttest.
 * <p>
 * Der Koordinator empfängt einen {@link TechnicalTestReport}, prüft ob korrigierbare
 * Befunde vorliegen, leitet daraus einen {@link CorrectionPlan} ab, zeigt dem Benutzer
 * einen gesammelten Bestätigungsdialog und führt die Korrekturen bei Bestätigung über
 * den {@link CorrectionExecutionService} auf einem Hintergrund-Worker-Thread aus.
 * Ergebnisse werden in die geteilte {@code pendingMessages}-Liste eingehängt.
 *
 * <h2>Ablauf</h2>
 * <ol>
 *   <li>Bericht erhält: prüfen ob {@code hasCorrectableFindings()}.</li>
 *   <li>Wenn keine korrigierbaren Befunde: kein Dialog, keine Aktion.</li>
 *   <li>Wenn korrigierbare Befunde: {@link CorrectionPlan} ableiten.</li>
 *   <li>Dialog auf FX-Thread anzeigen.</li>
 *   <li>Bei Bestätigung: Korrekturen auf Worker-Thread ausführen.</li>
 *   <li>Ergebnisse via {@code Platform.runLater} auf FX-Thread zurückführen.</li>
 *   <li>Meldungen in {@code pendingMessages} einhängen (Replace-Semantik).</li>
 * </ol>
 *
 * <h2>Threading-Kontrakt</h2>
 * <p>
 * {@link #offerCorrections(TechnicalTestReport)} muss auf dem JavaFX Application Thread
 * aufgerufen werden. I/O (Ausführung der Korrekturen) läuft auf einem dedizierten
 * Daemon-Hintergrund-Thread. UI-Updates erfolgen ausschließlich via
 * {@code Platform.runLater}.
 *
 * <h2>Keine stillen Korrekturen</h2>
 * <p>
 * Ohne ausdrückliche Benutzerbestätigung werden keine schreibenden Änderungen ausgeführt.
 * Bei Dialog-Abbruch bleibt der Zustand unverändert.
 *
 * <h2>Anti-Scope</h2>
 * <p>
 * Dieser Koordinator führt keine Provider-nahen Korrekturen, keine Änderung fachlich
 * riskanter Werte und keine automatischen Laufstarts durch.
 */
public final class GuiCorrectionDialogCoordinator {

    /** Quell-Tag für Einträge in {@code pendingMessages}, die von diesem Koordinator stammen. */
    static final String SOURCE_TAG = "Korrekturen";

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

    private final CorrectionExecutionService correctionExecutionService;
    private final List<GuiMessageEntry> pendingMessages;
    private final Consumer<Void> refreshCallback;

    /**
     * Funktion, die dem Benutzer den Bestätigungsdialog zeigt.
     * <p>
     * Erhält den {@link ConfirmationDialogContent} und gibt {@code true} zurück, wenn der
     * Benutzer „Fortfahren" wählt, andernfalls {@code false}. Standard: echter Alert.
     * Paket-privat für Test-Substitution.
     */
    Function<ConfirmationDialogContent, Boolean> dialogSupplier;

    /**
     * Factory für den Hintergrund-Worker-Thread.
     * Standard: Daemon-Thread namens {@code gui-correction-worker}.
     * Paket-privat für Test-Substitution.
     */
    Function<Runnable, Thread> correctionThreadFactory;

    /**
     * Verbraucher zur Rückführung des Ergebnisses auf den FX-Thread.
     * Standard: {@code Platform.runLater}. Paket-privat für Test-Substitution.
     */
    java.util.function.Consumer<Runnable> resultDelivery;

    /**
     * Erstellt einen neuen Koordinator.
     *
     * @param correctionExecutionService Service für die Ausführung von Korrekturen; darf nicht {@code null} sein
     * @param pendingMessages            geteilte veränderliche Nachrichtenliste; darf nicht {@code null} sein
     * @param refreshCallback            Callback nach Anwendung der Ergebnisse (z. B. View-Aktualisierung);
     *                                   darf nicht {@code null} sein
     * @throws NullPointerException wenn einer der Parameter {@code null} ist
     */
    public GuiCorrectionDialogCoordinator(CorrectionExecutionService correctionExecutionService,
                                           List<GuiMessageEntry> pendingMessages,
                                           Consumer<Void> refreshCallback) {
        this.correctionExecutionService = Objects.requireNonNull(correctionExecutionService,
                "correctionExecutionService must not be null");
        this.pendingMessages = Objects.requireNonNull(pendingMessages,
                "pendingMessages must not be null");
        this.refreshCallback = Objects.requireNonNull(refreshCallback,
                "refreshCallback must not be null");

        this.dialogSupplier = this::showConfirmationDialog;
        this.correctionThreadFactory = task -> {
            Thread t = new Thread(task, "gui-correction-worker");
            t.setDaemon(true);
            return t;
        };
        this.resultDelivery = Platform::runLater;
    }

    /**
     * Prüft den Bericht auf korrigierbare Befunde, zeigt bei Bedarf den Bestätigungsdialog
     * und führt die Korrekturen nach Bestätigung asynchron aus.
     * <p>
     * Wenn der Bericht keine korrigierbaren Befunde enthält ({@code hasCorrectableFindings()
     * == false}), wird kein Dialog angezeigt und keine Aktion ausgeführt.
     * <p>
     * Muss auf dem JavaFX Application Thread aufgerufen werden.
     *
     * @param report der vollständige Gesamttestbericht; darf nicht {@code null} sein
     */
    public void offerCorrections(TechnicalTestReport report) {
        Objects.requireNonNull(report, "report must not be null");

        if (!report.hasCorrectableFindings()) {
            LOG.debug("Gesamttest: Keine korrigierbaren Befunde – kein Bestätigungsdialog.");
            return;
        }

        CorrectionPlan plan = report.deriveCorrectionPlan();
        if (!plan.hasCorrections()) {
            LOG.debug("Gesamttest: Korrekturplan ist leer – kein Bestätigungsdialog.");
            return;
        }

        LOG.info("Gesamttest: {} korrigierbare Befunde. Bestätigungsdialog wird angezeigt.", plan.size());

        ConfirmationDialogContent dialogContent = ConfirmationDialogContent.fromPlan(plan);
        boolean confirmed = dialogSupplier.apply(dialogContent);

        if (!confirmed) {
            LOG.info("Bestätigungsdialog: Benutzer hat Korrekturen abgelehnt. Keine Änderungen.");
            return;
        }

        LOG.info("Bestätigungsdialog: Benutzer hat Korrekturen bestätigt. Ausführung startet.");
        Runnable task = () -> {
            CorrectionExecutionReport executionReport = correctionExecutionService.execute(plan);
            resultDelivery.accept(() -> {
                applyResult(executionReport);
                refreshCallback.accept(null);
            });
        };

        Thread worker = correctionThreadFactory.apply(task);
        worker.start();
    }

    /**
     * Wendet das Ergebnis der Korrekturausführung auf die geteilte Nachrichtenliste an.
     * <p>
     * Entfernt alle vorherigen Einträge mit Quelle {@link #SOURCE_TAG} und fügt für jedes
     * {@link CorrectionOutcome} einen neuen Eintrag hinzu. Zusätzlich wird eine Zusammenfassung
     * angehängt.
     * <p>
     * Muss nur auf dem JavaFX Application Thread aufgerufen werden (via {@code resultDelivery}).
     *
     * @param report das Ausführungsergebnis; darf nicht {@code null} sein
     */
    private void applyResult(CorrectionExecutionReport report) {
        // Alte Einträge mit Source-Tag entfernen (Replace-Semantik)
        pendingMessages.removeIf(msg -> SOURCE_TAG.equals(msg.source().orElse("")));

        long appliedCount = 0;
        long failedCount = 0;
        long notAttemptedCount = 0;

        for (CorrectionOutcome outcome : report.outcomes()) {
            switch (outcome) {
                case CorrectionOutcome.Applied applied -> {
                    pendingMessages.add(GuiMessageEntry.of(
                            GuiMessageSeverity.INFO,
                            "Korrektur angewendet: " + applied.suggestion().descriptionForUser()
                                    + " – " + applied.message(),
                            SOURCE_TAG));
                    appliedCount++;
                    LOG.info("Korrektur angewendet: {} – {}", applied.suggestion().descriptionForUser(),
                            applied.message());
                }
                case CorrectionOutcome.Failed failed -> {
                    pendingMessages.add(GuiMessageEntry.of(
                            GuiMessageSeverity.ERROR,
                            "Korrektur fehlgeschlagen: " + failed.suggestion().descriptionForUser()
                                    + " (" + failed.errorMessage() + ")",
                            SOURCE_TAG));
                    failedCount++;
                    LOG.warn("Korrektur fehlgeschlagen: {} – {}", failed.suggestion().descriptionForUser(),
                            failed.errorMessage());
                }
                case CorrectionOutcome.NotAttempted notAttempted -> {
                    pendingMessages.add(GuiMessageEntry.of(
                            GuiMessageSeverity.HINT,
                            "Korrektur nicht durchgeführt: " + notAttempted.suggestion().descriptionForUser()
                                    + " – " + notAttempted.reason(),
                            SOURCE_TAG));
                    notAttemptedCount++;
                    LOG.info("Korrektur nicht durchgeführt: {} – {}", notAttempted.suggestion().descriptionForUser(),
                            notAttempted.reason());
                }
            }
        }

        // Zusammenfassung
        String summary = "Korrekturausführung abgeschlossen: "
                + appliedCount + " angewendet, "
                + failedCount + " fehlgeschlagen, "
                + notAttemptedCount + " nicht versucht.";
        pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.INFO, summary, SOURCE_TAG));
        LOG.info("Korrekturausführung abgeschlossen: {} angewendet, {} fehlgeschlagen, {} nicht versucht.",
                appliedCount, failedCount, notAttemptedCount);
    }

    /**
     * Zeigt den echten JavaFX-Bestätigungsdialog und gibt die Benutzerentscheidung zurück.
     * <p>
     * Muss auf dem JavaFX Application Thread aufgerufen werden. Standard-Fokus liegt auf
     * dem Abbrechen-Button (kein versehentliches Bestätigen durch Enter).
     *
     * @param content der Dialoginhalt; darf nicht {@code null} sein
     * @return {@code true} wenn der Benutzer „Fortfahren" wählt, sonst {@code false}
     */
    private boolean showConfirmationDialog(ConfirmationDialogContent content) {
        javafx.scene.control.ButtonType proceedButton =
                new javafx.scene.control.ButtonType("Fortfahren",
                        javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
        javafx.scene.control.ButtonType cancelButton =
                new javafx.scene.control.ButtonType("Abbrechen",
                        javafx.scene.control.ButtonBar.ButtonData.CANCEL_CLOSE);

        javafx.scene.control.Alert alert = new javafx.scene.control.Alert(
                javafx.scene.control.Alert.AlertType.CONFIRMATION);
        alert.setTitle(content.title());
        alert.setHeaderText(content.introText());

        StringBuilder correctionListText = new StringBuilder();
        for (String line : content.correctionLines()) {
            correctionListText.append("• ").append(line).append("\n");
        }
        correctionListText.append("\nFortfahren?");
        alert.setContentText(correctionListText.toString());

        alert.getButtonTypes().setAll(proceedButton, cancelButton);

        // Standard-Fokus: Abbrechen
        javafx.scene.Node cancelNode = alert.getDialogPane().lookupButton(cancelButton);
        if (cancelNode instanceof javafx.scene.control.Button btn) {
            btn.setDefaultButton(true);
        }
        javafx.scene.Node proceedNode = alert.getDialogPane().lookupButton(proceedButton);
        if (proceedNode instanceof javafx.scene.control.Button btn) {
            btn.setDefaultButton(false);
        }

        java.util.Optional<javafx.scene.control.ButtonType> result = alert.showAndWait();
        return result.isPresent() && result.get() == proceedButton;
    }

    /**
     * Gibt eine unveränderliche Momentaufnahme der aktuell ausstehenden Nachrichten zurück.
     * <p>
     * Ausschließlich für Tests gedacht.
     *
     * @return unveränderliche Kopie der Nachrichtenliste; nie {@code null}
     */
    public List<GuiMessageEntry> pendingMessagesSnapshot() {
        return List.copyOf(pendingMessages);
    }
}
