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

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.AiModelCatalogPort;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ApiKeyOrigin;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogRequest;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult;
import de.gecheckt.pdf.umbenenner.application.validation.editor.ApiKeyResolutionPort;
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidationInput;

/**
 * Application-Service für die provider-nahen technischen Prüfpunkte des Gesamttests.
 * <p>
 * Dieser Service führt genau fünf providerbezogene Prüfpunkte aus:
 * <ul>
 *   <li>{@link CheckpointId#BASE_URL_REACHABLE} – Endpoint technisch erreichbar</li>
 *   <li>{@link CheckpointId#API_KEY_PRESENT} – API-Schlüssel in mindestens einer Quelle vorhanden</li>
 *   <li>{@link CheckpointId#API_KEY_ACCEPTED} – Authentifizierung am Endpoint erfolgreich</li>
 *   <li>{@link CheckpointId#MODEL_LIST_AVAILABLE} – Provider liefert eine Modellliste</li>
 *   <li>{@link CheckpointId#SELECTED_MODEL_PLAUSIBLE} – konfiguriertes Modell in der Liste enthalten</li>
 * </ul>
 * <p>
 * <strong>Port-Wiederverwendung:</strong> Der Service ruft den {@link AiModelCatalogPort}
 * exakt einmal auf und leitet aus dem Ergebnis alle fünf Prüfpunkte ab. Es findet keine
 * zweite HTTP-Implementierung statt.
 * <p>
 * <strong>API-Key-Vorrangregel:</strong> Der {@link ApiKeyResolutionPort} wird konsultiert,
 * damit auch reine Umgebungsvariablen-Setups korrekt als „API-Key vorhanden" bewertet werden.
 * Nur wenn der Deskriptor {@link ApiKeyOrigin#ABSENT} zurückliefert, gilt der Schlüssel als
 * fehlend. In diesem Fall werden alle Remote-Prüfpunkte als {@link CheckpointResult.NotApplicable}
 * markiert, ohne einen HTTP-Aufruf durchzuführen.
 * <p>
 * <strong>Mapping-Regeln für {@link ModelCatalogResult}-Varianten:</strong>
 * <ul>
 *   <li>{@link ModelCatalogResult.Success}: alle fünf Prüfpunkte auswertbar; Modellplausibilität
 *       anhand der zurückgegebenen Liste geprüft.</li>
 *   <li>{@link ModelCatalogResult.EmptyList}: Endpoint und Key akzeptiert, aber keine Modellliste;
 *       Modellplausibilität nicht prüfbar.</li>
 *   <li>{@link ModelCatalogResult.IncompleteConfiguration}: Konfiguration unvollständig; kein
 *       HTTP-Aufruf vom Adapter durchgeführt.</li>
 *   <li>{@link ModelCatalogResult.TechnicalFailure}: abhängig vom Fehlerkategorie-String;
 *       Authentifizierungsfehler, Verbindungsfehler, Serverfehler und ungültige Antworten
 *       werden unterschiedlich auf die Prüfpunkte abgebildet.</li>
 * </ul>
 * <p>
 * <strong>Threading-Kontrakt:</strong> Die Methode {@link #runProviderChecks(EditorValidationInput)}
 * ist <em>synchron blockierend</em>. Sie darf nicht auf dem JavaFX Application Thread aufgerufen
 * werden. Der Aufrufer (GUI-Orchestrierung) ist verantwortlich, den Aufruf auf einem
 * Hintergrund-Worker-Thread auszuführen und die Ergebnisse via {@code Platform.runLater}
 * in die UI zu überführen. Dieser Service enthält kein {@code Platform.runLater} und
 * startet keine eigenen Threads.
 * <p>
 * <strong>Fehlerklasse-Konstanten (TechnicalFailure.errorCategory):</strong>
 * Die Adapter-Out-Implementierungen verwenden stabile Kategorie-Strings. Dieser Service
 * erkennt folgende Präfixe bzw. Werte (Groß-/Kleinschreibung ignoriert):
 * {@code AUTHENTICATION_FAILED}, {@code CONNECTION_FAILURE}, {@code ENDPOINT_NOT_FOUND},
 * {@code SERVER_ERROR}, {@code INVALID_RESPONSE}. Unbekannte Kategorien werden als
 * allgemeiner technischer Fehler behandelt.
 */
