<?php
/**
 * VMPFence Events Observer
 *
 * Monitors all VMPFence-specific settings and configuration changes.
 * All hooks are custom vmpfence_* actions fired via do_action() in Phase 5.
 *
 * 58 event types covering WAF, scanning, blocking, 2FA, brute force,
 * rate limiting, security toggles, IP detection, network, allowlists,
 * import/export, audit log mode, and licensing.
 *
 * @package VMPFence
 * @since 2.2.5
 * @since 2.2.6 Fixed config changed issue for some keys, IP & Country blocking added to warning label, Custom Pattern Block Message Updated
 */

if (!defined('ABSPATH')) {
    exit;
}

class VMPFence_Audit_Log_Observer_VMPFence extends VMPFence_Audit_Log_Observer {

    /**
     * Register all VMPFence custom action hooks.
     *
     * @return void
     */
    public function register_hooks() {

        // ----- CENTRALIZED CONFIG CHANGE HOOK -----
        // Catches ALL VMPFence_Config::set() changes from ANY page/handler.
        add_action('vmpfence_config_changed', array($this, 'on_config_changed'), 10, 3);

        // ----- WAF (6 hooks — non-config actions only) -----
        add_action('vmpfence_waf_rule_created',             array($this, 'on_waf_rule_created'), 10, 1);
        add_action('vmpfence_waf_rule_deleted',             array($this, 'on_waf_rule_deleted'), 10, 1);
        add_action('vmpfence_waf_rule_toggled',             array($this, 'on_waf_rule_toggled'), 10, 3);
        add_action('vmpfence_waf_category_toggled',         array($this, 'on_waf_category_toggled'), 10, 2);
        add_action('vmpfence_waf_allow_entry_created',      array($this, 'on_waf_allow_entry_created'), 10, 1);
        add_action('vmpfence_waf_allow_entry_deleted',      array($this, 'on_waf_allow_entry_deleted'), 10, 1);
        add_action('vmpfence_waf_allow_entry_toggled',      array($this, 'on_waf_allow_entry_toggled'), 10, 2);

        // ----- Scanning (2 hooks — non-config actions) -----
        add_action('vmpfence_scan_started',           array($this, 'on_scan_started'), 10, 1);
        add_action('vmpfence_scan_completed',         array($this, 'on_scan_completed'), 10, 3);

        // ----- Blocking (4 hooks — non-config actions) -----
        add_action('vmpfence_ip_blocked',          array($this, 'on_ip_blocked'), 10, 3);
        add_action('vmpfence_ip_unblocked',        array($this, 'on_ip_unblocked'), 10, 1);
        add_action('vmpfence_country_blocked',     array($this, 'on_country_blocked'), 10, 1);
        add_action('vmpfence_country_unblocked',   array($this, 'on_country_unblocked'), 10, 1);

        // ----- Custom Patterns (3 hooks — non-config actions) -----
        add_action('vmpfence_custom_pattern_added',   array($this, 'on_custom_pattern_added'), 10, 3);
        add_action('vmpfence_custom_pattern_deleted',  array($this, 'on_custom_pattern_deleted'), 10, 3);
        add_action('vmpfence_custom_pattern_toggled',  array($this, 'on_custom_pattern_toggled'), 10, 2);

        // ----- Import / Export / Reset (3 hooks — non-config actions) -----
        add_action('vmpfence_settings_imported', array($this, 'on_settings_imported'), 10, 1);
        add_action('vmpfence_settings_exported', array($this, 'on_settings_exported'), 10, 1);
        add_action('vmpfence_config_reset',      array($this, 'on_config_reset'), 10, 0);

        // ----- Brute Force (2 hooks — runtime security events) -----
        add_action('vmpfence_brute_force_lockout', array($this, 'on_brute_force_lockout'), 10, 4);
        add_action('vmpfence_brute_force_detected', array($this, 'on_brute_force_detected'), 10, 2);

        // ----- Backups (3 hooks — non-config actions) -----
        add_action('vmpfence_backup_created',  array($this, 'on_backup_created'), 10, 2);
        add_action('vmpfence_backup_restored', array($this, 'on_backup_restored'), 10, 2);
        add_action('vmpfence_backup_deleted',  array($this, 'on_backup_deleted'), 10, 1);

        // ----- Database (2 hooks — infrastructure events) -----
        add_action('vmpfence_database_updated', array($this, 'on_database_updated'), 10, 2);
        add_action('vmpfence_database_cleanup', array($this, 'on_database_cleanup'), 10, 3);

        // ----- Support / Feedback (2 hooks) -----
        add_action('vmpfence_support_ticket_submitted', array($this, 'on_support_ticket_submitted'), 10, 3);
        add_action('vmpfence_feedback_submitted',       array($this, 'on_feedback_submitted'), 10, 2);

        // ----- Audit Log Mode (1 hook — uses update_option, not Config::set) -----
        add_action('vmpfence_audit_log_mode_changed', array($this, 'on_audit_log_mode_changed'), 10, 2);

        // ----- Config Deletion (1 hook) -----
        add_action('vmpfence_config_deleted', array($this, 'on_config_deleted'), 10, 1);

        // ----- IP Deletion (1 hook) -----
        add_action('vmpfence_ip_deleted', array($this, 'on_ip_deleted'), 10, 2);

        // ----- License (1 hook — special masking) -----
        add_action('vmpfence_license_changed', array($this, 'on_license_changed'), 10, 2);

        // ----- WORDPRESS OPTIONS HOOK -----
        // Catches all update_option() calls for vmpfence-prefixed options.
        // This covers settings NOT stored via VMPFence_Config::set() (traffic, country blocking, license, etc.).
        add_action('updated_option', array($this, 'on_wp_option_updated'), 10, 3);
        add_action('added_option',   array($this, 'on_wp_option_added'), 10, 2);
    }

