<?php

/**
 * Easy Setup REST Controller
 *
 * Handles export and import of plugin settings with granular section control.
 *
 * @package ArraySubs\Features\EasySetup\REST
 * @since 1.8.0
 */

namespace ArraySubs\Features\EasySetup\REST;

defined('ABSPATH') || exit;

use ArraySubs\Supports\BaseRestController;
use ArraySubs\Supports\Config;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;

/**
 * Setup REST Controller for export/import settings.
 */
class SetupController extends BaseRestController
{
    /**
     * Module version for export format compatibility.
     *
     * @var string
     */
    const MODULE_VERSION = '1.1.0';

    /**
     * Virtual section IDs allowed for import.
     *
     * @var array<string>
     */
    const ALLOWED_SECTIONS = [
        'subscription_settings',
        'retention_flow',
        'store_credit',
        'myaccount_builder',
        'checkout_builder',
        'members_access',
        'emails',
        'profile_fields',
    ];

    /**
     * Maps virtual sections to sub-keys within arraysubs_settings.
     *
     * @var array<string, array<string>>
     */
    const SETTINGS_KEY_MAP = [
        'retention_flow'   => ['cancellation'],
        'store_credit'     => ['store_credit'],
        'checkout_builder' => ['checkout_builder'],
        'members_access'   => ['members_access'],
        'emails'           => ['emails'],
    ];

    /**
     * Maps virtual sections to standalone option keys.
     *
     * @var array<string, array<string>>
     */
    const OPTION_KEY_MAP = [
        'myaccount_builder' => ['arraysubs_myaccount_menu_config'],
        'profile_fields'    => ['arraysubs_profile_fields_config', 'arraysubs_avatar_settings'],
    ];

    /**
     * WooCommerce email IDs managed by the plugin.
     *
     * @var array<string>
     */
    const WC_EMAIL_IDS = [
        'arraysubs_admin_new_subscription',
        'arraysubs_admin_payment_failed',
        'arraysubs_admin_subscription_cancelled',
        'arraysubs_auto_downgrade',
        'arraysubs_card_expiring',
        'arraysubs_new_subscription',
        'arraysubs_payment_failed',
        'arraysubs_payment_successful',
        'arraysubs_renewal_invoice',
        'arraysubs_renewal_requires_verification',
        'arraysubs_renewal_reminder',
        'arraysubs_retention_discount_accepted',
        'arraysubs_subscription_cancelled',
        'arraysubs_subscription_expired',
        'arraysubs_subscription_on_hold',
        'arraysubs_subscription_reactivated',
        'arraysubs_trial_converted',
        'arraysubs_trial_started',
        'arraysubs_credit_added',
        'arraysubs_credit_expired',
        'arraysubs_credit_expiring',
        'arraysubs_credit_used',
    ];

    /**
     * Sensitive setting paths to strip from exports.
     *
     * @var array<string>
     */
    const SENSITIVE_PATHS = [
        'automatic_payments.paypal_client_id',
        'automatic_payments.paypal_client_secret',
    ];

    /**
     * Customer email keys managed by the setup wizard.
     *
     * @var array<string>
     */
    const WIZARD_CUSTOMER_EMAILS = [
        'new_subscription',
        'subscription_activated',
        'subscription_on_hold',
        'subscription_cancelled',
        'subscription_expired',
        'auto_downgrade',
        'renewal_upcoming',
        'renewal_invoice',
        'renewal_payment_received',
        'payment_failed',
        'trial_started',
        'trial_ending',
        'trial_converted',
        'expiring_soon',
    ];

    /**
     * Email keys enabled when the wizard uses the essential preset.
     *
     * @var array<string>
     */
    const WIZARD_ESSENTIAL_EMAILS = [
        'new_subscription',
        'renewal_invoice',
        'renewal_payment_received',
        'payment_failed',
        'subscription_cancelled',
        'subscription_expired',
        'trial_started',
        'trial_converted',
    ];

    /**
     * Email keys enabled when the wizard uses the minimal preset.
     *
     * @var array<string>
     */
    const WIZARD_MINIMAL_EMAILS = [
        'payment_failed',
        'subscription_cancelled',
    ];

    /**
     * Register REST API routes.
     *
     * @return void
     */
    public function registerRoutes(): void
    {
        \register_rest_route(
            $this->namespace,
            '/easy-setup/export',
            [
                'methods'             => 'GET',
                'callback'            => [$this, 'exportSettings'],
                'permission_callback' => [$this, 'checkPermission'],
            ]
        );

        \register_rest_route(
            $this->namespace,
            '/easy-setup/import',
            [
                'methods'             => 'POST',
                'callback'            => [$this, 'importSettings'],
                'permission_callback' => [$this, 'checkPermission'],
                'args'                => [
                    'data'     => [
                        'required'    => true,
                        'type'        => 'object',
                        'description' => \esc_html__('The exported settings JSON object', 'arraysubs'),
                    ],
                    'sections' => [
                        'required'    => true,
                        'type'        => 'array',
                        'description' => \esc_html__('Option keys to import', 'arraysubs'),
                        'items'       => [
                            'type' => 'string',
                        ],
                    ],
                ],
            ]
        );

        \register_rest_route(
            $this->namespace,
            '/easy-setup/wizard',
            [
                'methods'             => 'POST',
                'callback'            => [$this, 'applyWizard'],
                'permission_callback' => [$this, 'checkPermission'],
                'args'                => [
                    'answers' => [
                        'required'    => true,
                        'type'        => 'object',
                        'description' => \esc_html__('Wizard answer payload', 'arraysubs'),
                    ],
                ],
            ]
        );
    }

