<?php

namespace WPUltimateSecurity\UltimateSecurity\Infrastructure\Settings;

use WPUltimateSecurity\UltimateSecurity\Config\ConfigLoader;
use WPUltimateSecurity\UltimateSecurity\Support\Helpers\ApiKeyCryptoService;

/**
 * Settings Service
 *
 * High-level service for managing plugin settings. Provides a clean API
 * that coordinates between the repository, defaults provider, and encryption.
 *
 * This service is the recommended way to interact with settings as it:
 * - Handles encryption/decryption transparently
 * - Provides section-based operations
 * - Supports batch updates
 * - Fires appropriate WordPress hooks
 *
 * Usage:
 * ```php
 * $service = SettingsService::getInstance();
 *
 * // Get a setting
 * $value = $service->get('security.2fa_authentication.enabled');
 *
 * // Update a setting
 * $service->set('security.2fa_authentication.enabled', '1');
 *
 * // Update multiple settings
 * $service->update([
 *     'security.2fa_authentication.enabled' => '1',
 *     'security.login_limit.enable' => '1',
 * ]);
 *
 * // Get a section
 * $loginSettings = $service->getSection('login');
 *
 * // Reset a section to defaults
 * $service->resetSection('security.2fa_authentication');
 * ```
 *
 * @package WPUltimateSecurity\UltimateSecurity\Infrastructure\Settings
 * @since 2.0.0
 */
class SettingsService
{
    /**
     * Singleton instance.
     *
     * @var self|null
     */
    private static ?self $instance = null;

    /**
     * The settings repository.
     *
     * @var SettingsRepository
     */
    private SettingsRepository $repository;

    /**
     * Private constructor.
     */
    private function __construct()
    {
        $this->repository = SettingsRepository::getInstance();
    }

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

