<?php

declare(strict_types=1);

namespace ShieldX\Core;

defined('ABSPATH') || exit;

use ShieldX\Scanner\ScheduledScanSettings;
use ShieldX\Maintenance\BackupStorageSettings;
use ShieldX\Maintenance\RestorePointManager;
use ShieldX\Maintenance\RestorePointSettings;
use ShieldX\Maintenance\ScheduledBackupSettings;
use ShieldX\Maintenance\ScheduledRestorePointSettings;
use ShieldX\Security\Hardening;
use ShieldX\Security\HardeningSettings;
use ShieldX\Security\HumanChallengeSettings;
use ShieldX\Security\LockdownSettings;
use ShieldX\Security\LoginSecuritySettings;
use ShieldX\Security\RateLimitSettings;
use ShieldX\Security\WafSettings;

final class SettingsPortability
{
    public const EXPORT_VERSION = 1;
    private const IMPORT_NONCE_ACTION = 'shieldx_import_settings';
    private const EXPORT_SIGNATURE_ALGORITHM = 'hmac-sha256-site-v2';

    /**
     * @return array<string,mixed>
     */
    public static function export(bool $includeSiteSecrets = false): array
    {
        $settings = [
            'waf' => WafSettings::fromWordPress()->all(),
            'hardening' => self::hardeningExport(HardeningSettings::fromWordPress()->all()),
            'lockdown' => self::lockdownExport(LockdownSettings::fromWordPress()->all()),
            'rate_limit' => RateLimitSettings::fromWordPress()->all(),
            'scheduled_scans' => self::scheduledScanExport(ScheduledScanSettings::fromWordPress()->all(), $includeSiteSecrets),
            'scheduled_backups' => self::scheduledBackupExport(ScheduledBackupSettings::fromWordPress()->all(), $includeSiteSecrets),
            'scheduled_restore_points' => self::scheduledRestorePointExport(ScheduledRestorePointSettings::fromWordPress()->all(), $includeSiteSecrets),
            'backup_storage' => self::backupStorageExport(BackupStorageSettings::fromWordPress()->all()),
            'login_security' => self::loginSecurityExport(LoginSecuritySettings::fromWordPress()->all(), $includeSiteSecrets),
            'human_challenge' => self::humanChallengeExport(HumanChallengeSettings::fromWordPress(), $includeSiteSecrets),
            'restore_policy' => RestorePointSettings::fromWordPress()->all(),
            'file_types' => FileTypeSettings::fromWordPress()->all(),
            'notifications' => NotificationSettings::fromWordPress()->export(),
        ];

        $export = [
            'product' => 'SHIELD-X',
            'type' => 'settings_export',
            'export_version' => self::EXPORT_VERSION,
            'plugin_version' => defined('SHIELDX_VERSION') ? SHIELDX_VERSION : 'unknown',
            'created_at_utc' => gmdate('c'),
            'site_url' => function_exists('home_url') ? home_url('/') : '',
            'include_site_secrets' => $includeSiteSecrets,
            'settings' => $settings,
        ];
        $export['signature'] = self::signatureFor($export);

        return $export;
    }

