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

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.ApiKeyOrigin;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor;

/**
 * Zentraler Validierungsbaustein für den aktuellen Editorzustand des Konfigurationseditors.
 * <p>
 * Dieser Validator arbeitet ausschließlich auf den übergebenen String-Werten des
 * {@link EditorValidationInput}, ohne Dateisystemzugriffe, Datenbankroundtrips oder
 * Netzwerkkommunikation. Er erzeugt Befunde der Stufen Fehler, Warnung, Hinweis und Info.
 * <p>
 * Folgende Prüfungen sind ausdrücklich ausgeschlossen (gehören in spätere technische Gesamtprüfungen):
 * <ul>
 *   <li>Pfad-Existenzprüfungen (Quellordner, Zielordner, SQLite-Datei, Prompt-Datei)</li>
 *   <li>SQLite-Roundtrips</li>
 *   <li>Netzwerkverbindungen (Modellabruf, API-Erreichbarkeit)</li>
 * </ul>
 * <p>
 * API-Key-Vorrangregel (fachlich verbindlich):
 * <ol>
 *   <li>Providerspezifische Umgebungsvariable</li>
 *   <li>Bei {@code openai-compatible}: Legacy-Umgebungsvariable als Fallback</li>
 *   <li>Property-Wert aus der Datei</li>
 *   <li>ABSENT, wenn keine Quelle einen Wert liefert</li>
 * </ol>
 * <p>
 * Warnlogik für {@code max.text.characters}:
 * <ul>
 *   <li>1–1.000: unkritisch (kein Befund)</li>
 *   <li>1.001–3.000: Warnung</li>
 *   <li>ab 3.001: starke Warnung</li>
 * </ul>
 */
public class EditorConfigurationValidator {

    // Property-Schlüssel als Konstanten (identisch mit den .properties-Schlüsseln)
    static final String FIELD_ACTIVE_PROVIDER = "ai.provider.active";
    static final String FIELD_SOURCE_FOLDER = "source.folder";
    static final String FIELD_TARGET_FOLDER = "target.folder";
    static final String FIELD_SQLITE_FILE = "sqlite.file";
    static final String FIELD_PROMPT_FILE = "prompt.template.file";
    static final String FIELD_MAX_RETRIES = "max.retries.transient";
    static final String FIELD_MAX_PAGES = "max.pages";
    static final String FIELD_MAX_CHARS = "max.text.characters";
    static final String FIELD_MAX_TITLE_LENGTH = "max.title.length";

    static final String FIELD_CLAUDE_BASE_URL = "ai.provider.claude.baseUrl";
    static final String FIELD_CLAUDE_MODEL = "ai.provider.claude.model";
    static final String FIELD_CLAUDE_TIMEOUT = "ai.provider.claude.timeoutSeconds";
    static final String FIELD_CLAUDE_API_KEY = "ai.provider.claude.apiKey";

    static final String FIELD_OPENAI_BASE_URL = "ai.provider.openai-compatible.baseUrl";
    static final String FIELD_OPENAI_MODEL = "ai.provider.openai-compatible.model";
    static final String FIELD_OPENAI_TIMEOUT = "ai.provider.openai-compatible.timeoutSeconds";
    static final String FIELD_OPENAI_API_KEY = "ai.provider.openai-compatible.apiKey";

    private static final int MAX_CHARS_WARNING_THRESHOLD = 1_000;
    private static final int MAX_CHARS_STRONG_WARNING_THRESHOLD = 3_000;
    private static final int MAX_PAGES_HINT_THRESHOLD = 100;

    // Grenzen für die maximale Basistitel-Länge.
    private static final int TITLE_LENGTH_MIN = 10;
    private static final int TITLE_LENGTH_MAX = 120;
    private static final int TITLE_LENGTH_LOW_WARN_THRESHOLD = 40;
    private static final int TITLE_LENGTH_HIGH_WARN_THRESHOLD = 100;

