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 java.util.function.Supplier;

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

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.editor.EditorValidationInput;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CheckpointId;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CheckpointResult;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestOrchestrator;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestReport;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestRequest;
import javafx.application.Platform;

/**
 * Koordiniert die asynchrone Ausführung der Aktion „Technische Tests ausführen"
 * für den GUI-Konfigurationseditor.
 * <p>
 * Dieser Koordinator ist verantwortlich für:
 * <ul>
 *   <li>Lesen des aktuellen GUI-Editorzustands (via {@link Supplier}).</li>
 *   <li>Aufbau eines {@link TechnicalTestRequest} aus dem aktuellen Zustand.</li>
 *   <li>Ausführung des {@link TechnicalTestOrchestrator} auf einem dedizierten
 *       Daemon-Hinterground-Thread namens {@code gui-technical-test}.</li>
 *   <li>Rückführung des {@link TechnicalTestReport} auf den JavaFX Application Thread
 *       via {@code Platform.runLater}.</li>
 *   <li>Einhängen der Ergebnisse als {@link GuiMessageEntry}-Einträge in die geteilte
 *       {@code pendingMessages}-Liste (Quelle: „Technische-Tests").</li>
 *   <li>Ersetzen vorheriger Test-Einträge (Replace-Semantik) bei jedem neuen Aufruf.</li>
 *   <li>Weitergabe des vollständigen Berichts an den {@code postResultCallback}, damit
 *       spätere Arbeitsschritte (z. B. Korrekturhilfen) auf das Ergebnis zugreifen können.</li>
 * </ul>
 * <p>
 * <strong>Threading-Kontrakt:</strong> {@link #triggerTechnicalTests()} darf nur auf dem
 * JavaFX Application Thread aufgerufen werden. Hintergrund-Worker-Threads dürfen nur über
 * den injizierten {@code resultDelivery}-Verbraucher mit der UI interagieren, der in der
 * Produktion {@code Platform.runLater} kapselt.
 * <p>
 * <strong>Kein implizites Speichern:</strong> Der Koordinator liest den aktuellen GUI-Zustand
 * und führt den Test aus, ohne die Konfigurationsdatei zu schreiben oder den Dirty-Zustand
 * des Editors zu ändern.
 * <p>
 * <strong>Anti-Scope:</strong> Dieser Koordinator führt keine schreibenden Korrekturen durch.
 * Korrekturvorschläge werden als Bestandteil des {@link TechnicalTestReport} zurückgegeben,
 * sind aber nicht ausführbar. Die Ausführung ist einem späteren Arbeitsschritt vorbehalten.
 * <p>
 * Die Worker-Thread-Factory und die Result-Delivery-Funktion sind injizierbar, damit Tests
 * deterministisch ohne echten Hintergrund-Thread laufen können.
 */
public final class GuiTechnicalTestCoordinator {

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

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

    private final TechnicalTestOrchestrator orchestrator;
    private final Supplier<EditorValidationInput> inputProvider;
    private final Supplier<String> configFilePathProvider;
    private final Supplier<String> logDirectoryProvider;
    private final List<GuiMessageEntry> pendingMessages;
    private final Consumer<TechnicalTestReport> postResultCallback;

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

    /**
     * Verbraucher zur Rückführung des Ergebnisses. In der Produktion kapselt er {@code Platform.runLater}.
     * In Tests kann er durch einen direkten Aufruf ersetzt werden, damit das Ergebnis sofort
     * auf dem Worker-Thread angewendet wird, ohne die FX-Warteschlange zu entwässern.
     * Paket-privat für Test-Substitution.
     */
    java.util.function.Consumer<Runnable> resultDelivery = Platform::runLater;

