<?php

namespace WPUltimateSecurity\UltimateSecurity\Infrastructure\Settings;

use WPUltimateSecurity\UltimateSecurity\Support\Helpers\ApiKeyCryptoService;
use WPUltimateSecurity\UltimateSecurity\Support\Helpers\Utils;

/**
 * Settings Repository
 *
 * Provides cached access to plugin settings. This class implements a singleton-like
 * pattern using static caching to ensure settings are loaded from the database only
 * once per request, significantly reducing database queries.
 *
 * Usage:
 * ```php
 * $settings = SettingsRepository::getInstance();
 *
 * // Get a top-level setting
 * $security = $settings->get('security');
 *
 * // Get a nested setting using dot notation
 * $enabled = $settings->get('security.2fa_authentication.email_authentication.2fa_email_verification_enable');
 *
 * // Get with default value
 * $value = $settings->get('some.missing.key', 'default_value');
 *
 * // Set a value (persists to database)
 * $settings->set('security.login_limit.enable', '1');
 *
 * // Get all settings
 * $all = $settings->all();
 *
 * // Force refresh from database
 * $settings->refresh();
 * ```
 *
 * @package WPUltimateSecurity\UltimateSecurity\Infrastructure\Settings
 * @since 1.0.0
 */
class SettingsRepository implements SettingsInterface
{
    /**
     * WordPress option name for storing settings.
     *
     * @var string
     */
    private const OPTION_NAME = 'ultimate_security_options';

    /**
     * Cached settings array.
     *
     * @var array|null
     */
    private static ?array $cache = null;

    /**
     * Singleton instance.
     *
     * @var self|null
     */
    private static ?self $instance = null;

    /**
     * Flag to track if plugins/themes data has been populated.
     *
     * @var bool
     */
    private static bool $pluginsThemesPopulated = false;

    /**
     * Private constructor to enforce singleton pattern.
     */
    private function __construct()
    {
        // Private constructor
    }

    /**
     * Prevent cloning of the instance.
     *
     * @return void
     */
    private function __clone(): void
    {
        // Prevent cloning
    }

    /**
     * Prevent unserializing of the instance.
     *
     * @return void
     * @throws \Exception Always throws to prevent unserialization.
     */
    public function __wakeup(): void
    {
        throw new \Exception('Cannot unserialize singleton');
    }

    /**
     * Get the singleton instance.
     *
     * @return self The singleton instance.
     */
    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    /**
     * Get a settings value by key.
     *
     * Supports dot notation for nested keys (e.g., 'security.2fa_authentication.enabled').
     * If no key is provided, returns all settings.
     *
     * @param string $key     The settings key to retrieve. Use dot notation for nested keys.
     * @param mixed  $default Default value if key doesn't exist.
     *
     * @return mixed The settings value or default.
     */
    public function get(string $key, mixed $default = null): mixed
    {
        $settings = $this->loadSettings();

        if (empty($key)) {
            return $settings;
        }

        return $this->getNestedValue($settings, $key, $default);
    }

    /**
     * Set a settings value by key.
     *
     * Supports dot notation for nested keys. Changes are persisted to the database
     * and the cache is updated.
     *
     * IMPORTANT: Uses raw database values to preserve encrypted API keys.
     *
     * @param string $key   The settings key to set. Use dot notation for nested keys.
     * @param mixed  $value The value to set.
     *
     * @return bool True on success, false on failure.
     */
    public function set(string $key, mixed $value): bool
    {
        // Get RAW database values - do NOT use loadSettings() as it decrypts API keys
        // We need to preserve encrypted values when persisting changes
        $settings = $this->loadRawSettings();

        // Set the nested value
        $this->setNestedValue($settings, $key, $value);

        // Persist to database
        $result = update_option(self::OPTION_NAME, $settings);

        if ($result) {
            // Invalidate cache so next read gets fresh decrypted data
            self::$cache = null;
        }

        return $result;
    }