    /**
     * Export all plugin settings.
     *
     * @return WP_REST_Response
     */
    public function exportSettings(): WP_REST_Response
    {
        $option_keys = [
            'arraysubs_settings',
            'arraysubs_profile_fields_config',
            'arraysubs_avatar_settings',
            'arraysubs_myaccount_menu_config',
        ];

        $options = [];

        foreach ($option_keys as $key) {
            $value = \get_option($key, null);
            if (null !== $value) {
                $options[$key] = $value;
            }
        }

        if (isset($options['arraysubs_settings'])) {
            $options['arraysubs_settings'] = $this->stripSensitiveData($options['arraysubs_settings']);
        }

        $wc_emails = [];
        foreach (self::WC_EMAIL_IDS as $email_id) {
            $option_key = 'woocommerce_' . $email_id . '_settings';
            $value      = \get_option($option_key, null);
            if (null !== $value && is_array($value)) {
                $wc_emails[$option_key] = $value;
            }
        }

        if (! empty($wc_emails)) {
            $options['wc_email_settings'] = $wc_emails;
        }

        $export = [
            'meta'    => $this->buildExportMeta(),
            'options' => $options,
        ];

        return arraysubs_rest_response(
            $export,
            200,
            \esc_html__('Settings exported successfully', 'arraysubs')
        );
    }

    /**
     * Import selected settings sections.
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response|WP_Error
     */
    public function importSettings(WP_REST_Request $request)
    {
        $data     = $request->get_param('data');
        $sections = $request->get_param('sections');

        $validation = $this->validateImportPayload($data, $sections);
        if (\is_wp_error($validation)) {
            return $validation;
        }

        $options  = $data['options'];
        $imported = [];
        $skipped  = [];
        $warnings = [];

        $main_settings   = isset($options['arraysubs_settings']) && is_array($options['arraysubs_settings'])
            ? $options['arraysubs_settings']
            : [];
        $current_settings = arraysubs_get_settings();
        $merged_settings  = $current_settings;
        $settings_changed = false;

        foreach (self::ALLOWED_SECTIONS as $section) {
            if (! in_array($section, $sections, true)) {
                $skipped[] = $section;
                continue;
            }

            $result = $this->importSection($section, $main_settings, $options, $merged_settings);

            if ($result['imported']) {
                $imported[] = $section;
                if ($result['settings_changed']) {
                    $merged_settings = $result['merged_settings'];
                    $settings_changed = true;
                }
            } else {
                $skipped[] = $section;
            }
        }

        if ($settings_changed) {
            $sanitized = $this->sanitizeMainSettings($merged_settings);
            arraysubs_save_settings($sanitized);
        }

        if (in_array('myaccount_builder', $imported, true)) {
            if (function_exists('arraysubs_schedule_myaccount_editor_flush')) {
                arraysubs_schedule_myaccount_editor_flush();
            }
        }

        if ($this->hasProSettings($main_settings) && ! $this->isProActive()) {
            $warnings[] = \esc_html__(
                'Pro plugin is not active — pro settings imported but dormant',
                'arraysubs'
            );
        }

        Config::add('settings', arraysubs_get_settings());

        return arraysubs_rest_response(
            [
                'imported' => $imported,
                'skipped'  => $skipped,
                'warnings' => $warnings,
            ],
            200,
            \esc_html__('Settings imported successfully', 'arraysubs')
        );
    }

    /**
     * Apply setup wizard answers to the current settings.
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response|WP_Error
     */
    public function applyWizard(WP_REST_Request $request)
    {
        $answers = $request->get_param('answers');

        if (! \is_array($answers)) {
            return new WP_Error(
                'invalid_answers',
                \esc_html__('Invalid wizard answers.', 'arraysubs'),
                ['status' => 400]
            );
        }

        $sanitized_answers = $this->sanitizeWizardAnswers($answers);
        if (\is_wp_error($sanitized_answers)) {
            return $sanitized_answers;
        }

        $wizard_settings    = $this->buildSettingsFromAnswers($sanitized_answers);
        $merged_settings    = \array_replace_recursive(arraysubs_get_settings(), $wizard_settings);
        $sanitized_settings = $this->sanitizeMainSettings($merged_settings);
        $saved              = arraysubs_save_settings($sanitized_settings);

        if (! $saved) {
            return arraysubs_rest_response(
                [],
                500,
                \esc_html__('Failed to apply setup wizard settings', 'arraysubs')
            );
        }

        Config::add('settings', arraysubs_get_settings());

        $applied_keys = arraysubs_get_changed_setting_keys([], $wizard_settings);
        $message      = \sprintf(
            /* translators: %d: number of settings paths configured by the setup wizard. */
            \esc_html__('Setup wizard applied successfully. %d settings configured.', 'arraysubs'),
            \count($applied_keys)
        );

        return arraysubs_rest_response(
            [
                'applied_keys'   => $applied_keys,
                'settings_count' => \count($applied_keys),
                'message'        => $message,
            ],
            200,
            $message
        );
    }

    /**
     * Import a single virtual section.
     *
     * @param string $section         Virtual section ID.
     * @param array  $main_settings   Imported arraysubs_settings data.
     * @param array  $options         Full imported options.
     * @param array  $merged_settings Current merged arraysubs_settings.
     * @return array{imported: bool, settings_changed: bool, merged_settings: array}
     */
    private function importSection(
        string $section,
        array $main_settings,
        array $options,
        array $merged_settings
    ): array {
        $result = [
            'imported'         => false,
            'settings_changed' => false,
            'merged_settings'  => $merged_settings,
        ];

        // Sections that map to sub-keys within arraysubs_settings.
        if (isset(self::SETTINGS_KEY_MAP[$section])) {
            $found = false;
            foreach (self::SETTINGS_KEY_MAP[$section] as $sub_key) {
                if (array_key_exists($sub_key, $main_settings)) {
                    $result['merged_settings'][$sub_key] = $main_settings[$sub_key];
                    $found = true;
                }
            }

            if ($found) {
                $result['imported']         = true;
                $result['settings_changed'] = true;
            }

            // Emails section also includes WC email options.
            if ('emails' === $section) {
                $wc_imported = $this->importWcEmailSettings($options);
                if ($wc_imported) {
                    $result['imported'] = true;
                }
            }

            return $result;
        }

        // Sections that map to standalone option keys.
        if (isset(self::OPTION_KEY_MAP[$section])) {
            $found = false;
            foreach (self::OPTION_KEY_MAP[$section] as $option_key) {
                if (! isset($options[$option_key]) || ! is_array($options[$option_key])) {
                    continue;
                }

                $sanitized = $this->sanitizeStandaloneOption($option_key, $options[$option_key]);
                \update_option($option_key, $sanitized);
                $found = true;
            }

            if ($found) {
                $result['imported'] = true;
            }

            return $result;
        }

        // subscription_settings: everything else in arraysubs_settings.
        if ('subscription_settings' === $section) {
            $claimed_keys = array_merge(...array_values(self::SETTINGS_KEY_MAP));
            $found        = false;

            foreach ($main_settings as $key => $value) {
                if (in_array($key, $claimed_keys, true)) {
                    continue;
                }
                $result['merged_settings'][$key] = $value;
                $found = true;
            }

            if ($found) {
                $result['imported']         = true;
                $result['settings_changed'] = true;
            }

            return $result;
        }

        return $result;
    }