    /**
     * Erstellt einen neuen Koordinator.
     *
     * @param orchestrator           Orchestrator für den vollständigen Gesamttest; darf nicht {@code null} sein
     * @param inputProvider          Lieferant des aktuellen {@link EditorValidationInput}; darf nicht {@code null} sein
     * @param configFilePathProvider Lieferant des aktuell geladenen Konfigurationsdateipfads als String;
     *                               gibt eine leere Zeichenkette zurück wenn keine Datei geladen ist;
     *                               darf nicht {@code null} sein
     * @param logDirectoryProvider   Lieferant des konfigurierten {@code log.directory}-Rohwerts;
     *                               gibt eine leere Zeichenkette zurück wenn kein Wert konfiguriert ist;
     *                               darf nicht {@code null} sein
     * @param pendingMessages        geteilte veränderliche Nachrichtenliste; darf nicht {@code null} sein
     * @param postResultCallback     Callback nach erfolgreicher Ergebnisanwendung; darf nicht {@code null} sein
     * @throws NullPointerException wenn einer der Parameter {@code null} ist
     */
    public GuiTechnicalTestCoordinator(TechnicalTestOrchestrator orchestrator,
                                       Supplier<EditorValidationInput> inputProvider,
                                       Supplier<String> configFilePathProvider,
                                       Supplier<String> logDirectoryProvider,
                                       List<GuiMessageEntry> pendingMessages,
                                       Consumer<TechnicalTestReport> postResultCallback) {
        this.orchestrator = Objects.requireNonNull(orchestrator, "orchestrator must not be null");
        this.inputProvider = Objects.requireNonNull(inputProvider, "inputProvider must not be null");
        this.configFilePathProvider = Objects.requireNonNull(configFilePathProvider, "configFilePathProvider must not be null");
        this.logDirectoryProvider = Objects.requireNonNull(logDirectoryProvider, "logDirectoryProvider must not be null");
        this.pendingMessages = Objects.requireNonNull(pendingMessages, "pendingMessages must not be null");
        this.postResultCallback = Objects.requireNonNull(postResultCallback, "postResultCallback must not be null");
        this.testThreadFactory = task -> {
            Thread t = new Thread(task, "gui-technical-test");
            t.setDaemon(true);
            return t;
        };
    }