    /**
     * Set multiple settings values at once.
     *
     * IMPORTANT: Uses raw database values to preserve encrypted API keys.
     *
     * @param array $values Associative array of key => value pairs.
     *                      Keys can use dot notation.
     *
     * @return bool True on success, false on failure.
     */
    public function setMultiple(array $values): bool
    {
        // Get RAW database values - do NOT use loadSettings() as it decrypts API keys
        $settings = $this->loadRawSettings();

        foreach ($values as $key => $value) {
            $this->setNestedValue($settings, $key, $value);
        }

        // Persist to database
        $result = update_option(self::OPTION_NAME, $settings);

        if ($result) {
            // Invalidate cache so next read gets fresh decrypted data
            self::$cache = null;
        }

        return $result;
    }

    /**
     * Get all settings.
     *
     * Returns the complete settings array merged with defaults.
     *
     * @return array The complete settings array.
     */
    public function all(): array
    {
        return $this->loadSettings();
    }

    /**
     * Get only the raw database settings without defaults.
     *
     * Useful for export/import functionality.
     *
     * @return array Raw database settings.
     */
    public function raw(): array
    {
        return get_option(self::OPTION_NAME, []);
    }

    /**
     * Refresh the settings cache.
     *
     * Forces a reload of settings from the database on next access.
     * Also clears the plugins/themes populated flag to ensure fresh data.
     *
     * @return void
     */
    public function refresh(): void
    {
        self::$cache = null;
        self::$pluginsThemesPopulated = false;
    }

    /**
     * Check if a settings key exists.
     *
     * @param string $key The settings key to check. Supports dot notation.
     *
     * @return bool True if the key exists, false otherwise.
     */
    public function has(string $key): bool
    {
        $settings = $this->loadSettings();
        $value = $this->getNestedValue($settings, $key, $this);

        return $value !== $this;
    }

    /**
     * Delete a settings key.
     *
     * Removes the key from settings and persists to database.
     *
     * IMPORTANT: Uses raw database values to preserve encrypted API keys.
     *
     * @param string $key The settings key to delete. Supports dot notation.
     *
     * @return bool True on success, false on failure.
     */
    public function delete(string $key): bool
    {
        // Get RAW database values - do NOT use loadSettings() as it decrypts API keys
        $settings = $this->loadRawSettings();

        if (!$this->deleteNestedValue($settings, $key)) {
            return false;
        }

        $result = update_option(self::OPTION_NAME, $settings);

        if ($result) {
            // Invalidate cache so next read gets fresh decrypted data
            self::$cache = null;
        }

        return $result;
    }

    /**
     * Get the default options.
     *
     * @return array Default options array.
     */
    public function getDefaults(): array
    {
        return Utils::ultimate_security_get_default_options();
    }

    /**
     * Reset settings to defaults.
     *
     * Optionally reset only a specific section.
     *
     * IMPORTANT: Uses raw database values to preserve encrypted API keys when resetting sections.
     *
     * @param string|null $section Optional section to reset. If null, resets all.
     *
     * @return bool True on success, false on failure.
     */
    public function reset(?string $section = null): bool
    {
        $defaults = $this->getDefaults();

        if ($section !== null) {
            // Get RAW database values - do NOT use loadSettings() as it decrypts API keys
            $settings = $this->loadRawSettings();
            $defaultValue = $this->getNestedValue($defaults, $section);

            if ($defaultValue === null) {
                return false;
            }

            $this->setNestedValue($settings, $section, $defaultValue);
            $result = update_option(self::OPTION_NAME, $settings);

            if ($result) {
                // Invalidate cache so next read gets fresh decrypted data
                self::$cache = null;
            }

            return $result;
        }

        // Reset all settings
        $result = update_option(self::OPTION_NAME, $defaults);

        if ($result) {
            self::$cache = null;
            self::$pluginsThemesPopulated = false;
        }

        return $result;
    }

