package de.gecheckt.pdf.umbenenner.application.validation.technicaltest;

import java.io.File;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator;
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidationFinding;
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidationInput;
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidationReport;
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidationSeverity;

/**
 * Orchestrator für den vollständigen technischen Gesamttest der GUI-Konfiguration.
 * <p>
 * Führt alle zwölf definierten Prüfpunkte in drei voneinander unabhängigen Blöcken aus:
 * <ol>
 *   <li><strong>Lokale Validierung:</strong> Prüft den Editorzustand ohne I/O mithilfe des
 *       {@link EditorConfigurationValidator}. Erzeugt Ergebnisse für
 *       {@link CheckpointId#CONFIGURATION_BASIC_VALIDATION} und
 *       {@link CheckpointId#PROVIDER_CONFIGURATION}.</li>
 *   <li><strong>Pfadprüfungen:</strong> Prüft Quellordner, Zielordner, Prompt-Datei,
 *       SQLite-Pfad und Log-Verzeichnis über den {@link PathCheckPort} sowie den
 *       {@link LogDiagnosticsPort}. Erzeugt Ergebnisse für
 *       {@link CheckpointId#PROMPT_FILE_PRESENT}, {@link CheckpointId#SOURCE_FOLDER_PRESENT},
 *       {@link CheckpointId#TARGET_FOLDER_USABLE}, {@link CheckpointId#SQLITE_PATH_USABLE}
 *       und {@link CheckpointId#LOG_DIRECTORY_USABLE}.</li>
 *   <li><strong>Provider-Prüfungen:</strong> Prüft Endpoint, API-Key, Modellliste und
 *       Modellplausibilität über den {@link ProviderTechnicalTestService}. Erzeugt Ergebnisse für
 *       {@link CheckpointId#BASE_URL_REACHABLE}, {@link CheckpointId#API_KEY_PRESENT},
 *       {@link CheckpointId#API_KEY_ACCEPTED}, {@link CheckpointId#MODEL_LIST_AVAILABLE}
 *       und {@link CheckpointId#SELECTED_MODEL_PLAUSIBLE}.</li>
 * </ol>
 * <p>
 * <strong>Kein Frühabbruch:</strong> Alle drei Prüfblöcke werden immer vollständig
 * ausgeführt, auch wenn ein Block eine Exception wirft. In diesem Fall werden die
 * betroffenen Checkpoints als {@link CheckpointResult.Failure} mit Schweregrad ERROR
 * und dem Präfix „Interner Fehler:" markiert. Der Gesamtbericht enthält immer genau
 * zwölf Einträge.
 * <p>
 * <strong>Threading-Kontrakt:</strong> Die Methode {@link #run(TechnicalTestRequest)}
 * ist synchron blockierend (der Provider-Prüfblock führt HTTP-Aufrufe durch). Sie darf
 * nicht auf dem JavaFX Application Thread aufgerufen werden. Der Aufrufer ist für die
 * Worker-Thread-Verwaltung und die Rückführung via {@code Platform.runLater} verantwortlich.
 * <p>
 * <strong>Prompt-Datei-Standardpfad:</strong> Wenn der Editorzustand keinen Prompt-Pfad
 * enthält, leitet der Orchestrator einen Standardpfad aus dem Konfigurationsdateipfad ab
 * ({@code <config-parent>/prompt.txt}). Ist auch kein Konfigurationsdateipfad gesetzt,
 * wird {@code config/prompt.txt} relativ zum Arbeitsverzeichnis verwendet.
 * <p>
 * Dieser Service enthält keine JavaFX-Typen, keine NIO-Pfadobjekte in Signaturen und
 * keine Infrastrukturabhängigkeiten jenseits der vier injizierten Abhängigkeiten.
 */
public class TechnicalTestOrchestrator {

    private final EditorConfigurationValidator editorValidator;
    private final PathCheckPort pathCheckPort;
    private final ProviderTechnicalTestService providerTestService;
    private final LogDiagnosticsPort logDiagnosticsPort;