    /**
     * Löst die asynchrone Ausführung des vollständigen technischen Gesamttests aus.
     * <p>
     * Vor dem Worker-Start wird die geteilte Nachrichtenliste auf dem FX-Thread geleert;
     * jeder Aufruf ersetzt die zuvor angefügten Einträge (Replace-Semantik).
     * <p>
     * Liest den aktuellen Editorzustand und den Konfigurationsdateipfad, baut einen
     * {@link TechnicalTestRequest} und startet den {@link TechnicalTestOrchestrator} auf
     * einem Hintergrund-Worker-Thread. Das Ergebnis wird via {@code resultDelivery} an den
     * JavaFX Application Thread zurückgegeben.
     * <p>
     * Der Konfigurationsdateipfad wird genutzt, um bei fehlender Prompt-Datei-Konfiguration
     * einen sinnvollen Standardpfad ({@code <config-parent>/prompt.txt}) zu bestimmen.
     * <p>
     * Muss auf dem JavaFX Application Thread aufgerufen werden.
     */
    public void triggerTechnicalTests() {
        // Bestehende Nachrichtenliste auf dem FX-Thread leeren, bevor der Worker-Thread
        // startet. Dadurch laufen clear() und nachfolgende add()-Aufrufe (die per
        // Platform.runLater wieder auf dem FX-Thread landen) auf demselben Thread und
        // es entsteht kein Race-Fenster mit der UI.
        pendingMessages.clear();
        EditorValidationInput input = inputProvider.get();
        String configFilePath = configFilePathProvider.get();
        String logDirectory = logDirectoryProvider.get();
        TechnicalTestRequest request = new TechnicalTestRequest(input, configFilePath, logDirectory);

        LOG.info("GUI-Gesamttest: Technische Tests ausführen gestartet.");

        Runnable task = () -> {
            TechnicalTestReport report = orchestrator.run(request);
            resultDelivery.accept(() -> {
                applyResult(report);
                postResultCallback.accept(report);
            });
        };

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

    /**
     * Wendet das Ergebnis des vollständigen Gesamttests auf die geteilte Nachrichtenliste an.
     * <p>
     * Fügt für jedes Checkpoint-Ergebnis einen neuen Eintrag zur geteilten Nachrichtenliste
     * hinzu. Die Liste wurde zuvor in {@link #triggerTechnicalTests()} geleert, sodass jeder
     * Aufruf einen frischen Stand erzeugt. Zusätzlich wird eine Zusammenfassung angehängt.
     * <p>
     * Muss nur auf dem JavaFX Application Thread aufgerufen werden (via {@code resultDelivery}).
     *
     * @param report der vollständige Gesamttestbericht; darf nicht {@code null} sein
     */
    private void applyResult(TechnicalTestReport report) {

        long successCount = 0;
        long failureErrorCount = 0;
        long failureWarnCount = 0;
        long notApplicableCount = 0;

        for (CheckpointResult result : report.results()) {
            String label = labelFor(result.checkpointId());
            switch (result) {
                case CheckpointResult.Success success -> {
                    pendingMessages.add(GuiMessageEntry.of(
                            GuiMessageSeverity.INFO,
                            label + ": OK – " + success.message(),
                            SOURCE_TAG));
                    successCount++;
                    LOG.info("GUI-Gesamttest: {} → OK", label);
                }
                case CheckpointResult.Failure failure -> {
                    GuiMessageSeverity severity = failure.severity() ==
                            de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CheckpointSeverity.ERROR
                            ? GuiMessageSeverity.ERROR : GuiMessageSeverity.WARNING;
                    pendingMessages.add(GuiMessageEntry.of(
                            severity,
                            label + ": " + failure.message(),
                            SOURCE_TAG));
                    if (severity == GuiMessageSeverity.ERROR) {
                        failureErrorCount++;
                        LOG.warn("GUI-Gesamttest: {} → FEHLER: {}", label, failure.message());
                    } else {
                        failureWarnCount++;
                        LOG.warn("GUI-Gesamttest: {} → WARNUNG: {}", label, failure.message());
                    }
                }
                case CheckpointResult.NotApplicable notApplicable -> {
                    pendingMessages.add(GuiMessageEntry.of(
                            GuiMessageSeverity.HINT,
                            label + ": nicht anwendbar – " + notApplicable.reason(),
                            SOURCE_TAG));
                    notApplicableCount++;
                    LOG.info("GUI-Gesamttest: {} → nicht anwendbar: {}", label, notApplicable.reason());
                }
            }
        }

        // Zusammenfassung
        long totalFindings = failureErrorCount + failureWarnCount;
        String summary = buildSummaryMessage(report.results().size(),
                successCount, failureErrorCount, failureWarnCount, notApplicableCount);
        pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.INFO, summary, SOURCE_TAG));
        LOG.info("GUI-Gesamttest abgeschlossen. {} Befunde ({} Erfolg, {} Fehler, {} Warnung, {} nicht anwendbar).",
                totalFindings, successCount, failureErrorCount, failureWarnCount, notApplicableCount);
    }

    /**
     * Gibt das deutsche Label für einen Prüfpunkt zurück.
     *
     * @param id der Prüfpunkt-Bezeichner; darf nicht {@code null} sein
     * @return das deutsche Label; nie {@code null}
     */
    static String labelFor(CheckpointId id) {
        return switch (id) {
            case CONFIGURATION_BASIC_VALIDATION -> "Konfiguration grundsätzlich validierbar";
            case PROVIDER_CONFIGURATION -> "Provider-Konfiguration prüfbar";
            case BASE_URL_REACHABLE -> "Basis-URL/Endpoint erreichbar";
            case API_KEY_PRESENT -> "API-Schlüssel vorhanden";
            case API_KEY_ACCEPTED -> "API-Schlüssel technisch akzeptiert";
            case MODEL_LIST_AVAILABLE -> "Modellliste abrufbar";
            case SELECTED_MODEL_PLAUSIBLE -> "Ausgewähltes Modell plausibel";
            case PROMPT_FILE_PRESENT -> "Prompt-Datei vorhanden und lesbar";
            case SOURCE_FOLDER_PRESENT -> "Quellordner vorhanden und lesbar";
            case TARGET_FOLDER_USABLE -> "Zielordner vorhanden oder anlegbar sowie schreibbar";
            case SQLITE_PATH_USABLE -> "SQLite-Pfad technisch nutzbar";
            case LOG_DIRECTORY_USABLE -> "Log-Verzeichnis beschreibbar";
        };
    }

    /**
     * Baut die deutsche Zusammenfassungsmeldung des Gesamttests.
     *
     * @param total            Gesamtzahl der Prüfpunkte
     * @param successCount     Anzahl der erfolgreichen Prüfpunkte
     * @param errorCount       Anzahl der fehlgeschlagenen Prüfpunkte (Schweregrad ERROR)
     * @param warningCount     Anzahl der Warnungs-Prüfpunkte
     * @param notApplicable    Anzahl der nicht-anwendbaren Prüfpunkte
     * @return deutsche Zusammenfassungsmeldung; nie {@code null}
     */
    private static String buildSummaryMessage(long total, long successCount, long errorCount,
                                               long warningCount, long notApplicable) {
        long findings = errorCount + warningCount;
        String base = "Gesamttest abgeschlossen. " + total + " Prüfpunkte: "
                + successCount + " Erfolg, "
                + errorCount + " Fehler, "
                + warningCount + " Warnung, "
                + notApplicable + " nicht anwendbar.";
        if (findings == 0) {
            return base + " Keine Befunde.";
        }
        return base;
    }

    /**
     * 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);
    }
}