    /**
     * Erstellt eine neue Instanz des Validators.
     * <p>
     * Dieser Validator benötigt keine Abhängigkeiten; alle Prüfungen sind rein in-memory.
     */
    public EditorConfigurationValidator() {
        // Kein State nötig; alle Prüfungen arbeiten auf dem übergebenen Input.
    }

    /**
     * Validiert den aktuellen Editorzustand und liefert einen Befund-Bericht.
     * <p>
     * Die Methode ist schnell (keine I/O) und darf auf dem JavaFX Application Thread
     * aufgerufen werden. Der zurückgegebene Bericht ist immutable.
     *
     * @param input  der aktuelle Editorzustand; darf nicht {@code null} sein
     * @return der Validierungsbericht mit allen gefundenen Befunden; nie {@code null}
     * @throws NullPointerException wenn {@code input} {@code null} ist
     */
    public EditorValidationReport validate(EditorValidationInput input) {
        Objects.requireNonNull(input, "input must not be null");

        List<EditorValidationFinding> findings = new ArrayList<>();

        validateActiveProvider(input, findings);
        validateRequiredPaths(input, findings);
        validateNumericLimits(input, findings);
        validateActiveProviderFields(input, findings);

        return new EditorValidationReport(findings);
    }

    // =========================================================================
    // Aktiver Provider
    // =========================================================================

    private void validateActiveProvider(EditorValidationInput input,
                                        List<EditorValidationFinding> findings) {
        String identifier = input.activeProviderIdentifier();
        if (identifier.isBlank()) {
            findings.add(EditorValidationFinding.error(FIELD_ACTIVE_PROVIDER,
                    "Es muss ein aktiver Provider ausgewählt sein."));
            return;
        }
        Optional<AiProviderFamily> resolved = AiProviderFamily.fromIdentifier(identifier);
        if (resolved.isEmpty()) {
            findings.add(EditorValidationFinding.error(FIELD_ACTIVE_PROVIDER,
                    "Der angegebene Provider '" + identifier + "' ist nicht bekannt. "
                    + "Erlaubt sind: 'claude' und 'openai-compatible'."));
        }
    }

    // =========================================================================
    // Pflichtpfade
    // =========================================================================

    private void validateRequiredPaths(EditorValidationInput input,
                                       List<EditorValidationFinding> findings) {
        if (input.sourceFolder().isBlank()) {
            findings.add(EditorValidationFinding.error(FIELD_SOURCE_FOLDER,
                    "Quellordner darf nicht leer sein."));
        }
        if (input.targetFolder().isBlank()) {
            findings.add(EditorValidationFinding.error(FIELD_TARGET_FOLDER,
                    "Zielordner darf nicht leer sein."));
        }
        if (input.sqliteFile().isBlank()) {
            findings.add(EditorValidationFinding.error(FIELD_SQLITE_FILE,
                    "SQLite-Datei darf nicht leer sein."));
        }
        if (input.promptTemplateFile().isBlank()) {
            findings.add(EditorValidationFinding.error(FIELD_PROMPT_FILE,
                    "Prompt-Datei darf nicht leer sein."));
        }
    }

    // =========================================================================
    // Numerische Limits
    // =========================================================================

    private void validateNumericLimits(EditorValidationInput input,
                                       List<EditorValidationFinding> findings) {
        validateMaxRetriesTransient(input.maxRetriesTransient(), findings);
        validateMaxPages(input.maxPages(), findings);
        validateMaxTextCharacters(input.maxTextCharacters(), findings);
        validateMaxTitleLength(input.maxTitleLength(), findings);
    }

