package vassistent.util;

import java.net.URI;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;

/**
 * Validates startup configuration and fails fast on invalid values.
 */
public final class AppConfigValidator {

    private static final Set<String> SUPPORTED_MQTT_SCHEMES =
            Set.of("tcp", "ssl", "ws", "wss");

    /**
     * Utility class constructor.
     */
    private AppConfigValidator() {}

    /**
     * Validates all required startup configuration values.
     *
     * @param config loaded application properties
     */
    public static void validate(Properties config) {
        Objects.requireNonNull(config, "config must not be null");

        List<String> errors = new ArrayList<>();

        validateRequiredMqttSettings(config, errors);
        validateCoreUiAndOutputSettings(config, errors);
        validateProcessStartupSettings(config, errors);

        boolean mqttSimulatorEnabled =
                readBoolean(config, errors, "mqtt_sim.enabled", false);
        boolean unrealEnabled =
                readBoolean(config, errors, "unreal.enabled", false);

        if (mqttSimulatorEnabled) {
            validateMqttSimulatorSettings(config, errors);
        }

        if (unrealEnabled) {
            validateUnrealSettings(config, errors);
        }

        if (!errors.isEmpty()) {
            throw new IllegalStateException(buildValidationMessage(errors));
        }
    }

    /**
     * Validates optional process startup health-check settings.
     *
     * @param config loaded properties
     * @param errors collected validation errors
     */
    private static void validateProcessStartupSettings(
            Properties config,
            List<String> errors
    ) {
        readBoolean(config, errors, "process.startup.healthcheck.enabled", true);
        validateOptionalIntegerInRange(
                config,
                errors,
                "process.startup.healthcheck.timeout.millis",
                1,
                Integer.MAX_VALUE
        );
        validateOptionalIntegerInRange(
                config,
                errors,
                "process.startup.healthcheck.poll.millis",
                1,
                Integer.MAX_VALUE
        );
    }

    /**
     * Validates mandatory MQTT runtime keys.
     *
     * @param config loaded properties
     * @param errors collected validation errors
     */
    private static void validateRequiredMqttSettings(
            Properties config,
            List<String> errors
    ) {
        validateRequiredNonBlank(config, errors, "mqtt.topic");
        validateRequiredNonBlank(config, errors, "mqtt.client.id");

        String brokerUrl =
                validateRequiredNonBlank(config, errors, "mqtt.broker.url");
        if (brokerUrl == null) {
            return;
        }

        try {
            URI uri = URI.create(brokerUrl);
            String scheme = uri.getScheme();
            if (scheme == null
                    || !SUPPORTED_MQTT_SCHEMES.contains(
                    scheme.toLowerCase(Locale.ROOT))) {
                errors.add(
                        "Property 'mqtt.broker.url' must use one of "
                                + SUPPORTED_MQTT_SCHEMES + ": "
                                + brokerUrl
                );
            }

            if (uri.getHost() == null || uri.getHost().isBlank()) {
                errors.add(
                        "Property 'mqtt.broker.url' must include a host: "
                                + brokerUrl
                );
            }

            int port = uri.getPort();
            if (port != -1 && (port < 1 || port > 65535)) {
                errors.add(
                        "Property 'mqtt.broker.url' contains invalid port "
                                + port + ": " + brokerUrl
                );
            }
        } catch (Exception e) {
            errors.add(
                    "Property 'mqtt.broker.url' is not a valid URI: " + brokerUrl
            );
        }
    }

