<?php
/**
 * AJAX Manager - Performance Optimized
 *
 * This file contains the AjaxManager class which provides a centralized,
 * high-performance system for handling AJAX requests within the UltraGuard Pro
 * plugin. The class implements security best practices, rate limiting, and
 * comprehensive error handling while maintaining optimal performance through
 * caching and optimized processing.
 *
 * @package    UltraguardPro\view
 * @author     UltraGuard Pro Team <support@ultraguardpro.com>
 * @copyright  2024 UltraGuard Pro Security Solutions
 * @license    GPL-2.0+
 * @version    1.2.0
 * @link       https://ultraguardpro.com/docs/ajax-manager
 */
namespace UltraguardPro\core;
use UltraguardPro\core\ValidationEngine;
use UltraguardPro\core\CacheManager;

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

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- AJAX analytics and maintenance endpoints intentionally query live security tables for real-time responses.

/**
 * AJAX Manager
 *
 * Handles AJAX requests with optimized performance, security features,
 * and comprehensive error handling. Implements a singleton pattern to
 * manage all AJAX operations centrally with support for custom action
 * registration, rate limiting, and security event logging.
 *
 * Features include:
 * - Singleton pattern for centralized management
 * - Automatic nonce verification with optimized key mapping
 * - Rate limiting to prevent abuse
 * - Client IP detection with static caching
 * - Security event logging for suspicious activities
 * - Optimized sanitization and validation methods
 * - Support for both public and admin-only AJAX actions
 * - Extensible action processing with filter hooks
 *
 * @package    UltraguardPro\view
 * @author     UltraGuard Pro Team
 * @version    1.2.0
 * @since      1.0.0
 * @see        \UltraguardPro\view\DataLoader
 * @see        \UltraguardPro\view\RenderEngine
 * @see        \UltraguardPro\core\ValidationEngine
 * @see        \UltraguardPro\core\Logger
 *
 * @example
 * // Initialize and register a custom action
 * $ajax_manager = AjaxManager::get_instance();
 * $ajax_manager->register_action('custom_action', [$this, 'callback_method'], true);
 *
 * // Handle AJAX request in callback
 * public function callback_method() {
 *     // Process request
 *     wp_send_json_success(['result' => 'success']);
 * }
 */
class AjaxManager {

    /**
     * Registered AJAX actions
     *
     * Stores all registered AJAX action callbacks with their configuration.
     * Structure: ['action_name' => callable, ...]
     *
     * @var array<string, callable>
     * @access private
     */
    private $actions = [];

    /**
     * Per-action configuration set by register_action().
     * Structure: ['action_name' => ['nonce' => ..., 'cap' => ..., 'public' => ..., ...]]
     *
     * @var array<string, array>
     * @access private
     */
    private $action_configs = [];

    private function current_user_can_for_capability( string $capability ): bool {
        if ( '' === $capability ) {
            return true;
        }

        if ( current_user_can( $capability ) ) {
            return true;
        }

        return is_multisite()
            && 'manage_options' === $capability
            && function_exists( '\\UltraguardPro\\ultraguard_current_user_can_manage' )
            && \UltraguardPro\ultraguard_current_user_can_manage();
    }

    private function current_user_can_manage(): bool {
        if ( function_exists( '\\UltraguardPro\\ultraguard_current_user_can_manage' ) ) {
            return \UltraguardPro\ultraguard_current_user_can_manage();
        }

        return current_user_can( 'manage_options' ) || ( is_multisite() && current_user_can( 'manage_network_options' ) );
    }

    /**
     * Instance for singleton pattern
     *
     * Static singleton instance to ensure only one AjaxManager
     * exists throughout the application.
     *
     * @var AjaxManager|null
     * @access private
     * @static
     */
    private static $instance = null;

    /**
     * Nonce key mapping for quick lookup
     *
     * Predefined mapping of action names to their corresponding nonce keys
     * for optimized nonce verification without string manipulation.
     *
     * @var array<string, string>
     * @access private
     * @static
     */
    private static $nonce_keys = [
        'ultraguard_save_options'    => 'ultraguard_save_options',
        'ultraguard_load_data'       => 'ultraguard_load_data',
        'ultraguard_refresh_widget'  => 'ultraguard_refresh_widget',
        'ultraguard_validate_field'  => 'ultraguard_validate_field',
        'ultraguard_search'          => 'ultraguard_search',
        'ultraguard_bulk_action'     => 'ultraguard_bulk_action',
        'ultraguard_widget_action'   => 'ultraguard_widget_action',
        'ultraguard_export'          => 'ultraguard_export',
        'ultraguard_import'          => 'ultraguard_import',
    ];

    /**
     * Static cache for client IP
     *
     * Caches the client IP address to avoid repeated detection
     * and sanitization during the same request.
     *
     * @var string|null
     * @access private
     * @static
     */
    private static $client_ip = null;