        return self::$instance;
    }

    /**
     * Get a setting value.
     *
     * @param string $key     The setting key (supports dot notation).
     * @param mixed  $default Default value if not found.
     *
     * @return mixed
     */
    public function get(string $key, mixed $default = null): mixed
    {
        return $this->repository->get($key, $default);
    }

    /**
     * Set a setting value.
     *
     * @param string $key   The setting key (supports dot notation).
     * @param mixed  $value The value to set.
     *
     * @return bool True on success.
     */
    public function set(string $key, mixed $value): bool
    {
        $oldValue = $this->get($key);
        $result = $this->repository->set($key, $value);

        if ($result) {
            do_action('ultimate_security_setting_changed', $key, $value, $oldValue);
        }

        return $result;
    }

    /**
     * Update multiple settings at once.
     *
     * @param array $settings Key-value pairs of settings to update.
     *
     * @return bool True on success.
     */
    public function update(array $settings): bool
    {
        $oldValues = [];
        foreach (array_keys($settings) as $key) {
            $oldValues[$key] = $this->get($key);
        }

        $result = $this->repository->setMultiple($settings);

        if ($result) {
            foreach ($settings as $key => $value) {
                do_action('ultimate_security_setting_changed', $key, $value, $oldValues[$key] ?? null);
            }
            do_action('ultimate_security_settings_updated', $settings, $oldValues);
        }

        return $result;
    }

    /**
     * Get all settings.
     *
     * @return array
     */
    public function all(): array
    {
        return $this->repository->all();
    }

    /**
     * Get a section of settings.
     *
     * @param string $section The section key (e.g., 'security', 'login').
     *
     * @return array|null The section settings or null if not found.
     */
    public function getSection(string $section): ?array
    {
        $value = $this->get($section);

        return is_array($value) ? $value : null;
    }

    /**
     * Update a section of settings.
     *
     * @param string $section  The section key.
     * @param array  $settings The settings for the section.
     *
     * @return bool True on success.
     */
    public function updateSection(string $section, array $settings): bool
    {
        $oldSection = $this->getSection($section);
        $result = $this->set($section, $settings);

        if ($result) {
            do_action('ultimate_security_section_updated', $section, $settings, $oldSection);
        }

        return $result;
    }

    /**
     * Get the default value for a setting.
     *
     * @param string $key The setting key (supports dot notation).
     *
     * @return mixed The default value or null.
     */
    public function getDefault(string $key): mixed
    {
        $defaults = ConfigLoader::loadDefaults();

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

    /**
     * Get all defaults.
     *
     * @return array
     */
    public function getDefaults(): array
    {
        return ConfigLoader::loadDefaults();
    }

    /**
     * Reset a setting to its default value.
     *
     * @param string $key The setting key.
     *
     * @return bool True on success.
     */
    public function resetToDefault(string $key): bool
    {
        $default = $this->getDefault($key);

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

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

    /**
     * Reset a section to defaults.
     *
     * @param string $section The section key.
     *
     * @return bool True on success.
     */
    public function resetSection(string $section): bool
    {
        return $this->repository->reset($section);
    }

    /**
     * Reset all settings to defaults.
     *
     * @return bool True on success.
     */
    public function resetAll(): bool
    {
        return $this->repository->reset();
    }

    /**
     * Check if a setting exists.
     *
     * @param string $key The setting key.
     *
     * @return bool
     */
    public function has(string $key): bool
    {
        return $this->repository->has($key);
    }

    /**
     * Check if a feature is enabled.
     *
     * Convenience method for checking boolean-like settings.
     *
     * @param string $key The setting key.
     *
     * @return bool
     */
    public function isEnabled(string $key): bool
    {
        $value = $this->get($key);

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

    /**
     * Enable a feature.
     *
     * @param string $key The setting key.
     *
     * @return bool True on success.
     */
    public function enable(string $key): bool
    {
        return $this->set($key, '1');
    }

    /**
     * Disable a feature.
     *
     * @param string $key The setting key.
     *
     * @return bool True on success.
     */
    public function disable(string $key): bool
    {
        return $this->set($key, '0');
    }

    /**
     * Refresh the settings cache.
     *
     * @return void
     */
    public function refresh(): void
    {
        $this->repository->refresh();
    }

    /**
     * Get raw settings (without defaults merged).
     *
     * @return array
     */
    public function getRaw(): array
    {
        return $this->repository->raw();
    }

    /**
     * Export settings for backup.
     *
     * @param bool $includeEncrypted Whether to include encrypted values.
     *
     * @return array
     */
    public function export(bool $includeEncrypted = false): array
    {
        $settings = $this->repository->raw();

        if (!$includeEncrypted) {
            $settings = $this->stripEncryptedValues($settings);
        }

        return [
            'version' => defined('ULTIMATE_SECURITY_VERSION') ? ULTIMATE_SECURITY_VERSION : '1.0.0',
            'exported_at' => gmdate('Y-m-d H:i:s'),
            'settings' => $settings,
        ];
    }

    /**
     * Import settings from a backup.
     *
     * @param array $data       The exported data.
     * @param bool  $merge      Whether to merge with existing settings.
     * @param bool  $encryptApi Whether to encrypt API keys.
     *
     * @return bool True on success.
     */
    public function import(array $data, bool $merge = true, bool $encryptApi = true): bool
    {
        if (!isset($data['settings']) || !is_array($data['settings'])) {
            return false;
        }

        $settings = $data['settings'];

        if ($encryptApi) {
            $settings = ApiKeyCryptoService::encrypt($settings);
        }

        if ($merge) {
            $current = $this->repository->raw();
            $settings = array_replace_recursive($current, $settings);
        }

        $result = update_option('ultimate_security_options', $settings);
        $this->refresh();

        if ($result) {
            do_action('ultimate_security_settings_imported', $data);
        }

        return $result;
    }

    /**
     * Get nested value using dot notation.
     *
     * @param array  $array   The array to search.
     * @param string $key     The dot-notation key.
     * @param mixed  $default Default value.
     *
     * @return mixed
     */
    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;
    }

    /**
     * Strip encrypted values from settings.
     *
     * @param array $settings The settings array.
     *
     * @return array Settings without encrypted values.
     */
    private function stripEncryptedValues(array $settings): array
    {
        $encryptedKeys = ['api_key', 'secret_key', 'private_key', 'password'];

        array_walk_recursive($settings, function (&$value, $key) use ($encryptedKeys) {
            foreach ($encryptedKeys as $encryptedKey) {
                if (str_contains(strtolower($key), $encryptedKey)) {
                    $value = '[REDACTED]';
                    break;
                }
            }
        });

        return $settings;
    }
}