public class ProviderTechnicalTestService {

    /** Fehlerkategorie-Konstante für Authentifizierungsfehler (case-insensitive Präfix-Erkennung). */
    static final String CATEGORY_AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED";
    /** Fehlerkategorie-Konstante für Verbindungsfehler. */
    static final String CATEGORY_CONNECTION_FAILURE = "CONNECTION_FAILURE";
    /** Fehlerkategorie-Konstante für nicht gefundenen Endpoint. */
    static final String CATEGORY_ENDPOINT_NOT_FOUND = "ENDPOINT_NOT_FOUND";
    /** Fehlerkategorie-Konstante für Serverfehler (5xx). */
    static final String CATEGORY_SERVER_ERROR = "SERVER_ERROR";
    /** Fehlerkategorie-Konstante für nicht parsierbare Antworten. */
    static final String CATEGORY_INVALID_RESPONSE = "INVALID_RESPONSE";

    private static final int DEFAULT_TIMEOUT_SECONDS = 30;

    private final AiModelCatalogPort modelCatalogPort;
    private final ApiKeyResolutionPort apiKeyResolutionPort;

    /**
     * Erstellt einen neuen Service mit den erforderlichen Ports.
     *
     * @param modelCatalogPort     Port für den Modellabruf; darf nicht {@code null} sein
     * @param apiKeyResolutionPort Port für die API-Key-Herkunftsauflösung; darf nicht {@code null} sein
     * @throws NullPointerException wenn einer der Parameter {@code null} ist
     */
    public ProviderTechnicalTestService(AiModelCatalogPort modelCatalogPort,
                                        ApiKeyResolutionPort apiKeyResolutionPort) {
        this.modelCatalogPort = Objects.requireNonNull(modelCatalogPort, "modelCatalogPort must not be null");
        this.apiKeyResolutionPort = Objects.requireNonNull(apiKeyResolutionPort,
                "apiKeyResolutionPort must not be null");
    }