    /**
     * Erstellt einen neuen Orchestrator mit den vier erforderlichen Abhängigkeiten.
     *
     * @param editorValidator     Lokaler Konfigurationsvalidator; darf nicht {@code null} sein
     * @param pathCheckPort       Port für Dateisystem-Pfadprüfungen; darf nicht {@code null} sein
     * @param providerTestService Service für provider-nahe technische Prüfungen; darf nicht {@code null} sein
     * @param logDiagnosticsPort  Port zur Auflösung des aktiven Log-Dateipfads; darf nicht {@code null} sein
     * @throws NullPointerException wenn einer der Parameter {@code null} ist
     */
    public TechnicalTestOrchestrator(EditorConfigurationValidator editorValidator,
                                     PathCheckPort pathCheckPort,
                                     ProviderTechnicalTestService providerTestService,
                                     LogDiagnosticsPort logDiagnosticsPort) {
        this.editorValidator = Objects.requireNonNull(editorValidator, "editorValidator must not be null");
        this.pathCheckPort = Objects.requireNonNull(pathCheckPort, "pathCheckPort must not be null");
        this.providerTestService = Objects.requireNonNull(providerTestService, "providerTestService must not be null");
        this.logDiagnosticsPort = Objects.requireNonNull(logDiagnosticsPort, "logDiagnosticsPort must not be null");
    }

    /**
     * Führt den vollständigen technischen Gesamttest gegen den angegebenen Editorzustand aus.
     * <p>
     * Alle drei Prüfblöcke werden immer vollständig ausgeführt. Ein Fehler in einem Block
     * führt nicht dazu, dass ein anderer Block übersprungen wird. Der zurückgegebene Bericht
     * enthält immer genau zwölf {@link CheckpointResult}-Einträge.
     * <p>
     * <strong>Prompt-Datei-Standardpfad:</strong> Wenn der Editorzustand keinen Prompt-Pfad
     * enthält, wird als Standardpfad der Elternordner der Konfigurationsdatei gewählt
     * (aus {@link TechnicalTestRequest#configFilePath()}), konkret
     * {@code <config-parent>/prompt.txt}. Falls kein Konfigurationsdateipfad gesetzt ist,
     * lautet der Fallback {@code config/prompt.txt} relativ zum Arbeitsverzeichnis.
     * <p>
     * Wenn der Zielpfad der Prompt-Datei nicht beschreibbar ist, wird keine
     * {@link CorrectionSuggestion} erzeugt, sondern eine Failure-Meldung mit dem Hinweis,
     * die Datei manuell anzulegen.
     * <p>
     * <strong>Threading-Kontrakt:</strong> Diese Methode blockiert, bis alle Prüfungen
     * abgeschlossen sind. Sie darf nicht auf dem JavaFX Application Thread aufgerufen werden.
     *
     * @param request Eingabedaten für den Gesamttest; darf nicht {@code null} sein
     * @return vollständiger Gesamttestbericht mit genau zwölf Einträgen; nie {@code null}
     * @throws NullPointerException wenn {@code request} {@code null} ist
     */
    public TechnicalTestReport run(TechnicalTestRequest request) {
        Objects.requireNonNull(request, "request must not be null");
        Instant startTime = Instant.now();
        EditorValidationInput input = request.validationInput();

        List<CheckpointResult> results = new ArrayList<>(12);

        // Block 1: Lokale Konfigurationsvalidierung (kein I/O)
        results.addAll(runLocalValidationBlock(input));

        // Block 2: Pfadprüfungen (Dateisystem-I/O)
        results.addAll(runPathCheckBlock(input, request.configFilePath(), request.logDirectory()));

        // Block 3: Provider-nahe technische Prüfungen (Netzwerk-I/O)
        results.addAll(runProviderCheckBlock(input));

        return new TechnicalTestReport(results, startTime);
    }

