package de.gecheckt.pdf.umbenenner.adapter.out.bootstrap.validation;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

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

import de.gecheckt.pdf.umbenenner.application.config.startup.StartConfiguration;


/**
 * Validates {@link StartConfiguration} before processing can begin.
 * <p>
 * Performs mandatory field checks, numeric range validation, URI scheme validation,
 * and basic path existence checks. Throws {@link InvalidStartConfigurationException}
 * if any validation rule fails.
 * <p>
 * Supports injected source and target folder validation for testability
 * (allows mocking of platform-dependent filesystem checks).
 *
 * <h2>Target folder validation</h2>
 * <p>
 * The target folder is validated as "present or technically creatable":
 * <ul>
 *   <li>If it already exists: must be a directory and writable.</li>
 *   <li>If it does not yet exist: the {@link TargetFolderChecker} attempts to create it
 *       via {@code Files.createDirectories}. Creation failure is a hard validation error.</li>
 * </ul>
 * <p>
 * This behaviour ensures the target write path is technically usable before any
 * document processing begins, without requiring the operator to create the folder manually.
 */
public class StartConfigurationValidator {

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

    /**
     * Abstraction for source folder existence, type, and readability checks.
     * <p>
     * Separates filesystem operations from validation logic to enable
     * platform-independent unit testing (mocking) of readability edge cases.
     * <p>
     * Implementation note: The default implementation uses {@code java.nio.file.Files}
     * static methods directly; tests can substitute alternative implementations.
     */
    @FunctionalInterface
    public interface SourceFolderChecker {
        /**
         * Checks source folder and returns validation error message, or null if valid.
         * <p>
         * Checks (in order):
         * 1. Folder exists
         * 2. Is a directory
         * 3. Is readable
         *
         * @param path the source folder path
         * @return error message string, or null if all checks pass
         */
        String checkSourceFolder(Path path);
    }

    /**
     * Abstraction for target folder existence, creatability, and write-access checks.
     * <p>
     * Separates filesystem operations from validation logic to enable
     * platform-independent unit testing (mocking) of write-access and creation edge cases.
     * <p>
     * The default implementation attempts to create the folder via
     * {@code Files.createDirectories} if it does not yet exist, then verifies it is a
     * directory and writable. Tests can substitute alternative implementations.
     */
    @FunctionalInterface
    public interface TargetFolderChecker {
        /**
         * Checks target folder usability and returns a validation error message, or null if valid.
         * <p>
         * Checks (in order):
         * <ol>
         *   <li>If folder does not exist: attempt to create it via {@code createDirectories}.</li>
         *   <li>Is a directory.</li>
         *   <li>Is writable (required for the file-copy write path).</li>
         * </ol>
         *
         * @param path the target folder path
         * @return error message string, or null if all checks pass
         */
        String checkTargetFolder(Path path);
    }

    private final SourceFolderChecker sourceFolderChecker;
    private final TargetFolderChecker targetFolderChecker;

    /**
     * Creates a validator with default NIO-based source and target folder checkers.
     */
    public StartConfigurationValidator() {
        this(new DefaultSourceFolderChecker(), new DefaultTargetFolderChecker());
    }

    /**
     * Creates a validator with a custom source folder checker (primarily for testing).
     * Uses the default NIO-based target folder checker.
     *
     * @param sourceFolderChecker the source folder checker to use (must not be null)
     */
    public StartConfigurationValidator(SourceFolderChecker sourceFolderChecker) {
        this(sourceFolderChecker, new DefaultTargetFolderChecker());
    }

    /**
     * Creates a validator with custom source and target folder checkers (primarily for testing).
     *
     * @param sourceFolderChecker the source folder checker to use (must not be null)
     * @param targetFolderChecker the target folder checker to use (must not be null)
     */
    public StartConfigurationValidator(SourceFolderChecker sourceFolderChecker,
                                       TargetFolderChecker targetFolderChecker) {
        this.sourceFolderChecker = sourceFolderChecker;
        this.targetFolderChecker = targetFolderChecker;
    }