    /**
     * Führt alle fünf provider-nahen technischen Prüfpunkte für den aktiven Provider aus.
     * <p>
     * Der aktive Provider wird aus {@code input.activeProviderIdentifier()} bestimmt.
     * Wenn der Bezeichner keiner bekannten Provider-Familie entspricht, werden alle fünf
     * Prüfpunkte als {@link CheckpointResult.Failure} mit Schweregrad ERROR zurückgegeben.
     * <p>
     * Diese Methode blockiert, bis das Ergebnis des Modellabrufs vorliegt oder ein
     * konfigurierter Timeout abläuft. Sie darf nicht auf dem JavaFX Application Thread
     * aufgerufen werden.
     *
     * @param input aktueller Editorzustand; darf nicht {@code null} sein
     * @return unveränderliche Liste mit genau fünf {@link CheckpointResult}-Einträgen
     *         (in der Reihenfolge: API_KEY_PRESENT, BASE_URL_REACHABLE, API_KEY_ACCEPTED,
     *         MODEL_LIST_AVAILABLE, SELECTED_MODEL_PLAUSIBLE); nie {@code null}
     * @throws NullPointerException wenn {@code input} {@code null} ist
     */
    public List<CheckpointResult> runProviderChecks(EditorValidationInput input) {
        Objects.requireNonNull(input, "input must not be null");

        Optional<AiProviderFamily> familyOpt = AiProviderFamily.fromIdentifier(
                input.activeProviderIdentifier());

        if (familyOpt.isEmpty()) {
            String msg = "Aktiver Provider-Bezeichner unbekannt: \""
                    + input.activeProviderIdentifier() + "\". Provider-Prüfungen können nicht ausgeführt werden.";
            return List.of(
                    CheckpointResult.Failure.of(CheckpointId.API_KEY_PRESENT, CheckpointSeverity.ERROR, msg),
                    CheckpointResult.Failure.of(CheckpointId.BASE_URL_REACHABLE, CheckpointSeverity.ERROR, msg),
                    CheckpointResult.Failure.of(CheckpointId.API_KEY_ACCEPTED, CheckpointSeverity.ERROR, msg),
                    CheckpointResult.Failure.of(CheckpointId.MODEL_LIST_AVAILABLE, CheckpointSeverity.ERROR, msg),
                    CheckpointResult.Failure.of(CheckpointId.SELECTED_MODEL_PLAUSIBLE, CheckpointSeverity.ERROR, msg)
            );
        }

        AiProviderFamily family = familyOpt.get();
        // Den bereits im EditorValidationInput enthaltenen Descriptor verwenden.
        // EditorValidationInput enthält keinen rohen API-Key-String, sondern nur den
        // vom GUI-Adapter bereits aufgelösten Descriptor. Dieser spiegelt die
        // API-Key-Vorrangregel (ENV → Legacy-ENV → Property) wider.
        EffectiveApiKeyDescriptor apiKeyDescriptor = resolveApiKeyDescriptor(input, family);

        // Prüfpunkt API_KEY_PRESENT: ohne HTTP-Aufruf
        CheckpointResult apiKeyPresentResult = checkApiKeyPresent(apiKeyDescriptor);

        if (apiKeyDescriptor.isAbsent()) {
            // Kein API-Key → alle Remote-Prüfpunkte als NotApplicable markieren
            String reason = "Kein API-Schlüssel vorhanden. Remote-Prüfungen können nicht ausgeführt werden.";
            return List.of(
                    apiKeyPresentResult,
                    new CheckpointResult.NotApplicable(CheckpointId.BASE_URL_REACHABLE, reason),
                    new CheckpointResult.NotApplicable(CheckpointId.API_KEY_ACCEPTED, reason),
                    new CheckpointResult.NotApplicable(CheckpointId.MODEL_LIST_AVAILABLE, reason),
                    new CheckpointResult.NotApplicable(CheckpointId.SELECTED_MODEL_PLAUSIBLE, reason)
            );
        }

        // API-Key vorhanden → Modellabruf durchführen
        String configuredModel = resolveModelValue(input, family);
        ModelCatalogRequest catalogRequest = buildCatalogRequest(input, family, apiKeyDescriptor);

        ModelCatalogResult catalogResult = modelCatalogPort.fetchAvailableModels(catalogRequest);

        List<CheckpointResult> results = new ArrayList<>();
        results.add(apiKeyPresentResult);
        results.addAll(mapCatalogResultToCheckpoints(catalogResult, configuredModel));
        return List.copyOf(results);
    }

    // ------------------------------------------------------------------ helpers

    /**
     * Erzeugt das {@link CheckpointResult} für {@link CheckpointId#API_KEY_PRESENT}.
     *
     * @param descriptor Herkunftsdeskriptor des API-Schlüssels
     * @return Success wenn ein Schlüssel vorhanden ist, Failure ERROR sonst
     */
    private CheckpointResult checkApiKeyPresent(EffectiveApiKeyDescriptor descriptor) {
        if (descriptor.isAbsent()) {
            return CheckpointResult.Failure.of(
                    CheckpointId.API_KEY_PRESENT,
                    CheckpointSeverity.ERROR,
                    "Kein API-Schlüssel vorhanden. Weder Umgebungsvariable noch Properties-Datei liefert einen Wert.");
        }
        String sourceInfo = descriptor.isFromEnvironmentVariable()
                ? "Umgebungsvariable " + descriptor.envVarName().orElse("(unbekannt)")
                : "Properties-Datei";
        return new CheckpointResult.Success(
                CheckpointId.API_KEY_PRESENT,
                "API-Schlüssel vorhanden (Quelle: " + sourceInfo + ").");
    }

