<?php

namespace SimplyBook\Services;

use SimplyBook\Bootstrap\App;
use SimplyBook\Traits\LegacySave;
use SimplyBook\Exceptions\FormException;
use SimplyBook\Support\Helpers\Storages\GeneralConfig;

/**
 * @SuppressWarnings("PHPMD.ExcessiveClassComplexity")
 * @SuppressWarnings("PHPMD.CyclomaticComplexity")
 * @SuppressWarnings("PHPMD.NPathComplexity") Reducing complexity for this class
 * should be done with caution but should still be done in a later stage.
 */
class DesignSettingsService
{
    use LegacySave;

    protected GeneralConfig $config;

    public function __construct(GeneralConfig $config)
    {
        $this->config = $config;
    }

    /**
     * Lazy-loaded theme color service for WordPress color palette extraction.
     * Provides default colors when users haven't set preferences.
     *
     */
    private ?ThemeColorService $themeColorService = null;

    /**
     * The key for the design settings in the WordPress options table.
     * This is used to store and retrieve the design settings.
     */
    private string $designOptionsKey = 'simplybook_design_settings';

    /**
     * The key map for legacy design settings. Used for upgrading legacy
     * design settings.
     */
    protected array $legacyKeyMap = [
        'template' => 'theme',
        'themeparams' => 'theme_settings',
    ];

    /**
     * Never save these setting keys.
     */
    protected array $blockList = [
        'withValues',
    ];

    /**
     * Get the design settings from the WordPress options table.
     * @uses wp_cache_get
     * @uses wp_cache_set Set the cache for 60 seconds.
     */
    public function getDesignOptions(): array
    {
        $found = false;
        $cacheName = 'design_settings';
        $cacheValue = wp_cache_get($cacheName, 'simplybook', false, $found);

        if ($found && is_array($cacheValue)) {
            return $cacheValue;
        }

        $designOptions = get_option($this->designOptionsKey, []);
        if (!is_array($designOptions)) {
            $designOptions = [];
        }

        $designOptions['server'] = $this->getServerURL();

        $configCanBeLoaded = (doing_action('init') || did_action('init'));
        if ($configCanBeLoaded === false) {
            return $designOptions; // Prevents translation issues, dont cache
        }

        // Append default values from the design config, prioritize saved values
        $designOptions = array_merge($this->getDefaultDesignSettings(), $designOptions);

        wp_cache_set($cacheName, $designOptions, 'simplybook', 60);
        return $designOptions;
    }

    /**
     * Saves the given array as the design settings in the WordPress options
     * table. This method will overwrite any existing design settings, it does
     * not do any checks.
     */
    public function saveAsDesignOptions(array $designSettings): bool
    {
        if (empty($designSettings)) {
            return false;
        }

        return update_option($this->designOptionsKey, $designSettings);
    }

    /**
     * Handle the legacy design upgrade. This method will take the legacy
     * design settings and convert them to the new format. It does not retain
     * the 'predefined' key, as it is not used in the new format. This method
     * will also remove the obsolete theme settings with key:
     * simplybookMePl_widget_settings
     */
    public function handleLegacyDesignUpgrade(): void
    {
        $legacyDesignSettings = $this->get_config_obsolete('widget_settings');

        foreach ($this->legacyKeyMap as $legacyKey => $currentKey) {
            if (empty($legacyDesignSettings[$legacyKey])) {
                continue;
            }
            $legacyDesignSettings[$currentKey] = $legacyDesignSettings[$legacyKey];
            unset($legacyDesignSettings[$legacyKey]);
        }

        unset($legacyDesignSettings['predefined']);

        update_option($this->designOptionsKey, $legacyDesignSettings);
        delete_option('simplybookMePl_widget_settings');
    }

    /**
     * Recursively merge the saved settings with the existing design settings.
     * This method ensures that existing values, that are not present in the
     * saved settings, are kept. Otherwise, the saved settings will override the
     * existing values. Missing keys in the savedSettings can occur for
     * design settings because not all theme settings apply for each theme.
     */
    public function updateOrRetainDesignSettings(array $saveAsDesignSettings, array $designSettings = []): array
    {
        $currentSettings = ($designSettings ?: $this->getDesignOptions());
        if (empty($currentSettings)) {
            return $saveAsDesignSettings;
        }

        foreach ($saveAsDesignSettings as $key => $value) {
            if (in_array($key, $this->blockList, true)) {
                continue;
            }

            if (is_array($value)) {
                if (!isset($currentSettings[$key]) || !is_array($currentSettings[$key])) {
                    $currentSettings[$key] = [];
                }
                $currentSettings[$key] = $this->updateOrRetainDesignSettings($value, $currentSettings[$key]);
                continue;
            }

            $currentSettings[$key] = (is_bool($value) ? $value : sanitize_text_field($value));
        }

        return $currentSettings;
    }