    /**
     * Validates the given configuration.
     * <p>
     * Checks all mandatory fields, numeric constraints, URI validity, and path existence.
     * If validation fails, throws {@link InvalidStartConfigurationException} with an
     * aggregated error message listing all problems.
     *
     * @param config the configuration to validate, must not be null
     * @throws InvalidStartConfigurationException if any validation rule fails
     */
    public void validate(StartConfiguration config) {
        List<String> errors = new ArrayList<>();

        // Mandatory fields and required paths
        validateMandatoryFields(config, errors);

        // Numeric constraints
        validateNumericConstraints(config, errors);

        // Path relationships and optional paths
        validateSourceAndTargetNotSame(config.sourceFolder(), config.targetFolder(), errors);
        validateOptionalPaths(config, errors);

        if (!errors.isEmpty()) {
            String errorMessage = "Invalid startup configuration:\n" + String.join("\n", errors);
            throw new InvalidStartConfigurationException(errorMessage);
        }

        LOG.info("Configuration validation successful.");
    }

    private void validateMandatoryFields(StartConfiguration config, List<String> errors) {
        validateSourceFolder(config.sourceFolder(), errors);
        validateTargetFolder(config.targetFolder(), errors);
        validateSqliteFile(config.sqliteFile(), errors);
        validatePromptTemplateFile(config.promptTemplateFile(), errors);
        if (config.multiProviderConfiguration() == null) {
            errors.add("- ai provider configuration: must not be null");
        }
    }

    private void validateNumericConstraints(StartConfiguration config, List<String> errors) {
        validateMaxRetriesTransient(config.maxRetriesTransient(), errors);
        validateMaxPages(config.maxPages(), errors);
        validateMaxTextCharacters(config.maxTextCharacters(), errors);
        validateMaxTitleLength(config.maxTitleLength(), errors);
    }

    private void validateOptionalPaths(StartConfiguration config, List<String> errors) {
        validateRuntimeLockFile(config.runtimeLockFile(), errors);
        validateLogDirectory(config.logDirectory(), errors);
    }

    private void validateSourceFolder(Path sourceFolder, List<String> errors) {
        if (sourceFolder == null) {
            errors.add("- source.folder: must not be null");
            return;
        }
        String checkError = sourceFolderChecker.checkSourceFolder(sourceFolder);
        if (checkError != null) {
            errors.add(checkError);
        }
    }

    private void validateTargetFolder(Path targetFolder, List<String> errors) {
        if (targetFolder == null) {
            errors.add("- target.folder: must not be null");
            return;
        }
        String checkError = targetFolderChecker.checkTargetFolder(targetFolder);
        if (checkError != null) {
            errors.add(checkError);
        }
    }

    private void validateSqliteFile(Path sqliteFile, List<String> errors) {
        validateRequiredFileParentDirectory(sqliteFile, "sqlite.file", errors);
    }

    private void validateMaxRetriesTransient(int maxRetriesTransient, List<String> errors) {
        if (maxRetriesTransient < 1) {
            errors.add("- max.retries.transient: must be >= 1, got: " + maxRetriesTransient);
        }
    }

    private void validateMaxPages(int maxPages, List<String> errors) {
        if (maxPages <= 0) {
            errors.add("- max.pages: must be > 0, got: " + maxPages);
        }
    }

    private void validateMaxTextCharacters(int maxTextCharacters, List<String> errors) {
        if (maxTextCharacters <= 0) {
            errors.add("- max.text.characters: must be > 0, got: " + maxTextCharacters);
        }
    }

