<?php
/**
 * Config Service
 *
 * Centralized configuration management with typed getters and validation.
 *
 * @package     WPUltimateSecurity\UltimateSecurity\Services
 * @since       2.0.0
 */

declare(strict_types=1);

namespace WPUltimateSecurity\UltimateSecurity\Services;

use WPUltimateSecurity\UltimateSecurity\Events\ConfigChangedEvent;
use WPUltimateSecurity\UltimateSecurity\Infrastructure\Settings\SettingsService;
use WPUltimateSecurity\UltimateSecurity\Support\Exceptions\ConfigValidationException;

/**
 * Centralized configuration access with type-safe getters and validation.
 *
 * Wraps SettingsService with a static interface and provides
 * typed getters for common configuration value types. Supports
 * schema-based validation and dispatches events on changes.
 *
 * @since 2.0.0
 */
class ConfigService
{
    /**
     * Registered configuration schemas for validation.
     *
     * @since 2.0.0
     *
     * @var array<string, array{type?: string, min?: int|float, max?: int|float, enum?: array<mixed>}>
     */
    private static array $schemas = [];
    /**
     * Register a validation schema for a configuration key.
     *
     * Schema options:
     * - 'type': string, int, integer, float, double, bool, boolean, array
     * - 'min': minimum value for numbers
     * - 'max': maximum value for numbers
     * - 'enum': array of allowed values
     *
     * @since 2.0.0
     *
     * @param string $key    Configuration key.
     * @param array  $schema Validation schema definition.
     *
     * @return void
     */
    public static function registerSchema(string $key, array $schema): void
    {
        self::$schemas[$key] = $schema;
    }

    /**
     * Validate a value against a registered schema.
     *
     * @since 2.0.0
     *
     * @param string $key   Configuration key.
     * @param mixed  $value Value to validate.
     *
     * @return array<int, string> Array of validation error messages (empty if valid).
     */
    public static function validate(string $key, mixed $value): array
    {
        $errors = [];
        $schema = self::$schemas[$key] ?? null;

        if (!$schema) {
            return $errors; // No schema, no validation
        }

        // Type validation
        if (isset($schema['type'])) {
            if (!self::validateType($value, $schema['type'])) {
                $errors[] = sprintf('Expected type %s', $schema['type']);
            }
        }

        // Min/max for numbers
        if (is_numeric($value)) {
            if (isset($schema['min']) && $value < $schema['min']) {
                $errors[] = sprintf('Value must be at least %s', $schema['min']);
            }
            if (isset($schema['max']) && $value > $schema['max']) {
                $errors[] = sprintf('Value must be at most %s', $schema['max']);
            }
        }

        // Enum validation
        if (isset($schema['enum']) && !in_array($value, $schema['enum'], true)) {
            $errors[] = sprintf('Value must be one of: %s', implode(', ', $schema['enum']));
        }

        return $errors;
    }

    /**
     * Get a config value.
     *
     * @since 2.0.0
     *
     * @param string $key     Configuration key (dot notation supported).
     * @param mixed  $default Default value if not found.
     *
     * @return mixed
     */
    public static function get(string $key, mixed $default = null): mixed
    {
        return SettingsService::getInstance()->get($key, $default);
    }

    /**
     * Get a string config value.
     *
     * @since 2.0.0
     *
     * @param string $key     Configuration key.
     * @param string $default Default value if not found or not a string.
     *
     * @return string
     */
    public static function getString(string $key, string $default = ''): string
    {
        $value = self::get($key, $default);
        return is_string($value) ? $value : $default;
    }

    /**
     * Get an integer config value.
     *
     * @since 2.0.0
     *
     * @param string $key     Configuration key.
     * @param int    $default Default value if not found or not numeric.
     *
     * @return int
     */
    public static function getInt(string $key, int $default = 0): int
    {
        $value = self::get($key, $default);
        return is_numeric($value) ? (int) $value : $default;
    }

    /**
     * Get a boolean config value.
     *
     * Interprets various truthy string values ('1', 'true', 'yes', 'on').
     *
     * @since 2.0.0
     *
     * @param string $key     Configuration key.
     * @param bool   $default Default value if not found.
     *
     * @return bool
     */
    public static function getBool(string $key, bool $default = false): bool
    {
        $value = self::get($key, $default);

        if (is_bool($value)) {
            return $value;
        }

        if (is_string($value)) {
            return in_array(strtolower($value), ['1', 'true', 'yes', 'on'], true);
        }

        if (is_numeric($value)) {
            return (bool) $value;
        }

        return $default;
    }

    /**
     * Get an array config value.
     *
     * @since 2.0.0
     *
     * @param string       $key     Configuration key.
     * @param array<mixed> $default Default value if not found or not an array.
     *
     * @return array<mixed>
     */
    public static function getArray(string $key, array $default = []): array
    {
        $value = self::get($key, $default);
        return is_array($value) ? $value : $default;
    }

    /**
     * Set a config value with validation and event dispatching.
     *
     * Validates the value against a registered schema (if any),
     * throws ConfigValidationException on validation failure,
     * and dispatches ConfigChangedEvent when the value changes.
     *
     * @since 2.0.0
     *
     * @param string $key   Configuration key.
     * @param mixed  $value Value to set.
     *
     * @return bool True on success.
     *
     * @throws ConfigValidationException When validation fails.
     */
    public static function set(string $key, mixed $value): bool
    {
        // Validate against schema
        $errors = self::validate($key, $value);
        if (!empty($errors)) {
            throw ConfigValidationException::forKey(esc_html($key), esc_html($value), esc_html($errors));
        }

        // Get old value for event
        $oldValue = self::get($key);

        // Set the value
        $result = SettingsService::getInstance()->set($key, $value);

        // Dispatch event if changed
        if ($result && $oldValue !== $value) {
            $event = new ConfigChangedEvent($key, $oldValue, $value);
            do_action('ultimate_security_config_changed', $event);
        }

        return $result;
    }

    /**
     * Check if a config key exists.
     *
     * @since 2.0.0
     *
     * @param string $key Configuration key.
     *
     * @return bool
     */
    public static function has(string $key): bool
    {
        return SettingsService::getInstance()->has($key);
    }

    /**
     * Check if a feature is enabled.
     *
     * Convenience method that checks for a '.enabled' suffixed key.
     *
     * @since 2.0.0
     *
     * @param string $key Base configuration key (without .enabled suffix).
     *
     * @return bool
     */
    public static function isEnabled(string $key): bool
    {
        return self::getBool($key . '.enabled', false);
    }

    /**
     * Validate a value against an expected type.
     *
     * @since 2.0.0
     *
     * @param mixed  $value The value to validate.
     * @param string $type  The expected type.
     *
     * @return bool True if value matches the expected type.
     */
    private static function validateType(mixed $value, string $type): bool
    {
        return match ($type) {
            'string' => is_string($value),
            'int', 'integer' => is_int($value) || (is_string($value) && ctype_digit($value)),
            'float', 'double' => is_float($value) || is_numeric($value),
            'bool', 'boolean' => is_bool($value) || in_array($value, ['0', '1', 0, 1, true, false], true),
            'array' => is_array($value),
            default => true,
        };
    }
}
