package de.gecheckt.pdf.umbenenner.application.service;

import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.Objects;
import java.util.Set;

import de.gecheckt.pdf.umbenenner.application.port.out.ClockPort;
import de.gecheckt.pdf.umbenenner.domain.model.AiErrorClassification;
import de.gecheckt.pdf.umbenenner.domain.model.DateSource;
import de.gecheckt.pdf.umbenenner.domain.model.NamingProposal;
import de.gecheckt.pdf.umbenenner.domain.model.ParsedAiResponse;

/**
 * Validates the semantics of a structurally parsed AI response and produces a
 * {@link NamingProposal} or a classified validation error.
 *
 * <h2>What this validator checks</h2>
 * <p>
 * All objectively computable rules are enforced here. Rules that depend on linguistic
 * judgement (German language, comprehensibility, treatment of proper nouns) are
 * delegated to the AI via the prompt contract and are not verified programmatically.
 *
 * <h3>Title rules (objective)</h3>
 * <ul>
 *   <li>Base title must not exceed the configured maximum length.</li>
 *   <li>Title must not contain characters other than letters, digits, and space
 *       (Umlauts and ß are permitted).</li>
 *   <li>Title must not be a generic placeholder (e.g., "Dokument", "Datei", "Scan",
 *       "PDF", "Seite", "Unbekannt").</li>
 * </ul>
 *
 * <h3>Date rules (objective)</h3>
 * <ul>
 *   <li>If the AI provides a {@code date}, it must be interpretable as ISO-8601
 *       {@code YYYY-MM-DD}. A provided but unparseable date is a
 *       {@link AiErrorClassification#FUNCTIONAL functional} error.</li>
 *   <li>If the AI provides no {@code date}, the current date from {@link ClockPort} is
 *       used as a fallback ({@link DateSource#FALLBACK_CURRENT}).</li>
 * </ul>
 *
 * <h2>Result</h2>
 * <ul>
 *   <li>{@link AiValidationResult.Valid} — semantically sound; contains a ready
 *       {@link NamingProposal}.</li>
 *   <li>{@link AiValidationResult.Invalid} — contains an error message and the
 *       {@link AiErrorClassification} (always {@link AiErrorClassification#FUNCTIONAL}
 *       for validation failures from this class).</li>
 * </ul>
 */
public final class AiResponseValidator {

    /**
     * Known generic placeholder titles that are not acceptable as document names.
     * These are case-insensitive matches.
     */
    private static final Set<String> GENERIC_TITLES = Set.of(
            "dokument", "datei", "scan", "pdf", "seite", "unbekannt",
            "document", "file", "unknown", "page"
    );

    private final ClockPort clockPort;
    private final int maxTitleLength;

    /**
     * Creates the validator with the given clock for date fallback and the configured
     * maximum base title length.
     *
     * @param clockPort       the clock for current-date fallback; must not be null
     * @param maxTitleLength  the configured maximum length for the base title; must be &gt;= 1
     * @throws NullPointerException     if {@code clockPort} is null
     * @throws IllegalArgumentException if {@code maxTitleLength} is less than 1
     */
    public AiResponseValidator(ClockPort clockPort, int maxTitleLength) {
        this.clockPort = Objects.requireNonNull(clockPort, "clockPort must not be null");
        if (maxTitleLength < 1) {
            throw new IllegalArgumentException(
                    "maxTitleLength must be >= 1, but was: " + maxTitleLength);
        }
        this.maxTitleLength = maxTitleLength;
    }