    /**
     * Bildet ein {@link ModelCatalogResult} auf die vier Remote-Prüfpunkte ab.
     *
     * @param result          Ergebnis des Modellabrufs
     * @param configuredModel konfigurierter Modellname aus dem Editor
     * @return Liste mit genau vier Prüfpunkt-Ergebnissen
     */
    private List<CheckpointResult> mapCatalogResultToCheckpoints(ModelCatalogResult result,
                                                                  String configuredModel) {
        return switch (result) {
            case ModelCatalogResult.Success success -> mapSuccess(success, configuredModel);
            case ModelCatalogResult.EmptyList emptyList -> mapEmptyList();
            case ModelCatalogResult.IncompleteConfiguration incomplete -> mapIncompleteConfiguration(incomplete);
            case ModelCatalogResult.TechnicalFailure failure -> mapTechnicalFailure(failure);
        };
    }

    private List<CheckpointResult> mapSuccess(ModelCatalogResult.Success success, String configuredModel) {
        CheckpointResult baseUrl = new CheckpointResult.Success(
                CheckpointId.BASE_URL_REACHABLE, "Endpoint erreichbar.");
        CheckpointResult apiKeyAccepted = new CheckpointResult.Success(
                CheckpointId.API_KEY_ACCEPTED, "API-Schlüssel akzeptiert.");
        CheckpointResult modelList = new CheckpointResult.Success(
                CheckpointId.MODEL_LIST_AVAILABLE,
                "Modellliste verfügbar (" + success.models().size() + " Modell(e)).");
        CheckpointResult modelPlausible = checkModelPlausible(success.models(), configuredModel);
        return List.of(baseUrl, apiKeyAccepted, modelList, modelPlausible);
    }

    private List<CheckpointResult> mapEmptyList() {
        CheckpointResult baseUrl = new CheckpointResult.Success(
                CheckpointId.BASE_URL_REACHABLE, "Endpoint erreichbar.");
        CheckpointResult apiKeyAccepted = new CheckpointResult.Success(
                CheckpointId.API_KEY_ACCEPTED, "API-Schlüssel akzeptiert.");
        CheckpointResult modelList = CheckpointResult.Failure.of(
                CheckpointId.MODEL_LIST_AVAILABLE,
                CheckpointSeverity.WARNING,
                "Provider liefert keine Modellliste.");
        CheckpointResult modelPlausible = new CheckpointResult.NotApplicable(
                CheckpointId.SELECTED_MODEL_PLAUSIBLE,
                "Keine Modellliste vorhanden, Modellplausibilität nicht prüfbar.");
        return List.of(baseUrl, apiKeyAccepted, modelList, modelPlausible);
    }

    private List<CheckpointResult> mapIncompleteConfiguration(ModelCatalogResult.IncompleteConfiguration incomplete) {
        String reason = incomplete.missingReason();
        CheckpointResult baseUrl = new CheckpointResult.NotApplicable(
                CheckpointId.BASE_URL_REACHABLE,
                "Konfiguration unvollständig – kein Verbindungsversuch: " + reason);
        CheckpointResult apiKeyAccepted = new CheckpointResult.NotApplicable(
                CheckpointId.API_KEY_ACCEPTED,
                "Konfiguration unvollständig – Authentifizierung nicht prüfbar.");
        CheckpointResult modelList = CheckpointResult.Failure.of(
                CheckpointId.MODEL_LIST_AVAILABLE,
                CheckpointSeverity.ERROR,
                "Provider-Konfiguration unvollständig: " + reason);
        CheckpointResult modelPlausible = new CheckpointResult.NotApplicable(
                CheckpointId.SELECTED_MODEL_PLAUSIBLE,
                "Konfiguration unvollständig – Modellplausibilität nicht prüfbar.");
        return List.of(baseUrl, apiKeyAccepted, modelList, modelPlausible);
    }