    /**
     * Validates UI and animation output settings.
     *
     * @param config loaded properties
     * @param errors collected validation errors
     */
    private static void validateCoreUiAndOutputSettings(
            Properties config,
            List<String> errors
    ) {
        String streamingUrl =
                validateRequiredNonBlank(config, errors, "streaming.url");
        if (streamingUrl != null) {
            try {
                URI uri = URI.create(streamingUrl);
                String scheme = uri.getScheme();
                if (scheme == null
                        || (!"http".equalsIgnoreCase(scheme)
                        && !"https".equalsIgnoreCase(scheme))) {
                    errors.add(
                            "Property 'streaming.url' must start with http:// or https://: "
                                    + streamingUrl
                    );
                }
            } catch (Exception e) {
                errors.add(
                        "Property 'streaming.url' is not a valid URI: " + streamingUrl
                );
            }
        }

        validateRequiredNonBlank(config, errors, "animation.output.path");
    }

    /**
     * Validates simulator settings that are required only when simulator startup is enabled.
     *
     * @param config loaded properties
     * @param errors collected validation errors
     */
    private static void validateMqttSimulatorSettings(
            Properties config,
            List<String> errors
    ) {
        validateExistingFile(config, errors, "python.path");
        validateExistingFile(config, errors, "mqtt_sim.script");
        validateRequiredNonBlank(config, errors, "mqtt_sim.broker");
        validateRequiredIntegerInRange(config, errors, "mqtt_sim.port", 1, 65535);
        validateOptionalIntegerInRange(config, errors, "mqtt_sim.qos", 0, 2);
        validateOptionalPositiveDouble(config, errors, "mqtt_sim.interval_seconds");
    }

    /**
     * Validates Unreal-related settings that are required only when Unreal startup is enabled.
     *
     * @param config loaded properties
     * @param errors collected validation errors
     */
    private static void validateUnrealSettings(
            Properties config,
            List<String> errors
    ) {
        validateExistingFile(config, errors, "unreal.executable");
        validateExistingFile(config, errors, "unreal.signalling_server.script");
        validateExistingFile(config, errors, "unreal.target.executable");
        validateExistingDirectory(config, errors, "unreal.target.working_dir");
        validateOptionalExistingDirectory(config, errors, "unreal.pid.dir");
        validateOptionalNonNegativeInteger(config, errors, "unreal.startup.delay.seconds");
        validateOptionalPidFileParent(config, errors, "unreal.pid.file");
        validateOptionalPidFileParent(config, errors, "unreal.signalling.pid.file");
    }

    /**
     * Validates that a required property exists and is not blank.
     *
     * @param config loaded properties
     * @param errors collected validation errors
     * @param key property key
     * @return normalized value or {@code null} when missing/blank
     */
    private static String validateRequiredNonBlank(
            Properties config,
            List<String> errors,
            String key
    ) {
        String value = normalizeConfigValue(config.getProperty(key));
        if (value == null || value.isBlank()) {
            errors.add("Missing required property '" + key + "'.");
            return null;
        }
        return value;
    }

    /**
     * Validates that a property points to an existing regular file.
     *
     * @param config loaded properties
     * @param errors collected validation errors
     * @param key property key
     */
    private static void validateExistingFile(
            Properties config,
            List<String> errors,
            String key
    ) {
        String rawValue = config.getProperty(key);
        String normalized = validateRequiredNonBlank(config, errors, key);
        if (normalized == null) {
            return;
        }

        Path path = parsePath(rawValue, normalized, key, errors);
        if (path == null) {
            return;
        }

        if (!Files.isRegularFile(path)) {
            errors.add(
                    "Property '" + key + "' must reference an existing file: " + normalized
            );
        }
    }

    /**
     * Validates that a property points to an existing directory.
     *
     * @param config loaded properties
     * @param errors collected validation errors
     * @param key property key
     */
    private static void validateExistingDirectory(
            Properties config,
            List<String> errors,
            String key
    ) {
        String rawValue = config.getProperty(key);
        String normalized = validateRequiredNonBlank(config, errors, key);
        if (normalized == null) {
            return;
        }

        Path path = parsePath(rawValue, normalized, key, errors);
        if (path == null) {
            return;
        }

        if (!Files.isDirectory(path)) {
            errors.add(
                    "Property '" + key + "' must reference an existing directory: " + normalized
            );
        }
    }