    // =========================================================================
    // CONFIG KEY → EVENT TYPE MAPPING (centralized handler)
    // =========================================================================

    /**
     * Map of VMPFence_Config keys to their audit log event type and severity.
     *
     * @return array
     */
    private function get_config_key_map() {
        return array(
            // WAF settings
            'waf_enabled'                     => array('event' => 'vmpfence_waf_mode_changed',                'severity' => 'critical'),
            'waf_protection_level'            => array('event' => 'vmpfence_waf_protection_level_changed',    'severity' => 'critical'),
            'realtime_ip_blocklist_enabled'    => array('event' => 'vmpfence_waf_blocklist_toggled',           'severity' => 'warning'),
            'auto_update_rules'               => array('event' => 'vmpfence_waf_auto_update_toggled',         'severity' => 'warning'),

            // Brute force settings
            'brute_force_enabled'             => array('event' => 'vmpfence_brute_force_toggled',             'severity' => 'critical'),
            'max_login_attempts'              => array('event' => 'vmpfence_login_threshold_changed',         'severity' => 'warning'),
            'max_forgot_password_attempts'    => array('event' => 'vmpfence_forgot_password_threshold_changed','severity' => 'warning'),
            'count_failures_window'           => array('event' => 'vmpfence_login_security_period_changed',   'severity' => 'warning'),
            'lockout_duration'                => array('event' => 'vmpfence_lockout_duration_changed',        'severity' => 'warning'),
            'permanent_ban_threshold'         => array('event' => 'vmpfence_block_duration_changed',          'severity' => 'warning'),
            'custom_block_text'               => array('event' => 'vmpfence_custom_block_text_changed',       'severity' => 'info'),
            'lock_invalid_usernames'          => array('event' => 'vmpfence_lock_invalid_usernames_toggled',  'severity' => 'warning'),
            'blocked_usernames'               => array('event' => 'vmpfence_banned_usernames_updated',        'severity' => 'warning'),

            // Security toggles
            'prevent_leaked_passwords_enabled'=> array('event' => 'vmpfence_breached_passwords_toggled',      'severity' => 'warning'),
            'enforce_strong_passwords'        => array('event' => 'vmpfence_strong_passwords_toggled',        'severity' => 'info'),
            'dont_reveal_users'               => array('event' => 'vmpfence_mask_login_errors_toggled',       'severity' => 'info'),
            'prevent_admin_username'          => array('event' => 'vmpfence_prevent_admin_username_toggled',  'severity' => 'info'),
            'prevent_username_discovery'      => array('event' => 'vmpfence_block_author_scan_toggled',       'severity' => 'info'),
            'disable_app_passwords'           => array('event' => 'vmpfence_app_passwords_toggled',           'severity' => 'warning'),
            'block_post_requests'             => array('event' => 'vmpfence_block_bad_post_toggled',          'severity' => 'info'),
            'check_password_strength'         => array('event' => 'vmpfence_check_password_strength_toggled', 'severity' => 'info'),
            'participate_security_network'    => array('event' => 'vmpfence_security_network_toggled',        'severity' => 'warning'),

            // Rate limiting
            'rate_limiting_enabled'           => array('event' => 'vmpfence_rate_limiting_toggled',           'severity' => 'warning'),
            'google_crawler_treatment'        => array('event' => 'vmpfence_google_crawler_treatment_changed','severity' => 'info'),
            'global_rate_limit'               => array('event' => 'vmpfence_global_rate_limit_changed',       'severity' => 'info'),
            'global_rate_action'              => array('event' => 'vmpfence_global_rate_action_changed',      'severity' => 'warning'),
            'crawler_rate_limit'              => array('event' => 'vmpfence_crawler_rate_limit_changed',      'severity' => 'info'),
            'crawler_rate_action'             => array('event' => 'vmpfence_crawler_rate_action_changed',     'severity' => 'warning'),
            'crawler_404_limit'               => array('event' => 'vmpfence_crawler_404_limit_changed',       'severity' => 'info'),
            'crawler_404_action'              => array('event' => 'vmpfence_crawler_404_action_changed',      'severity' => 'warning'),
            'human_rate_limit'                => array('event' => 'vmpfence_human_rate_limit_changed',        'severity' => 'info'),
            'human_rate_action'               => array('event' => 'vmpfence_human_rate_action_changed',       'severity' => 'warning'),
            'human_404_limit'                 => array('event' => 'vmpfence_human_404_limit_changed',         'severity' => 'info'),
            'human_404_action'                => array('event' => 'vmpfence_human_404_action_changed',        'severity' => 'warning'),
            'rate_block_duration'             => array('event' => 'vmpfence_rate_block_duration_changed',     'severity' => 'warning'),
            'throttle_behavior'               => array('event' => 'vmpfence_throttle_behavior_changed',       'severity' => 'info'),
            'throttle_delay_duration'         => array('event' => 'vmpfence_throttle_delay_changed',          'severity' => 'info'),
            'allowlisted_404_urls'            => array('event' => 'vmpfence_allowlisted_404_urls_updated',    'severity' => 'info'),

            // IP detection
            'ip_detection_method'             => array('event' => 'vmpfence_ip_source_changed',               'severity' => 'critical'),
            'trusted_proxies'                 => array('event' => 'vmpfence_trusted_proxies_changed',         'severity' => 'warning'),
            'trusted_proxy_preset'            => array('event' => 'vmpfence_trusted_proxy_preset_changed',    'severity' => 'warning'),

            // Advanced firewall
            'whitelisted_ips_bypass'          => array('event' => 'vmpfence_allowed_ips_updated',             'severity' => 'warning'),
            'blocked_urls'                    => array('event' => 'vmpfence_banned_urls_updated',             'severity' => 'warning'),
            'whitelisted_services'            => array('event' => 'vmpfence_allowlisted_services_updated',   'severity' => 'warning'),
            'delay_ip_blocking'               => array('event' => 'vmpfence_delay_ip_blocking_toggled',      'severity' => 'warning'),
            'ignored_ips_waf'                 => array('event' => 'vmpfence_ignored_ips_waf_updated',         'severity' => 'info'),

            // Country blocking
            'country_block_action'            => array('event' => 'vmpfence_country_block_action_changed',    'severity' => 'warning'),
            'country_block_logged_in'         => array('event' => 'vmpfence_country_block_logged_in_changed', 'severity' => 'info'),

            // Scan settings
            'scanType'                        => array('event' => 'vmpfence_scan_type_changed',               'severity' => 'info'),
            'scheduledScansEnabled'            => array('event' => 'vmpfence_scan_schedule_toggled',           'severity' => 'warning'),
            'schedMode'                       => array('event' => 'vmpfence_scan_schedule_changed',           'severity' => 'info'),
            'lowResourceScansEnabled'         => array('event' => 'vmpfence_scan_low_resource_toggled',       'severity' => 'info'),

            // 2FA / Login security (stored with vmpfence_ prefix)
            'vmpfence_2fa_role_administrator' => array('event' => 'vmpfence_2fa_required_roles_changed',      'severity' => 'warning'),
            'vmpfence_2fa_role_editor'        => array('event' => 'vmpfence_2fa_required_roles_changed',      'severity' => 'warning'),
            'vmpfence_2fa_role_author'        => array('event' => 'vmpfence_2fa_required_roles_changed',      'severity' => 'warning'),
            'vmpfence_2fa_role_contributor'   => array('event' => 'vmpfence_2fa_required_roles_changed',      'severity' => 'warning'),
            'vmpfence_2fa_role_subscriber'    => array('event' => 'vmpfence_2fa_required_roles_changed',      'severity' => 'warning'),
            'vmpfence_2fa_grace_period'       => array('event' => 'vmpfence_2fa_grace_period_changed',        'severity' => 'info'),
            'vmpfence_2fa_remember_device'    => array('event' => 'vmpfence_2fa_remember_device_toggled',     'severity' => 'info'),
            'vmpfence_xmlrpc_2fa_required'    => array('event' => 'vmpfence_xmlrpc_2fa_toggled',              'severity' => 'warning'),
            'vmpfence_disable_xmlrpc_auth'    => array('event' => 'vmpfence_xmlrpc_toggled',                  'severity' => 'warning'),
            'vmpfence_woocommerce_integration'=> array('event' => 'vmpfence_woocommerce_2fa_toggled',         'severity' => 'info'),
            'vmpfence_2fa_shortcode_enabled'  => array('event' => 'vmpfence_2fa_shortcode_toggled',           'severity' => 'info'),
            'vmpfence_recaptcha_enabled'      => array('event' => 'vmpfence_captcha_toggled',                 'severity' => 'warning'),
            'vmpfence_recaptcha_threshold'    => array('event' => 'vmpfence_captcha_threshold_changed',       'severity' => 'info'),
            'vmpfence_ip_allowlist'           => array('event' => 'vmpfence_2fa_bypass_ips_changed',          'severity' => 'warning'),

            // License
            'license_key'                     => array('event' => 'vmpfence_license_changed',                 'severity' => 'critical'),
            'central_api_key'                 => array('event' => 'vmpfence_api_key_changed',                 'severity' => 'critical'),
        );
    }