    private void validateMaxRetriesTransient(String rawValue, List<EditorValidationFinding> findings) {
        if (rawValue.isBlank()) {
            findings.add(EditorValidationFinding.error(FIELD_MAX_RETRIES,
                    "Maximale transiente Retries darf nicht leer sein."));
            return;
        }
        try {
            int value = Integer.parseInt(rawValue.strip());
            if (value < 1) {
                findings.add(EditorValidationFinding.error(FIELD_MAX_RETRIES,
                        "Maximale transiente Retries muss mindestens 1 sein (aktuell: " + value + "). "
                        + "Der Wert 0 ist unzulässig."));
            }
        } catch (NumberFormatException e) {
            findings.add(EditorValidationFinding.error(FIELD_MAX_RETRIES,
                    "Maximale transiente Retries muss eine ganze Zahl >= 1 sein."));
        }
    }

    private void validateMaxPages(String rawValue, List<EditorValidationFinding> findings) {
        if (rawValue.isBlank()) {
            findings.add(EditorValidationFinding.error(FIELD_MAX_PAGES,
                    "Maximale Seitenzahl darf nicht leer sein."));
            return;
        }
        try {
            int value = Integer.parseInt(rawValue.strip());
            if (value <= 0) {
                findings.add(EditorValidationFinding.error(FIELD_MAX_PAGES,
                        "Maximale Seitenzahl muss positiv sein (aktuell: " + value + ")."));
            } else if (value > MAX_PAGES_HINT_THRESHOLD) {
                findings.add(EditorValidationFinding.hint(FIELD_MAX_PAGES,
                        "Plausibilitätshinweis: Über " + MAX_PAGES_HINT_THRESHOLD
                        + " Seiten je Datei könnten die Verarbeitung verlangsamen (aktuell: " + value + ")."));
            }
        } catch (NumberFormatException e) {
            findings.add(EditorValidationFinding.error(FIELD_MAX_PAGES,
                    "Maximale Seitenzahl muss eine positive ganze Zahl sein."));
        }
    }

    private void validateMaxTextCharacters(String rawValue, List<EditorValidationFinding> findings) {
        if (rawValue.isBlank()) {
            findings.add(EditorValidationFinding.error(FIELD_MAX_CHARS,
                    "Maximale Zeichenzahl darf nicht leer sein."));
            return;
        }
        try {
            int value = Integer.parseInt(rawValue.strip());
            if (value <= 0) {
                findings.add(EditorValidationFinding.error(FIELD_MAX_CHARS,
                        "Maximale Zeichenzahl muss positiv sein (aktuell: " + value + ")."));
            } else if (value > MAX_CHARS_STRONG_WARNING_THRESHOLD) {
                findings.add(EditorValidationFinding.warning(FIELD_MAX_CHARS,
                        "Stark erhöhte Zeichenmenge: " + value + " Zeichen können zu riskant hohen "
                        + "API-Kosten je Verarbeitungsaufruf führen."));
            } else if (value > MAX_CHARS_WARNING_THRESHOLD) {
                findings.add(EditorValidationFinding.warning(FIELD_MAX_CHARS,
                        "Erhöhte Zeichenmenge: " + value + " Zeichen. Beachten Sie mögliche Auswirkungen "
                        + "auf die API-Kosten je Verarbeitungsaufruf."));
            }
            // 1–1000: unkritisch, kein Befund
        } catch (NumberFormatException e) {
            findings.add(EditorValidationFinding.error(FIELD_MAX_CHARS,
                    "Maximale Zeichenzahl muss eine positive ganze Zahl sein."));
        }
    }