    /**
     * @return array{ok:bool,message:string,modules:array<int,string>}
     */
    public static function import(string $json, string $mode = 'merge', string $nonceAction = '', string $nonce = ''): array
    {
        $nonceAction = trim($nonceAction);
        $nonce = trim($nonce);
        $capability = defined('SHIELDX_MANAGE_CAPABILITY') ? (string) SHIELDX_MANAGE_CAPABILITY : 'manage_options';

        if (!function_exists('current_user_can') || !current_user_can($capability)) {
            return [
                'ok' => false,
                'message' => 'Administrator permission is required to import SHIELD-X settings.',
                'modules' => [],
            ];
        }

        if (
            $nonceAction !== self::IMPORT_NONCE_ACTION
            || $nonce === ''
            || !function_exists('wp_verify_nonce')
            || wp_verify_nonce($nonce, self::IMPORT_NONCE_ACTION) === false
        ) {
            return [
                'ok' => false,
                'message' => 'A valid SHIELD-X settings import security token is required.',
                'modules' => [],
            ];
        }

        $decoded = \ShieldX\Support\Json::decodeArray($json);

        if (!is_array($decoded)) {
            return [
                'ok' => false,
                'message' => 'Import file is not valid JSON.',
                'modules' => [],
            ];
        }

        if (!self::verifyExportSignature($decoded)) {
            return [
                'ok' => false,
                'message' => 'Import file signature could not be verified for this site.',
                'modules' => [],
            ];
        }

        $settings = isset($decoded['settings']) && is_array($decoded['settings'])
            ? $decoded['settings']
            : (is_array($decoded) ? $decoded : []);

        if (empty($settings)) {
            return [
                'ok' => false,
                'message' => 'Import file does not contain SHIELD-X settings.',
                'modules' => [],
            ];
        }

        $replace = $mode === 'replace';
        $modules = [];

        if (self::wouldDisableRequiredTwoFactor($settings) && !self::currentAdminHasTwoFactor()) {
            return [
                'ok' => false,
                'message' => 'Settings import cannot disable required admin 2FA until the current administrator is enrolled.',
                'modules' => [],
            ];
        }

        if (self::wouldEnableReservedAdminGate($settings)) {
            return [
                'ok' => false,
                'message' => 'Settings import cannot enable the admin gate with the reserved secretpath keyword.',
                'modules' => [],
            ];
        }

        if (self::wouldImportInvalidAdminGateKey($settings)) {
            return [
                'ok' => false,
                'message' => 'Settings import cannot save an invalid admin gate keyword.',
                'modules' => [],
            ];
        }

        try {
            if (isset($settings['waf']) && is_array($settings['waf'])) {
                self::saveWaf($settings['waf'], $replace);
                $modules[] = 'waf';
            }

            if (isset($settings['hardening']) && is_array($settings['hardening'])) {
                self::saveHardening($settings['hardening'], $replace, $nonceAction, $nonce);
                $modules[] = 'hardening';
            }

            if (isset($settings['lockdown']) && is_array($settings['lockdown'])) {
                self::saveLockdown($settings['lockdown'], $replace);
                $modules[] = 'lockdown';
            }

            if (isset($settings['rate_limit']) && is_array($settings['rate_limit'])) {
                self::saveRateLimit($settings['rate_limit'], $replace);
                $modules[] = 'rate_limit';
            }

            if (isset($settings['scheduled_scans']) && is_array($settings['scheduled_scans'])) {
                self::saveScheduledScans($settings['scheduled_scans'], $replace);
                $modules[] = 'scheduled_scans';
            }

            if (isset($settings['scheduled_backups']) && is_array($settings['scheduled_backups'])) {
                self::saveScheduledBackups($settings['scheduled_backups'], $replace);
                $modules[] = 'scheduled_backups';
            }

            if (isset($settings['scheduled_restore_points']) && is_array($settings['scheduled_restore_points'])) {
                self::saveScheduledRestorePoints($settings['scheduled_restore_points'], $replace);
                $modules[] = 'scheduled_restore_points';
            }

            if (isset($settings['backup_storage']) && is_array($settings['backup_storage'])) {
                self::saveBackupStorage($settings['backup_storage'], $replace);
                $modules[] = 'backup_storage';
            }

            if (isset($settings['login_security']) && is_array($settings['login_security'])) {
                self::saveLoginSecurity($settings['login_security'], $replace);
                $modules[] = 'login_security';
            }

            if (isset($settings['human_challenge']) && is_array($settings['human_challenge'])) {
                self::saveHumanChallenge($settings['human_challenge'], $replace);
                $modules[] = 'human_challenge';
            }

            if (isset($settings['restore_policy']) && is_array($settings['restore_policy'])) {
                self::saveRestorePolicy($settings['restore_policy'], $replace);
                $modules[] = 'restore_policy';
            }

            if (isset($settings['file_types']) && is_array($settings['file_types'])) {
                self::saveFileTypes($settings['file_types'], $replace);
                $modules[] = 'file_types';
            }

            if (isset($settings['notifications']) && is_array($settings['notifications'])) {
                self::saveNotifications($settings['notifications'], $replace);
                $modules[] = 'notifications';
            }

            if (empty($modules)) {
                return [
                    'ok' => false,
                    'message' => 'No supported settings modules were found in the import file.',
                    'modules' => [],
                ];
            }

            SchedulerBridge::refresh();
            self::safeDoAction('shieldx_settings_imported', $modules);
        } catch (\Throwable $error) {
            \ShieldX\Support\ErrorLog::write('SHIELD-X settings import failed: ' . $error->getMessage());

            return [
                'ok' => false,
                'message' => 'Settings import failed: ' . $error->getMessage(),
                'modules' => $modules,
            ];
        }

        return [
            'ok' => true,
            'message' => 'Imported settings for: ' . implode(', ', $modules) . '.',
            'modules' => $modules,
        ];
    }