    /**
     * Centralized handler for all VMPFence_Config::set() changes.
     * Maps config keys to specific audit log event types.
     *
     * @param string $key        The config key that changed.
     * @param mixed  $old_value  Previous value.
     * @param mixed  $new_value  New value.
     * @return void
     */
    public function on_config_changed($key, $old_value, $new_value) {
        // Skip internal/operational keys that aren't user-facing settings.
        // These change automatically (scan scheduling, timestamps, caches, sync).
        static $always_ignored_keys = array(
            'allScansScheduled', 'vmpfenceScanSchedule', 'lastScheduleUpdate',
            'lastActivityAnalysis', 'last_blocked_ip_sync_time', 'last_scan_time',
            'last_scan_result', 'scan_progress', 'scan_status', 'scan_stage',
            'next_scan_at', 'scan_last_summary', 'waf_rules_last_updated',
            'waf_last_rule_count', 'blocked_ip_list_last_sync', 'ip_blocklist_version',
            'config_cache_timestamp', 'db_version', 'plugin_version',
            'activation_time', 'first_activation', 'install_source',
            // Scheduler sync timestamps
            'last_signature_sync_time', 'last_waf_sync_time',
            // UI state
            'onboarding_dismissed',
            // Notification / alerting sub-options
            'alertOn_scanIssues', 'alertOn_critical', 'alertOn_update',
            'alert_maxHourly', 'email_summary_enabled', 'email_summary_excluded_directories',
            // Scheduling sub-fields (schedMode is in config_key_map, so not ignored)
            'schedStartHour', 'schedStartDayOfWeek', 'schedStartDate',
            'manualScanType',
            // API / meta keys
            'isPaid', 'keyType', 'apiKey', 'howGetIPs', 'serverIPs',
            'google_safebrowsing_api_key',
            // WAF allowlist entries (dedicated events handle these)
            'allowlisted_url_params',
        );

        // Scan sub-options: suppressed for preset scan types (limited/standard/high_sensitivity)
        // but ALLOWED for custom scan type so individual toggles are logged.
        static $scan_sub_option_keys = array(
            'scansEnabled_checkGSB', 'scansEnabled_checkReadableConfig',
            'spamvertizeCheck', 'checkSpamIP',
            'scansEnabled_checkHowGetIPs', 'scansEnabled_suspectedFiles',
            'scansEnabled_core', 'scansEnabled_themes', 'scansEnabled_plugins',
            'scansEnabled_coreUnknown', 'scansEnabled_oldVersions',
            'scansEnabled_malware', 'scansEnabled_fileContents',
            'scansEnabled_fileContentsGSB', 'scansEnabled_scanImages',
            'scansEnabled_suspiciousAdminUsers',
            'scansEnabled_risky_usernames', 'scansEnabled_admin_users',
            'scansEnabled_user_enumeration',
            'scansEnabled_posts', 'scansEnabled_comments',
            'scansEnabled_passwds', 'scansEnabled_diskSpace',
            'scansEnabled_suspiciousOptions', 'scansEnabled_wafStatus',
            'scansEnabled_geoipSupport', 'scansEnabled_highSense',
            'other_scanOutside', 'other_hideWP', 'deleteTablesOnDeact', 'debugOn',
            'scan_maxDuration', 'scan_maxIssues', 'scan_maxExecTime',
            'maxMem', 'scan_max_resume_attempts', 'scan_force_ipv4_start',
            'scan_exclude', 'scan_include_extra',
        );

        // Check config_key_map FIRST so explicitly mapped keys always log,
        // even if they also appear in ignored lists by mistake.
        $map = $this->get_config_key_map();

        if (isset($map[$key])) {
            $entry = $map[$key];
            $this->log($entry['event'], array(
                'key'       => $key,
                'old_value' => $this->sanitize_log_value($key, $old_value),
                'new_value' => $this->sanitize_log_value($key, $new_value),
            ), $entry['severity']);
            return;
        }

        // Skip always-ignored operational keys
        if (in_array($key, $always_ignored_keys, true)) {
            return;
        }

        // Scan sub-options: only log when scan type is "custom"
        if (in_array($key, $scan_sub_option_keys, true)) {
            $current_scan_type = VMPFence_Config::get('scanType', 'standard');
            if ($current_scan_type !== 'custom') {
                return; // Preset scan type — suppress individual option noise
            }
            // Custom scan type — log each individual option change
            $label = $this->get_scan_option_label($key);
            $this->log('vmpfence_scan_option_changed', array(
                'key'       => $key,
                'label'     => $label,
                'old_value' => $old_value,
                'new_value' => $new_value,
            ), 'info');
            return;
        }

        // Unknown config key — log generically so nothing is missed
        $this->log('vmpfence_config_changed', array(
            'key'       => $key,
            'old_value' => $this->sanitize_log_value($key, $old_value),
            'new_value' => $this->sanitize_log_value($key, $new_value),
        ), 'info');
    }