    private List<CheckpointResult> mapTechnicalFailure(ModelCatalogResult.TechnicalFailure failure) {
        String category = failure.errorCategory().toUpperCase();
        String detail = failure.errorDetail();

        if (category.contains(CATEGORY_AUTHENTICATION_FAILED)) {
            return List.of(
                    new CheckpointResult.Success(
                            CheckpointId.BASE_URL_REACHABLE,
                            "Endpoint hat geantwortet (Authentifizierungsfehler erhalten)."),
                    CheckpointResult.Failure.of(
                            CheckpointId.API_KEY_ACCEPTED,
                            CheckpointSeverity.ERROR,
                            "API-Schlüssel technisch nicht akzeptiert: " + detail),
                    new CheckpointResult.NotApplicable(
                            CheckpointId.MODEL_LIST_AVAILABLE,
                            "Authentifizierung fehlgeschlagen – Modellliste nicht abrufbar."),
                    new CheckpointResult.NotApplicable(
                            CheckpointId.SELECTED_MODEL_PLAUSIBLE,
                            "Authentifizierung fehlgeschlagen – Modellplausibilität nicht prüfbar.")
            );
        }

        if (category.contains(CATEGORY_CONNECTION_FAILURE) || category.contains(CATEGORY_ENDPOINT_NOT_FOUND)) {
            String baseUrlMessage = category.contains(CATEGORY_ENDPOINT_NOT_FOUND)
                    ? "Endpoint nicht gefunden: " + detail
                    : "Verbindung zum Endpoint fehlgeschlagen: " + detail;
            return List.of(
                    CheckpointResult.Failure.of(
                            CheckpointId.BASE_URL_REACHABLE,
                            CheckpointSeverity.ERROR,
                            baseUrlMessage),
                    new CheckpointResult.NotApplicable(
                            CheckpointId.API_KEY_ACCEPTED,
                            "Endpoint nicht erreichbar – Authentifizierung nicht prüfbar."),
                    new CheckpointResult.NotApplicable(
                            CheckpointId.MODEL_LIST_AVAILABLE,
                            "Endpoint nicht erreichbar – Modellliste nicht abrufbar."),
                    new CheckpointResult.NotApplicable(
                            CheckpointId.SELECTED_MODEL_PLAUSIBLE,
                            "Endpoint nicht erreichbar – Modellplausibilität nicht prüfbar.")
            );
        }

        if (category.contains(CATEGORY_SERVER_ERROR)) {
            return List.of(
                    new CheckpointResult.Success(
                            CheckpointId.BASE_URL_REACHABLE,
                            "Endpoint hat geantwortet (Serverfehler erhalten)."),
                    new CheckpointResult.NotApplicable(
                            CheckpointId.API_KEY_ACCEPTED,
                            "Serverfehler – Authentifizierung nicht eindeutig prüfbar."),
                    CheckpointResult.Failure.of(
                            CheckpointId.MODEL_LIST_AVAILABLE,
                            CheckpointSeverity.WARNING,
                            "Provider antwortet mit Serverfehler: " + detail),
                    new CheckpointResult.NotApplicable(
                            CheckpointId.SELECTED_MODEL_PLAUSIBLE,
                            "Serverfehler – Modellplausibilität nicht prüfbar.")
            );
        }

        if (category.contains(CATEGORY_INVALID_RESPONSE)) {
            return List.of(
                    new CheckpointResult.Success(
                            CheckpointId.BASE_URL_REACHABLE,
                            "Endpoint hat geantwortet (Antwort nicht verarbeitbar)."),
                    new CheckpointResult.NotApplicable(
                            CheckpointId.API_KEY_ACCEPTED,
                            "Antwort nicht parsierbar – Authentifizierung nicht eindeutig prüfbar."),
                    CheckpointResult.Failure.of(
                            CheckpointId.MODEL_LIST_AVAILABLE,
                            CheckpointSeverity.ERROR,
                            "Antwort des Providers nicht verarbeitbar: " + detail),
                    new CheckpointResult.NotApplicable(
                            CheckpointId.SELECTED_MODEL_PLAUSIBLE,
                            "Antwort nicht parsierbar – Modellplausibilität nicht prüfbar.")
            );
        }

        // Unbekannte Fehlerkategorie
        String unknownMsg = "Unbekannter technischer Fehler beim Modellabruf: " + detail;
        return List.of(
                CheckpointResult.Failure.of(
                        CheckpointId.BASE_URL_REACHABLE,
                        CheckpointSeverity.ERROR,
                        unknownMsg),
                CheckpointResult.Failure.of(
                        CheckpointId.API_KEY_ACCEPTED,
                        CheckpointSeverity.ERROR,
                        unknownMsg),
                CheckpointResult.Failure.of(
                        CheckpointId.MODEL_LIST_AVAILABLE,
                        CheckpointSeverity.ERROR,
                        unknownMsg),
                CheckpointResult.Failure.of(
                        CheckpointId.SELECTED_MODEL_PLAUSIBLE,
                        CheckpointSeverity.ERROR,
                        unknownMsg)
        );
    }