    /**
     * Validates the configured maximum base title length.
     * <p>
     * Hard errors (abort startup):
     * <ul>
     *   <li>{@code value < 10}</li>
     *   <li>{@code value > 120}</li>
     * </ul>
     * Non-blocking warnings (logged but accepted):
     * <ul>
     *   <li>{@code 10 <= value <= 39}: low-range warning (AI results may be incomplete)</li>
     *   <li>{@code 100 <= value <= 120}: high-range warning (filename compatibility with
     *       encrypted Synology volumes)</li>
     * </ul>
     *
     * @param value  the configured value
     * @param errors collector for aggregated error messages
     */
    private void validateMaxTitleLength(int value, List<String> errors) {
        if (value < 10) {
            errors.add("- max.title.length: must be >= 10 (got: " + value
                    + "). Minimum ist 10 Zeichen.");
            return;
        }
        if (value > 120) {
            errors.add("- max.title.length: must be <= 120 (got: " + value
                    + "). Überschreitet sicheres Limit für verschlüsselte Synology-Volumes.");
            return;
        }
        if (value <= 39) {
            LOG.warn("Titellänge {} unter 40 Zeichen – KI-Ergebnisse können unvollständig sein, "
                    + "da Absender allein bereits 15–20 Zeichen benötigt", value);
        } else if (value >= 100) {
            LOG.warn("Titellänge {} ist hoch – Kompatibilität mit verschlüsselten Volumes "
                    + "(Limit ~143 Zeichen inkl. Datumspräfix) prüfen",
                    value);
        }
    }

    private void validatePromptTemplateFile(Path promptTemplateFile, List<String> errors) {
        validateRequiredRegularFile(promptTemplateFile, "prompt.template.file", errors);
    }

    private void validateSourceAndTargetNotSame(Path sourceFolder, Path targetFolder, List<String> errors) {
        if (sourceFolder != null && targetFolder != null) {
            try {
                Path normalizedSource = sourceFolder.toRealPath();
                Path normalizedTarget = targetFolder.toRealPath();
                if (normalizedSource.equals(normalizedTarget)) {
                    errors.add("- source.folder and target.folder must not resolve to the same path: " + normalizedSource);
                }
            } catch (Exception e) {
                // If toRealPath fails (e.g., path doesn't exist), skip this check
                // The individual existence checks will catch missing paths
            }
        }
    }

    private void validateRuntimeLockFile(Path runtimeLockFile, List<String> errors) {
        validateOptionalFileParentDirectory(runtimeLockFile, "runtime.lock.file", errors);
    }

    private void validateLogDirectory(Path logDirectory, List<String> errors) {
        validateOptionalExistingDirectory(logDirectory, "log.directory", errors);
    }

    // === Helper methods for common validation patterns ===

    /**
     * Validates that a required file path is not null and its parent directory exists and is a directory.
     * <p>
     * The file itself may not exist yet (e.g., SQLite will create it on first use), but the parent
     * directory must be present and writable. Used for files like sqlite.file where the application
     * will create the file if needed.
     */
    private void validateRequiredFileParentDirectory(Path filePath, String fieldName, List<String> errors) {
        if (filePath == null) {
            errors.add("- " + fieldName + ": must not be null");
            return;
        }
        Path parent = filePath.getParent();
        if (parent == null) {
            errors.add("- " + fieldName + ": has no parent directory: " + filePath);
        } else if (!Files.exists(parent)) {
            errors.add("- " + fieldName + ": parent directory does not exist: " + parent);
        } else if (!Files.isDirectory(parent)) {
            errors.add("- " + fieldName + ": parent is not a directory: " + parent);
        }
    }

    /**
     * Validates that a required file path is not null, exists, and is a regular file.
     */
    private void validateRequiredRegularFile(Path filePath, String fieldName, List<String> errors) {
        if (filePath == null) {
            errors.add("- " + fieldName + ": must not be null");
            return;
        }
        if (!Files.exists(filePath)) {
            errors.add("- " + fieldName + ": path does not exist: " + filePath);
        } else if (!Files.isRegularFile(filePath)) {
            errors.add("- " + fieldName + ": path is not a regular file: " + filePath);
        }
    }