    /**
     * Get human-readable label for a scan sub-option key.
     *
     * @param string $key The scan option config key.
     * @return string Human-readable label.
     */
    /**
     * Resolve a country code to its full name.
     *
     * @param string $country_code ISO country code.
     * @return string Country name, or the code itself as fallback.
     */
    private function resolve_country_name($country_code) {
        if (class_exists('VMPFence_Blocked_Country')) {
            $country = VMPFence_Blocked_Country::get_country($country_code);
            if ($country && !empty($country['country_name'])) {
                return $country['country_name'];
            }
        }
        // Fallback: try loading from countries.json
        $json_path = VMPFENCE_PLUGIN_DIR . 'admin/data/countries.json';
        if (file_exists($json_path)) {
            $countries = json_decode(file_get_contents($json_path), true);
            if (is_array($countries)) {
                foreach ($countries as $c) {
                    if (isset($c['code']) && strtoupper($c['code']) === strtoupper($country_code)) {
                        return $c['name'] ?? $country_code;
                    }
                }
            }
        }
        return $country_code;
    }

    private function get_scan_option_label($key) {
        static $labels = array(
            // Reputation & Security Checks
            'scansEnabled_checkGSB'            => 'Google Safe Browsing Check',
            'scansEnabled_checkReadableConfig'  => 'Readable Config Check',
            'spamvertizeCheck'                 => 'Spamvertize Check',
            'checkSpamIP'                      => 'Spam IP Check',
            'scansEnabled_checkHowGetIPs'      => 'IP Detection Method Check',
            'scansEnabled_suspectedFiles'       => 'Suspected Files Check',
            // File Integrity
            'scansEnabled_core'                => 'Scan Core Files',
            'scansEnabled_themes'              => 'Scan Theme Files',
            'scansEnabled_plugins'             => 'Scan Plugin Files',
            'scansEnabled_coreUnknown'         => 'Scan Unknown Core Files',
            'scansEnabled_oldVersions'         => 'Scan Old Versions',
            // Malware & Content
            'scansEnabled_malware'             => 'Malware Scan',
            'scansEnabled_fileContents'        => 'File Content Scan',
            'scansEnabled_fileContentsGSB'     => 'File Content GSB Scan',
            'scansEnabled_scanImages'          => 'Scan Image Files',
            'scansEnabled_suspiciousAdminUsers' => 'Suspicious Admin Users Check',
            // User Security
            'scansEnabled_risky_usernames'     => 'Risky Usernames Check',
            'scansEnabled_admin_users'         => 'Admin Users Check',
            'scansEnabled_user_enumeration'    => 'User Enumeration Check',
            // Posts & Comments
            'scansEnabled_posts'               => 'Scan Posts',
            'scansEnabled_comments'            => 'Scan Comments',
            'scansEnabled_passwds'             => 'Password Strength Check',
            'scansEnabled_diskSpace'           => 'Disk Space Check',
            // Database & Options
            'scansEnabled_suspiciousOptions'    => 'Suspicious Options Check',
            'scansEnabled_wafStatus'           => 'WAF Status Check',
            'scansEnabled_geoipSupport'        => 'GeoIP Support',
            'scansEnabled_highSense'           => 'High Sensitivity Mode',
            // Advanced
            'other_scanOutside'                => 'Scan Outside WordPress',
            'other_hideWP'                     => 'Hide WordPress Version',
            'deleteTablesOnDeact'              => 'Delete Tables on Deactivation',
            'debugOn'                          => 'Debug Mode',
            // Performance
            'lowResourceScansEnabled'          => 'Low Resource Scans',
            'scan_maxDuration'                 => 'Max Scan Duration',
            'scan_maxIssues'                   => 'Max Scan Issues',
            'scan_maxExecTime'                 => 'Max Execution Time',
            'maxMem'                           => 'Max Memory',
            'scan_max_resume_attempts'         => 'Max Resume Attempts',
            'scan_force_ipv4_start'            => 'Force IPv4 Start',
            // Exclusions
            'scan_exclude'                     => 'Scan Exclusions',
            'scan_include_extra'               => 'Extra Scan Inclusions',
        );

        return isset($labels[$key]) ? $labels[$key] : ucwords(str_replace('_', ' ', $key));
    }