    /**
     * @param array<string,mixed> $settings
     * @return array<string,mixed>
     */
    private static function lockdownExport(array $settings): array
    {
        unset($settings['enabled'], $settings['applied_at'], $settings['last_action'], $settings['last_error'], $settings['last_coverage']);

        return $settings;
    }

    /**
     * @param array<string,mixed> $settings
     * @return array<string,mixed>
     */
    private static function scheduledScanExport(array $settings, bool $includeSiteSecrets): array
    {
        $settings = self::withoutRuntimeKeys($settings, [
            'external_url',
            'last_run_at',
            'last_scan_id',
            'last_status',
            'last_issues_found',
            'last_error',
            'last_report_sent_at',
        ]);

        if (!$includeSiteSecrets) {
            unset($settings['external_token'], $settings['external_token_hash']);
        }

        if (isset($settings['sections']) && is_array($settings['sections'])) {
            foreach ($settings['sections'] as $type => $section) {
                if (!is_array($section)) {
                    unset($settings['sections'][$type]);
                    continue;
                }

                $section = self::withoutRuntimeKeys($section, [
                    'external_url',
                    'next_run',
                    'next_run_timestamp',
                    'wp_schedule',
                    'locked',
                    'last_run_at',
                    'last_scan_id',
                    'last_status',
                    'last_issues_found',
                    'last_error',
                    'last_report_sent_at',
                ]);

                if (!$includeSiteSecrets) {
                    unset($section['external_token'], $section['external_token_hash']);
                }

                $settings['sections'][$type] = $section;
            }
        }

        return $settings;
    }

    /**
     * @param array<string,mixed> $settings
     * @return array<string,mixed>
     */
    private static function scheduledBackupExport(array $settings, bool $includeSiteSecrets): array
    {
        $settings = self::withoutRuntimeKeys($settings, [
            'external_url',
            'next_run',
            'next_run_timestamp',
            'wp_schedule',
            'locked',
            'last_run_at',
            'last_backup_id',
            'last_status',
            'last_error',
            'last_report_sent_at',
            'last_rows_exported',
            'last_tables',
            'last_size',
            'last_warning_count',
        ]);

        if (!$includeSiteSecrets) {
            unset($settings['external_token'], $settings['external_token_hash']);
        }

        return $settings;
    }

    /**
     * @param array<string,mixed> $settings
     * @return array<string,mixed>
     */
    private static function scheduledRestorePointExport(array $settings, bool $includeSiteSecrets): array
    {
        $settings = self::withoutRuntimeKeys($settings, [
            'external_url',
            'next_run',
            'next_run_timestamp',
            'wp_schedule',
            'locked',
            'last_run_at',
            'last_restore_id',
            'last_status',
            'last_error',
            'last_report_sent_at',
            'last_files',
            'last_bytes_exported',
            'last_database_backup_id',
            'last_warning_count',
        ]);

        if (!$includeSiteSecrets) {
            unset($settings['external_token'], $settings['external_token_hash']);
        }

        return $settings;
    }

    /**
     * @param array<string,mixed> $settings
     * @return array<string,mixed>
     */
    private static function backupStorageExport(array $settings): array
    {
        return [
            'mode' => BackupStorageSettings::sanitizeMode((string) ($settings['mode'] ?? BackupStorageSettings::MODE_LOCAL)),
        ];
    }

    /**
     * @param array<string,mixed> $settings
     * @return array<string,mixed>
     */
    private static function loginSecurityExport(array $settings, bool $includeSiteSecrets): array
    {
        $settings = self::withoutRuntimeKeys($settings, [
            'activation_email_sent_at',
            'last_recovery_email_sent_at',
            'recovery_email_status',
            'recovery_email_error',
            'emergency_recovery_used_at',
        ]);

        unset($settings['owner_recovery_token_hash'], $settings['owner_recovery_token_created_at']);

        if (!$includeSiteSecrets) {
            unset(
                $settings['emergency_recovery_token'],
                $settings['emergency_recovery_token_hash'],
                $settings['emergency_recovery_token_created_at']
            );
        }

        return $settings;
    }