    /**
     * Validates that an optional file path, if present and non-blank, has a parent directory
     * that exists and is a directory.
     */
    private void validateOptionalFileParentDirectory(Path filePath, String fieldName, List<String> errors) {
        if (filePath != null && !filePath.toString().isBlank()) {
            Path parent = filePath.getParent();
            if (parent != null) {
                if (!Files.exists(parent)) {
                    errors.add("- " + fieldName + ": parent directory does not exist: " + parent);
                } else if (!Files.isDirectory(parent)) {
                    errors.add("- " + fieldName + ": parent is not a directory: " + parent);
                }
            }
        }
    }

    /**
     * Validates that an optional directory path, if present and non-blank, either does not exist
     * or exists and is a directory.
     */
    private void validateOptionalExistingDirectory(Path directoryPath, String fieldName, List<String> errors) {
        if (directoryPath != null
                && !directoryPath.toString().isBlank()
                && Files.exists(directoryPath)
                && !Files.isDirectory(directoryPath)) {
            errors.add("- " + fieldName + ": exists but is not a directory: " + directoryPath);
        }
        // If it doesn't exist yet, that's acceptable - we don't auto-create
    }

    /**
     * Default NIO-based implementation of {@link SourceFolderChecker}.
     * <p>
     * Uses {@code java.nio.file.Files} static methods to check existence, type, and readability.
     * <p>
     * This separation allows unit tests to inject alternative implementations
     * that control the outcome of readability checks without relying on actual filesystem
     * permissions (which are platform-dependent).
     */
    private static class DefaultSourceFolderChecker implements SourceFolderChecker {
        @Override
        public String checkSourceFolder(Path path) {
            if (!Files.exists(path)) {
                return "- source.folder: path does not exist: " + path;
            }
            if (!Files.isDirectory(path)) {
                return "- source.folder: path is not a directory: " + path;
            }
            if (!Files.isReadable(path)) {
                return "- source.folder: directory is not readable: " + path;
            }
            return null;  // All checks passed
        }
    }

    /**
     * Default NIO-based implementation of {@link TargetFolderChecker}.
     * <p>
     * Validates that the target folder is present and writable for the file-copy write path.
     * If the folder does not yet exist, creation is attempted via {@code Files.createDirectories}.
     * <p>
     * This satisfies the "present or technically creatable" requirement: the folder need not
     * exist before the application starts, but must be reachable at startup time.
     * <p>
     * The write-access check is performed by creating and immediately deleting a temporary
     * probe file inside the target folder. A pure {@code Files.isWritable} check is not used
     * because it is known to return false negatives on mapped SMB network drives under
     * Windows (ACL information as seen by the client can deny writes even when the actual
     * write succeeds). The probe reflects the real ability to create files in the target.
     * <p>
     * This separation allows unit tests to inject alternative implementations
     * that control the outcome of write-access or creation checks without relying on actual
     * filesystem permissions (which are platform-dependent).
     */
    private static class DefaultTargetFolderChecker implements TargetFolderChecker {
        @Override
        public String checkTargetFolder(Path path) {
            if (!Files.exists(path)) {
                try {
                    Files.createDirectories(path);
                } catch (IOException e) {
                    return "- target.folder: path does not exist and could not be created: "
                            + path + " (" + e.getMessage() + ")";
                }
            }
            if (!Files.isDirectory(path)) {
                return "- target.folder: path is not a directory: " + path;
            }
            try {
                Path probe = Files.createTempFile(path, ".writetest-", ".tmp");
                try {
                    Files.deleteIfExists(probe);
                } catch (IOException cleanupFailure) {
                    LOG.debug("Could not delete write-probe file {}: {}", probe, cleanupFailure.toString());
                }
            } catch (IOException e) {
                return "- target.folder: directory is not writable: " + path;
            }
            return null;  // All checks passed
        }
    }
}