    // =========================================================================
    // Block 1: Lokale Konfigurationsvalidierung
    // =========================================================================

    /**
     * Führt die lokale Konfigurationsvalidierung durch und bildet das Ergebnis auf
     * {@link CheckpointId#CONFIGURATION_BASIC_VALIDATION} und
     * {@link CheckpointId#PROVIDER_CONFIGURATION} ab.
     *
     * @param input aktueller Editorzustand
     * @return Liste mit genau zwei Einträgen
     */
    private List<CheckpointResult> runLocalValidationBlock(EditorValidationInput input) {
        try {
            EditorValidationReport report = editorValidator.validate(input);
            return mapLocalValidationToCheckpoints(report);
        } catch (Exception e) {
            String errorMsg = "Interner Fehler bei der lokalen Konfigurationsvalidierung: " + e.getMessage();
            return List.of(
                    CheckpointResult.Failure.of(CheckpointId.CONFIGURATION_BASIC_VALIDATION,
                            CheckpointSeverity.ERROR, errorMsg),
                    CheckpointResult.Failure.of(CheckpointId.PROVIDER_CONFIGURATION,
                            CheckpointSeverity.ERROR, errorMsg)
            );
        }
    }

    /**
     * Bildet den {@link EditorValidationReport} auf die zwei lokalen Prüfpunkte ab.
     * <p>
     * Befunde ohne Feldbezug oder mit allgemeinen Feldbezügen werden
     * {@link CheckpointId#CONFIGURATION_BASIC_VALIDATION} zugeordnet. Provider-spezifische
     * Feldbefunde werden {@link CheckpointId#PROVIDER_CONFIGURATION} zugeordnet.
     *
     * @param report Validierungsergebnis
     * @return Liste mit genau zwei Einträgen
     */
    private static List<CheckpointResult> mapLocalValidationToCheckpoints(EditorValidationReport report) {
        // Trennen: allgemeine Befunde vs. provider-spezifische Befunde
        List<EditorValidationFinding> generalFindings = report.findings().stream()
                .filter(f -> !isProviderSpecificField(f.fieldKey().orElse("")))
                .toList();
        List<EditorValidationFinding> providerFindings = report.findings().stream()
                .filter(f -> isProviderSpecificField(f.fieldKey().orElse("")))
                .toList();

        CheckpointResult basicValidation = buildCheckpointFromFindings(
                CheckpointId.CONFIGURATION_BASIC_VALIDATION,
                generalFindings,
                "Konfiguration grundsätzlich gültig.");

        CheckpointResult providerValidation = buildCheckpointFromFindings(
                CheckpointId.PROVIDER_CONFIGURATION,
                providerFindings,
                "Provider-Konfiguration vollständig.");

        return List.of(basicValidation, providerValidation);
    }

    /**
     * Prüft, ob ein Feldschlüssel zu einem provider-spezifischen Feld gehört.
     *
     * @param fieldKey Property-Schlüssel
     * @return {@code true} wenn es ein provider-spezifisches Feld ist
     */
    private static boolean isProviderSpecificField(String fieldKey) {
        return fieldKey.startsWith("ai.provider.claude.")
                || fieldKey.startsWith("ai.provider.openai-compatible.");
    }

    /**
     * Erzeugt ein {@link CheckpointResult} aus einer Liste von Befunden.
     *
     * @param id             Prüfpunkt-ID
     * @param findings       Liste der relevanten Befunde
     * @param successMessage Meldung bei leerem Befund-Ergebnis
     * @return Success bei leerer Befund-Liste, Failure andernfalls
     */
    private static CheckpointResult buildCheckpointFromFindings(CheckpointId id,
                                                                  List<EditorValidationFinding> findings,
                                                                  String successMessage) {
        if (findings.isEmpty()) {
            return new CheckpointResult.Success(id, successMessage);
        }

        // Höchsten Schweregrad bestimmen
        boolean hasError = findings.stream()
                .anyMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
        CheckpointSeverity severity = hasError ? CheckpointSeverity.ERROR : CheckpointSeverity.WARNING;

        // Befunde zusammenfassen
        String summary = findings.size() == 1
                ? findings.get(0).message()
                : findings.size() + " Befunde: " + findings.get(0).message()
                  + (findings.size() > 1 ? " (und " + (findings.size() - 1) + " weitere)" : "");

        return CheckpointResult.Failure.of(id, severity, summary);
    }