    /**
     * Load settings from cache or database.
     *
     * Merges database settings with defaults, decrypts API keys,
     * and populates dynamic plugin/theme data on first load.
     *
     * @return array The complete settings array.
     */
    private function loadSettings(): array
    {
        if (self::$cache !== null) {
            return self::$cache;
        }

        // Get settings from database
        $databaseSettings = get_option(self::OPTION_NAME, []);

        // Merge with defaults
        $defaults = $this->getDefaults();
        $settings = $this->arrayReplaceRecursive($defaults, $databaseSettings);

        // Decrypt API keys (handles both encrypted and plaintext data gracefully)
        $settings = ApiKeyCryptoService::decrypt($settings);

        // Populate plugins and themes data (only once per request)
        if (!self::$pluginsThemesPopulated) {
            $settings = $this->populatePluginsThemesData($settings);
            self::$pluginsThemesPopulated = true;
        }

        // Apply filter for extensibility
        $settings = apply_filters('ultimate_security_settings_loaded', $settings);

        // Cache the result
        self::$cache = $settings;

        return $settings;
    }

    /**
     * Load raw settings from database without decryption.
     *
     * Used by set/delete/reset methods to preserve encrypted API keys
     * when persisting changes to non-encrypted settings.
     *
     * @return array Raw settings with defaults merged (but NOT decrypted).
     */
    private function loadRawSettings(): array
    {
        // Get raw settings from database
        $databaseSettings = get_option(self::OPTION_NAME, []);

        // Merge with defaults (but do NOT decrypt)
        $defaults = $this->getDefaults();
        $settings = $this->arrayReplaceRecursive($defaults, $databaseSettings);

        return $settings;
    }

    /**
     * Populate plugin and theme functionality settings.
     *
     * Ensures all installed plugins and themes have entries in the settings.
     * Uses cached versions of get_plugins() and wp_get_themes() for performance.
     *
     * @param array $settings The current settings array.
     *
     * @return array Modified settings with plugin/theme data populated.
     */
    private function populatePluginsThemesData(array $settings): array
    {
        // Only populate if we're in admin context
        if (!is_admin()) {
            return $settings;
        }

        // Use cached plugins list from Utils for performance
        $allPlugins = Utils::get_plugins_cached();

        if (!is_array($settings['security']['updates']['plugins_functionalities'])) {
            $settings['security']['updates']['plugins_functionalities'] = [];
        }

        foreach ($allPlugins as $pluginFile => $pluginData) {
            if (!isset($settings['security']['updates']['plugins_functionalities'][$pluginFile])) {
                $settings['security']['updates']['plugins_functionalities'][$pluginFile] = [
                    'disable_updates' => false,
                    'auto_updates' => true,
                    'translation_updates' => true,
                    'hide' => false,
                ];
            }
        }

        // Use cached themes list from Utils for performance
        $allThemes = Utils::get_themes_cached();

        if (!is_array($settings['security']['updates']['themes_functionalities'])) {
            $settings['security']['updates']['themes_functionalities'] = [];
        }

        foreach ($allThemes as $themeSlug => $themeData) {
            if (!isset($settings['security']['updates']['themes_functionalities'][$themeSlug])) {
                $settings['security']['updates']['themes_functionalities'][$themeSlug] = [
                    'disable_updates' => 0,
                    'auto_updates' => 1,
                    'translation_updates' => 1,
                    'hide' => 0,
                ];
            }
        }

        return $settings;
    }

    /**
     * Get a nested value from an array using dot notation.
     *
     * @param array  $array   The array to search.
     * @param string $key     The dot-notation key (e.g., 'security.login.enable').
     * @param mixed  $default Default value if not found.
     *
     * @return mixed The value or default.
     */
    private function getNestedValue(array $array, string $key, mixed $default = null): mixed
    {
        $keys = explode('.', $key);
        $value = $array;

        foreach ($keys as $segment) {
            if (!is_array($value) || !array_key_exists($segment, $value)) {
                return $default;
            }

            $value = $value[$segment];
        }

        return $value;
    }