    /**
     * Get default wizard answers.
     *
     * @return array<string, mixed>
     */
    private function getDefaultWizardAnswers(): array
    {
        return [
            'business_type'                => 'other',
            'billing_cycle'                => 'monthly',
            'billing_interval'             => '1',
            'billing_period'               => 'month',
            'plan_structure'               => 'single',
            'offer_trials'                 => 'no',
            'trial_require_payment'        => 'yes',
            'one_trial_per_customer'       => 'yes',
            'grace_period'                 => 'standard',
            'custom_grace_active_days'     => '3',
            'custom_grace_hold_days'       => '7',
            'invoice_timing'               => '6_hours',
            'billing_flexibility'          => [],
            'max_skips'                    => '3',
            'skip_cutoff_days'             => '2',
            'max_pause_days'               => '30',
            'max_pauses'                   => '2',
            'multiple_subscriptions'       => 'allow',
            'auto_migrate'                 => 'yes',
            'mixed_cart'                   => 'yes',
            'one_click_checkout'           => 'default',
            'disable_cart_page'            => 'no',
            'auto_create_account'          => 'yes',
            'plan_switching'               => 'disabled',
            'proration_type'               => 'prorate_immediately',
            'cancel_timing'                => 'immediately',
            'require_cancel_reason'        => 'yes',
            'retention_offers'             => 'no',
            'retention_offer_types'        => ['discount'],
            'retention_discount_percent'   => '20',
            'retention_discount_cycles'    => '3',
            'refund_policy'                => 'none',
            'enable_access_control'        => 'no',
            'restricted_redirect'          => 'redirect',
            'restricted_redirect_url'      => '/pricing',
            'require_login'                => 'yes',
            'email_level'                  => 'all',
            'custom_emails'                => [
                'new_subscription',
                'subscription_activated',
                'subscription_on_hold',
                'subscription_cancelled',
                'subscription_expired',
                'renewal_upcoming',
                'renewal_invoice',
                'renewal_payment_received',
                'payment_failed',
            ],
            'renewal_reminder_days'        => '3',
            'admin_emails'                 => [
                'admin_new_subscription',
                'admin_cancelled',
                'admin_payment_failed',
            ],
            'additional_features'          => [],
            'store_credit_auto_apply'      => 'yes',
            'store_credit_expiration'      => '0',
            'feature_manager_product_page' => 'yes',
            'multi_login_max_sessions'     => '1',
            'dashboard_redirect'           => 'my_account',
        ];
    }