    // =========================================================================
    // Block 2: Pfadprüfungen
    // =========================================================================

    /**
     * Führt die Dateisystem-Pfadprüfungen für Prompt-Datei, Quellordner, Zielordner,
     * SQLite-Pfad und Log-Verzeichnis durch.
     * <p>
     * Der {@code configFilePath} wird genutzt, um bei fehlendem Prompt-Pfad im Editorzustand
     * einen sinnvollen Standardpfad zu bestimmen ({@code <config-parent>/prompt.txt}).
     * Der {@code logDirectory} ist der konfigurierte Rohwert von {@code log.directory};
     * leer bedeutet Standardwert {@code ./logs/}.
     *
     * @param input          aktueller Editorzustand
     * @param configFilePath Pfad der geladenen Konfigurationsdatei; leer wenn keine geladen
     * @param logDirectory   konfigurierter Rohwert von {@code log.directory}; leer = Standard
     * @return Liste mit genau fünf Einträgen
     */
    private List<CheckpointResult> runPathCheckBlock(EditorValidationInput input,
                                                     String configFilePath,
                                                     String logDirectory) {
        try {
            List<CheckpointResult> results = new ArrayList<>(5);
            results.add(checkPromptFile(input.promptTemplateFile(), configFilePath,
                    resolveMaxTitleLengthForPromptCreation(input.maxTitleLength())));
            results.add(checkSourceFolder(input.sourceFolder()));
            results.add(checkTargetFolder(input.targetFolder()));
            results.add(checkSqlitePath(input.sqliteFile()));
            results.add(checkLogDirectory(logDirectory));
            return results;
        } catch (Exception e) {
            String errorMsg = "Interner Fehler bei den Pfadprüfungen: " + e.getMessage();
            return List.of(
                    CheckpointResult.Failure.of(CheckpointId.PROMPT_FILE_PRESENT,
                            CheckpointSeverity.ERROR, errorMsg),
                    CheckpointResult.Failure.of(CheckpointId.SOURCE_FOLDER_PRESENT,
                            CheckpointSeverity.ERROR, errorMsg),
                    CheckpointResult.Failure.of(CheckpointId.TARGET_FOLDER_USABLE,
                            CheckpointSeverity.ERROR, errorMsg),
                    CheckpointResult.Failure.of(CheckpointId.SQLITE_PATH_USABLE,
                            CheckpointSeverity.ERROR, errorMsg),
                    CheckpointResult.Failure.of(CheckpointId.LOG_DIRECTORY_USABLE,
                            CheckpointSeverity.ERROR, errorMsg)
            );
        }
    }