    /**
     * @return array<string,mixed>
     */
    private static function humanChallengeExport(HumanChallengeSettings $settings, bool $includeSiteSecrets): array
    {
        $export = $settings->all(false);
        unset($export['configured'], $export['secret_key_configured']);

        unset(
            $export['secret_key'],
            $export['secret_key_plain'],
            $export['last_test_status'],
            $export['last_tested_at']
        );

        return $export;
    }

    /**
     * @param array<string,mixed> $settings
     * @param array<int,string> $keys
     * @return array<string,mixed>
     */
    private static function withoutRuntimeKeys(array $settings, array $keys): array
    {
        foreach ($keys as $key) {
            unset($settings[$key]);
        }

        return $settings;
    }

    /**
     * @param array<string,mixed> $settings
     * @return array<string,mixed>
     */
    private static function hardeningExport(array $settings): array
    {
        $settings = self::withoutRuntimeKeys($settings, ['admin_entry_url', 'admin_gate_key']);
        $settings['admin_gate_enabled'] = false;

        return $settings;
    }

    /**
     * @param array<string,mixed> $incoming
     */
    private static function saveWaf(array $incoming, bool $replace): void
    {
        $incoming = self::allowKeys($incoming, [
            'mode',
            'temporary_rule_block_minutes',
            'max_body_inspection_bytes',
            'anomaly_threshold_enabled',
            'anomaly_challenge_threshold',
            'anomaly_block_threshold',
            'disabled_rules',
            'rule_actions',
            'plugin_whitelist',
            'ip_allowlist',
            'ip_blocklist',
            'country_blocklist',
            'user_agent_blocklist',
            'referrer_blocklist',
            'trusted_proxy_header',
            'trusted_proxy_cidrs',
            'temporary_blocks',
        ]);
        $base = $replace ? [] : WafSettings::fromWordPress()->all();
        $settings = new WafSettings(array_merge($base, $incoming));
        $all = $settings->all();
        $settings->save((array) ($all['disabled_rules'] ?? []), (array) ($all['plugin_whitelist'] ?? []), $all);
    }

    /**
     * @param array<string,mixed> $incoming
     */
    private static function saveHardening(array $incoming, bool $replace, string $nonceAction, string $nonce): void
    {
        $incoming = self::allowKeys($incoming, [
            'hide_wordpress_signature',
            'force_ssl_enabled',
            'disable_file_edit',
            'response_headers',
            'block_xmlrpc',
            'disable_pingbacks',
            'disable_admin_scripts_concatenation',
            'block_author_scans',
            'bot_protection_enabled',
            'direct_core_access_protection',
            'sensitive_file_protection',
            'directory_browsing_protection',
            'data_php_execution_protection',
            'admin_gate_enabled',
            'admin_gate_key',
            'admin_gate_strict_cookie',
        ]);
        $base = $replace ? [] : HardeningSettings::fromWordPress()->all();
        $settings = new HardeningSettings(array_replace_recursive($base, $incoming));
        self::createPreChangeRestorePoint('pre_settings_import_hardening');
        $settings->save(
            $settings->hideWordPressSignature(),
            $settings->adminGateEnabled(),
            $settings->adminGateKey(),
            $settings->forceSslEnabled(),
            $settings->disableFileEdit(),
            $settings->responseHeaders()
        );
        Hardening::syncFileEditConfig($settings->disableFileEdit(), '', $nonceAction, $nonce);
        Hardening::syncWebServerRules($settings, '', '', null, $nonceAction, $nonce);

        if ($settings->adminGateEnabled()) {
            Hardening::setAccessCookie($settings);
        } else {
            Hardening::clearAccessCookie();
        }
    }

    private static function createPreChangeRestorePoint(string $reason): void
    {
        try {
            $userId = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;
            RestorePointManager::fromWordPress()->create($reason, [], $userId, true);
        } catch (\Throwable $error) {
            // Import must not fail only because a host cannot create a restore point.
        }
    }

    /**
     * @param array<string,mixed> $incoming
     */
    private static function saveLockdown(array $incoming, bool $replace): void
    {
        $incoming = self::allowKeys($incoming, ['policy']);
        $base = $replace ? [] : LockdownSettings::fromWordPress()->all();
        $settings = new LockdownSettings(array_replace_recursive($base, $incoming));
        $all = $settings->all();
        $all['enabled'] = false;
        $all['applied_at'] = '';
        $all['last_action'] = 'imported_policy';
        $all['last_error'] = '';
        $all['last_coverage'] = [];
        (new LockdownSettings())->save($all);
    }