    /**
     * Sanitize wizard answer payload.
     *
     * @param array<string, mixed> $answers Raw wizard answers.
     * @return array<string, mixed>|WP_Error
     */
    private function sanitizeWizardAnswers(array $answers)
    {
        $defaults = $this->getDefaultWizardAnswers();
        $errors   = [];

        foreach (\array_diff(\array_keys($answers), \array_keys($defaults)) as $unknown_key) {
            $errors[\sanitize_key((string) $unknown_key)] = \esc_html__('This answer key is not allowed.', 'arraysubs');
        }

        $sanitized = $defaults;

        $sanitized['business_type'] = $this->sanitizeEnum(
            $answers['business_type'] ?? $defaults['business_type'],
            ['saas', 'box', 'membership', 'content', 'services', 'nonprofit', 'other'],
            $defaults['business_type']
        );
        $sanitized['billing_cycle'] = $this->sanitizeEnum(
            $answers['billing_cycle'] ?? $defaults['billing_cycle'],
            ['weekly', 'monthly', 'yearly', 'custom'],
            $defaults['billing_cycle']
        );
        $sanitized['billing_interval'] = (string) $this->sanitizeInt(
            $answers['billing_interval'] ?? $defaults['billing_interval'],
            1,
            365
        );
        $sanitized['billing_period'] = $this->sanitizeEnum(
            $answers['billing_period'] ?? $defaults['billing_period'],
            ['day', 'week', 'month', 'year'],
            $defaults['billing_period']
        );
        $sanitized['plan_structure'] = $this->sanitizeEnum(
            $answers['plan_structure'] ?? $defaults['plan_structure'],
            ['single', 'multiple'],
            $defaults['plan_structure']
        );
        $sanitized['offer_trials'] = $this->sanitizeEnum(
            $answers['offer_trials'] ?? $defaults['offer_trials'],
            ['yes', 'no'],
            $defaults['offer_trials']
        );
        $sanitized['trial_require_payment'] = $this->sanitizeEnum(
            $answers['trial_require_payment'] ?? $defaults['trial_require_payment'],
            ['yes', 'no'],
            $defaults['trial_require_payment']
        );
        $sanitized['one_trial_per_customer'] = $this->sanitizeEnum(
            $answers['one_trial_per_customer'] ?? $defaults['one_trial_per_customer'],
            ['yes', 'no'],
            $defaults['one_trial_per_customer']
        );
        $sanitized['grace_period'] = $this->sanitizeEnum(
            $answers['grace_period'] ?? $defaults['grace_period'],
            ['strict', 'standard', 'lenient', 'custom'],
            $defaults['grace_period']
        );
        $sanitized['custom_grace_active_days'] = (string) $this->sanitizeInt(
            $answers['custom_grace_active_days'] ?? $defaults['custom_grace_active_days'],
            0,
            30
        );
        $sanitized['custom_grace_hold_days'] = (string) $this->sanitizeInt(
            $answers['custom_grace_hold_days'] ?? $defaults['custom_grace_hold_days'],
            1,
            60
        );
        $sanitized['invoice_timing'] = $this->sanitizeEnum(
            $answers['invoice_timing'] ?? $defaults['invoice_timing'],
            ['6_hours', '1_day', '3_days'],
            $defaults['invoice_timing']
        );
        $sanitized['billing_flexibility'] = $this->sanitizeWizardSelectionArray(
            $answers['billing_flexibility'] ?? $defaults['billing_flexibility'],
            ['skip', 'pause']
        );
        $sanitized['max_skips'] = $this->sanitizeEnum(
            $answers['max_skips'] ?? $defaults['max_skips'],
            ['1', '2', '3', '5'],
            $defaults['max_skips']
        );
        $sanitized['skip_cutoff_days'] = $this->sanitizeEnum(
            $answers['skip_cutoff_days'] ?? $defaults['skip_cutoff_days'],
            ['0', '2', '5', '7'],
            $defaults['skip_cutoff_days']
        );
        $sanitized['max_pause_days'] = $this->sanitizeEnum(
            $answers['max_pause_days'] ?? $defaults['max_pause_days'],
            ['14', '30', '60', '90'],
            $defaults['max_pause_days']
        );
        $sanitized['max_pauses'] = $this->sanitizeEnum(
            $answers['max_pauses'] ?? $defaults['max_pauses'],
            ['1', '2', '3', '5'],
            $defaults['max_pauses']
        );
        $sanitized['multiple_subscriptions'] = $this->sanitizeEnum(
            $answers['multiple_subscriptions'] ?? $defaults['multiple_subscriptions'],
            ['allow', 'one_per_customer', 'one_per_product'],
            $defaults['multiple_subscriptions']
        );
        $sanitized['auto_migrate'] = $this->sanitizeEnum(
            $answers['auto_migrate'] ?? $defaults['auto_migrate'],
            ['yes', 'no'],
            $defaults['auto_migrate']
        );
        $sanitized['mixed_cart'] = $this->sanitizeEnum(
            $answers['mixed_cart'] ?? $defaults['mixed_cart'],
            ['yes', 'no'],
            $defaults['mixed_cart']
        );
        $sanitized['one_click_checkout'] = $this->sanitizeEnum(
            $answers['one_click_checkout'] ?? $defaults['one_click_checkout'],
            ['default', 'subscription_items', 'all_items'],
            $defaults['one_click_checkout']
        );
        $sanitized['disable_cart_page'] = $this->sanitizeEnum(
            $answers['disable_cart_page'] ?? $defaults['disable_cart_page'],
            ['yes', 'no'],
            $defaults['disable_cart_page']
        );
        $sanitized['auto_create_account'] = $this->sanitizeEnum(
            $answers['auto_create_account'] ?? $defaults['auto_create_account'],
            ['yes', 'no'],
            $defaults['auto_create_account']
        );
        $sanitized['plan_switching'] = $this->sanitizeEnum(
            $answers['plan_switching'] ?? $defaults['plan_switching'],
            ['all', 'upgrades_downgrades', 'upgrades_only', 'disabled'],
            $defaults['plan_switching']
        );
        $sanitized['proration_type'] = $this->sanitizeEnum(
            $answers['proration_type'] ?? $defaults['proration_type'],
            ['prorate_immediately', 'apply_at_renewal', 'no_proration'],
            $defaults['proration_type']
        );
        $sanitized['cancel_timing'] = $this->sanitizeEnum(
            $answers['cancel_timing'] ?? $defaults['cancel_timing'],
            ['immediately', 'end_of_period'],
            $defaults['cancel_timing']
        );
        $sanitized['require_cancel_reason'] = $this->sanitizeEnum(
            $answers['require_cancel_reason'] ?? $defaults['require_cancel_reason'],
            ['yes', 'no'],
            $defaults['require_cancel_reason']
        );
        $sanitized['retention_offers'] = $this->sanitizeEnum(
            $answers['retention_offers'] ?? $defaults['retention_offers'],
            ['yes', 'no'],
            $defaults['retention_offers']
        );
        $sanitized['retention_offer_types'] = $this->sanitizeWizardSelectionArray(
            $answers['retention_offer_types'] ?? $defaults['retention_offer_types'],
            ['discount', 'pause', 'downgrade', 'contact_support']
        );
        $sanitized['retention_discount_percent'] = $this->sanitizeEnum(
            $answers['retention_discount_percent'] ?? $defaults['retention_discount_percent'],
            ['10', '20', '30', '50'],
            $defaults['retention_discount_percent']
        );
        $sanitized['retention_discount_cycles'] = $this->sanitizeEnum(
            $answers['retention_discount_cycles'] ?? $defaults['retention_discount_cycles'],
            ['1', '2', '3', '6'],
            $defaults['retention_discount_cycles']
        );
        $sanitized['refund_policy'] = $this->sanitizeEnum(
            $answers['refund_policy'] ?? $defaults['refund_policy'],
            ['immediate', 'end_of_period', 'none'],
            $defaults['refund_policy']
        );
        $sanitized['enable_access_control'] = $this->sanitizeEnum(
            $answers['enable_access_control'] ?? $defaults['enable_access_control'],
            ['yes', 'no'],
            $defaults['enable_access_control']
        );
        $sanitized['restricted_redirect'] = $this->sanitizeEnum(
            $answers['restricted_redirect'] ?? $defaults['restricted_redirect'],
            ['redirect', 'message', 'login'],
            $defaults['restricted_redirect']
        );
        $sanitized['restricted_redirect_url'] = \sanitize_text_field(
            $answers['restricted_redirect_url'] ?? $defaults['restricted_redirect_url']
        );
        $sanitized['require_login'] = $this->sanitizeEnum(
            $answers['require_login'] ?? $defaults['require_login'],
            ['yes', 'no'],
            $defaults['require_login']
        );
        $sanitized['email_level'] = $this->sanitizeEnum(
            $answers['email_level'] ?? $defaults['email_level'],
            ['all', 'essential', 'minimal', 'custom'],
            $defaults['email_level']
        );
        $sanitized['custom_emails'] = $this->sanitizeWizardSelectionArray(
            $answers['custom_emails'] ?? $defaults['custom_emails'],
            self::WIZARD_CUSTOMER_EMAILS
        );
        $sanitized['renewal_reminder_days'] = $this->sanitizeEnum(
            $answers['renewal_reminder_days'] ?? $defaults['renewal_reminder_days'],
            ['1', '3', '5', '7'],
            $defaults['renewal_reminder_days']
        );
        $sanitized['admin_emails'] = $this->sanitizeWizardSelectionArray(
            $answers['admin_emails'] ?? $defaults['admin_emails'],
            ['admin_new_subscription', 'admin_cancelled', 'admin_payment_failed']
        );
        $sanitized['additional_features'] = $this->sanitizeWizardSelectionArray(
            $answers['additional_features'] ?? $defaults['additional_features'],
            [
                'store_credit',
                'feature_manager',
                'audit_logging',
                'custom_profile_fields',
                'myaccount_editor',
                'hide_admin_bar',
                'restrict_dashboard',
                'multi_login',
            ]
        );
        $sanitized['store_credit_auto_apply'] = $this->sanitizeEnum(
            $answers['store_credit_auto_apply'] ?? $defaults['store_credit_auto_apply'],
            ['yes', 'no'],
            $defaults['store_credit_auto_apply']
        );
        $sanitized['store_credit_expiration'] = $this->sanitizeEnum(
            $answers['store_credit_expiration'] ?? $defaults['store_credit_expiration'],
            ['0', '90', '180', '365'],
            $defaults['store_credit_expiration']
        );
        $sanitized['feature_manager_product_page'] = $this->sanitizeEnum(
            $answers['feature_manager_product_page'] ?? $defaults['feature_manager_product_page'],
            ['yes', 'no'],
            $defaults['feature_manager_product_page']
        );
        $sanitized['multi_login_max_sessions'] = $this->sanitizeEnum(
            $answers['multi_login_max_sessions'] ?? $defaults['multi_login_max_sessions'],
            ['1', '2', '3', '5'],
            $defaults['multi_login_max_sessions']
        );
        $sanitized['dashboard_redirect'] = $this->sanitizeEnum(
            $answers['dashboard_redirect'] ?? $defaults['dashboard_redirect'],
            ['my_account', 'not_found'],
            $defaults['dashboard_redirect']
        );

        if ('custom' === $sanitized['billing_cycle'] && ! $this->isWizardIntegerInRange($answers['billing_interval'] ?? null, 1, 365)) {
            $errors['billing_interval'] = \esc_html__('Enter a billing interval between 1 and 365.', 'arraysubs');
        }

        if ('custom' === $sanitized['grace_period']) {
            if (! $this->isWizardIntegerInRange($answers['custom_grace_active_days'] ?? null, 0, 30)) {
                $errors['custom_grace_active_days'] = \esc_html__('Enter an active grace period from 0 to 30 days.', 'arraysubs');
            }

            if (! $this->isWizardIntegerInRange($answers['custom_grace_hold_days'] ?? null, 1, 60)) {
                $errors['custom_grace_hold_days'] = \esc_html__('Enter an on-hold grace period from 1 to 60 days.', 'arraysubs');
            }
        }

        $access_control_enabled = \in_array($sanitized['business_type'], ['membership', 'content', 'saas'], true)
            || 'yes' === $sanitized['enable_access_control'];

        if (
            $access_control_enabled
            && 'redirect' === $sanitized['restricted_redirect']
            && '' === \trim($sanitized['restricted_redirect_url'])
        ) {
            $errors['restricted_redirect_url'] = \esc_html__('Enter a redirect path such as /pricing.', 'arraysubs');
        }

        if (! empty($errors)) {
            return new WP_Error(
                'invalid_answers',
                \esc_html__('Invalid wizard answers.', 'arraysubs'),
                [
                    'status' => 400,
                    'errors' => $errors,
                ]
            );
        }

        return $sanitized;
    }