    /**
     * Prüft die Prompt-Datei auf Vorhandensein und Lesbarkeit.
     * <p>
     * <strong>Pfad-Auflösung:</strong> Wenn der konfigurierte Prompt-Pfad leer ist,
     * wird ein Standardpfad bestimmt:
     * <ul>
     *   <li>Wenn {@code configFilePath} gesetzt ist: {@code <configFilePath-Elternordner>/prompt.txt}</li>
     *   <li>Sonst: {@code config/prompt.txt} relativ zum Arbeitsverzeichnis</li>
     * </ul>
     * <p>
     * <strong>Schreibbarkeits-Prüfung:</strong> Wenn der Zielpfad fehlt, wird geprüft, ob der
     * Elternordner beschreibbar wäre. Nur dann wird eine {@link CorrectionSuggestion.CreatePromptFile}
     * angeboten. Ist der Elternordner nicht beschreibbar, wird eine Failure ohne Korrekturvorschlag
     * zurückgegeben, aber mit einem Hinweis, die Datei manuell anzulegen.
     *
     * @param configuredPath   konfigurierter Prompt-Pfad aus dem Editorzustand; kann leer sein
     * @param configFilePath   Pfad der geladenen Konfigurationsdatei; leer wenn keine geladen
     * @param maxTitleLength   konfigurierte maximale Titellänge für den Standardinhalt der
     *                         Prompt-Datei; muss {@code >= 1} sein
     * @return Prüfpunkt-Ergebnis
     */
    private CheckpointResult checkPromptFile(String configuredPath, String configFilePath,
                                             int maxTitleLength) {
        // Effektiven Prompt-Pfad bestimmen
        String effectivePath = resolvePromptPath(configuredPath, configFilePath);

        if (pathCheckPort.isFileReadable(effectivePath)) {
            return new CheckpointResult.Success(CheckpointId.PROMPT_FILE_PRESENT,
                    "Prompt-Datei vorhanden und lesbar: " + effectivePath);
        }

        // Datei fehlt – Elternordner auf Beschreibbarkeit prüfen
        String parentPath = extractParentPath(effectivePath);
        boolean parentWritable = !parentPath.isBlank()
                && pathCheckPort.isDirectoryWritableOrCreatable(parentPath);

        if (parentWritable) {
            // Elternordner beschreibbar → Korrekturvorschlag anbieten
            CorrectionSuggestion suggestion = new CorrectionSuggestion.CreatePromptFile(
                    effectivePath, "Prompt-Datei anlegen: " + effectivePath,
                    maxTitleLength);
            return CheckpointResult.Failure.withCorrection(
                    CheckpointId.PROMPT_FILE_PRESENT,
                    CheckpointSeverity.ERROR,
                    "Prompt-Datei nicht vorhanden oder nicht lesbar: " + effectivePath,
                    suggestion);
        } else {
            // Elternordner nicht beschreibbar → kein Korrekturvorschlag, nur Hinweis
            return CheckpointResult.Failure.of(
                    CheckpointId.PROMPT_FILE_PRESENT,
                    CheckpointSeverity.ERROR,
                    "Prompt-Datei fehlt und kann nicht automatisch erzeugt werden. "
                            + "Bitte manuell anlegen: " + effectivePath);
        }
    }

    /**
     * Ermittelt aus dem Editor-Rohwert einen für die Prompt-Erzeugung nutzbaren Wert für
     * die maximale Titellänge.
     * <p>
     * Diese Methode ist bewusst tolerant: die eigentliche Validierung des Editorwerts
     * findet im lokalen Validierungsblock statt und erzeugt dort gegebenenfalls eigene
     * Befunde. Für die Standard-Prompt-Erzeugung wird ein plausibler Wert ausgewählt:
     * <ul>
     *   <li>Wenn der Rohwert eine positive Ganzzahl ist, wird dieser Wert verwendet.</li>
     *   <li>Andernfalls (leer, nicht parsebar, oder {@code < 1}) wird der projektweite
     *       Fallback-Wert {@value #DEFAULT_MAX_TITLE_LENGTH_FOR_PROMPT} verwendet, der
     *       dem historischen Standardwert entspricht.</li>
     * </ul>
     *
     * @param rawValue Rohwert aus {@link EditorValidationInput#maxTitleLength()}; nie {@code null}
     * @return zu verwendender Wert für die Prompt-Standardinhalt-Erzeugung; immer {@code >= 1}
     */
    private static int resolveMaxTitleLengthForPromptCreation(String rawValue) {
        if (rawValue == null || rawValue.isBlank()) {
            return DEFAULT_MAX_TITLE_LENGTH_FOR_PROMPT;
        }
        try {
            int parsed = Integer.parseInt(rawValue.strip());
            return parsed >= 1 ? parsed : DEFAULT_MAX_TITLE_LENGTH_FOR_PROMPT;
        } catch (NumberFormatException e) {
            return DEFAULT_MAX_TITLE_LENGTH_FOR_PROMPT;
        }
    }