    /**
     * Set a nested value in an array using dot notation.
     *
     * @param array  $array The array to modify (passed by reference).
     * @param string $key   The dot-notation key (e.g., 'security.login.enable').
     * @param mixed  $value The value to set.
     *
     * @return void
     */
    private function setNestedValue(array &$array, string $key, mixed $value): void
    {
        $keys = explode('.', $key);
        $current = &$array;

        foreach ($keys as $i => $segment) {
            // If this is the last key, set the value
            if ($i === count($keys) - 1) {
                $current[$segment] = $value;
                return;
            }

            // Create the nested array if it doesn't exist
            if (!isset($current[$segment]) || !is_array($current[$segment])) {
                $current[$segment] = [];
            }

            $current = &$current[$segment];
        }
    }

    /**
     * Delete a nested value from an array using dot notation.
     *
     * @param array  $array The array to modify (passed by reference).
     * @param string $key   The dot-notation key (e.g., 'security.login.enable').
     *
     * @return bool True if the key was found and deleted, false otherwise.
     */
    private function deleteNestedValue(array &$array, string $key): bool
    {
        $keys = explode('.', $key);
        $current = &$array;

        // Navigate to the parent of the target key
        for ($i = 0; $i < count($keys) - 1; $i++) {
            $segment = $keys[$i];

            if (!isset($current[$segment]) || !is_array($current[$segment])) {
                return false;
            }

            $current = &$current[$segment];
        }

        // Delete the target key
        $lastKey = end($keys);

        if (!array_key_exists($lastKey, $current)) {
            return false;
        }

        unset($current[$lastKey]);

        return true;
    }

    /**
     * Recursively merge arrays, with later arrays overwriting earlier ones.
     *
     * This is similar to array_replace_recursive but handles edge cases better.
     *
     * @param array $base    The base array.
     * @param array $overlay The array to merge on top.
     *
     * @return array The merged array.
     */
    private function arrayReplaceRecursive(array $base, array $overlay): array
    {
        foreach ($overlay as $key => $value) {
            // If both values are arrays, recurse
            if (is_array($value) && isset($base[$key]) && is_array($base[$key])) {
                // Check if this is a sequential array (list) vs associative array
                if ($this->isSequentialArray($value) && $this->isSequentialArray($base[$key])) {
                    // For sequential arrays, replace entirely (don't merge)
                    $base[$key] = $value;
                } else {
                    // For associative arrays, merge recursively
                    $base[$key] = $this->arrayReplaceRecursive($base[$key], $value);
                }
            } else {
                // Otherwise, simply overwrite
                $base[$key] = $value;
            }
        }

        return $base;
    }

    /**
     * Check if an array is sequential (numeric keys starting from 0).
     *
     * @param array $array The array to check.
     *
     * @return bool True if sequential, false if associative.
     */
    private function isSequentialArray(array $array): bool
    {
        if (empty($array)) {
            return true;
        }

        return array_keys($array) === range(0, count($array) - 1);
    }

    /**
     * Static helper for quick access without instantiation.
     *
     * @param string $key     The settings key to retrieve.
     * @param mixed  $default Default value if key doesn't exist.
     *
     * @return mixed The settings value or default.
     */
    public static function getValue(string $key, mixed $default = null): mixed
    {
        return self::getInstance()->get($key, $default);
    }

    /**
     * Static helper for quick setting without instantiation.
     *
     * @param string $key   The settings key to set.
     * @param mixed  $value The value to set.
     *
     * @return bool True on success, false on failure.
     */
    public static function setValue(string $key, mixed $value): bool
    {
        return self::getInstance()->set($key, $value);
    }

    /**
     * Get statistics about cache usage.
     *
     * Useful for debugging and performance monitoring.
     *
     * @return array Cache statistics.
     */
    public static function getCacheStats(): array
    {
        return [
            'is_cached' => self::$cache !== null,
            'plugins_themes_populated' => self::$pluginsThemesPopulated,
            'cache_size' => self::$cache !== null ? strlen(serialize(self::$cache)) : 0,
        ];
    }
}