    /**
     * @param array<string,mixed> $incoming
     */
    private static function saveRateLimit(array $incoming, bool $replace): void
    {
        $incoming = self::allowKeys($incoming, [
            'request_rate_enabled',
            'request_rate_mode',
            'request_limit',
            'request_window_seconds',
            'burst_limit',
            'burst_window_seconds',
            'login_protection_enabled',
            'login_failure_limit',
            'login_window_seconds',
            'login_lockout_seconds',
            'endpoint_profiles',
        ]);
        $base = $replace ? [] : RateLimitSettings::fromWordPress()->all();
        (new RateLimitSettings())->save(array_merge($base, $incoming));
    }

    /**
     * @param array<string,mixed> $incoming
     */
    private static function saveScheduledScans(array $incoming, bool $replace): void
    {
        $incoming = self::allowKeys($incoming, [
            'sections',
            'enabled',
            'recurrence',
            'scopes',
            'max_files',
            'email_reports_enabled',
            'alert_on_issues',
            'alert_on_failure',
            'external_runner_enabled',
            'recipients',
        ]);
        $current = ScheduledScanSettings::fromWordPress();
        $base = $replace ? self::scheduledScanExport((new ScheduledScanSettings())->all(), false) : [];
        unset($base['external_url'], $incoming['external_url']);
        if (isset($incoming['sections']) && is_array($incoming['sections'])) {
            foreach ($incoming['sections'] as $type => $section) {
                if (is_array($section)) {
                    $incoming['sections'][$type] = self::allowKeys($section, [
                        'enabled',
                        'recurrence',
                        'scopes',
                        'max_files',
                        'database_depth',
                        'email_reports_enabled',
                        'alert_on_issues',
                        'alert_on_failure',
                        'external_runner_enabled',
                        'recipients',
                    ]);
                    unset(
                        $incoming['sections'][$type]['external_url'],
                        $incoming['sections'][$type]['next_run'],
                        $incoming['sections'][$type]['next_run_timestamp'],
                        $incoming['sections'][$type]['wp_schedule'],
                        $incoming['sections'][$type]['locked']
                    );
                }
            }
        }
        $current->save(array_replace_recursive($base, $incoming));
    }

    /**
     * @param array<string,mixed> $incoming
     */
    private static function saveScheduledBackups(array $incoming, bool $replace): void
    {
        $incoming = self::allowKeys($incoming, [
            'enabled',
            'recurrence',
            'max_rows_per_table',
            'email_reports_enabled',
            'alert_on_warning',
            'alert_on_failure',
            'external_runner_enabled',
            'recipients',
        ]);
        $current = ScheduledBackupSettings::fromWordPress();
        $base = $replace ? self::scheduledBackupExport((new ScheduledBackupSettings())->all(), false) : [];
        unset(
            $base['external_url'],
            $incoming['external_url'],
            $incoming['next_run'],
            $incoming['next_run_timestamp'],
            $incoming['wp_schedule'],
            $incoming['locked']
        );
        $current->save(array_merge($base, $incoming));
    }

    /**
     * @param array<string,mixed> $incoming
     */
    private static function saveScheduledRestorePoints(array $incoming, bool $replace): void
    {
        $incoming = self::allowKeys($incoming, [
            'enabled',
            'recurrence',
            'include_database',
            'email_reports_enabled',
            'alert_on_warning',
            'alert_on_failure',
            'external_runner_enabled',
            'recipients',
        ]);
        $current = ScheduledRestorePointSettings::fromWordPress();
        $base = $replace ? self::scheduledRestorePointExport((new ScheduledRestorePointSettings())->all(), false) : [];
        unset(
            $base['external_url'],
            $incoming['external_url'],
            $incoming['next_run'],
            $incoming['next_run_timestamp'],
            $incoming['wp_schedule'],
            $incoming['locked'],
            $incoming['last_run_at'],
            $incoming['last_restore_id'],
            $incoming['last_status'],
            $incoming['last_error'],
            $incoming['last_report_sent_at'],
            $incoming['last_files'],
            $incoming['last_bytes_exported'],
            $incoming['last_database_backup_id'],
            $incoming['last_warning_count']
        );
        $current->save(array_merge($base, $incoming));
    }