    /**
     * Validates an optional directory property when a value is provided.
     *
     * @param config loaded properties
     * @param errors collected validation errors
     * @param key property key
     */
    private static void validateOptionalExistingDirectory(
            Properties config,
            List<String> errors,
            String key
    ) {
        String rawValue = config.getProperty(key);
        String normalized = normalizeConfigValue(rawValue);
        if (normalized == null || normalized.isBlank()) {
            return;
        }

        Path path = parsePath(rawValue, normalized, key, errors);
        if (path == null) {
            return;
        }

        if (!Files.isDirectory(path)) {
            errors.add(
                    "Property '" + key + "' must reference an existing directory: " + normalized
            );
        }
    }

    /**
     * Validates an optional integer property with a minimum/maximum range.
     *
     * @param config loaded properties
     * @param errors collected validation errors
     * @param key property key
     * @param min minimum valid value
     * @param max maximum valid value
     */
    private static void validateOptionalIntegerInRange(
            Properties config,
            List<String> errors,
            String key,
            int min,
            int max
    ) {
        String value = normalizeConfigValue(config.getProperty(key));
        if (value == null || value.isBlank()) {
            return;
        }

        try {
            int parsed = Integer.parseInt(value);
            if (parsed < min || parsed > max) {
                errors.add(
                        "Property '" + key + "' must be between "
                                + min + " and " + max + ", but was " + parsed + "."
                );
            }
        } catch (NumberFormatException e) {
            errors.add(
                    "Property '" + key + "' must be an integer, but was '" + value + "'."
            );
        }
    }

    /**
     * Validates a required integer property with a minimum/maximum range.
     *
     * @param config loaded properties
     * @param errors collected validation errors
     * @param key property key
     * @param min minimum valid value
     * @param max maximum valid value
     */
    private static void validateRequiredIntegerInRange(
            Properties config,
            List<String> errors,
            String key,
            int min,
            int max
    ) {
        String value = validateRequiredNonBlank(config, errors, key);
        if (value == null) {
            return;
        }

        try {
            int parsed = Integer.parseInt(value);
            if (parsed < min || parsed > max) {
                errors.add(
                        "Property '" + key + "' must be between "
                                + min + " and " + max + ", but was " + parsed + "."
                );
            }
        } catch (NumberFormatException e) {
            errors.add(
                    "Property '" + key + "' must be an integer, but was '" + value + "'."
            );
        }
    }

    /**
     * Validates an optional numeric property that must be greater than zero.
     *
     * @param config loaded properties
     * @param errors collected validation errors
     * @param key property key
     */
    private static void validateOptionalPositiveDouble(
            Properties config,
            List<String> errors,
            String key
    ) {
        String value = normalizeConfigValue(config.getProperty(key));
        if (value == null || value.isBlank()) {
            return;
        }

        try {
            double parsed = Double.parseDouble(value);
            if (parsed <= 0.0) {
                errors.add(
                        "Property '" + key + "' must be greater than 0, but was " + parsed + "."
                );
            }
        } catch (NumberFormatException e) {
            errors.add(
                    "Property '" + key + "' must be numeric, but was '" + value + "'."
            );
        }
    }

    /**
     * Validates an optional integer that must be non-negative.
     *
     * @param config loaded properties
     * @param errors collected validation errors
     * @param key property key
     */
    private static void validateOptionalNonNegativeInteger(
            Properties config,
            List<String> errors,
            String key
    ) {
        String value = normalizeConfigValue(config.getProperty(key));
        if (value == null || value.isBlank()) {
            return;
        }

        try {
            int parsed = Integer.parseInt(value);
            if (parsed < 0) {
                errors.add(
                        "Property '" + key + "' must be >= 0, but was " + parsed + "."
                );
            }
        } catch (NumberFormatException e) {
            errors.add(
                    "Property '" + key + "' must be an integer, but was '" + value + "'."
            );
        }
    }