    /**
     * Validiert den Rohwert für die maximale Basistitel-Länge.
     * <p>
     * Regeln:
     * <ul>
     *   <li>leer: Fehler</li>
     *   <li>nicht als ganze Zahl parsebar: Fehler</li>
     *   <li>&lt; {@value #TITLE_LENGTH_MIN}: Fehler (Minimum)</li>
     *   <li>&gt; {@value #TITLE_LENGTH_MAX}: Fehler (sicheres Maximum für verschlüsselte Volumes)</li>
     *   <li>{@value #TITLE_LENGTH_MIN}–{@value #TITLE_LENGTH_LOW_WARN_THRESHOLD} (einschließlich
     *       39): Warnung (unter 40 Zeichen – KI-Ergebnisse können unvollständig sein)</li>
     *   <li>&ge; {@value #TITLE_LENGTH_HIGH_WARN_THRESHOLD}: Warnung (Kompatibilitätsrisiko
     *       mit verschlüsselten Volumes)</li>
     *   <li>andernfalls (40–99): kein Befund</li>
     * </ul>
     *
     * @param rawValue Rohwert aus dem Editor; nie {@code null}
     * @param findings Zielliste für neue Befunde
     */
    private void validateMaxTitleLength(String rawValue, List<EditorValidationFinding> findings) {
        if (rawValue.isBlank()) {
            findings.add(EditorValidationFinding.error(FIELD_MAX_TITLE_LENGTH,
                    "Maximale Titellänge darf nicht leer sein."));
            return;
        }
        int value;
        try {
            value = Integer.parseInt(rawValue.strip());
        } catch (NumberFormatException e) {
            findings.add(EditorValidationFinding.error(FIELD_MAX_TITLE_LENGTH,
                    "Maximale Titellänge muss eine ganze Zahl sein."));
            return;
        }

        if (value < TITLE_LENGTH_MIN) {
            findings.add(EditorValidationFinding.error(FIELD_MAX_TITLE_LENGTH,
                    "Maximale Titellänge muss mindestens " + TITLE_LENGTH_MIN
                    + " sein (aktuell: " + value + ")."));
            return;
        }
        if (value > TITLE_LENGTH_MAX) {
            findings.add(EditorValidationFinding.error(FIELD_MAX_TITLE_LENGTH,
                    "Maximale Titellänge überschreitet sicheres Limit von " + TITLE_LENGTH_MAX
                    + " Zeichen (aktuell: " + value + ")."));
            return;
        }
        if (value < TITLE_LENGTH_LOW_WARN_THRESHOLD) {
            findings.add(EditorValidationFinding.warning(FIELD_MAX_TITLE_LENGTH,
                    "Titellänge unter 40 Zeichen – KI-Ergebnisse können unvollständig sein, "
                    + "da Absender allein bereits 15–20 Zeichen benötigt (aktuell: "
                    + value + ")."));
            return;
        }
        if (value >= TITLE_LENGTH_HIGH_WARN_THRESHOLD) {
            findings.add(EditorValidationFinding.warning(FIELD_MAX_TITLE_LENGTH,
                    "Hohe Titellänge – Kompatibilität mit verschlüsselten Volumes prüfen "
                    + "(aktuell: " + value + ")."));
        }
        // 40–99: unkritisch, kein Befund
    }

    // =========================================================================
    // Aktiver Provider – providerabhängige Felder
    // =========================================================================

    private void validateActiveProviderFields(EditorValidationInput input,
                                              List<EditorValidationFinding> findings) {
        Optional<AiProviderFamily> resolvedProvider =
                AiProviderFamily.fromIdentifier(input.activeProviderIdentifier());
        if (resolvedProvider.isEmpty()) {
            // Provider unbekannt – feldspezifische Provider-Prüfungen nicht möglich
            return;
        }

        AiProviderFamily family = resolvedProvider.get();
        switch (family) {
            case CLAUDE -> validateClaudeFields(input, findings);
            case OPENAI_COMPATIBLE -> validateOpenAiFields(input, findings);
        }
    }

    private void validateClaudeFields(EditorValidationInput input,
                                      List<EditorValidationFinding> findings) {
        // Basis-URL: leer ist Warnung (Default-URL wird verwendet)
        if (input.claudeBaseUrl().isBlank()) {
            findings.add(EditorValidationFinding.warning(FIELD_CLAUDE_BASE_URL,
                    "Basis-URL nicht gesetzt – es wird die Standard-URL verwendet."));
        }

        // Modell: Pflichtfeld
        if (input.claudeModel().isBlank()) {
            findings.add(EditorValidationFinding.error(FIELD_CLAUDE_MODEL,
                    "Modellname darf nicht leer sein."));
        }

        // Timeout
        validateTimeoutField(input.claudeTimeoutSeconds(), FIELD_CLAUDE_TIMEOUT, findings);

        // API-Key
        validateApiKeyFindings(input.claudeApiKeyDescriptor(), FIELD_CLAUDE_API_KEY, findings);
    }