    /**
     * Sanitize an array of wizard selections.
     *
     * @param mixed         $value   Raw value.
     * @param array<string> $allowed Allowed values.
     * @return array<string>
     */
    private function sanitizeWizardSelectionArray($value, array $allowed): array
    {
        if (! \is_array($value)) {
            return [];
        }

        $sanitized = \array_map('sanitize_key', $value);

        return \array_values(\array_intersect($sanitized, $allowed));
    }

    /**
     * Check whether an integer answer is within range.
     *
     * @param mixed $value Raw value.
     * @param int   $min   Minimum value.
     * @param int   $max   Maximum value.
     * @return bool
     */
    private function isWizardIntegerInRange($value, int $min, int $max): bool
    {
        if (! \is_scalar($value) || '' === (string) $value || ! \is_numeric($value)) {
            return false;
        }

        $int_value = (int) $value;

        return $int_value >= $min && $int_value <= $max;
    }

    /**
     * Build partial settings from sanitized wizard answers.
     *
     * @param array<string, mixed> $answers Sanitized wizard answers.
     * @return array<string, mixed>
     */
    private function buildSettingsFromAnswers(array $answers): array
    {
        $billing_period_map = [
            'weekly'  => 'week',
            'monthly' => 'month',
            'yearly'  => 'year',
        ];
        $grace_map = [
            'strict'   => ['on_hold' => 1, 'cancel' => 3],
            'standard' => ['on_hold' => 3, 'cancel' => 7],
            'lenient'  => ['on_hold' => 5, 'cancel' => 14],
        ];
        $invoice_map = [
            '6_hours' => ['value' => 6, 'unit' => 'hours'],
            '1_day'   => ['value' => 1, 'unit' => 'days'],
            '3_days'  => ['value' => 3, 'unit' => 'days'],
        ];

        $selected_flexibility = $answers['billing_flexibility'] ?? [];
        $selected_retention    = $answers['retention_offer_types'] ?? [];
        $selected_features     = $answers['additional_features'] ?? [];

        $billing_period = $billing_period_map[$answers['billing_cycle']] ?? $answers['billing_period'];
        $billing_interval = 'custom' === $answers['billing_cycle']
            ? (int) $answers['billing_interval']
            : 1;
        $grace_settings = 'custom' === $answers['grace_period']
            ? [
                'on_hold' => (int) $answers['custom_grace_active_days'],
                'cancel'  => (int) $answers['custom_grace_hold_days'],
            ]
            : ($grace_map[$answers['grace_period']] ?? $grace_map['standard']);
        $invoice_settings = $invoice_map[$answers['invoice_timing']] ?? $invoice_map['6_hours'];
        $plan_switching_mode = 'single' === $answers['plan_structure']
            ? 'disabled'
            : $answers['plan_switching'];
        $retention_enabled = 'yes' === $answers['retention_offers'];
        $access_control_enabled = \in_array($answers['business_type'], ['membership', 'content', 'saas'], true)
            || 'yes' === $answers['enable_access_control'];

        return [
            'defaults' => [
                'billing_period'   => $billing_period,
                'billing_interval' => $billing_interval,
            ],
            'renewals' => [
                'grace_days_before_on_hold' => $grace_settings['on_hold'],
                'grace_days_before_cancel'  => $grace_settings['cancel'],
                'invoice_before_due_value'  => $invoice_settings['value'],
                'invoice_before_due_unit'   => $invoice_settings['unit'],
            ],
            'multiple_subscriptions' => [
                'allow_multiple_in_cart'   => 'one_per_customer' !== $answers['multiple_subscriptions'],
                'one_per_customer'         => 'one_per_customer' === $answers['multiple_subscriptions'],
                'auto_migrate_on_checkout' => 'one_per_customer' === $answers['multiple_subscriptions']
                    ? 'yes' === $answers['auto_migrate']
                    : false,
                'one_per_product'          => 'one_per_product' === $answers['multiple_subscriptions'],
                'allow_mixed_cart'         => 'one_per_customer' === $answers['multiple_subscriptions']
                    ? false
                    : 'yes' === $answers['mixed_cart'],
            ],
            'checkout' => [
                'one_click_mode'      => $answers['one_click_checkout'],
                'disable_cart_page'   => 'yes' === $answers['disable_cart_page'],
                'auto_create_account' => 'yes' === $answers['auto_create_account'],
            ],
            'trials' => [
                'require_payment_method' => 'yes' === $answers['offer_trials']
                    ? 'yes' === $answers['trial_require_payment']
                    : true,
                'one_trial_per_customer' => 'yes' === $answers['offer_trials']
                    ? 'yes' === $answers['one_trial_per_customer']
                    : true,
            ],
            'plan_switching' => [
                'enabled'               => 'disabled' !== $plan_switching_mode,
                'allow_upgrades'        => \in_array($plan_switching_mode, ['all', 'upgrades_downgrades', 'upgrades_only'], true),
                'allow_downgrades'      => \in_array($plan_switching_mode, ['all', 'upgrades_downgrades'], true),
                'allow_crossgrades'     => 'all' === $plan_switching_mode,
                'allow_customer_switch' => 'disabled' !== $plan_switching_mode,
                'proration_type'        => $answers['proration_type'],
            ],
            'cancellation' => [
                'cancel_immediately'       => 'immediately' === $answers['cancel_timing'],
                'require_reason'           => 'yes' === $answers['require_cancel_reason'],
                'retention_offers_enabled' => $retention_enabled,
                'retention_offers'         => [
                    'discount' => [
                        'enabled'          => $retention_enabled && \in_array('discount', $selected_retention, true),
                        'discount_percent' => (int) $answers['retention_discount_percent'],
                        'discount_cycles'  => (int) $answers['retention_discount_cycles'],
                    ],
                    'pause' => [
                        'enabled'        => $retention_enabled && \in_array('pause', $selected_retention, true),
                        'max_pause_days' => (int) $answers['max_pause_days'],
                    ],
                    'downgrade' => [
                        'enabled' => $retention_enabled && \in_array('downgrade', $selected_retention, true),
                    ],
                    'skip' => [
                        'enabled' => false,
                    ],
                    'contact_support' => [
                        'enabled' => $retention_enabled && \in_array('contact_support', $selected_retention, true),
                    ],
                ],
            ],
            'refunds' => [
                'cancellation_behavior'  => $answers['refund_policy'],
                'auto_gateway_refund'    => \in_array($answers['refund_policy'], ['immediate', 'end_of_period'], true),
                'allow_prorated_refunds' => \in_array($answers['refund_policy'], ['immediate', 'end_of_period'], true),
            ],
            'members_access' => [
                'enabled'              => $access_control_enabled,
                'default_redirect_url' => 'redirect' === $answers['restricted_redirect']
                    ? $answers['restricted_redirect_url']
                    : '/pricing',
                'default_message'      => 'This content is restricted. Please subscribe to access.',
                'require_login'        => 'login' === $answers['restricted_redirect']
                    ? true
                    : 'yes' === $answers['require_login'],
            ],
            'emails' => $this->buildWizardEmailSettings($answers),
            'skip_renewal' => [
                'enabled'           => \in_array('skip', $selected_flexibility, true),
                'customer_can_skip' => \in_array('skip', $selected_flexibility, true),
                'max_cycles'        => (int) $answers['max_skips'],
                'cutoff_days'       => (int) $answers['skip_cutoff_days'],
            ],
            'pause_subscription' => [
                'enabled'                     => \in_array('pause', $selected_flexibility, true),
                'customer_can_pause'          => \in_array('pause', $selected_flexibility, true),
                'max_duration_days'           => (int) $answers['max_pause_days'],
                'max_pauses_per_subscription' => (int) $answers['max_pauses'],
            ],
            'store_credit' => [
                'enabled'                => \in_array('store_credit', $selected_features, true),
                'auto_apply_to_renewals' => 'yes' === $answers['store_credit_auto_apply'],
                'expiration_days'        => (int) $answers['store_credit_expiration'],
            ],
            'feature_manager' => [
                'enabled'              => \in_array('feature_manager', $selected_features, true),
                'show_on_product_page' => 'yes' === $answers['feature_manager_product_page'],
            ],
            'audit_logging' => [
                'product'  => \in_array('audit_logging', $selected_features, true),
                'coupon'   => \in_array('audit_logging', $selected_features, true),
                'email'    => \in_array('audit_logging', $selected_features, true),
                'settings' => \in_array('audit_logging', $selected_features, true),
            ],
            'profile_builder' => [
                'custom_profile_fields_enabled' => \in_array('custom_profile_fields', $selected_features, true),
                'my_account_menu_items_enabled' => \in_array('myaccount_editor', $selected_features, true),
            ],
            'toolkit' => [
                'hide_admin_bar'         => \in_array('hide_admin_bar', $selected_features, true),
                'restrict_wp_admin'      => \in_array('restrict_dashboard', $selected_features, true),
                'wp_admin_redirect'      => $answers['dashboard_redirect'],
                'multi_login_prevention' => \in_array('multi_login', $selected_features, true),
                'multi_login_max_sessions' => (int) $answers['multi_login_max_sessions'],
            ],
        ];
    }