    /**
     * Mask sensitive values before logging.
     *
     * Handles:
     *  - Direct sensitive keys (license_key, api_key, etc.)
     *  - Arrays/objects that may contain sensitive nested fields
     *
     * @param string $key   Config/option key.
     * @param mixed  $value Value to sanitize.
     * @return mixed
     */
    private function sanitize_log_value($key, $value) {
        // Keys whose entire value is sensitive
        $sensitive_keys = array(
            'license_key', 'central_api_key',
            'vmpfence_recaptcha_secret_key', 'vmpfence_recaptcha_site_key',
            'vmpfence_api_key', 'vmpfence_license_key', 'vmpfence_central_api_key',
        );

        if (in_array($key, $sensitive_keys, true)) {
            if (is_string($value) && strlen($value) > 4) {
                return substr($value, 0, 4) . '****';
            }
            return '****';
        }

        // Keys whose value is an array/JSON that may contain sensitive nested fields
        $sensitive_nested_keys = array('vmpfence_license');
        $sensitive_fields = array('license_key', 'api_key', 'secret_key');

        if (in_array($key, $sensitive_nested_keys, true)) {
            return $this->mask_nested_sensitive($value, $sensitive_fields);
        }

        return $value;
    }

    /**
     * Recursively mask sensitive fields inside arrays or decoded JSON.
     *
     * @param mixed $value           The value (array, JSON string, or scalar).
     * @param array $sensitive_fields Field names to mask.
     * @return mixed
     */
    private function mask_nested_sensitive($value, $sensitive_fields) {
        // If JSON string, decode first
        if (is_string($value)) {
            $decoded = json_decode($value, true);
            if (is_array($decoded)) {
                return wp_json_encode($this->mask_nested_sensitive($decoded, $sensitive_fields));
            }
            // Plain string on a sensitive-nested key — mask it
            return (strlen($value) > 4) ? substr($value, 0, 4) . '****' : '****';
        }

        if (is_array($value)) {
            foreach ($value as $k => &$v) {
                if (in_array($k, $sensitive_fields, true) && is_string($v) && strlen($v) > 4) {
                    $v = substr($v, 0, 4) . '****';
                } elseif (is_array($v)) {
                    $v = $this->mask_nested_sensitive($v, $sensitive_fields);
                }
            }
            unset($v);
            return $value;
        }

        return $value;
    }