    /**
     * Prüft, ob das konfigurierte Modell in der Modellliste enthalten ist.
     *
     * @param models          verfügbare Modelle vom Provider
     * @param configuredModel konfigurierter Modellname aus dem Editor
     * @return Success wenn das Modell enthalten ist, Failure WARNING sonst
     */
    private CheckpointResult checkModelPlausible(List<String> models, String configuredModel) {
        if (configuredModel.isBlank()) {
            return CheckpointResult.Failure.of(
                    CheckpointId.SELECTED_MODEL_PLAUSIBLE,
                    CheckpointSeverity.ERROR,
                    "Kein Modell konfiguriert.");
        }
        if (models.contains(configuredModel)) {
            return new CheckpointResult.Success(
                    CheckpointId.SELECTED_MODEL_PLAUSIBLE,
                    "Konfiguriertes Modell \"" + configuredModel + "\" in verfügbarer Liste gefunden.");
        }
        return CheckpointResult.Failure.of(
                CheckpointId.SELECTED_MODEL_PLAUSIBLE,
                CheckpointSeverity.WARNING,
                "Konfiguriertes Modell \"" + configuredModel
                        + "\" nicht in verfügbarer Liste gefunden. Bitte Modellname prüfen.");
    }

    /**
     * Baut den {@link ModelCatalogRequest} aus dem aktuellen Editorzustand auf.
     * <p>
     * Der effektive API-Key-Rohwert wird über den {@link ApiKeyResolutionPort} ermittelt
     * (Vorrangregel: providerspezifische ENV → Legacy-ENV → Property-Wert) und in den
     * Request übernommen. Dadurch ist der Schlüssel bereits beim Adapter verfügbar und
     * spiegelt exakt die Quelle wider, die zuvor im Deskriptor ausgewiesen wurde.
     *
     * @param input          aktueller Editorzustand
     * @param family         aktive Provider-Familie
     * @param apiKeyDesc     bereits aufgelöster Herkunftsdeskriptor des API-Schlüssels
     * @return fertiger Request; nie {@code null}
     */
    private ModelCatalogRequest buildCatalogRequest(EditorValidationInput input,
                                                     AiProviderFamily family,
                                                     EffectiveApiKeyDescriptor apiKeyDesc) {
        String propertyValue = resolveApiKeyPropertyValue(input, family);
        Optional<String> apiKeyForRequest = apiKeyResolutionPort
                .resolveEffectiveApiKeyValue(family, propertyValue);

        String rawBaseUrl = resolveBaseUrlValue(input, family);
        Optional<String> baseUrl = rawBaseUrl.isBlank() ? Optional.empty() : Optional.of(rawBaseUrl);

        int timeout = parseTimeoutOrDefault(resolveTimeoutValue(input, family));

        return new ModelCatalogRequest(
                family.getIdentifier(),
                baseUrl,
                apiKeyForRequest,
                timeout);
    }