    /**
     * Validates the parsed AI response and produces a {@link NamingProposal} on success.
     *
     * @param parsed the structurally parsed AI response; must not be null
     * @return a {@link AiValidationResult} indicating validity or the specific failure;
     *         never null
     * @throws NullPointerException if {@code parsed} is null
     */
    public AiValidationResult validate(ParsedAiResponse parsed) {
        Objects.requireNonNull(parsed, "parsed must not be null");

        // --- Title validation ---
        String title = parsed.title().trim();

        if (title.length() > maxTitleLength) {
            return AiValidationResult.invalid(
                    "Title exceeds " + maxTitleLength + " characters (base title): '" + title + "'",
                    AiErrorClassification.FUNCTIONAL);
        }

        if (!TitleCharacterRule.isAllowed(title)) {
            return AiValidationResult.invalid(
                    "Title contains disallowed characters (only letters, digits, spaces, and hyphens are permitted): '"
                            + title + "'",
                    AiErrorClassification.FUNCTIONAL);
        }

        if (isGenericTitle(title)) {
            return AiValidationResult.invalid(
                    "Title is a generic placeholder and not acceptable: '" + title + "'",
                    AiErrorClassification.FUNCTIONAL);
        }

        // --- Date validation / fallback ---
        LocalDate resolvedDate;
        DateSource dateSource;

        if (parsed.dateString().isPresent()) {
            String dateStr = parsed.dateString().get().trim();
            // Leere Strings werden wie fehlende Daten behandelt – Fallback auf aktuelles Datum
            if (dateStr.isEmpty()) {
                resolvedDate = clockPort.now().atZone(java.time.ZoneOffset.UTC).toLocalDate();
                dateSource = DateSource.FALLBACK_CURRENT;
            } else {
                try {
                    resolvedDate = LocalDate.parse(dateStr);
                    dateSource = DateSource.AI_PROVIDED;
                } catch (DateTimeParseException e) {
                    return AiValidationResult.invalid(
                            "AI-provided date '" + dateStr + "' is not a valid YYYY-MM-DD date: " + e.getMessage(),
                            AiErrorClassification.FUNCTIONAL);
                }
            }
        } else {
            // Kein Datum von der KI bereitgestellt → Fallback auf aktuelles Datum
            resolvedDate = clockPort.now().atZone(java.time.ZoneOffset.UTC).toLocalDate();
            dateSource = DateSource.FALLBACK_CURRENT;
        }

        NamingProposal proposal = new NamingProposal(resolvedDate, dateSource, title, parsed.reasoning());
        return AiValidationResult.valid(proposal);
    }

    /**
     * Returns {@code true} if the title is a known generic placeholder.
     * Comparison is case-insensitive.
     */
    private static boolean isGenericTitle(String title) {
        return GENERIC_TITLES.contains(title.toLowerCase());
    }

    // -------------------------------------------------------------------------
    // Result type
    // -------------------------------------------------------------------------

    /**
     * The result of a semantic AI response validation.
     */
    public sealed interface AiValidationResult permits AiValidationResult.Valid, AiValidationResult.Invalid {

        /**
         * Returns a valid result containing the produced {@link NamingProposal}.
         *
         * @param proposal the validated naming proposal; must not be null
         * @return a valid result; never null
         */
        static AiValidationResult valid(NamingProposal proposal) {
            return new Valid(proposal);
        }

        /**
         * Returns an invalid result with an error message and classification.
         *
         * @param errorMessage    human-readable description of the validation failure;
         *                        must not be null
         * @param classification  always {@link AiErrorClassification#FUNCTIONAL} for
         *                        semantic title/date violations
         * @return an invalid result; never null
         */
        static AiValidationResult invalid(String errorMessage, AiErrorClassification classification) {
            return new Invalid(errorMessage, classification);
        }

        /**
         * A successful validation result containing the ready {@link NamingProposal}.
         *
         * @param proposal the validated and complete naming proposal; never null
         */
        record Valid(NamingProposal proposal) implements AiValidationResult {
            public Valid {
                Objects.requireNonNull(proposal, "proposal must not be null");
            }
        }

        /**
         * A failed validation result carrying the error details.
         *
         * @param errorMessage   the reason for the failure; never null
         * @param classification the error category; never null
         */
        record Invalid(String errorMessage, AiErrorClassification classification)
                implements AiValidationResult {
            public Invalid {
                Objects.requireNonNull(errorMessage, "errorMessage must not be null");
                Objects.requireNonNull(classification, "classification must not be null");
            }
        }
    }
}