    /**
     * @param array<string,mixed> $incoming
     */
    private static function saveBackupStorage(array $incoming, bool $replace): void
    {
        unset($replace);
        $incoming = self::allowKeys($incoming, ['mode']);
        (new BackupStorageSettings())->save($incoming);
    }

    /**
     * @param array<string,mixed> $incoming
     */
    private static function saveLoginSecurity(array $incoming, bool $replace): void
    {
        $incoming = self::allowKeys($incoming, [
            'enabled',
            'two_factor_required_roles',
            'session_policy_enabled',
            'session_protected_roles',
            'session_maximum_age',
            'session_idle_timeout',
            'magic_login_enabled',
            'recovery_email',
        ]);
        $base = $replace ? [] : LoginSecuritySettings::fromWordPress()->all();
        (new LoginSecuritySettings())->save(array_merge($base, $incoming));
    }

    /**
     * @param array<string,mixed> $incoming
     */
    private static function saveHumanChallenge(array $incoming, bool $replace): void
    {
        $incoming = self::allowKeys($incoming, [
            'provider',
            'endpoint_url',
            'site_key',
            'timeout_seconds',
            'fail_behavior',
            'login_enabled',
            'login_fullscreen_enabled',
            'password_reset_enabled',
            'waf_enabled',
        ]);
        $base = $replace ? [] : HumanChallengeSettings::fromWordPress()->all(true);
        if (isset($base['secret_key_plain'])) {
            $base['secret_key'] = (string) $base['secret_key_plain'];
        }
        unset($incoming['secret_key']);
        unset($base['configured'], $base['secret_key_configured'], $base['secret_key_plain']);
        (new HumanChallengeSettings())->save(array_merge($base, $incoming));
    }

    /**
     * @param array<string,mixed> $settings
     */
    private static function wouldDisableRequiredTwoFactor(array $settings): bool
    {
        if (!isset($settings['login_security']) || !is_array($settings['login_security'])) {
            return false;
        }

        if (!array_key_exists('two_factor_required_roles', $settings['login_security'])) {
            return false;
        }

        $roles = $settings['login_security']['two_factor_required_roles'];

        return !is_array($roles) || $roles === [];
    }

    /**
     * @param array<string,mixed> $settings
     */
    private static function wouldEnableReservedAdminGate(array $settings): bool
    {
        if (!isset($settings['hardening']) || !is_array($settings['hardening'])) {
            return false;
        }

        $hardening = $settings['hardening'];

        return array_key_exists('admin_gate_enabled', $hardening)
            && self::toBool($hardening['admin_gate_enabled'])
            && HardeningSettings::isReservedAdminGateKey((string) ($hardening['admin_gate_key'] ?? ''));
    }

    /**
     * @param array<string,mixed> $settings
     */
    private static function wouldImportInvalidAdminGateKey(array $settings): bool
    {
        if (!isset($settings['hardening']) || !is_array($settings['hardening'])) {
            return false;
        }

        $hardening = $settings['hardening'];

        return array_key_exists('admin_gate_key', $hardening)
            && !HardeningSettings::isValidAdminGateKey((string) $hardening['admin_gate_key']);
    }

    private static function currentAdminHasTwoFactor(): bool
    {
        if (!function_exists('get_current_user_id')) {
            return false;
        }

        $userId = (int) get_current_user_id();
        if ($userId <= 0) {
            return false;
        }

        return (new \ShieldX\Security\LoginSecurity(LoginSecuritySettings::fromWordPress()))->isTwoFactorEnabled($userId);
    }

    /**
     * @param array<string,mixed> $incoming
     */
    private static function saveRestorePolicy(array $incoming, bool $replace): void
    {
        $incoming = self::allowKeys($incoming, [
            'enabled',
            'include_database',
            'max_files',
            'max_bytes',
            'retention_count',
            'default_paths',
        ]);
        $base = $replace ? [] : RestorePointSettings::fromWordPress()->all();
        (new RestorePointSettings())->save(array_merge($base, $incoming));
    }

    /**
     * @param array<string,mixed> $incoming
     */
    private static function saveFileTypes(array $incoming, bool $replace): void
    {
        $incoming = self::allowKeys($incoming, [
            FileTypeSettings::CONTEXT_MALWARE,
            FileTypeSettings::CONTEXT_BACKUP,
        ]);
        $base = $replace ? [] : FileTypeSettings::fromWordPress()->all();
        (new FileTypeSettings(array_merge($base, $incoming)))->save(array_merge($base, $incoming));
    }