    // =========================================================================
    // WORDPRESS OPTIONS TRACKING (update_option-based settings)
    // =========================================================================

    /**
     * Map of WordPress option names to audit log event types.
     * Only user-facing settings are tracked; internal/operational options are excluded.
     *
     * @return array
     */
    private function get_wp_option_key_map() {
        return array(
            // Traffic options
            'vmpfence_traffic_logging_mode'       => array('event' => 'vmpfence_traffic_logging_mode_changed',      'severity' => 'warning'),
            'vmpfence_ignore_publishing_users'     => array('event' => 'vmpfence_traffic_ignore_setting_changed',    'severity' => 'info'),
            'vmpfence_ignore_usernames'            => array('event' => 'vmpfence_traffic_ignore_setting_changed',    'severity' => 'info'),
            'vmpfence_ignore_ips'                  => array('event' => 'vmpfence_traffic_ignore_setting_changed',    'severity' => 'info'),
            'vmpfence_ignore_user_agent'           => array('event' => 'vmpfence_traffic_ignore_setting_changed',    'severity' => 'info'),
            'vmpfence_max_traffic_rows'            => array('event' => 'vmpfence_traffic_limit_changed',             'severity' => 'info'),
            'vmpfence_max_traffic_days'            => array('event' => 'vmpfence_traffic_limit_changed',             'severity' => 'info'),

            // Country blocking options
            'vmpfence_country_block_redirect'      => array('event' => 'vmpfence_country_block_redirect_changed',    'severity' => 'warning'),
            'vmpfence_country_bypass_field'        => array('event' => 'vmpfence_country_bypass_changed',            'severity' => 'warning'),
            'vmpfence_country_bypass_value'        => array('event' => 'vmpfence_country_bypass_changed',            'severity' => 'warning'),
            'vmpfence_country_bypass_cookie'       => array('event' => 'vmpfence_country_bypass_changed',            'severity' => 'info'),

            // General options
            'vmpfence_enable_deactivation_choice'  => array('event' => 'vmpfence_deactivation_choice_changed',       'severity' => 'info'),
            'vmpfence_preserve_data_on_uninstall'  => array('event' => 'vmpfence_data_preservation_changed',         'severity' => 'warning'),

            // License / API — Only track user-initiated changes, not auto-refresh timestamps.
            // vmpfence_license and vmpfence_license_status are excluded because they auto-update
            // activated_at/last_check on every background license check (noise).
            // Actual license key changes are captured via the dedicated vmpfence_license_changed hook
            // and the config key 'license_key' in get_config_key_map().
            'vmpfence_api_key'                     => array('event' => 'vmpfence_api_key_option_changed',            'severity' => 'critical'),
            'vmpfence_license_email'               => array('event' => 'vmpfence_license_email_changed',             'severity' => 'warning'),
            'vmpfence_license_key'                 => array('event' => 'vmpfence_license_key_option_changed',        'severity' => 'critical'),
            'vmpfence_license_type'                => array('event' => 'vmpfence_license_type_changed',              'severity' => 'warning'),
            'vmpfence_central_api_key'             => array('event' => 'vmpfence_central_api_key_changed',           'severity' => 'critical'),
            'vmpfence_email_alerts_enabled'        => array('event' => 'vmpfence_email_alerts_toggled',              'severity' => 'info'),

            // Scan options (bulk blob) — REMOVED: this is noise from bulk scan-type saves.
            // Individual scan performance keys are already in ignored_keys, and scanType
            // has its own dedicated event (vmpfence_scan_type_changed).

            // YARA threat management
            'vmpfence_yara_whitelisted_files'      => array('event' => 'vmpfence_yara_whitelist_changed',            'severity' => 'warning'),
            'vmpfence_yara_threat_files'           => array('event' => 'vmpfence_yara_threats_changed',              'severity' => 'warning'),

            // Scan issue management
            'vmpfence_ignored_issues'              => array('event' => 'vmpfence_scan_issues_ignored',               'severity' => 'info'),
            'vmpfence_fixed_issues'                => array('event' => 'vmpfence_scan_issues_fixed',                 'severity' => 'info'),
        );
    }