    /**
     * Projektweiter Fallback-Wert für die maximale Titellänge bei der Prompt-Standardinhalt-
     * Erzeugung, identisch mit dem Default bei fehlendem Property in der Konfigurationsdatei.
     */
    private static final int DEFAULT_MAX_TITLE_LENGTH_FOR_PROMPT = 60;

    /**
     * Bestimmt den effektiven Prompt-Pfad aus dem konfigurierten Pfad und dem Konfigurationsdateipfad.
     * <p>
     * Wenn der konfigurierte Pfad nicht leer ist, wird dieser unverändert zurückgegeben.
     * Andernfalls wird ein Standardpfad aus dem Konfigurationsdateipfad abgeleitet:
     * {@code <configFilePath-Elternordner>/prompt.txt}. Falls auch der Konfigurationsdateipfad
     * leer ist, lautet der Fallback {@code config/prompt.txt}.
     *
     * @param configuredPath konfigurierter Prompt-Pfad; kann leer sein
     * @param configFilePath Pfad der geladenen Konfigurationsdatei; kann leer sein
     * @return effektiver Prompt-Pfad; nie {@code null}, nie leer
     */
    static String resolvePromptPath(String configuredPath, String configFilePath) {
        if (!configuredPath.isBlank()) {
            return configuredPath;
        }
        // Standardpfad aus dem Konfigurationsdatei-Elternordner ableiten
        if (!configFilePath.isBlank()) {
            String parent = extractParentPath(configFilePath);
            if (!parent.isBlank()) {
                return parent + File.separator + "prompt.txt";
            }
        }
        // Absoluter Fallback
        return "config" + File.separator + "prompt.txt";
    }

    /**
     * Extrahiert den Elternpfad aus einem Dateipfad.
     * <p>
     * Gibt eine leere Zeichenkette zurück, wenn kein Elternpfad bestimmbar ist.
     *
     * @param filePath Dateipfad als String
     * @return Elternpfad oder leere Zeichenkette
     */
    private static String extractParentPath(String filePath) {
        if (filePath == null || filePath.isBlank()) {
            return "";
        }
        try {
            java.nio.file.Path path = Paths.get(filePath);
            java.nio.file.Path parent = path.getParent();
            return parent != null ? parent.toString() : "";
        } catch (InvalidPathException e) {
            return "";
        }
    }

    /**
     * Prüft den Quellordner auf Vorhandensein und Lesbarkeit.
     *
     * @param path Pfad des Quellordners
     * @return Prüfpunkt-Ergebnis
     */
    private CheckpointResult checkSourceFolder(String path) {
        if (path.isBlank()) {
            return CheckpointResult.Failure.of(CheckpointId.SOURCE_FOLDER_PRESENT,
                    CheckpointSeverity.ERROR, "Quellordner: Kein Pfad konfiguriert.");
        }
        if (pathCheckPort.isDirectoryReadable(path)) {
            return new CheckpointResult.Success(CheckpointId.SOURCE_FOLDER_PRESENT,
                    "Quellordner vorhanden und lesbar: " + path);
        }
        return CheckpointResult.Failure.of(CheckpointId.SOURCE_FOLDER_PRESENT,
                CheckpointSeverity.ERROR,
                "Quellordner nicht vorhanden oder nicht lesbar: " + path);
    }