    /**
     * Build email settings from wizard answers.
     *
     * @param array<string, mixed> $answers Sanitized wizard answers.
     * @return array<string, mixed>
     */
    private function buildWizardEmailSettings(array $answers): array
    {
        $enabled_emails = self::WIZARD_CUSTOMER_EMAILS;

        if ('essential' === $answers['email_level']) {
            $enabled_emails = self::WIZARD_ESSENTIAL_EMAILS;
        } elseif ('minimal' === $answers['email_level']) {
            $enabled_emails = self::WIZARD_MINIMAL_EMAILS;
        } elseif ('custom' === $answers['email_level']) {
            $enabled_emails = $answers['custom_emails'] ?? [];
        }

        $email_settings = [
            'admin_new_subscription' => \in_array('admin_new_subscription', $answers['admin_emails'] ?? [], true),
            'admin_cancelled'        => \in_array('admin_cancelled', $answers['admin_emails'] ?? [], true),
            'admin_payment_failed'   => \in_array('admin_payment_failed', $answers['admin_emails'] ?? [], true),
        ];

        foreach (self::WIZARD_CUSTOMER_EMAILS as $email_key) {
            $email_settings[$email_key] = [
                'enabled' => \in_array($email_key, $enabled_emails, true),
            ];
        }

        $email_settings['renewal_upcoming']['days_before'] = (int) $answers['renewal_reminder_days'];

        return $email_settings;
    }