    /**
     * @param array<string,mixed> $incoming
     */
    private static function saveNotifications(array $incoming, bool $replace): void
    {
        $incoming = self::allowKeys($incoming, [
            'profile',
            'enabled_events',
            'recipients',
        ]);
        $base = $replace ? [] : NotificationSettings::fromWordPress()->all();
        unset($incoming['last_saved_at'], $incoming['last_saved_by']);
        NotificationSettings::save(array_merge($base, $incoming));
    }

    /**
     * @param array<string,mixed> $incoming
     * @param array<int,string>   $allowed
     * @return array<string,mixed>
     */
    private static function allowKeys(array $incoming, array $allowed): array
    {
        return array_intersect_key($incoming, array_fill_keys($allowed, true));
    }

    /**
     * @param array<string,mixed> $export
     * @return array{alg:string,value:string}
     */
    private static function signatureFor(array $export): array
    {
        unset($export['signature']);

        return [
            'alg' => self::EXPORT_SIGNATURE_ALGORITHM,
            'value' => hash_hmac('sha256', self::canonicalJson($export), self::signatureKey()),
        ];
    }

    /**
     * @param array<string,mixed> $decoded
     */
    private static function verifyExportSignature(array $decoded): bool
    {
        if (($decoded['product'] ?? '') !== 'SHIELD-X' || ($decoded['type'] ?? '') !== 'settings_export') {
            return false;
        }

        $signature = isset($decoded['signature']) && is_array($decoded['signature']) ? $decoded['signature'] : [];
        $algorithm = isset($signature['alg']) && is_scalar($signature['alg']) ? (string) $signature['alg'] : '';
        $value = isset($signature['value']) && is_scalar($signature['value']) ? (string) $signature['value'] : '';
        if ($algorithm !== self::EXPORT_SIGNATURE_ALGORITHM || $value === '') {
            return false;
        }

        $expected = self::signatureFor($decoded);

        return hash_equals($expected['value'], $value);
    }

    /**
     * @param mixed $value
     */
    private static function canonicalJson($value): string
    {
        $normalized = self::canonicalValue($value);
        $json = wp_json_encode($normalized, JSON_UNESCAPED_SLASHES);

        return is_string($json) ? $json : '';
    }

    /**
     * @param mixed $value
     * @return mixed
     */
    private static function canonicalValue($value)
    {
        if (!is_array($value)) {
            return $value;
        }

        $isList = array_keys($value) === range(0, count($value) - 1);
        if (!$isList) {
            ksort($value);
        }

        foreach ($value as $key => $item) {
            $value[$key] = self::canonicalValue($item);
        }

        return $value;
    }

    private static function signatureKey(): string
    {
        $auth = function_exists('wp_salt') ? (string) wp_salt('auth') : '';
        $secure = function_exists('wp_salt') ? (string) wp_salt('secure_auth') : '';
        $key = $auth . '|' . $secure;

        if (trim($key, '|') === '') {
            throw new \RuntimeException('WordPress salts are required to sign SHIELD-X settings exports.');
        }

        return hash('sha256', 'shieldx-settings-export-v2|' . $key . '|' . self::siteBinding(), true);
    }

    private static function siteBinding(): string
    {
        $siteUrl = function_exists('home_url') ? (string) home_url('/') : '';
        $siteUrl = strtolower(rtrim(trim($siteUrl), '/'));
        $blogId = function_exists('get_current_blog_id') ? max(0, (int) get_current_blog_id()) : 0;

        if ($siteUrl === '') {
            throw new \RuntimeException('The WordPress site URL is required to sign SHIELD-X settings exports.');
        }

        return $siteUrl . '|blog=' . (string) $blogId;
    }

    /**
     * @param mixed ...$args
     */
    private static function safeDoAction(string $hook, ...$args): void
    {
        if (!function_exists('do_action')) {
            return;
        }

        try {
            // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Settings portability dispatches only SHIELD-X-prefixed lifecycle hooks through this helper.
            do_action($hook, ...$args);
        } catch (\Throwable $error) {
            \ShieldX\Support\ErrorLog::write(sprintf('SHIELD-X settings action %s failed: %s', $hook, $error->getMessage()));
        }
    }

    /**
     * @param mixed $value
     */
    private static function toBool($value): bool
    {
        if (is_bool($value)) {
            return $value;
        }

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