    private void validateOpenAiFields(EditorValidationInput input,
                                      List<EditorValidationFinding> findings) {
        // Basis-URL: leer ist Warnung
        if (input.openaiBaseUrl().isBlank()) {
            findings.add(EditorValidationFinding.warning(FIELD_OPENAI_BASE_URL,
                    "Basis-URL nicht gesetzt – es wird die Standard-URL verwendet."));
        }

        // Modell: Pflichtfeld
        if (input.openaiModel().isBlank()) {
            findings.add(EditorValidationFinding.error(FIELD_OPENAI_MODEL,
                    "Modellname darf nicht leer sein."));
        }

        // Timeout
        validateTimeoutField(input.openaiTimeoutSeconds(), FIELD_OPENAI_TIMEOUT, findings);

        // API-Key
        validateApiKeyFindings(input.openaiApiKeyDescriptor(), FIELD_OPENAI_API_KEY, findings);
    }

    private void validateTimeoutField(String rawValue, String fieldKey,
                                      List<EditorValidationFinding> findings) {
        if (rawValue.isBlank()) {
            findings.add(EditorValidationFinding.error(fieldKey,
                    "Timeout-Wert darf nicht leer sein."));
            return;
        }
        try {
            int value = Integer.parseInt(rawValue.strip());
            if (value <= 0) {
                findings.add(EditorValidationFinding.error(fieldKey,
                        "Timeout-Wert muss positiv sein (aktuell: " + value + ")."));
            }
        } catch (NumberFormatException e) {
            findings.add(EditorValidationFinding.error(fieldKey,
                    "Timeout-Wert muss eine positive ganze Zahl sein."));
        }
    }

    /**
     * Erzeugt API-Key-Befunde gemäß der Vorrangregel.
     * <p>
     * Vorrangregel:
     * <ol>
     *   <li>ENV-Variable aktiv: INFO-Befund mit Variablenname</li>
     *   <li>Property-Wert in Datei, keine ENV: normal (kein Befund)</li>
     *   <li>Leeres Property-Feld und keine ENV: WARNING</li>
     * </ol>
     *
     * @param descriptor  Herkunft des effektiven API-Schlüssels
     * @param fieldKey    Property-Schlüssel des API-Key-Felds
     * @param findings    Zielliste für neue Befunde
     */
    private void validateApiKeyFindings(EffectiveApiKeyDescriptor descriptor,
                                        String fieldKey,
                                        List<EditorValidationFinding> findings) {
        ApiKeyOrigin origin = descriptor.origin();

        switch (origin) {
            case FROM_PROVIDER_ENV_VAR -> {
                String varName = descriptor.envVarName().orElse("unbekannte ENV-Variable");
                findings.add(EditorValidationFinding.info(fieldKey,
                        "API-Schlüssel stammt aus Umgebungsvariable " + varName
                        + " (hat Vorrang vor dem Datei-Wert)."));
            }
            case FROM_LEGACY_ENV_VAR -> {
                String varName = descriptor.envVarName().orElse("unbekannte Legacy-ENV-Variable");
                findings.add(EditorValidationFinding.info(fieldKey,
                        "API-Schlüssel stammt aus Legacy-Umgebungsvariable " + varName
                        + " (hat Vorrang vor dem Datei-Wert)."));
            }
            case FROM_PROPERTY_FILE -> {
                // Property-Wert vorhanden, kein ENV-Override: alles in Ordnung, kein Befund
            }
            case ABSENT -> {
                findings.add(EditorValidationFinding.warning(fieldKey,
                        "Kein API-Schlüssel hinterlegt. Ohne Schlüssel kann der Provider "
                        + "nicht genutzt werden."));
            }
        }
    }
}