    /**
     * Sanitize integer within range.
     *
     * @param mixed $value Value to sanitize.
     * @param int   $min   Minimum value.
     * @param int   $max   Maximum value.
     * @return int
     */
    private function sanitizeInt($value, int $min, int $max): int
    {
        return max($min, min($max, \absint($value)));
    }

    /**
     * Sanitize enum value.
     *
     * @param mixed         $value   Value to sanitize.
     * @param array<string> $allowed Allowed values.
     * @param string        $default Default value.
     * @return string
     */
    private function sanitizeEnum($value, array $allowed, string $default): string
    {
        $value = \sanitize_text_field((string) $value);

        return \in_array($value, $allowed, true) ? $value : $default;
    }

    /**
     * Import WooCommerce email notification settings.
     *
     * @param array $options Full imported options.
     * @return bool True if any email settings were imported.
     */
    private function importWcEmailSettings(array $options): bool
    {
        if (! isset($options['wc_email_settings']) || ! is_array($options['wc_email_settings'])) {
            return false;
        }

        $imported = false;

        foreach ($options['wc_email_settings'] as $option_key => $value) {
            if (! is_array($value)) {
                continue;
            }

            // Only allow known arraysubs email option keys.
            $email_id = str_replace(['woocommerce_', '_settings'], '', $option_key);
            if (! in_array($email_id, self::WC_EMAIL_IDS, true)) {
                continue;
            }

            $sanitized = $this->sanitizeWcEmailOption($value);
            \update_option($option_key, $sanitized);
            $imported = true;
        }

        return $imported;
    }

    /**
     * Sanitize a WooCommerce email option array.
     *
     * @param array $settings Raw email settings.
     * @return array Sanitized values.
     */
    private function sanitizeWcEmailOption(array $settings): array
    {
        $sanitized = [];
        $allowed   = [
            'enabled',
            'subject',
            'heading',
            'additional_content',
            'email_type',
            'recipient',
        ];

        foreach ($settings as $key => $value) {
            $safe_key = \sanitize_text_field($key);
            if (! in_array($safe_key, $allowed, true)) {
                continue;
            }

            if ('enabled' === $safe_key) {
                $sanitized[$safe_key] = in_array($value, ['yes', 'no'], true)
                    ? $value
                    : 'yes';
            } elseif ('email_type' === $safe_key) {
                $sanitized[$safe_key] = in_array($value, ['html', 'plain', 'multipart'], true)
                    ? $value
                    : 'html';
            } elseif ('recipient' === $safe_key) {
                $sanitized[$safe_key] = \sanitize_email($value);
            } else {
                $sanitized[$safe_key] = \sanitize_text_field($value);
            }
        }

        return $sanitized;
    }

    /**
     * Sanitize a standalone option using its dedicated sanitizer.
     *
     * @param string $key   Option key.
     * @param array  $value Raw value.
     * @return array Sanitized value.
     */
    private function sanitizeStandaloneOption(string $key, array $value): array
    {
        switch ($key) {
            case 'arraysubs_profile_fields_config':
                return arraysubs_sanitize_profile_fields_config($value);

            case 'arraysubs_avatar_settings':
                return $this->sanitizeAvatarSettings($value);

            case 'arraysubs_myaccount_menu_config':
                return arraysubs_sanitize_myaccount_menu_config($value);

            default:
                return [];
        }
    }

    /**
     * Build export metadata.
     *
     * @return array<string, string|null>
     */
    private function buildExportMeta(): array
    {
        return [
            'plugin_version' => defined('ARRAYSUBS_VERSION') ? ARRAYSUBS_VERSION : '0.0.0',
            'pro_version'    => defined('ARRAYSUBSPRO_VERSION') ? ARRAYSUBSPRO_VERSION : null,
            'module_version' => self::MODULE_VERSION,
            'export_date'    => \current_time('c'),
            'site_url'       => \get_site_url(),
            'php_version'    => phpversion(),
            'wp_version'     => \get_bloginfo('version'),
            'wc_version'     => \defined('WC_VERSION') ? (string) \constant('WC_VERSION') : null,
        ];
    }