    /**
     * Prüft den Zielordner auf Vorhandensein oder Anlegbarkeit und Schreibbarkeit.
     * Bietet eine {@link CorrectionSuggestion.CreateDirectory} an, wenn der Ordner
     * fehlt, aber anlegbar wäre.
     *
     * @param path Pfad des Zielordners
     * @return Prüfpunkt-Ergebnis
     */
    private CheckpointResult checkTargetFolder(String path) {
        if (path.isBlank()) {
            return CheckpointResult.Failure.of(CheckpointId.TARGET_FOLDER_USABLE,
                    CheckpointSeverity.ERROR, "Zielordner: Kein Pfad konfiguriert.");
        }
        if (pathCheckPort.isDirectoryWritableOrCreatable(path)) {
            return new CheckpointResult.Success(CheckpointId.TARGET_FOLDER_USABLE,
                    "Zielordner vorhanden/anlegbar und schreibbar: " + path);
        }
        // Ordner ist weder vorhanden/schreibbar noch anlegbar
        // Wenn der Ordner fehlt, könnte isDirectoryWritableOrCreatable false liefern weil
        // auch der Elternpfad fehlt. Trotzdem einen Korrekturvorschlag anbieten.
        CorrectionSuggestion suggestion = new CorrectionSuggestion.CreateDirectory(
                path, "Zielordner anlegen: " + path);
        return CheckpointResult.Failure.withCorrection(
                CheckpointId.TARGET_FOLDER_USABLE,
                CheckpointSeverity.ERROR,
                "Zielordner nicht vorhanden oder nicht schreibbar: " + path,
                suggestion);
    }

    /**
     * Prüft, ob der SQLite-Pfad technisch nutzbar ist.
     * Bietet eine {@link CorrectionSuggestion.PrepareSqlitePath} an, wenn der Pfad
     * noch nicht nutzbar, aber vorbereitbar wäre.
     *
     * @param path Pfad der SQLite-Datei
     * @return Prüfpunkt-Ergebnis
     */
    private CheckpointResult checkSqlitePath(String path) {
        if (path.isBlank()) {
            return CheckpointResult.Failure.of(CheckpointId.SQLITE_PATH_USABLE,
                    CheckpointSeverity.ERROR, "SQLite-Pfad: Kein Pfad konfiguriert.");
        }
        if (pathCheckPort.isSqlitePathUsable(path)) {
            return new CheckpointResult.Success(CheckpointId.SQLITE_PATH_USABLE,
                    "SQLite-Pfad technisch nutzbar: " + path);
        }
        CorrectionSuggestion suggestion = new CorrectionSuggestion.PrepareSqlitePath(
                path, "SQLite-Pfad vorbereiten: " + path);
        return CheckpointResult.Failure.withCorrection(
                CheckpointId.SQLITE_PATH_USABLE,
                CheckpointSeverity.ERROR,
                "SQLite-Pfad nicht nutzbar: " + path,
                suggestion);
    }