    /**
     * Liest den Roh-Property-Wert des API-Schlüssels für die angegebene Provider-Familie
     * aus dem Editorzustand.
     *
     * @param input  aktueller Editorzustand
     * @param family aktive Provider-Familie
     * @return Property-Wert; nie {@code null}, leer wenn nicht gesetzt
     */
    private String resolveApiKeyPropertyValue(EditorValidationInput input, AiProviderFamily family) {
        return switch (family) {
            case CLAUDE -> input.claudeApiKeyPropertyValue();
            case OPENAI_COMPATIBLE -> input.openaiApiKeyPropertyValue();
        };
    }

    /**
     * Liest den bereits aufgelösten {@link EffectiveApiKeyDescriptor} für die aktive Provider-Familie
     * direkt aus dem {@link EditorValidationInput}.
     * <p>
     * {@link EditorValidationInput} enthält keinen rohen API-Key-String, sondern nur den vom
     * GUI-Adapter bereits aufgelösten Descriptor. Der Descriptor spiegelt die Vorrangregel
     * (ENV → Legacy-ENV → Property) zum Zeitpunkt des letzten Editor-Refreshs wider.
     * <p>
     * Für Tests kann der Descriptor im Eingabeobjekt direkt gesetzt werden.
     *
     * @param input  aktueller Editorzustand
     * @param family aktive Provider-Familie
     * @return der Herkunftsdeskriptor; nie {@code null}
     */
    private EffectiveApiKeyDescriptor resolveApiKeyDescriptor(EditorValidationInput input,
                                                               AiProviderFamily family) {
        return switch (family) {
            case CLAUDE -> input.claudeApiKeyDescriptor();
            case OPENAI_COMPATIBLE -> input.openaiApiKeyDescriptor();
        };
    }

    /**
     * Liest den Base-URL-Wert für die angegebene Provider-Familie aus dem Editorzustand.
     *
     * @param input  aktueller Editorzustand
     * @param family aktive Provider-Familie
     * @return Base-URL-String; nie {@code null}, leer wenn nicht gesetzt
     */
    private String resolveBaseUrlValue(EditorValidationInput input, AiProviderFamily family) {
        return switch (family) {
            case CLAUDE -> input.claudeBaseUrl();
            case OPENAI_COMPATIBLE -> input.openaiBaseUrl();
        };
    }

    /**
     * Liest den konfigurierten Modellnamen für die angegebene Provider-Familie aus dem Editorzustand.
     *
     * @param input  aktueller Editorzustand
     * @param family aktive Provider-Familie
     * @return Modellname; nie {@code null}, leer wenn nicht gesetzt
     */
    private String resolveModelValue(EditorValidationInput input, AiProviderFamily family) {
        return switch (family) {
            case CLAUDE -> input.claudeModel();
            case OPENAI_COMPATIBLE -> input.openaiModel();
        };
    }

    /**
     * Liest den Timeout-Wert für die angegebene Provider-Familie aus dem Editorzustand.
     *
     * @param input  aktueller Editorzustand
     * @param family aktive Provider-Familie
     * @return Timeout-String; nie {@code null}, leer wenn nicht gesetzt
     */
    private String resolveTimeoutValue(EditorValidationInput input, AiProviderFamily family) {
        return switch (family) {
            case CLAUDE -> input.claudeTimeoutSeconds();
            case OPENAI_COMPATIBLE -> input.openaiTimeoutSeconds();
        };
    }

    /**
     * Parst einen Timeout-String zu einem Integer. Liefert den Standard-Timeout, wenn der
     * String leer ist oder nicht als positive Ganzzahl parsierbar ist.
     *
     * @param raw roher Timeout-String
     * @return geparster Timeout in Sekunden (mindestens 1)
     */
    private int parseTimeoutOrDefault(String raw) {
        try {
            int parsed = Integer.parseInt(raw.trim());
            return parsed > 0 ? parsed : DEFAULT_TIMEOUT_SECONDS;
        } catch (NumberFormatException e) {
            return DEFAULT_TIMEOUT_SECONDS;
        }
    }
}