    /**
     * Strip sensitive data from settings before export.
     *
     * @param array|mixed $settings Settings array.
     * @return array|mixed
     */
    private function stripSensitiveData($settings)
    {
        if (! is_array($settings)) {
            return $settings;
        }

        foreach (self::SENSITIVE_PATHS as $path) {
            $keys    = explode('.', $path);
            $pointer = &$settings;

            foreach ($keys as $i => $segment) {
                if (! is_array($pointer) || ! array_key_exists($segment, $pointer)) {
                    break;
                }

                if ($i === count($keys) - 1) {
                    unset($pointer[$segment]);
                } else {
                    $pointer = &$pointer[$segment];
                }
            }

            unset($pointer);
        }

        return $settings;
    }

    /**
     * Validate the import payload structure.
     *
     * @param mixed $data     Import data.
     * @param mixed $sections Sections to import.
     * @return true|WP_Error
     */
    private function validateImportPayload($data, $sections)
    {
        if (! is_array($data) || ! isset($data['meta']) || ! isset($data['options'])) {
            return new WP_Error(
                'invalid_import_data',
                \esc_html__('Import data is missing required structure (meta and options)', 'arraysubs'),
                ['status' => 400]
            );
        }

        if (! is_array($data['options'])) {
            return new WP_Error(
                'invalid_import_data',
                \esc_html__('Import data options must be an object', 'arraysubs'),
                ['status' => 400]
            );
        }

        if (! is_array($sections) || empty($sections)) {
            return new WP_Error(
                'invalid_sections',
                \esc_html__('At least one section must be selected for import', 'arraysubs'),
                ['status' => 400]
            );
        }

        $invalid_sections = array_diff($sections, self::ALLOWED_SECTIONS);
        if (! empty($invalid_sections)) {
            return new WP_Error(
                'invalid_sections',
                \esc_html__('One or more selected sections are not allowed', 'arraysubs'),
                ['status' => 400]
            );
        }

        $module_version = $data['meta']['module_version'] ?? null;
        if (null !== $module_version && version_compare($module_version, self::MODULE_VERSION, '>')) {
            return new WP_Error(
                'incompatible_version',
                \esc_html__(
                    'This export was created with a newer format version. Please update the plugin before importing.',
                    'arraysubs'
                ),
                ['status' => 400]
            );
        }

        return true;
    }

    /**
     * Sanitize the main settings.
     *
     * Uses the SettingsController pipeline for known sections,
     * then merges back any sections not explicitly handled (e.g. members_access,
     * audit_logging) with a recursive scalar sanitizer to avoid data loss.
     *
     * @param array $settings Raw settings.
     * @return array
     */
    private function sanitizeMainSettings(array $settings): array
    {
        $controller = new \ArraySubs\Features\MainAdmin\REST\SettingsController();

        $sanitized = [];
        if (method_exists($controller, 'sanitizeSettings')) {
            $reflection = new \ReflectionMethod($controller, 'sanitizeSettings');
            $reflection->setAccessible(true);
            $sanitized = $reflection->invoke($controller, $settings);
        }

        // Sections present in the import but not handled by the sanitizer
        // are preserved with a generic recursive sanitizer so that data
        // like members_access rules and audit_logging flags are not dropped.
        $unhandled = array_diff_key($settings, $sanitized);

        foreach ($unhandled as $key => $value) {
            $safe_key = sanitize_text_field($key);
            if (is_array($value)) {
                $sanitized[$safe_key] = $this->fallbackSanitizeSettings($value);
            } elseif (is_bool($value)) {
                $sanitized[$safe_key] = $value;
            } elseif (is_int($value)) {
                $sanitized[$safe_key] = (int) $value;
            } elseif (is_float($value)) {
                $sanitized[$safe_key] = (float) $value;
            } else {
                $sanitized[$safe_key] = sanitize_text_field((string) $value);
            }
        }

        return $sanitized;
    }

    /**
     * Fallback sanitizer for main settings when the controller method is unavailable.
     *
     * Walks the array recursively and sanitizes scalar values.
     *
     * @param array $settings Raw settings.
     * @return array
     */
    private function fallbackSanitizeSettings(array $settings): array
    {
        $sanitized = [];

        foreach ($settings as $key => $value) {
            $safe_key = sanitize_text_field($key);

            if (is_array($value)) {
                $sanitized[$safe_key] = $this->fallbackSanitizeSettings($value);
            } elseif (is_bool($value)) {
                $sanitized[$safe_key] = $value;
            } elseif (is_int($value)) {
                $sanitized[$safe_key] = (int) $value;
            } elseif (is_float($value)) {
                $sanitized[$safe_key] = (float) $value;
            } else {
                $sanitized[$safe_key] = sanitize_text_field((string) $value);
            }
        }

        return $sanitized;
    }

    /**
     * Sanitize avatar settings using the same rules as the save function.
     *
     * @param array $settings Raw avatar settings.
     * @return array
     */
    private function sanitizeAvatarSettings(array $settings): array
    {
        $valid_image_types = ['jpg', 'jpeg', 'png', 'gif', 'webp'];

        return [
            'enabled'       => ! empty($settings['enabled']),
            'max_file_size' => min(absint($settings['max_file_size'] ?? 2), 100),
            'allowed_types' => array_values(array_intersect(
                array_map('sanitize_text_field', (array) ($settings['allowed_types'] ?? $valid_image_types)),
                $valid_image_types
            )),
        ];
    }

    /**
     * Check if imported settings contain pro-specific keys.
     *
     * @param array $settings The arraysubs_settings array.
     * @return bool
     */
    private function hasProSettings(array $settings): bool
    {
        $pro_keys = ['automatic_payments', 'store_credit', 'feature_manager', 'audit_logging', 'checkout_builder'];

        foreach ($pro_keys as $pro_key) {
            if (! empty($settings[$pro_key])) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check whether the pro addon plugin is active.
     *
     * @return bool
     */
    private function isProActive(): bool
    {
        return \defined('ARRAYSUBSPRO_VERSION');
    }
}