    /**
     * Prüft das Log-Verzeichnis auf Schreibbarkeit und zeigt den tatsächlichen
     * Log-Dateipfad aus der aktiven Log4j2-Konfiguration an.
     * <p>
     * <strong>Verzeichnis-Ermittlung:</strong> Wenn der konfigurierte {@code log.directory}-Wert
     * leer ist, wird der Standard {@code ./logs} relativ zum Arbeitsverzeichnis angenommen.
     * Der Wert wird zu einem absoluten Pfad aufgelöst.
     * <p>
     * <strong>Ergebnis:</strong>
     * <ul>
     *   <li>{@link CheckpointResult.Success}: Verzeichnis ist vorhanden und schreibbar.
     *       Die Meldung enthält den aufgelösten absoluten Pfad sowie – sofern ermittelbar –
     *       den tatsächlichen Log-Dateipfad aus Log4j2.</li>
     *   <li>{@link CheckpointResult.Failure} mit Schweregrad {@link CheckpointSeverity#WARNING}:
     *       Verzeichnis ist nicht vorhanden oder nicht schreibbar. Ein nicht beschreibbares
     *       Log-Verzeichnis ist eine Warnung, kein harter Fehler, da die Anwendung auch ohne
     *       Datei-Logging lauffähig ist. Die Meldung enthält Konfiguration und aufgelösten
     *       absoluten Pfad als Diagnoseinformation.</li>
     * </ul>
     *
     * @param configuredLogDir konfigurierter Rohwert von {@code log.directory}; leer = Standard
     * @return Prüfpunkt-Ergebnis
     */
    private CheckpointResult checkLogDirectory(String configuredLogDir) {
        String effectiveDir = (configuredLogDir == null || configuredLogDir.isBlank())
                ? "./logs" : configuredLogDir;

        String absolutePath;
        try {
            absolutePath = Paths.get(effectiveDir).toAbsolutePath().toString();
        } catch (java.nio.file.InvalidPathException e) {
            return CheckpointResult.Failure.of(CheckpointId.LOG_DIRECTORY_USABLE,
                    CheckpointSeverity.WARNING,
                    "Log-Verzeichnis: ungültiger Pfad: " + effectiveDir);
        }

        boolean configuredExplicitly = configuredLogDir != null && !configuredLogDir.isBlank();
        String configLabel = configuredExplicitly
                ? "konfiguriert: " + configuredLogDir + " → "
                : "Standard → ";

        java.util.Optional<String> activeLogFile = logDiagnosticsPort.resolveActiveLogFilePath();
        String logFileInfo = activeLogFile
                .map(p -> " | Aktive Log-Datei: " + p)
                .orElse("");

        if (pathCheckPort.isDirectoryWritableOrCreatable(absolutePath)) {
            return new CheckpointResult.Success(CheckpointId.LOG_DIRECTORY_USABLE,
                    "Log-Verzeichnis beschreibbar (" + configLabel + absolutePath + ")" + logFileInfo);
        }

        return CheckpointResult.Failure.of(CheckpointId.LOG_DIRECTORY_USABLE,
                CheckpointSeverity.WARNING,
                "Log-Verzeichnis nicht beschreibbar (" + configLabel + absolutePath + ")"
                        + logFileInfo
                        + ". Tipp: Absoluten Pfad mit Forward-Slashes verwenden, z. B. C:/Benutzer/Logs");
    }

    // =========================================================================
    // Block 3: Provider-nahe technische Prüfungen
    // =========================================================================

    /**
     * Führt die provider-nahen technischen Prüfungen über den {@link ProviderTechnicalTestService} aus.
     * <p>
     * Der Service liefert genau fünf Ergebnisse in der Reihenfolge:
     * API_KEY_PRESENT, BASE_URL_REACHABLE, API_KEY_ACCEPTED, MODEL_LIST_AVAILABLE, SELECTED_MODEL_PLAUSIBLE.
     *
     * @param input aktueller Editorzustand
     * @return Liste mit genau fünf Einträgen
     */
    private List<CheckpointResult> runProviderCheckBlock(EditorValidationInput input) {
        try {
            return providerTestService.runProviderChecks(input);
        } catch (Exception e) {
            String errorMsg = "Interner Fehler bei den Provider-Prüfungen: " + e.getMessage();
            return List.of(
                    CheckpointResult.Failure.of(CheckpointId.API_KEY_PRESENT,
                            CheckpointSeverity.ERROR, errorMsg),
                    CheckpointResult.Failure.of(CheckpointId.BASE_URL_REACHABLE,
                            CheckpointSeverity.ERROR, errorMsg),
                    CheckpointResult.Failure.of(CheckpointId.API_KEY_ACCEPTED,
                            CheckpointSeverity.ERROR, errorMsg),
                    CheckpointResult.Failure.of(CheckpointId.MODEL_LIST_AVAILABLE,
                            CheckpointSeverity.ERROR, errorMsg),
                    CheckpointResult.Failure.of(CheckpointId.SELECTED_MODEL_PLAUSIBLE,
                            CheckpointSeverity.ERROR, errorMsg)
            );
        }
    }
}