    /**
     * Get singleton instance
     *
     * Returns the singleton instance of AjaxManager, creating it
     * if it doesn't exist (lazy initialization).
     *
     * @return AjaxManager  The singleton instance
     * @access public
     * @static
     */
    public static function get_instance() {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Constructor
     *
     * Private constructor to enforce singleton pattern.
     * Initializes with delayed hook setup for performance optimization.
     *
     * @access private
     */
    private function __construct() {
        // Delayed hook initialization to reduce overhead
        add_action( 'init', [ $this, 'init_hooks' ], 1 );
    }

    /**
     * Initialize WordPress hooks
     *
     * Sets up WordPress action hooks for AJAX processing.
     * Separated from constructor for performance optimization.
     *
     * @return void
     * @access public
     */
    public function init_hooks() {
        // Register all built-in AJAX handlers
        $admin_actions = [
            'ultraguard_save_options'   => 'handle_save_options',
            'ultraguard_load_data'      => 'handle_load_data',
            'ultraguard_refresh_widget' => 'handle_refresh_widget',
            'ultraguard_validate_field' => 'handle_validate_field',
            'ultraguard_search'         => 'handle_search',
            'ultraguard_bulk_action'    => 'handle_bulk_action',
            'ultraguard_widget_action'  => 'handle_widget_action',
            'ultraguard_export'         => 'handle_export',
            'ultraguard_import'         => 'handle_import',
            'ultraguard_rt_security_score' => 'handle_rt_security_score',
            'ultraguard_rt_threat_feed'    => 'handle_rt_threat_feed',
        ];

        foreach ( $admin_actions as $action => $method ) {
            add_action( 'wp_ajax_' . $action, [ $this, $method ] );
        }
    }

    /**
     * Register AJAX action
     *
     * Registers a new AJAX action with the WordPress AJAX system.
     * Supports both public (nopriv) and admin-only actions.
     *
     * @param  string    $action   AJAX action name (without wp_ajax_ prefix)
     * @param  callable  $callback Callback function to handle the action
     * @param  bool      $public   Whether it's a public action (default: false)
     * @return bool                True if registration successful, false otherwise
     * @access public
     *
     * @example
     * $ajax_manager->register_action('process_data', [$this, 'process_callback'], false);
     */
    /**
     * Register an AJAX action with full per-action configuration.
     *
     * Signature A — legacy positional (backwards-compatible):
     *   register_action( string $action, callable $callback, bool $public = false, string|null $nonce_key = null )
     *
     * Signature B — config array (preferred):
     *   register_action( string $action, callable $callback, array $config )
     *
     *   $config keys:
     *     'nonce'        string   Nonce action string the client will send.
     *                             Defaults to 'ultraguard_admin_nonce'.
     *     'public'       bool     If true, also registers wp_ajax_nopriv_ hook so
     *                             non-logged-in users can call this action.
     *                             Defaults to false.
     *     'nopriv_only'  bool     If true, registers ONLY the nopriv hook (no priv
     *                             hook). Useful for pure-guest endpoints.
     *                             Defaults to false.
     *     'cap'          string   WordPress capability required to run this action.
     *                             Empty string means no capability check (guest-safe).
     *                             Defaults to 'manage_options'.
     *     'rate_limit'   int      Max calls per 2-minute window. 0 = disabled.
     *                             Defaults to the ultraguard_ajax_rate_limit filter value.
     *
     * Examples:
     *
     *   // Admin-only, default nonce
     *   $ajax->register_action( 'my_action', [ $this, 'handler' ] );
     *
     *   // Public (guest + admin), custom nonce, no capability check
     *   $ajax->register_action( 'passkey_auth', [ $this, 'handler' ], [
     *       'nonce'  => 'ultraguard_admin_nonce',
     *       'public' => true,
     *       'cap'    => '',
     *   ] );
     *
     *   // Guest-only endpoint (login flow)
     *   $ajax->register_action( 'verify_otp', [ $this, 'handler' ], [
     *       'nonce'       => 'ultraguard_admin_nonce',
     *       'nopriv_only' => true,
     *       'cap'         => '',
     *   ] );
     *
     * @param  string          $action   AJAX action name (without wp_ajax_ prefix).
     * @param  callable        $callback Handler to call when the action fires.
     * @param  bool|array      $config   Bool (legacy $public) or config array.
     * @param  string|null     $nonce_key Legacy 4th argument — ignored when $config is an array.
     * @return bool  True on success, false if $action is empty or $callback is not callable.
     */
    public function register_action( $action, $callback, $config = false, $nonce_key = null ): bool {
        if ( empty( $action ) || ! is_callable( $callback ) ) {
            return false;
        }

        // Normalise the $config argument — accept both legacy positional API
        // (bool $public, string|null $nonce_key) and the new array API.
        if ( is_array( $config ) ) {
            $cfg = array_merge( [
                'nonce'       => 'ultraguard_admin_nonce',
                'public'      => false,
                'nopriv_only' => false,
                'cap'         => 'manage_options',
                'rate_limit'  => -1,   // -1 = use filter default
            ], $config );
        } else {
            // Legacy: register_action( $action, $cb, $public = false, $nonce_key = null )
            $cfg = [
                'nonce'       => $nonce_key ?? 'ultraguard_admin_nonce',
                'public'      => (bool) $config,
                'nopriv_only' => false,
                'cap'         => 'manage_options',
                'rate_limit'  => -1,
            ];
        }

        // Store callback and resolved config for use in handle_custom_action().
        $this->actions[ $action ] = $callback;
        $this->action_configs[ $action ] = $cfg;

        // Keep the legacy $nonce_keys map in sync so existing callers of
        // get_nonce_key_for_action() continue to work.
        self::$nonce_keys[ $action ] = $cfg['nonce'];

        // Register WordPress AJAX hooks.
        if ( $cfg['nopriv_only'] ) {
            add_action( 'wp_ajax_nopriv_' . $action, [ $this, 'handle_custom_action' ] );
        } elseif ( $cfg['public'] ) {
            add_action( 'wp_ajax_'        . $action, [ $this, 'handle_custom_action' ] );
            add_action( 'wp_ajax_nopriv_' . $action, [ $this, 'handle_custom_action' ] );
        } else {
            add_action( 'wp_ajax_' . $action, [ $this, 'handle_custom_action' ] );
        }

        return true;
    }

    /**
     * Handle custom AJAX action
     *
     * Central handler for all registered custom AJAX actions.
     * Performs security checks, rate limiting, and dispatches to
     * the appropriate callback function.
     *
     * @return void
     * @access public
     *
     * @throws \Exception  Sends JSON error responses but doesn't throw exceptions
     */
    /**
     * Central dispatcher for all registered custom AJAX actions.
     *
     * Security pipeline (per-action config respected):
     *   1. Nonce verification  — uses the nonce registered for this action.
     *   2. Capability check    — skipped when config 'cap' is empty string.
     *   3. Rate-limit check    — can be disabled per-action via config 'rate_limit' => 0.
     *   4. Callback dispatch   — filterable via 'ultraguard_ajax_callback'.
     */
    public function handle_custom_action(): void {
        $current_action = current_action();
        if ( empty( $current_action ) || ! wp_doing_ajax() ) {
            $this->send_error( esc_html__( 'Invalid request', 'ultraguard-security-suite' ) );
        }

        // Strip hook prefix to get bare action name.
        $action = str_replace( [ 'wp_ajax_nopriv_', 'wp_ajax_' ], '', $current_action );

        if ( ! isset( $this->actions[ $action ] ) || ! is_callable( $this->actions[ $action ] ) ) {
            $this->send_error( esc_html__( 'Action not found', 'ultraguard-security-suite' ) );
        }

        // Resolve per-action config (fall back to sensible defaults for legacy
        // actions that were registered before action_configs existed).
        $cfg = $this->action_configs[ $action ] ?? [
            'nonce'      => $this->get_nonce_key_for_action( $action ),
            'cap'        => 'manage_options',
            'rate_limit' => -1,
        ];

        do_action( 'ultraguard_before_ajax_action', $action, $current_action );

        // 1. Nonce verification.
        $this->verify_nonce( $cfg['nonce'] );

        // 2. Capability check — empty string means guest-safe (no login required).
        $required_cap = $cfg['cap'] ?? 'manage_options';
        if ( ! $this->current_user_can_for_capability( (string) $required_cap ) ) {
            $this->log_security_event( 'unauthorized_ajax_access', [
                'action'  => $action,
                'user_id' => get_current_user_id(),
                'cap'     => $required_cap,
            ] );
            $this->send_error( esc_html__( 'Insufficient permissions', 'ultraguard-security-suite' ), 403 );
            return;
        }

        // 3. Rate-limit check — skip when rate_limit config is 0.
        $rate_limit_cfg = (int) ( $cfg['rate_limit'] ?? -1 );
        if ( $rate_limit_cfg !== 0 && ! $this->check_rate_limit( $action ) ) {
            $this->log_security_event( 'rate_limit_exceeded', [
                'action' => $action,
                'ip'     => $this->get_client_ip(),
            ] );
            $this->send_error( esc_html__( 'Rate limit exceeded. Please try again later.', 'ultraguard-security-suite' ), 429 );
            return;
        }

        // 4. Dispatch.
        $callback = apply_filters( 'ultraguard_ajax_callback', $this->actions[ $action ], $action );
        call_user_func( $callback );

        do_action( 'ultraguard_after_ajax_action', $action, $current_action );
    }

    /**
     * Handle save options
     *
     * Processes AJAX requests to save plugin options with comprehensive
     * sanitization and validation.
     *
     * @return void
     * @access public
     *
     * @example
     * // JavaScript AJAX call
     * jQuery.ajax({
     *     url: ajaxurl,
     *     method: 'POST',
     *     data: {
     *         action: 'ultraguard_save_options',
     *         nonce: nonce_value,
     *         options: {key1: 'value1', key2: 'value2'}
     *     }
     * });
     */
    public function handle_save_options() {
        $this->verify_nonce( 'ultraguard_save_options' );

        if ( ! $this->current_user_can_manage() ) {
            $this->send_error( esc_html__( 'Insufficient permissions', 'ultraguard-security-suite' ) );
        }

        $options = isset( $_POST['options'] ) ? wp_unslash( $_POST['options'] ) : []; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce verified above; array is sanitized field-by-field below.

        if ( empty( $options ) || ! is_array( $options ) ) {
            $this->send_error( esc_html__( 'No options provided', 'ultraguard-security-suite' ) );
        }

        // Sanitize options
        $sanitized_options = [];
        foreach ( $options as $key => $value ) {
            if ( ! is_string( $key ) ) {
                continue; // Skip invalid keys
            }
            $sanitized_key = sanitize_key( $key );
            $sanitized_options[ $sanitized_key ] = $this->sanitize_option( $sanitized_key, $value );
        }

        if ( empty( $sanitized_options ) ) {
            $this->send_error( esc_html__( 'No valid options to save', 'ultraguard-security-suite' ) );
        }

        // Allow filtering of options before saving
        $sanitized_options = apply_filters( 'ultraguard_options_before_save', $sanitized_options );

        // Save options with batched updates where possible
        $saved_count = 0;
        foreach ( $sanitized_options as $key => $value ) {
            if ( update_option( 'ultraguard_' . $key, $value, false ) ) { // autoload = false for performance
                $saved_count++;
            }
        }

        do_action( 'ultraguard_options_saved', $sanitized_options, $saved_count );

        $this->send_success( [
            /* translators: %1$d: count of saved options */
            'message'     => sprintf( esc_html__( '%1$d options saved successfully', 'ultraguard-security-suite' ), $saved_count ),
            'options'     => $sanitized_options,
            'saved_count' => $saved_count,
        ] );
    }

    /**
     * Handle load data
     *
     * Processes AJAX requests to load data from various sources
     * using the DataLoader class.
     *
     * @return void
     * @access public
     */
    public function handle_load_data() {
        $this->verify_nonce( 'ultraguard_load_data' );

        if ( ! $this->current_user_can_manage() ) {
            $this->send_error( esc_html__( 'Insufficient permissions', 'ultraguard-security-suite' ) );
        }

        $source  = isset( $_POST['source'] ) ? $this->sanitize_array( (array) wp_unslash( $_POST['source'] ) ) : []; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce verified above and values are sanitized through sanitize_array().
        $context = isset( $_POST['context'] ) ? $this->sanitize_array( (array) wp_unslash( $_POST['context'] ) ) : []; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce verified above and values are sanitized through sanitize_array().

        if ( ! class_exists( 'UltraguardPro\view\DataLoader' ) ) {
            $this->send_error( esc_html__( 'Data loader not available', 'ultraguard-security-suite' ) );
        }

        $loader = \UltraguardPro\view\DataLoader::get_instance();
        $data   = $loader->load( $source, $context );

        // Allow filtering of loaded data
        $data = apply_filters( 'ultraguard_loaded_data', $data, $source, $context );

        $this->send_success( [
            'data' => $data,
        ] );
    }

    /**
     * Handle widget refresh
     *
     * Processes AJAX requests to refresh widget content dynamically.
     *
     * @return void
     * @access public
     */
    public function handle_refresh_widget() {
        $this->verify_nonce( 'ultraguard_refresh_widget' );

        if ( ! $this->current_user_can_manage() ) {
            $this->send_error( esc_html__( 'Insufficient permissions', 'ultraguard-security-suite' ) );
        }

        $widget_id = isset( $_POST['widget_id'] ) ? sanitize_key( wp_unslash( $_POST['widget_id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above.
        $config    = isset( $_POST['config'] ) ? $this->sanitize_array( (array) wp_unslash( $_POST['config'] ) ) : []; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce verified above and values are sanitized through sanitize_array().
        $data      = isset( $_POST['data'] ) ? $this->sanitize_array( (array) wp_unslash( $_POST['data'] ) ) : []; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce verified above and values are sanitized through sanitize_array().

        if ( empty( $widget_id ) ) {
            $this->send_error( esc_html__( 'Widget ID required', 'ultraguard-security-suite' ) );
        }

        if ( ! class_exists( 'UltraguardPro\view\RenderEngine' ) ) {
            $this->send_error( esc_html__( 'Render engine not available', 'ultraguard-security-suite' ) );
        }

        // Render widget
        $render_engine  = \UltraguardPro\view\RenderEngine::get_instance();
        $widget_scheme  = [
            'type' => $widget_id,
            'id'   => $widget_id . '_' . uniqid( '', true ),
            'data' => $data,
        ];
        $widget_html    = $render_engine->render( $widget_scheme, $config );

        $this->send_success( [
            'html'      => $widget_html,
            'widget_id' => $widget_id,
        ] );
    }

    /**
     * Handle field validation
     *
     * Processes AJAX requests for real-time field validation
     * using the ValidationEngine class.
     *
     * @return void
     * @access public
     */
    public function handle_validate_field() {
        $this->verify_nonce( 'ultraguard_validate_field' );

        $field = isset( $_POST['field'] ) ? sanitize_key( wp_unslash( $_POST['field'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above.
        $value = isset( $_POST['value'] ) ? $this->sanitize_validation_value( wp_unslash( $_POST['value'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce verified above and value is sanitized by sanitize_validation_value().
        $rules = isset( $_POST['rules'] ) ? $this->sanitize_array( (array) wp_unslash( $_POST['rules'] ) ) : []; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce verified above and values are sanitized through sanitize_array().

        if ( empty( $field ) ) {
            $this->send_error( esc_html__( 'Field name required', 'ultraguard-security-suite' ) );
        }

        if ( ! class_exists( 'UltraguardPro\core\ValidationEngine' ) ) {
            $this->send_error( esc_html__( 'Validation engine not available', 'ultraguard-security-suite' ) );
        }

        $validation_engine = ValidationEngine::get_instance();
        $is_valid          = $validation_engine->validate( [ $field => $value ], [ $field => $rules ] );

        $this->send_success( [
            'valid'   => $is_valid,
            'errors'  => $validation_engine->get_errors(),
            'field'   => $field,
        ] );
    }

    /**
     * Handle search request
     *
     * Processes AJAX search requests with support for different
     * search types (users, posts, generic).
     *
     * @return void
     * @access public
     */
    public function handle_search() {
        $this->verify_nonce( 'ultraguard_search' );

        if ( ! $this->current_user_can_manage() ) {
            $this->send_error( esc_html__( 'Insufficient permissions', 'ultraguard-security-suite' ) );
        }

        $query = isset( $_POST['query'] ) ? sanitize_text_field( wp_unslash( $_POST['query'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above.
        $type  = isset( $_POST['type'] ) ? sanitize_key( wp_unslash( $_POST['type'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above.
        $limit = isset( $_POST['limit'] ) ? absint( wp_unslash( $_POST['limit'] ) ) : 10; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above.

        if ( empty( $query ) ) {
            $this->send_error( esc_html__( 'Search query required', 'ultraguard-security-suite' ) );
        }

        // Allow filtering of search parameters
        $search_params = apply_filters( 'ultraguard_search_params', compact( 'query', 'type', 'limit' ) );

        // Implement search logic based on type
        switch ( $search_params['type'] ) {
            case 'users':
                $results = $this->search_users( $search_params['query'], $search_params['limit'] );
                break;
            case 'posts':
                $results = $this->search_posts( $search_params['query'], $search_params['limit'] );
                break;
            default:
                $results = $this->search_generic( $search_params['query'], $search_params['limit'] );
        }

        // Allow filtering of search results
        $results = apply_filters( 'ultraguard_search_results', $results, $search_params );

        $this->send_success( [
            'results' => $results,
            'query'   => $search_params['query'],
            'type'    => $search_params['type'],
        ] );
    }

    /**
     * Handle bulk action
     *
     * Processes AJAX bulk action requests for performing operations
     * on multiple items simultaneously.
     *
     * @return void
     * @access public
     */
    public function handle_bulk_action() {
        $this->verify_nonce( 'ultraguard_bulk_action' );

        if ( ! $this->current_user_can_manage() ) {
            $this->send_error( esc_html__( 'Insufficient permissions', 'ultraguard-security-suite' ) );
        }

        $action = isset( $_POST['bulk_action'] ) ? sanitize_key( wp_unslash( $_POST['bulk_action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above.
        $items  = isset( $_POST['items'] ) ? array_map( 'absint', (array) wp_unslash( $_POST['items'] ) ) : []; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above and each item is sanitized with absint().

        if ( empty( $action ) ) {
            $this->send_error( esc_html__( 'No action specified', 'ultraguard-security-suite' ) );
        }

        if ( empty( $items ) ) {
            $this->send_error( esc_html__( 'No items selected', 'ultraguard-security-suite' ) );
        }

        $processed = $this->process_bulk_action( $action, $items );

        do_action( 'ultraguard_bulk_action_processed', $action, $items, $processed );

        $this->send_success( [
            /* translators: %1$d: number of items processed */
            'message'    => sprintf( esc_html__( 'Processed %1$d items', 'ultraguard-security-suite' ), $processed ),
            'processed'  => $processed,
            'action'     => $action,
        ] );
    }

    /**
     * Verify nonce
     *
     * Verifies the security nonce for AJAX requests with
     * comprehensive error handling and security event logging.
     *
     * @param  string  $nonce_key  Nonce key to verify against
     * @return void
     * @access protected
     *
     * @throws \Exception  Sends JSON error if nonce verification fails
     */
    protected function verify_nonce( $nonce_key ) {
        if ( ! isset( $_REQUEST['nonce'] ) || empty( $_REQUEST['nonce'] ) ) {
            $this->send_error( esc_html__( 'Security nonce missing', 'ultraguard-security-suite' ) );
        }

        $nonce = sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) );
        $keys_to_try = array_unique( array_filter( [
            $nonce_key,
            'ultraguard_admin_nonce',
        ] ) );

        foreach ( $keys_to_try as $key ) {
            if ( wp_verify_nonce( $nonce, $key ) ) {
                return; // Nonce is valid — carry on.
            }
        }

        // None of the keys matched — genuine security failure.
        $this->log_security_event( 'invalid_nonce', [
            'nonce_key'  => $nonce_key,
            'tried_keys' => implode( ', ', $keys_to_try ),
            'ip'         => $this->get_client_ip(),
        ] );
        $this->send_error( esc_html__( 'Security verification failed', 'ultraguard-security-suite' ) );
    }

    /**
     * Send JSON success response
     *
     * Sends a standardized JSON success response with optional
     * data payload. Supports response filtering.
     *
     * @param  mixed  $data  Response data (default: null)
     * @return void
     * @access protected
     */
    protected function send_success( $data = null ) {
        if ( null === $data ) {
            $data = [];
        }

        // Allow filtering of success response
        $data = apply_filters( 'ultraguard_ajax_success_response', $data );

        $response = \UltraguardPro\support\AjaxResponseBuilder::make()
            ->success()
            ->mergeData( is_array( $data ) ? $data : [ 'result' => $data ] );

        $response->send( 200 );
    }

    /**
     * Send JSON error response
     *
     * Sends a standardized JSON error response with error message
     * and HTTP status code. Supports error response filtering.
     *
     * @param  string  $message  Error message
     * @param  int     $code     HTTP status code (default: 400)
     * @return void
     * @access protected
     */
    protected function send_error( $message, $code = 400 ) {
        // Allow filtering of error response
        $message = apply_filters( 'ultraguard_ajax_error_response_message', $message, $code );

        \UltraguardPro\support\AjaxResponseBuilder::make()
            ->error( $message, (string) $code )
            ->send( is_int( $code ) ? $code : 400 );
    }

    /**
     * Sanitize option value
     *
     * Sanitizes option values based on their data type with
     * optimized handling for common data types.
     *
     * @param  string  $key    Option key
     * @param  mixed   $value  Option value to sanitize
     * @return mixed           Sanitized value
     * @access private
     */
    private function sanitize_option( $key, $value ) {
        if ( is_array( $value ) ) {
            return $this->sanitize_array( $value );
        }

        if ( is_string( $value ) ) {
            return sanitize_text_field( $value );
        }

        if ( is_numeric( $value ) ) {
            return $value + 0; // Convert to int/float without function call
        }

        if ( is_bool( $value ) ) {
            return (bool) $value;
        }

        return '';
    }

    /**
     * Sanitize array values
     *
     * Recursively sanitizes all values in an array using
     * appropriate sanitization functions.
     *
     * @param  array  $array  Array to sanitize
     * @return array          Sanitized array
     * @access private
     */
    private function sanitize_array( $array ) {
        if ( ! is_array( $array ) ) {
            return [];
        }

        $sanitized = [];
        foreach ( $array as $key => $value ) {
            $sanitized_key = is_string( $key ) ? sanitize_key( $key ) : $key;
            if ( is_array( $value ) ) {
                $sanitized[ $sanitized_key ] = $this->sanitize_array( $value );
            } else {
                $sanitized[ $sanitized_key ] = sanitize_text_field( (string) $value );
            }
        }

        return $sanitized;
    }

    /**
     * Sanitize validation value
     *
     * Sanitizes validation values with type-specific handling
     * for arrays and strings.
     *
     * @param  mixed  $value  Value to sanitize
     * @return mixed          Sanitized value
     * @access private
     */
    private function sanitize_validation_value( $value ) {
        if ( is_array( $value ) ) {
            return $this->sanitize_array( $value );
        }

        if ( is_string( $value ) ) {
            return sanitize_text_field( $value );
        }

        return $value;
    }

    /**
     * Check rate limiting
     *
     * Implements rate limiting for AJAX actions based on user ID
     * and IP address to prevent abuse.
     *
     * @param  string  $action  Action name to check rate limit for
     * @return bool             True if request allowed, false if rate limited
     * @access protected
     */
    protected function check_rate_limit( $action ) {
        global $wpdb;

        $user_id    = get_current_user_id();
        $ip         = $this->get_client_ip();
        $rate_limit = (int) apply_filters( 'ultraguard_ajax_rate_limit', 30, $action );
        $window_sec = 120; // 2-minute rolling window

        // Use a single atomic SQL UPSERT to avoid the TOCTOU race condition that
        // exists when using a read-then-write transient pattern.  Two concurrent
        // requests can no longer both pass the limit check simultaneously.
        $option_name = '_transient_ultraguard_rate_' . md5( $action . '_' . $user_id . '_' . $ip );
        $timeout_key = '_transient_timeout_' . substr( $option_name, strlen( '_transient_' ) );
        $expires_at  = time() + $window_sec;

        // Atomic increment: INSERT new row with count=1, or UPDATE existing count+1.
        // The WHERE clause on the timeout row ensures we reset expired windows cleanly.
        $wpdb->query( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query uses prepare(); interpolated table name comes from trusted wpdb property.
            "INSERT INTO {$wpdb->options} (option_name, option_value, autoload)
             VALUES (%s, 1, 'no')
             ON DUPLICATE KEY UPDATE option_value = option_value + 1",
            $option_name
        ) );

        // Ensure the timeout row exists (standard WP transient expiry mechanism).
        $wpdb->query( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query uses prepare(); interpolated table name comes from trusted wpdb property.
            "INSERT IGNORE INTO {$wpdb->options} (option_name, option_value, autoload)
             VALUES (%s, %d, 'no')",
            $timeout_key, $expires_at
        ) );

        // Read the current count atomically.
        $attempts = (int) $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query uses prepare(); interpolated table name comes from trusted wpdb property.
            "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s",
            $option_name
        ) );

        // Honour the rolling window: if the timeout has passed, reset the counter.
        $stored_expiry = (int) $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query uses prepare(); interpolated table name comes from trusted wpdb property.
            "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s",
            $timeout_key
        ) );
        if ( $stored_expiry && $stored_expiry < time() ) {
            // Window expired — reset counter and write a fresh timeout.
            $wpdb->update( $wpdb->options, [ 'option_value' => 1 ],  [ 'option_name' => $option_name ] );
            $wpdb->update( $wpdb->options, [ 'option_value' => $expires_at ], [ 'option_name' => $timeout_key ] );
            $attempts = 1;
        }

        return $attempts <= $rate_limit;
    }

    /**
     * Get client IP address with static caching
     *
     * Detects and returns the client's IP address with comprehensive
     * proxy support and static caching for performance.
     *
     * @return string  Client IP address or empty string if not detectable
     * @access protected
     */
    protected function get_client_ip() {
        if ( null !== self::$client_ip ) {
            return self::$client_ip;
        }

        // Sanitize once and cache
        self::$client_ip = \UltraguardPro\core\Utils::get_client_ip();

        return self::$client_ip;
    }

    /**
     * Log security event
     *
     * Logs security-related events using the SecurityLogger class
     * for auditing and monitoring purposes.
     *
     * @param  string  $event  Event type identifier
     * @param  array   $data   Additional event data
     * @return void
     * @access protected
     */
    protected function log_security_event( $event, $data = [] ) {
        if ( class_exists( \UltraguardPro\core\Logger::class ) ) {
            $message = is_string( $event ) ? $event : 'security_event';
            $context = array_merge( [ 'ip' => $this->get_client_ip() ], (array) $data );
            \UltraguardPro\core\Logger::warning( $message, $context );
        }
    }

    /**
     * Get nonce key for action with optimized lookup
     *
     * Returns the appropriate nonce key for a given action using
     * predefined mapping for optimized performance.
     *
     * @param  string  $action  Action name
     * @return string           Corresponding nonce key
     * @access protected
     */
    protected function get_nonce_key_for_action( $action ) {
        return self::$nonce_keys[ $action ] ?? 'ultraguard_admin_nonce';
    }

    /**
     * Check if action is public
     *
     * Determines if an action hook corresponds to a public (nopriv)
     * AJAX action or an admin-only action.
     *
     * @param  string  $action  Action hook name
     * @return bool             True if public action, false otherwise
     * @access protected
     */
    protected function is_public_action( $action ) {
        return 0 === strpos( $action, 'wp_ajax_nopriv_' );
    }

    /**
     * Process bulk action
     *
     * Processes a bulk action by iterating through items and
     * applying the action to each one individually.
     *
     * @param  string  $action  Action name to perform
     * @param  array   $items   Array of item IDs to process
     * @return int              Number of successfully processed items
     * @access protected
     */
    protected function process_bulk_action( $action, $items ) {
        $processed = 0;

        foreach ( $items as $item_id ) {
            if ( $this->process_single_item( $action, $item_id ) ) {
                $processed++;
            }
        }

        return $processed;
    }

    /**
     * Process single item for bulk action
     *
     * Processes a single item for a bulk action with extensible
     * filtering support for custom processing logic.
     *
     * @param  string  $action   Action name to perform
     * @param  int     $item_id  Item ID to process
     * @return bool              True if processed successfully, false otherwise
     * @access protected
     */
    protected function process_single_item( $action, $item_id ) {
        // Allow filtering of bulk action processing
        $result = apply_filters( 'ultraguard_process_bulk_item', false, $action, $item_id );

        if ( false !== $result ) {
            return $result;
        }

        // Implement specific item processing logic
        switch ( $action ) {
            case 'delete':
                // Delegate to registered handler or perform generic option/post delete
                if ( get_post( $item_id ) ) {
                    return (bool) wp_delete_post( $item_id, true );
                }
                // Fall back to filter for non-post items (firewall rules, quarantine, etc.)
                return (bool) apply_filters( 'ultraguard_bulk_delete_item', false, $item_id );

            case 'activate':
                return (bool) apply_filters( 'ultraguard_bulk_activate_item', false, $item_id );

            case 'deactivate':
                return (bool) apply_filters( 'ultraguard_bulk_deactivate_item', false, $item_id );
            default:
                return false;
        }
    }

    /**
     * Search users with optimized query
     *
     * Performs user search with optimized WordPress user query
     * for maximum performance.
     *
     * @param  string  $query  Search query string
     * @param  int     $limit  Maximum number of results
     * @return array           Array of search results
     * @access protected
     */
    protected function search_users( $query, $limit ) {
        $users = get_users( [
            'search'         => '*' . $query . '*',
            'search_columns' => [ 'user_login', 'user_email', 'display_name' ],
            'number'         => $limit,
            'fields'         => [ 'ID', 'user_login', 'display_name', 'user_email' ],
        ] );

        $results = [];
        foreach ( $users as $user ) {
            $results[] = [
                'id'   => $user->ID,
                'text' => $user->display_name . ' (' . $user->user_email . ')',
                'data' => $user,
            ];
        }

        return $results;
    }

    /**
     * Search posts with optimized query
     *
     * Performs post search with optimized WordPress post query
     * for maximum performance.
     *
     * @param  string  $query  Search query string
     * @param  int     $limit  Maximum number of results
     * @return array           Array of search results
     * @access protected
     */
    protected function search_posts( $query, $limit ) {
        $posts = get_posts( [
            's'                => $query,
            'posts_per_page'   => $limit,
            'post_type'        => 'any',
            'post_status'      => 'publish',
            'no_found_rows'    => true, // Don't calculate total rows for performance
            'suppress_filters' => false,
        ] );

        $results = [];
        foreach ( $posts as $post ) {
            $results[] = [
                'id'   => $post->ID,
                'text' => $post->post_title,
                'data' => $post,
            ];
        }

        return $results;
    }

    /**
     * Generic search
     *
     * Placeholder for generic search functionality.
     * Can be extended to implement custom search logic.
     *
     * @param  string  $query  Search query string
     * @param  int     $limit  Maximum number of results
     * @return array           Empty array (placeholder)
     * @access protected
     */
    protected function search_generic( $query, $limit ) {
        $results = [];
        $query   = sanitize_text_field( $query );

        if ( empty( $query ) ) {
            return $results;
        }

        // 1. WordPress options whose keys contain the query (site-level settings)
        global $wpdb;
        $like  = '%' . $wpdb->esc_like( 'ultraguard_' . $query ) . '%';
        $rows  = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query uses prepare(); interpolated table name comes from trusted wpdb property.
            "SELECT option_name, option_value FROM {$wpdb->options}
             WHERE option_name LIKE %s LIMIT %d",
            $like, min( $limit, 20 )
        ) );
        foreach ( (array) $rows as $row ) {
            // Never expose raw option values — they may contain API keys,
            // SMTP passwords, or licence keys.  Return only the option name.
            $results[] = [
                'id'      => sanitize_key( $row->option_name ),
                'text'    => esc_html( $row->option_name ),
                'type'    => 'setting',
                'excerpt' => __( '(value hidden for security)', 'ultraguard-security-suite' ),
            ];
        }


        // 2. Firewall rules whose description contains the query
        // 3. Security log entries
        try {
            $db_tables = function_exists( '\\UltraguardPro\\ultraguard_pro' )
                ? \UltraguardPro\ultraguard_pro()->modules->get_db_tables()
                : null;

            if ( $db_tables ) {
                $fw_table = $db_tables->get_table( 'firewall_rules' );
                if ( $fw_table && method_exists( $fw_table, 'search' ) ) {
                    $fw_results = $fw_table->search( $query, $limit );
                    foreach ( (array) $fw_results as $rule ) {
                        $results[] = [
                            'id'      => 'fw_' . ( $rule['id'] ?? '' ),
                            'text'    => esc_html( $rule['name'] ?? $rule['description'] ?? $query ),
                            'type'    => 'firewall_rule',
                            'excerpt' => esc_html( $rule['description'] ?? '' ),
                        ];
                    }
                }

                $log_table = $db_tables->get_table( 'security_logs' );
                if ( $log_table && method_exists( $log_table, 'search' ) ) {
                    $log_results = $log_table->search( $query, $limit );
                    foreach ( (array) $log_results as $entry ) {
                        $results[] = [
                            'id'      => 'log_' . ( $entry['id'] ?? '' ),
                            'text'    => esc_html( $entry['event_type'] ?? $query ),
                            'type'    => 'security_log',
                            'excerpt' => esc_html( wp_trim_words( $entry['message'] ?? '', 12 ) ),
                        ];
                    }
                }
            }
        } catch ( \Exception $e ) {
            // Non-fatal — return whatever we have so far
        }

        // Allow modules to append their own results
        $results = apply_filters( 'ultraguard_generic_search_results', $results, $query, $limit );

        return array_slice( $results, 0, $limit );
    }

    /**
     * Clear cached client IP (for testing)
     *
     * Clears the cached client IP address. Primarily used
     * for testing purposes.
     *
     * @return void
     * @access public
     * @static
     */
    public static function clear_ip_cache() {
        self::$client_ip = null;
    }

    /**
     * Handle widget action (placeholder)
     *
     * Placeholder method for widget-specific AJAX actions.
     *
     * @return void
     * @access public
     */
    public function handle_widget_action() {
        $this->verify_nonce( 'ultraguard_widget_action' );

        if ( ! $this->current_user_can_manage() ) {
            $this->send_error( esc_html__( 'Insufficient permissions', 'ultraguard-security-suite' ) );
        }

        $widget_id = isset( $_POST['widget_id'] ) ? sanitize_key( wp_unslash( $_POST['widget_id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above.
        $action    = isset( $_POST['widget_action'] ) ? sanitize_key( wp_unslash( $_POST['widget_action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above.
        $data      = isset( $_POST['data'] ) ? $this->sanitize_array( (array) wp_unslash( $_POST['data'] ) ) : []; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce verified above and values are sanitized through sanitize_array().

        if ( empty( $widget_id ) || empty( $action ) ) {
            $this->send_error( esc_html__( 'Widget ID and action are required', 'ultraguard-security-suite' ) );
        }

        // Allow modules to handle widget-specific actions via filter
        $result = apply_filters( 'ultraguard_widget_action', null, $widget_id, $action, $data );

        if ( null === $result ) {
            $this->send_error(
                /* translators: %1$s: text, %2$s: text */
                sprintf( esc_html__( 'No handler registered for widget "%1$s" action "%2$s"', 'ultraguard-security-suite' ), $widget_id, $action )
            );
        }

        $this->send_success( [
            'widget_id' => $widget_id,
            'action'    => $action,
            'result'    => $result,
        ] );
    }

    /**
     * Handle export (placeholder)
     *
     * Placeholder method for export AJAX actions.
     *
     * @return void
     * @access public
     */
    public function handle_export() {
        $this->verify_nonce( 'ultraguard_export' );

        if ( ! $this->current_user_can_manage() ) {
            $this->send_error( esc_html__( 'Insufficient permissions', 'ultraguard-security-suite' ) );
        }

        $export_type = isset( $_POST['export_type'] ) ? sanitize_key( wp_unslash( $_POST['export_type'] ) ) : 'settings'; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above.
        $format      = isset( $_POST['format'] ) ? sanitize_key( wp_unslash( $_POST['format'] ) ) : 'json'; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above.

        $data = apply_filters( 'ultraguard_export_data', [], $export_type );

        if ( empty( $data ) ) {
            // Default: export all ultraguard_ options
            global $wpdb;
            $rows = $wpdb->get_results(  // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query built via prepare()
                "SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE %s",
                $wpdb->esc_like( 'ultraguard_' ) . '%',
                ARRAY_A
            );
            foreach ( (array) $rows as $row ) {
                $data[ $row['option_name'] ] = maybe_unserialize( $row['option_value'] );
            }
        }

        $filename = 'ultraguard-export-' . $export_type . '-' . gmdate( 'Y-m-d' ) . '.json';
        $json     = wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );

        // Write to a temp file in the private uploads directory.
        $upload_dir = wp_upload_dir();
        $export_dir = trailingslashit( $upload_dir['basedir'] ) . 'ultraguard-security-suite/exports/';
        wp_mkdir_p( $export_dir );

        // Protect the exports directory so direct web access is denied.
        $htaccess = $export_dir . '.htaccess';
        if ( ! file_exists( $htaccess ) ) {
            file_put_contents( $htaccess, "Deny from all\nOptions -Indexes\n" );
        }
        $index = $export_dir . 'index.php';
        if ( ! file_exists( $index ) ) {
            file_put_contents( $index, "<?php
// Silence is golden
" );
        }

        $tmp_file = $export_dir . $filename;
        file_put_contents( $tmp_file, $json );

        // Serve the file as a direct download and delete the temp copy immediately.
        // This avoids leaving sensitive settings accessible on disk after the request.
        if ( ! headers_sent() ) {
            header( 'Content-Type: application/json; charset=utf-8' );
            header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
            header( 'Content-Length: ' . strlen( $json ) );
            header( 'Cache-Control: no-cache, no-store, must-revalidate' );
            header( 'Pragma: no-cache' );
            // $json is the output of wp_json_encode() — inherently safe.
            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
            echo $json; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON-encoded AJAX response  // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON-encoded or nonce value, safe to output
            wp_delete_file( $tmp_file );
            wp_die();
        }

        // Fallback: return a URL if headers were already sent (should not happen in AJAX).
        $download_url = trailingslashit( $upload_dir['baseurl'] ) . 'ultraguard-security-suite/exports/' . $filename;
        $this->send_success( [
            'filename'     => $filename,
            'download_url' => $download_url,
            'record_count' => count( $data ),
            'note'         => 'File will be auto-deleted after first download',
        ] );
    }

    /**
     * Handle import (placeholder)
     *
     * Placeholder method for import AJAX actions.
     *
     * @return void
     * @access public
     */
    public function handle_import() {
        $this->verify_nonce( 'ultraguard_import' );

        if ( ! $this->current_user_can_manage() ) {
            $this->send_error( esc_html__( 'Insufficient permissions', 'ultraguard-security-suite' ) );
        }

        $import_data = null;

        // Accept JSON payload in POST body
        if ( ! empty( $_POST['import_data'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above.
            $raw = wp_unslash( $_POST['import_data'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce verified above; JSON payload is decoded and validated immediately after.
            $import_data = json_decode( $raw, true );
        }

        // Or accept uploaded JSON file
        if ( null === $import_data && ! empty( $_FILES['import_file']['tmp_name'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above.
            $tmp = isset( $_FILES['import_file'] ) ? sanitize_text_field( wp_unslash( $_FILES['import_file']['tmp_name'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- nonce verified above; uploaded file path is unslashed before sanitization.
            if ( is_uploaded_file( $tmp ) ) {
                // Enforce a 512 KB size cap before reading to prevent memory exhaustion.
                if ( filesize( $tmp ) > 512 * 1024 ) {
                    $this->send_error( esc_html__( 'Import file exceeds the 512 KB size limit.', 'ultraguard-security-suite' ) );
                    return;
                }
                $contents    = file_get_contents( $tmp );
                $import_data = json_decode( $contents, true );
            }
        }

        if ( ! is_array( $import_data ) ) {
            $this->send_error( esc_html__( 'Invalid import data: expected JSON object', 'ultraguard-security-suite' ) );
            return;
        }

        // Allow modules to pre-process or reject the import
        $import_data = apply_filters( 'ultraguard_import_data', $import_data );

        // Retrieve the Settings sanitizer once so we can run each value through
        // the same pipeline used when settings are saved normally.
        $settings_obj = function_exists( '\UltraguardPro\ultraguard_pro' )
            ? \UltraguardPro\ultraguard_pro()->settings
            : null;

        $imported = 0;
        $skipped  = 0;

        foreach ( $import_data as $key => $value ) {
            // Only allow ultraguard_ prefixed keys for safety
            if ( strpos( $key, 'ultraguard_' ) !== 0 ) {
                $skipped++;
                continue;
            }

            $sanitized_key = sanitize_key( $key );

            // Strip the "ultraguard_" prefix to get the bare setting name,
            // then run it through the Settings sanitizer if available.
            $bare_key = substr( $sanitized_key, strlen( 'ultraguard_' ) );
            if ( $settings_obj && method_exists( $settings_obj, 'sanitize_settings' ) ) {
                $sanitized_pair = $settings_obj->sanitize_settings( [ $bare_key => $value ] );
                $value = $sanitized_pair[ $bare_key ] ?? null;
                if ( $value === null ) {
                    $skipped++;
                    continue;
                }
            } else {
                // Fallback sanitizer: scalar text only, reject complex structures.
                if ( is_string( $value ) ) {
                    $value = sanitize_textarea_field( $value );
                } elseif ( is_array( $value ) ) {
                    array_walk_recursive( $value, function ( &$v ) {
                        $v = is_string( $v ) ? sanitize_text_field( $v ) : (int) $v;
                    } );
                } elseif ( is_numeric( $value ) ) {
                    $value = is_float( $value + 0 ) ? (float) $value : (int) $value;
                } elseif ( is_bool( $value ) ) {
                    $value = (bool) $value;
                } else {
                    $skipped++; // Reject unknown types
                    continue;
                }
            }

            update_option( $sanitized_key, $value, false );
            $imported++;
        }

        do_action( 'ultraguard_settings_imported', $import_data, $imported );

        $this->send_success( [
            /* translators: %1$d: number, %2$d: number */
            'message'  => sprintf( esc_html__( 'Import complete: %1$d settings imported, %2$d skipped', 'ultraguard-security-suite' ), $imported, $skipped ),
            'imported' => $imported,
            'skipped'  => $skipped,
        ] );
    }
    // ── Realtime fallback handlers — dashboard-level channels ─────────────────

    /**
     * RT fallback: security_score channel.
     *
     * Delegates to DashboardDataHandler::get_security_score() which is the same
     * source of truth used by the dashboard page render and SSE stream.
     */
    public function handle_rt_security_score(): void {
        check_ajax_referer( 'ultraguard_admin_nonce', 'nonce' );
        if ( ! $this->current_user_can_manage() ) {
            wp_send_json_error( [ 'code' => 'permission_denied' ], 403 );
            return;
        }

        try {
            $plugin  = function_exists( '\\UltraguardPro\\ultraguard_pro' ) ? \UltraguardPro\ultraguard_pro() : null;
            $handler = \UltraguardPro\modules\dashboard\DashboardDataHandler::get_instance( $plugin );
            $score   = $handler->get_security_score();
        } catch ( \Throwable $e ) {
            // Inline fallback — mirrors DashboardDataHandler::get_security_score()
            $s = 30;
            $s += (bool) get_option( 'ultraguard_firewall_enabled',      true ) ? 20 : 0;
            $s += (bool) get_option( 'ultraguard_login_limiter_enabled', true ) ? 15 : 0;
            $s += (bool) get_option( 'ultraguard_antivirus_enabled',     true ) ? 20 : 0;
            $s += (bool) get_option( 'ultraguard_file_checker_enabled',  true ) ? 10 : 0;
            $s += is_ssl()                                                        ?  5 : 0;
            $s  = max( 0, min( 100, $s ) );
            $level = match ( true ) {
                $s >= 80 => 'excellent',
                $s >= 60 => 'good',
                $s >= 40 => 'warning',
                default  => 'critical',
            };
            $score = [ 'score' => $s, 'level' => $level ];
        }

        wp_send_json_success( [
            'score'       => $score['score'] ?? 0,
            'level'       => $score['level'] ?? 'warning',
            'label'       => ucfirst( $score['level'] ?? 'warning' ),
            'status_text' => ucfirst( $score['level'] ?? 'warning' ),
        ] );
    }

    /**
     * RT fallback: threat_feed channel.
     *
     * Reads from the firewall_logs table — the same source used by both the
     * SSE SseStream::fetch_threat_feed() and the dashboard overview endpoint.
     */
    public function handle_rt_threat_feed(): void {
        check_ajax_referer( 'ultraguard_admin_nonce', 'nonce' );
        if ( ! $this->current_user_can_manage() ) {
            wp_send_json_error( [ 'code' => 'permission_denied' ], 403 );
            return;
        }

        $plugin  = function_exists( '\\UltraguardPro\\ultraguard_pro' ) ? \UltraguardPro\ultraguard_pro() : null;
        $db      = $plugin ? $plugin->modules->get_db_tables() : null;
        $fw_tbl  = $db ? $db->get_table( 'firewall_logs' ) : null;
        $today   = gmdate( 'Y-m-d 00:00:00' );

        $threats_blocked  = $fw_tbl ? (int) $fw_tbl->count( [ 'action' => 'block', 'created_at >=' => $today ] ) : 0;
        $threats_detected = $fw_tbl ? (int) $fw_tbl->count( [ 'created_at >=' => $today ] ) : 0;

        $feed = [];
        if ( $fw_tbl && method_exists( $fw_tbl, 'get_recent' ) ) {
            $rows = (array) $fw_tbl->get_recent( 10 );
            foreach ( $rows as $row ) {
                $row     = (array) $row;
                $created = $row['created_at'] ?? '';
                $feed[]  = [
                    'time_human' => $created
                        ? human_time_diff( strtotime( $created ), time() ) . ' ' . __( 'ago', 'ultraguard-security-suite' )
                        : '',
                    'ip'       => $row['ip_address'] ?? $row['ip'] ?? '',
                    'rule'     => $row['rule_name']  ?? $row['rule'] ?? '',
                    'action'   => $row['action']     ?? 'block',
                    'severity' => $row['severity']   ?? 'medium',
                ];
            }
        }

        wp_send_json_success( [
            'threats_blocked'  => $threats_blocked,
            'threats_detected' => $threats_detected,
            'feed'             => $feed,
        ] );
    }

}