    /**
     * Handler for WordPress updated_option action.
     * Fires after any update_option() call succeeds.
     *
     * @param string $option    Option name.
     * @param mixed  $old_value Previous value.
     * @param mixed  $value     New value.
     * @return void
     */
    public function on_wp_option_updated($option, $old_value, $value) {
        // Skip the audit log mode option — it has a dedicated handler to avoid duplicate logging
        if ($option === 'vmpfence_audit_log_logging_mode') {
            return;
        }

        $map = $this->get_wp_option_key_map();

        if (!isset($map[$option])) {
            return; // Not a tracked option
        }

        // Skip if value hasn't actually changed
        if ($old_value == $value) {
            return;
        }

        $entry = $map[$option];

        $this->log($entry['event'], array(
            'option'    => $option,
            'old_value' => $this->sanitize_log_value($option, $old_value),
            'new_value' => $this->sanitize_log_value($option, $value),
        ), $entry['severity']);
    }

    /**
     * Handler for WordPress added_option action.
     * Fires when an option is created for the first time.
     *
     * @param string $option Option name.
     * @param mixed  $value  New value.
     * @return void
     */
    public function on_wp_option_added($option, $value) {
        $map = $this->get_wp_option_key_map();

        if (!isset($map[$option])) {
            return;
        }

        $entry = $map[$option];

        $this->log($entry['event'], array(
            'option'    => $option,
            'old_value' => null,
            'new_value' => $this->sanitize_log_value($option, $value),
        ), $entry['severity']);
    }

    // =========================================================================
    // WAF CALLBACKS (non-config actions)
    // =========================================================================

    public function on_waf_rule_created($rule_data) {
        $this->log('vmpfence_waf_rule_created', array(
            'rule' => $rule_data,
        ), 'warning');
    }

    public function on_waf_rule_deleted($rule_id) {
        $this->log('vmpfence_waf_rule_deleted', array(
            'rule_id' => $rule_id,
        ), 'warning');
    }

    public function on_waf_rule_toggled($rule_id, $enabled, $rule_name = '') {
        $this->log('vmpfence_waf_rule_toggled', array(
            'rule_id'   => $rule_id,
            'enabled'   => $enabled,
            'rule_name' => $rule_name,
        ), 'warning');
    }

    public function on_waf_category_toggled($category, $enable) {
        $this->log('vmpfence_waf_category_toggled', array(
            'category' => $category,
            'enabled'  => $enable,
        ), 'warning');
    }

    public function on_waf_allow_entry_created($entry_data) {
        $this->log('vmpfence_waf_allow_entry_created', array(
            'entry' => $entry_data,
        ), 'warning');
    }

    public function on_waf_allow_entry_deleted($entry_id) {
        $this->log('vmpfence_waf_allow_entry_deleted', array(
            'entry_id' => $entry_id,
        ), 'warning');
    }

    public function on_waf_allow_entry_toggled($entry_id, $enabled) {
        $this->log('vmpfence_waf_allow_entry_toggled', array(
            'entry_id' => $entry_id,
            'enabled'  => $enabled,
        ), 'warning');
    }

    // =========================================================================
    // SCANNING CALLBACKS (non-config actions)
    // =========================================================================

    public function on_scan_started($scan_type) {
        $this->log('scan_started', array(
            'scan_type' => $scan_type,
        ), 'info');
    }

    public function on_scan_completed($scan_type, $result = array(), $duration = 0) {
        $this->log('scan_completed', array(
            'scan_type' => $scan_type,
            'result'    => $result,
            'duration'  => $duration,
        ), 'info');
    }

    // =========================================================================
    // BLOCKING CALLBACKS (non-config actions)
    // =========================================================================

    public function on_ip_blocked($ip_address, $reason = '', $block_type = '') {
        $this->log('ip_blocked', array(
            'ip'         => $ip_address,
            'reason'     => $reason,
            'block_type' => $block_type,
        ), 'warning');
    }

    public function on_ip_unblocked($ip_address) {
        $this->log('vmpfence_ip_unblocked', array(
            'ip' => $ip_address,
        ), 'warning');
    }

    public function on_country_blocked($country_code) {
        $country_name = $this->resolve_country_name($country_code);
        $this->log('vmpfence_country_blocked', array(
            'country'      => $country_code,
            'country_name' => $country_name,
        ), 'warning');
    }

    public function on_country_unblocked($country_code) {
        $country_name = $this->resolve_country_name($country_code);
        $this->log('vmpfence_country_unblocked', array(
            'country'      => $country_code,
            'country_name' => $country_name,
        ), 'warning');
    }

    // =========================================================================
    // CUSTOM PATTERN CALLBACKS (non-config actions)
    // =========================================================================

    public function on_custom_pattern_added($group_id, $count, $rules = array()) {
        $this->log('vmpfence_custom_pattern_added', array(
            'group_id' => $group_id,
            'count'    => $count,
            'rules'    => $rules,
        ), 'warning');
    }