    /**
     * Validates that an optional PID file path has an existing parent directory.
     *
     * @param config loaded properties
     * @param errors collected validation errors
     * @param key property key
     */
    private static void validateOptionalPidFileParent(
            Properties config,
            List<String> errors,
            String key
    ) {
        String rawValue = config.getProperty(key);
        String normalized = normalizeConfigValue(rawValue);

        if (normalized == null || normalized.isBlank()) {
            return;
        }

        Path path = parsePath(rawValue, normalized, key, errors);
        if (path == null) {
            return;
        }

        Path parent = path.getParent();
        if (parent != null && !Files.isDirectory(parent)) {
            errors.add(
                    "Parent directory for '" + key + "' does not exist: " + parent
            );
        }
    }

    /**
     * Validates a boolean property using strict true/false values.
     *
     * @param config loaded properties
     * @param errors collected validation errors
     * @param key property key
     * @param defaultValue fallback when value is absent/blank
     * @return parsed boolean value or default value
     */
    private static boolean readBoolean(
            Properties config,
            List<String> errors,
            String key,
            boolean defaultValue
    ) {
        String value = normalizeConfigValue(config.getProperty(key));
        if (value == null || value.isBlank()) {
            return defaultValue;
        }

        if ("true".equalsIgnoreCase(value)) {
            return true;
        }

        if ("false".equalsIgnoreCase(value)) {
            return false;
        }

        errors.add(
                "Property '" + key + "' must be 'true' or 'false', but was '" + value + "'."
        );
        return defaultValue;
    }

    /**
     * Parses a path value and reports invalid quoting/path syntax.
     *
     * @param rawValue raw property value
     * @param normalized normalized property value
     * @param key property key
     * @param errors collected validation errors
     * @return parsed path or {@code null} when invalid
     */
    private static Path parsePath(
            String rawValue,
            String normalized,
            String key,
            List<String> errors
    ) {
        if (hasMismatchedWrappingQuote(rawValue)) {
            errors.add(
                    "Property '" + key + "' has mismatched wrapping quotes: " + rawValue
            );
        }

        try {
            return Path.of(normalized);
        } catch (InvalidPathException e) {
            errors.add(
                    "Property '" + key + "' contains an invalid path: " + normalized
            );
            return null;
        }
    }

    /**
     * Detects mismatched leading/trailing single or double quote wrappers.
     *
     * @param rawValue raw property value
     * @return {@code true} when wrapper quotes are mismatched
     */
    private static boolean hasMismatchedWrappingQuote(String rawValue) {
        if (rawValue == null) {
            return false;
        }

        String trimmed = rawValue.trim();
        if (trimmed.isEmpty()) {
            return false;
        }

        boolean startsDouble = trimmed.startsWith("\"");
        boolean endsDouble = trimmed.endsWith("\"");

        boolean startsSingle = trimmed.startsWith("'");
        boolean endsSingle = trimmed.endsWith("'");

        return startsDouble != endsDouble || startsSingle != endsSingle;
    }

    /**
     * Trims config values and removes matching single/double quote wrappers.
     *
     * @param value raw config value
     * @return normalized value or {@code null}
     */
    private static String normalizeConfigValue(String value) {
        if (value == null) {
            return null;
        }

        String trimmed = value.trim();

        if ((trimmed.startsWith("\"") && trimmed.endsWith("\""))
                || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
            return trimmed.substring(1, trimmed.length() - 1);
        }

        return trimmed;
    }

    /**
     * Builds a single human-readable validation exception message.
     *
     * @param errors collected validation errors
     * @return formatted message
     */
    private static String buildValidationMessage(List<String> errors) {
        StringBuilder message = new StringBuilder(
                "Configuration validation failed (" + errors.size() + " issue(s)):"
        );

        for (int i = 0; i < errors.size(); i++) {
            message.append(System.lineSeparator())
                    .append(i + 1)
                    .append(". ")
                    .append(errors.get(i));
        }

        return message.toString();
    }
}