    /**
     * Validate the settings based on the config. This method will throw an
     * exception if the settings do not match the config.
     * @throws \Exception
     */
    public function validateSettings(array $settings): bool
    {
        $errors = [];

        $designConfiguration = $this->config->get('fields.design');

        foreach ($settings as $key => $value) {
            if (empty($designConfiguration[$key])) {
                continue; // No config so no validating. We manage the config so this is safe enough.
            }

            $config = $designConfiguration[$key];

            // No type so no validating. We manage the config so this is safe enough.
            if (empty($config['type'])) {
                continue;
            }

            // No validation callback so no validating. We manage the config so this is safe enough.
            if (!empty($config['validate']) && !is_callable($config['validate'])) {
                continue;
            }

            $invalid = false;
            $errorMessage = __('Invalid value for setting', 'simplybook') . ': ' . ($config['label'] ?? $config['id']);

            // Saved value does not match regex
            if (!empty($config['regex']) && (preg_match($config['regex'], $value) !== 1)) {
                $invalid = true;
            }

            // Saved value is not one of our options
            if (($config['type'] === 'select') && !isset($config['options'][$value])) {
                $invalid = true;
            }

            // No hex color received from colorpicker
            if (($config['type'] === 'colorpicker') && empty(sanitize_hex_color($value))) {
                $invalid = true;
            }

            // If email is not empty, but not a valid email
            if (($config['type'] === 'email') && !empty($value) && !is_email($value)) {
                $invalid = true;
            }

            // If url is not empty, but not a valid url
            if (($config['type'] === 'url') && !empty($value) && !filter_var($value, FILTER_VALIDATE_URL)) {
                $invalid = true;
            }

            // If number is not empty, but not a valid number
            if (($config['type'] === 'number') && !empty($value) && !is_numeric($value)) {
                $invalid = true;
            }

            // If text is not empty, but not a valid string
            if (($config['type'] === 'text') && (empty(sanitize_text_field($value)) || !is_string($value))) {
                $invalid = true;
            }

            // Validate via the callable function
            if (!empty($config['validate']) && is_callable($config['validate'])) {
                $result = call_user_func($config['validate'], $value);
                if ($result !== true) {
                    $invalid = true;
                }
            }

            if ($invalid) {
                $errors[] = [
                    'key' => $key,
                    'message' => $errorMessage,
                ];
            }
        }

        if (!empty($errors)) {
            throw (new FormException())->setErrors($errors);
        }

        return true;
    }

    /**
     * Get fallback settings for the widget. These are based on the default
     * values from the design.php config file. Colors can be set when the widget
     * is loaded in the onboarding {@see WidgetEndpoint::getPreviewWidget}
     *
     * @internal ONLY use this method if the user has not set any preferences
     * yet. This can be the case in the onboarding process.
     */
    public function getFallbackSettings(string $primary = '', string $secondary = '', string $active = ''): array
    {
        $defaultDesignSettings = $this->getDefaultDesignSettings($primary, $secondary, $active);
        return array_merge($defaultDesignSettings, [
            'server' => $this->getServerURL(),
            'theme' => 'default',
            'predefined' => [],
        ]);
    }

    /**
     * Get theme color service with lazy initialization. Creates instance only
     * when needed for efficient resource usage.
     */
    public function getThemeColorService(): ThemeColorService
    {
        if ($this->themeColorService instanceof ThemeColorService === false) {
            $this->themeColorService = App::getInstance()->get(ThemeColorService::class);
        }

        return $this->themeColorService;
    }

    /**
     * Get the default design settings from the design.php config file. The
     * color parameters can be used to override the default values for primary,
     * secondary and active colors. This is used in the onboarding process when
     * the user sets colors for the widget.
     *
     * @internal Do NOT use this method before the `init` hook.
     */
    private function getDefaultDesignSettings(string $primary = '', string $secondary = '', string $active = ''): array
    {
        $designConfig = $this->config->get('fields.design');
        $defaultDesignSettings = [];

        // Get theme colors if no specific colors are provided
        if (empty($primary) && empty($secondary) && empty($active)) {
            $themeColors = $this->getThemeColorService()->getThemeColors();
            $primary = $themeColors['primary'];
            $secondary = $themeColors['secondary'];
            $active = $themeColors['active'];
        }

        foreach ($designConfig as $settingID => $config) {
            if (isset($config['default'])) {
                $defaultDesignSettings[$settingID] = $config['default'];
            }

            // This could be the theme_settings config for example
            if (isset($config['sub_settings'])) {
                foreach ($config['sub_settings'] as $subSetting) {
                    if (empty($subSetting['id'])) {
                        continue;
                    }

                    $subSettingID = $subSetting['id'];

                    // First set the default value from the config
                    if (isset($subSetting['default'])) {
                        $defaultDesignSettings[$settingID][$subSettingID] = $subSetting['default'];
                    }

                    // Override sub setting value when marked as primary and
                    // primary color is set
                    if (isset($subSetting['is_primary']) && $subSetting['is_primary'] && !empty($primary)) {
                        $defaultDesignSettings[$settingID][$subSettingID] = $primary;
                    }

                    // Override sub setting value when marked as secondary and
                    // secondary color is set
                    if (isset($subSetting['is_secondary']) && $subSetting['is_secondary'] && !empty($secondary)) {
                        $defaultDesignSettings[$settingID][$subSettingID] = $secondary;
                    }

                    // Override sub setting value when marked as active and
                    // active color is set
                    if (isset($subSetting['is_active']) && $subSetting['is_active'] && !empty($active)) {
                        $defaultDesignSettings[$settingID][$subSettingID] = $active;
                    }
                }
            }
        }

        return $defaultDesignSettings;
    }

    /**
     * Get the server URL
     */
    public function getServerURL(): string
    {
        // Setting the validation to false prevents exceeding the maximum
        // execution time when the server URL is not set.
        $domain = $this->get_domain(false);
        $login = get_option('simplybook_company_login', '');

        if (empty($login)) {
            return '';
        }

        return "https://$login.$domain";
    }
}