    public function on_custom_pattern_deleted($identifier, $count, $rules = array()) {
        $this->log('vmpfence_custom_pattern_deleted', array(
            'identifier' => $identifier,
            'count'      => $count,
            'rules'      => $rules,
        ), 'warning');
    }

    public function on_custom_pattern_toggled($pattern_id, $new_state) {
        $this->log('vmpfence_custom_pattern_toggled', array(
            'pattern_id' => $pattern_id,
            'enabled'    => $new_state,
        ), 'info');
    }

    // =========================================================================
    // IMPORT / EXPORT CALLBACKS
    // =========================================================================

    public function on_settings_imported($imported_sections) {
        $this->log('config_imported', array(
            'sections' => $imported_sections,
        ), 'warning');
    }

    public function on_settings_exported($exported_keys) {
        $this->log('config_exported', array(
            'keys' => $exported_keys,
        ), 'info');
    }

    public function on_config_reset() {
        $this->log('config_reset', array(
            'time' => time(),
        ), 'warning');
    }

    // =========================================================================
    // AUDIT LOG MODE CALLBACK (uses update_option, not Config::set)
    // =========================================================================

    public function on_audit_log_mode_changed($old_mode, $new_mode) {
        $this->log('vmpfence_audit_log_mode_changed', array(
            'old_mode' => $old_mode,
            'new_mode' => $new_mode,
        ), 'warning');
    }

    // =========================================================================
    // LICENSE CALLBACK (special masking via dedicated hook)
    // =========================================================================

    public function on_license_changed($old_key, $new_key) {
        $this->log('vmpfence_license_changed', array(
            'old_key' => $old_key ? substr($old_key, 0, 4) . '****' : '',
            'new_key' => $new_key ? substr($new_key, 0, 4) . '****' : '',
        ), 'critical');
    }

    // =========================================================================
    // BRUTE FORCE CALLBACK
    // =========================================================================

    public function on_brute_force_lockout($ip, $duration, $permanent, $failed_attempts) {
        $this->log('brute_force_lockout', array(
            'ip'              => $ip,
            'duration'        => $duration,
            'permanent'       => $permanent,
            'failed_attempts' => $failed_attempts,
        ), 'critical');
    }

    public function on_brute_force_detected($ip, $data) {
        $this->log('brute_force_detected', array(
            'ip'             => $ip,
            'username'       => isset($data['username']) ? $data['username'] : '',
            'total_attempts' => isset($data['total_attempts']) ? $data['total_attempts'] : 0,
            'time_window'    => isset($data['time_window']) ? $data['time_window'] : 0,
            'blocked_until'  => isset($data['blocked_until']) ? $data['blocked_until'] : '',
        ), 'critical');
    }

    // =========================================================================
    // CONFIG DELETION CALLBACK
    // =========================================================================

    public function on_config_deleted($key) {
        $this->log('config_deleted', array('key' => $key), 'warning');
    }

    // =========================================================================
    // IP DELETION CALLBACK
    // =========================================================================

    public function on_ip_deleted($ip, $deleted_by) {
        $this->log('ip_deleted', array(
            'ip_address'  => $ip,
            'deleted_by'  => $deleted_by,
            'deleted_at'  => current_time('mysql'),
        ), 'warning');
    }

    // =========================================================================
    // BACKUP CALLBACKS
    // =========================================================================

    public function on_backup_created($backup_id, $backup_type) {
        $this->log('backup_created', array(
            'backup_id'   => $backup_id,
            'backup_type' => $backup_type,
        ), 'info');
    }

    public function on_backup_restored($backup_id, $current_backup_id) {
        $this->log('backup_restored', array(
            'backup_id'         => $backup_id,
            'current_backup_id' => $current_backup_id,
        ), 'warning');
    }

    public function on_backup_deleted($backup_id) {
        $this->log('backup_deleted', array(
            'backup_id' => $backup_id,
        ), 'info');
    }

    // =========================================================================
    // DATABASE CALLBACKS
    // =========================================================================

    public function on_database_updated($from_version, $to_version) {
        $this->log('database_updated', array(
            'from_version' => $from_version,
            'to_version'   => $to_version,
        ), 'info');
    }

    public function on_database_cleanup($retention_days, $max_rows, $deleted_by_days) {
        $this->log('database_cleanup', array(
            'retention_days'  => $retention_days,
            'max_rows'        => $max_rows,
            'deleted_by_days' => $deleted_by_days,
            'cleanup_time'    => time(),
        ), 'info');
    }

    // =========================================================================
    // SUPPORT / FEEDBACK CALLBACKS
    // =========================================================================

    public function on_support_ticket_submitted($ticket_id, $subject, $priority) {
        $this->log('support_ticket_submitted', array(
            'ticket_id' => $ticket_id,
            'subject'   => $subject,
            'priority'  => $priority,
        ), 'info');
    }

    public function on_feedback_submitted($type, $rating) {
        $this->log('feedback_submitted', array(
            'type'   => $type,
            'rating' => $rating,
        ), 'info');
    }
}
