<?php
/**
 * Real-time Manager for WordPress
 *
 * This file contains the RealtimeManager class which handles real-time updates,
 * notifications, WebSocket connections, and broadcasting with maximum performance
 * for the Ultraguard Pro plugin. It implements a singleton pattern with AJAX
 * endpoints, connection management, rate limiting, and automatic cleanup.
 *
 * The manager supports multiple channels, persistent broadcasts, client
 * subscriptions, and integrates with WordPress Heartbeat API for connection
 * health monitoring.
 *
 * @package    UltraguardPro\core
 * @author     Vikas Bhardwaj <support@ultraguard.net>
 * @copyright  2024 Ultraguard Pro
 * @license    GPL v2 or later
 * @version    1.0.0
 * @link       https://ultraguardpro.com/docs/realtime-manager
 */

namespace UltraguardPro\view;

use UltraguardPro\core\CacheManager;

defined( 'ABSPATH' ) || exit;

/**
 * Real-time Manager class
 *
 * Handles all real-time communication, updates, and notifications within the
 * Ultraguard Pro plugin. Implements a singleton pattern to ensure a single
 * instance manages all real-time operations across the system.
 *
 * The class provides:
 * - Channel-based update registration and polling
 * - Client connection management with timeouts
 * - Broadcast messaging with persistence
 * - Rate limiting and security validation
 * - Automatic cleanup of stale connections
 * - WordPress Heartbeat API integration
 * - AJAX endpoints for client communication
 *
 * @package    UltraguardPro\view
 * @author     Vikas Bhardwaj
 * @version    1.0.0
 * @since      1.0.0
 * @see        wp_ajax_{action}
 * @see        add_action()
 * @see        wp_send_json_success()
 * @see        wp_send_json_error()
 *
 * @example
 * $manager = RealtimeManager::get_instance();
 * $manager->register_update('security_alerts', function($last_update) {
 *     return get_latest_alerts_since($last_update);
 * }, ['interval' => 2000]);
 *
 * $manager->broadcast('security_alerts', [
 *     'type' => 'critical',
 *     'message' => 'Intrusion detected'
 * ]);
 */
class RealtimeManager {

    /**
     * Singleton instance
     *
     * Stores the single instance of the RealtimeManager class.
     * Ensures only one instance exists throughout the application.
     *
     * @var RealtimeManager|null
     * @access private
     * @static
     */
    private static $instance = null;

    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' ) );
    }

    /**
     * Registered real-time updates
     *
     * Associative array of registered update channels where keys are channel names
     * and values contain callback functions and configuration arrays.
     *
     * @var array<string, array{callback: callable, config: array}>
     * @access private
     * @static
     */
    private static $updates = array();

    /**
     * WebSocket connections
     *
     * Nested associative array tracking active connections by channel and client ID.
     * Structure: [channel][client_id] => connection_data
     *
     * @var array<string, array<string, array{
     *     connected_at: int,
     *     last_activity: int,
     *     retry_count: int,
     *     rate_count: int,
     *     rate_time: int
     * }>>
     * @access private
     * @static
     */
    private static $connections = array();

    /**
     * Connection timeouts by channel
     *
     * Associative array mapping channel names to their timeout values in seconds.
     * Used for automatic cleanup of inactive connections.
     *
     * @var array<string, int>
     * @access private
     * @static
     */
    private static $timeouts = array();

    /**
     * Cache for polling configurations
     *
     * Stores cached configuration data and update results to improve performance
     * and reduce redundant processing for frequent requests.
     *
     * @var array<string, mixed>
     * @access private
     * @static
     */
    private static $config_cache = array();

    /**
     * Broadcast message cache
     *
     * Temporary in-memory storage for recent broadcast messages, indexed by
     * cache keys. Automatically cleans old entries (older than 5 minutes).
     *
     * @var array<string, array{
     *     channel: string,
     *     data: mixed,
     *     timestamp: int,
     *     options: array,
     *     message_id: string
     * }>
     * @access private
     * @static
     */
    private static $broadcast_cache = array();

    /**
     * Active AJAX handlers
     *
     * Tracks registered AJAX action handlers for proper management and
     * potential deregistration if needed.
     *
     * @var array<string, callable>
     * @access private
     * @static
     */
    private static $ajax_handlers = array();

    /**
     * Connection cleanup threshold
     *
     * Minimum number of total connections (recursive count) required before
     * triggering automatic cleanup. Prevents unnecessary cleanup operations
     * when connection count is low.
     *
     * @var int
     * @access private
     * @const
     */
    private const CLEANUP_THRESHOLD = 1000;

    /**
     * Default update configuration
     *
     * Default values for update channel configuration. These values are used
     * when no specific configuration is provided during registration.
     *
     * @var array{
     *     interval: int,
     *     persistent: bool,
     *     capability: string,
     *     max_clients: int,
     *     timeout: int,
     *     retry_limit: int,
     *     rate_limit: int
     * }
     * @access private
     * @const
     */
    private const DEFAULT_UPDATE_CONFIG = array(
        'interval'    => 5000,    // 5 seconds
        'persistent'  => false,
        'capability'  => 'manage_options',
        'max_clients' => 100,
        'timeout'     => 300,     // 5 minutes
        'retry_limit' => 3,
        'rate_limit'  => 60,      // requests per minute
    );

    /**
     * Default broadcast options
     *
     * Default values for broadcast message options. These values are used
     * when no specific options are provided during broadcasting.
     *
     * @var array{
     *     ttl: int,
     *     persistent: bool,
     *     priority: string
     * }
     * @access private
     * @const
     */
    private const DEFAULT_BROADCAST_OPTIONS = array(
        'ttl'        => 60,       // 1 minute
        'persistent' => false,
        'priority'   => 'normal',
    );

    /**
     * Get singleton instance
     *
     * Returns the single instance of the RealtimeManager class, creating it
     * if it doesn't exist. Initializes WordPress hooks on first instantiation.
     *
     * @return RealtimeManager The singleton instance
     * @access public
     * @static
     * @since  1.0.0
     *
     * @example
     * $manager = RealtimeManager::get_instance();
     */
    public static function get_instance(): self {
        if ( null === self::$instance ) {
            self::$instance = new self();
            self::$instance->init_hooks();
        }
        return self::$instance;
    }

    /**
     * Private constructor
     *
     * Initializes static properties to ensure clean state. Prevents direct
     * instantiation of the class (singleton pattern).
     *
     * @return void
     * @access private
     */
    private function __construct() {
        // Initialize static properties
        self::$updates = array();
        self::$connections = array();
        self::$timeouts = array();
        self::$config_cache = array();
        self::$broadcast_cache = array();
    }

    /**
     * Initialize WordPress hooks
     *
     * Registers all necessary WordPress hooks including AJAX handlers,
     * shutdown cleanup, and Heartbeat API integration.
     *
     * @return void
     * @access private
     * @since  1.0.0
     */
    private function init_hooks(): void {
        // Register AJAX endpoints conditionally
        if ( wp_doing_ajax() ) {
            $this->register_ajax_handlers();
        }

        // Register cleanup on shutdown
        add_action( 'shutdown', array( $this, 'auto_cleanup' ), 9999 );

        // Heartbeat API integration for connection health checks
        add_filter( 'heartbeat_received', array( $this, 'handle_heartbeat' ), 10, 2 );
    }

    /**
     * Register AJAX handlers
     *
     * Registers all AJAX action handlers for real-time operations including
     * updates, subscriptions, unsubscriptions, and cleanup.
     *
     * @return void
     * @access private
     * @since  1.0.0
     *
     * @see    add_action()
     */
    private function register_ajax_handlers(): void {
        $handlers = array(
            'ultraguard_realtime_updates'    => array( $this, 'handle_realtime_updates' ),
            'ultraguard_realtime_subscribe'  => array( $this, 'handle_subscription' ),
            'ultraguard_realtime_unsubscribe' => array( $this, 'handle_unsubscription' ),
            'ultraguard_realtime_cleanup'    => array( $this, 'handle_cleanup' ),
        );

        foreach ( $handlers as $action => $callback ) {
            add_action( "wp_ajax_{$action}", $callback );
            add_action( "wp_ajax_nopriv_{$action}", $callback );
            self::$ajax_handlers[ $action ] = $callback;
        }
    }

    /**
     * Register real-time update
     *
     * Registers a new real-time update channel with a callback function that
     * provides data updates. Applies filters to configuration and caches results.
     *
     * @param  string   $channel  Channel name (alphanumeric and underscores)
     * @param  callable $callback Function that returns update data when called
     * @param  array    $config   Optional configuration overrides
     * @return bool               True if registration successful, false otherwise
     * @access public
     * @since  1.0.0
     *
     * @example
     * $manager->register_update('notifications', function($last_update) {
     *     return get_notifications_since($last_update);
     * }, ['interval' => 1000, 'capability' => 'edit_posts']);
     */
    public function register_update( string $channel, callable $callback, array $config = array() ): bool {
        if ( empty( $channel ) ) {
            return false;
        }

        $channel = sanitize_key( $channel );

        // Check if already registered
        if ( isset( self::$updates[ $channel ] ) ) {
            return false;
        }

        $parsed_config = wp_parse_args( $config, self::DEFAULT_UPDATE_CONFIG );

        // Apply filters with caching
        $filter_key = 'update_config_' . $channel;
        if ( ! isset( self::$config_cache[ $filter_key ] ) ) {
            self::$config_cache[ $filter_key ] = apply_filters(
                'ultraguard_realtime_update_config',
                $parsed_config,
                $channel,
                $callback
            );
        }

        self::$updates[ $channel ] = array(
            'callback' => $callback,
            'config'   => self::$config_cache[ $filter_key ],
        );

        // Cache timeout for quick access
        self::$timeouts[ $channel ] = self::$config_cache[ $filter_key ]['timeout'];

        return true;
    }

    /**
     * Handle real-time updates (AJAX endpoint)
     *
     * Processes AJAX requests for real-time updates from clients. Validates
     * requests, checks permissions, applies rate limiting, executes callback,
     * and returns update data with broadcasts.
     *
     * @return void
     * @access public
     * @since  1.0.0
     * @throws \Exception If callback execution fails
     *
     * @see    wp_verify_nonce()
     * @see    current_user_can()
     * @see    wp_send_json_success()
     * @see    wp_send_json_error()
     */
    public function handle_realtime_updates(): void {
        // Early validation
        if ( ! $this->is_valid_request() ) {
            $this->send_error( __( 'Invalid request', 'ultraguard-security-suite' ), 400 );
        }

        // Get and validate parameters
        $channel = $this->get_sanitized_post_param( 'channel' );
        $last_update = absint( $this->get_sanitized_post_param( 'last_update', 0 ) );
        $client_id = $this->get_sanitized_post_param( 'client_id' );
        $nonce = $this->get_sanitized_post_param( 'nonce' );

        $this->verify_nonce( $nonce, 'ultraguard_admin_nonce' );

        // Check channel registration
        if ( ! $channel || ! isset( self::$updates[ $channel ] ) ) {
            $this->send_error( __( 'Invalid channel', 'ultraguard-security-suite' ), 404 );
        }

        $update_config = self::$updates[ $channel ];

        // Check rate limiting
        if ( ! $this->check_rate_limit( $channel, $client_id ) ) {
            $this->send_error( __( 'Rate limit exceeded', 'ultraguard-security-suite' ), 429 );
        }

        // Check capability
        $capability = $update_config['config']['capability'] ?? 'manage_options';
        $can_manage = 'manage_options' === $capability
            ? $this->current_user_can_manage()
            : current_user_can( $capability );

        if ( ! $can_manage ) {
            $this->send_error( __( 'Insufficient permissions', 'ultraguard-security-suite' ), 403 );
        }

        // Update client activity
        if ( $client_id && isset( self::$connections[ $channel ][ $client_id ] ) ) {
            self::$connections[ $channel ][ $client_id ]['last_activity'] = time();
        }

        // Execute callback — do NOT cache across channels/clients to ensure fresh data per-request
        try {
            $data = call_user_func( $update_config['callback'], $last_update );

            // Ensure array for consistent response
            if ( ! is_array( $data ) ) {
                $data = array( 'value' => $data );
            }

            // Check for broadcast messages
            $broadcasts = $this->get_broadcasts( $channel, $last_update );
            if ( ! empty( $broadcasts ) ) {
                $data['_broadcasts'] = $broadcasts;
            }

        } catch ( \Exception $e ) {
            $this->log_error( 'update', $e );
            $this->send_error( __( 'Error processing update', 'ultraguard-security-suite' ), 500 );
        }

        $this->send_success( array(
            'channel'    => $channel,
            'data'       => $data,
            'timestamp'  => time(),
            'next_poll'  => $update_config['config']['interval'],
            'client_id'  => $client_id,
        ) );
    }

    /**
     * Handle channel subscription
     *
     * Processes AJAX requests for clients to subscribe to a real-time channel.
     * Validates request, checks maximum client limits, and registers connection.
     *
     * @return void
     * @access public
     * @since  1.0.0
     *
     * @see    do_action()
     * @see    wp_send_json_success()
     */
    public function handle_subscription(): void {
        if ( ! $this->is_valid_request() ) {
            $this->send_error( __( 'Invalid request', 'ultraguard-security-suite' ), 400 );
        }

        $channel = $this->get_sanitized_post_param( 'channel' );
        $client_id = $this->get_sanitized_post_param( 'client_id' );
        $nonce = $this->get_sanitized_post_param( 'nonce' );

        $this->verify_nonce( $nonce, 'ultraguard_admin_nonce' );

        if ( ! $channel || ! $client_id ) {
            $this->send_error( __( 'Invalid subscription request', 'ultraguard-security-suite' ), 400 );
        }

        if ( ! isset( self::$updates[ $channel ] ) ) {
            $this->send_error( __( 'Channel not registered', 'ultraguard-security-suite' ), 404 );
        }

        // Initialize connections array if needed
        if ( ! isset( self::$connections[ $channel ] ) ) {
            self::$connections[ $channel ] = array();
        }

        // Check max clients
        $max_clients = self::$updates[ $channel ]['config']['max_clients'] ?? 100;
        if ( count( self::$connections[ $channel ] ) >= $max_clients ) {
            $this->send_error( __( 'Maximum clients reached', 'ultraguard-security-suite' ), 429 );
        }

        $current_time = time();
        self::$connections[ $channel ][ $client_id ] = array(
            'connected_at'  => $current_time,
            'last_activity' => $current_time,
            'retry_count'   => 0,
            'rate_count'    => 1,
            'rate_time'     => $current_time,
        );

        // Fire subscription event
        do_action( 'ultraguard_realtime_client_subscribed', $channel, $client_id, self::$connections[ $channel ][ $client_id ] );

        $this->send_success( array(
            'message'   => __( 'Subscribed successfully', 'ultraguard-security-suite' ),
            'channel'   => $channel,
            'client_id' => $client_id,
            'config'    => $this->get_polling_config( $channel ),
        ) );
    }

    /**
     * Handle channel unsubscription
     *
     * Processes AJAX requests for clients to unsubscribe from a real-time channel.
     * Removes client connection data and fires unsubscription event.
     *
     * @return void
     * @access public
     * @since  1.0.0
     *
     * @see    do_action()
     * @see    wp_send_json_success()
     */
    public function handle_unsubscription(): void {
        if ( ! $this->is_valid_request() ) {
            $this->send_error( __( 'Invalid request', 'ultraguard-security-suite' ), 400 );
        }

        $channel = $this->get_sanitized_post_param( 'channel' );
        $client_id = $this->get_sanitized_post_param( 'client_id' );
        $nonce = $this->get_sanitized_post_param( 'nonce' );

        $this->verify_nonce( $nonce, 'ultraguard_admin_nonce' );

        $unsubscribed = false;
        $connection_data = array();

        if ( $channel && $client_id && isset( self::$connections[ $channel ][ $client_id ] ) ) {
            $connection_data = self::$connections[ $channel ][ $client_id ];
            unset( self::$connections[ $channel ][ $client_id ] );
            $unsubscribed = true;

            // Clean up empty channels
            if ( empty( self::$connections[ $channel ] ) ) {
                unset( self::$connections[ $channel ] );
                unset( self::$timeouts[ $channel ] );
            }

            // Fire unsubscription event
            do_action( 'ultraguard_realtime_client_unsubscribed', $channel, $client_id, $connection_data );
        }

        $this->send_success( array(
            'message'      => __( 'Unsubscribed successfully', 'ultraguard-security-suite' ),
            'channel'      => $channel,
            'client_id'    => $client_id,
            'unsubscribed' => $unsubscribed,
        ) );
    }

    /**
     * Handle connection cleanup via AJAX
     *
     * Processes AJAX requests to manually clean up inactive connections.
     * Requires manage_options capability and validates nonce.
     *
     * @return void
     * @access public
     * @since  1.0.0
     *
     * @see    current_user_can()
     * @see    wp_send_json_success()
     */
    public function handle_cleanup(): void {
        if ( ! $this->is_valid_request() ) {
            $this->send_error( __( 'Invalid request', 'ultraguard-security-suite' ), 400 );
        }

        $nonce = $this->get_sanitized_post_param( 'nonce' );
        $force = wp_validate_boolean( $this->get_sanitized_post_param( 'force', false ) );

        $this->verify_nonce( $nonce, 'ultraguard_admin_nonce' );

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

        $cleaned_count = $this->cleanup_connections( $force );

        $this->send_success( array(
            'message'               => __( 'Cleanup completed', 'ultraguard-security-suite' ),
            'cleaned_count'         => $cleaned_count,
            'remaining_connections' => $this->get_connection_stats(),
        ) );
    }

    /**
     * Broadcast message to channel
     *
     * Sends a broadcast message to all clients subscribed to a channel.
     * Supports persistent storage, TTL (time to live), and event firing.
     *
     * @param  string $channel Channel name to broadcast to
     * @param  mixed  $data    Data to broadcast (any serializable type)
     * @param  array  $options Optional broadcast options (TTL, persistence, priority)
     * @return bool            True if broadcast stored successfully, false otherwise
     * @access public
     * @since  1.0.0
     *
     * @example
     * $manager->broadcast('alerts', [
     *     'type' => 'warning',
     *     'message' => 'System maintenance scheduled'
     * ], ['ttl' => 300, 'persistent' => true]);
     */
    public function broadcast( string $channel, $data, array $options = array() ): bool {
        if ( ! $channel || ! isset( self::$updates[ $channel ] ) ) {
            return false;
        }

        $channel = sanitize_key( $channel );
        $parsed_options = wp_parse_args( $options, self::DEFAULT_BROADCAST_OPTIONS );

        // Generate message ID
        $message_id = 'ultraguard_broadcast_' . $channel . '_' . time() . '_' . wp_rand( 1000, 9999 );

        $broadcast_data = array(
            'channel'   => $channel,
            'data'      => $data,
            'timestamp' => time(),
            'options'   => $parsed_options,
            'message_id' => $message_id,
        );

        // Cache broadcast message
        $cache_key = 'broadcast_' . $channel . '_' . time();
        self::$broadcast_cache[ $cache_key ] = $broadcast_data;

        // Store via CacheManager (realtime group — short TTL, cross-process)
        $result = CacheManager::get_instance()->set_realtime( $message_id, $broadcast_data );

        // Store persistent broadcasts if enabled
        if ( $result && $parsed_options['persistent'] ) {
            $this->store_persistent_broadcast( $channel, $broadcast_data );
        }

        // Fire broadcast event
        do_action( 'ultraguard_realtime_broadcast_sent', $channel, $data, $parsed_options, $result );

        return $result;
    }

    /**
     * Get polling configuration for client
     *
     * Returns the polling configuration for a specific channel including
     * interval, endpoint URL, nonce, and other client-side settings.
     *
     * @param  string $channel Channel name
     * @return array          Polling configuration or error array
     * @access public
     * @since  1.0.0
     *
     * @see    wp_create_nonce()
     * @see    admin_url()
     */
    public function get_polling_config( string $channel ): array {
        if ( ! isset( self::$updates[ $channel ] ) ) {
            return array(
                'error'     => 'Channel not registered',
                'available' => false,
            );
        }

        $cache_key = 'polling_config_' . $channel;
        if ( isset( self::$config_cache[ $cache_key ] ) ) {
            return self::$config_cache[ $cache_key ];
        }

        $update_config = self::$updates[ $channel ];

        $config = array(
            'interval'    => $update_config['config']['interval'] ?? 5000,
            'endpoint'    => self_admin_url( 'admin-ajax.php' ),
            'nonce'       => wp_create_nonce( 'ultraguard_admin_nonce' ),
            'timeout'     => $update_config['config']['timeout'] ?? 300,
            'retry_limit' => $update_config['config']['retry_limit'] ?? 3,
            'available'   => true,
        );

        // Cache the configuration
        self::$config_cache[ $cache_key ] = apply_filters(
            'ultraguard_realtime_polling_config',
            $config,
            $channel,
            $update_config
        );

        return self::$config_cache[ $cache_key ];
    }

    /**
     * Clean up inactive connections
     *
     * Removes client connections that have exceeded their timeout period or
     * when forced. Fires cleanup events for each removed connection.
     *
     * @param  bool $force If true, removes all connections regardless of activity
     * @return int         Number of connections cleaned
     * @access public
     * @since  1.0.0
     *
     * @see    do_action()
     */
    public function cleanup_connections( bool $force = false ): int {
        // Skip if no connections
        if ( empty( self::$connections ) ) {
            return 0;
        }

        $now = time();
        $cleaned_count = 0;

        foreach ( self::$connections as $channel => &$clients ) {
            $channel_timeout = self::$timeouts[ $channel ] ?? 300;

            foreach ( $clients as $client_id => $client_data ) {
                if ( $force || ( $now - $client_data['last_activity'] ) > $channel_timeout ) {
                    $connection_data = $clients[ $client_id ];
                    unset( $clients[ $client_id ] );
                    $cleaned_count++;

                    // Fire cleanup event
                    do_action(
                        'ultraguard_realtime_connection_cleaned',
                        $channel,
                        $client_id,
                        $connection_data,
                        $force
                    );
                }
            }

            // Clean up empty channels
            if ( empty( $clients ) ) {
                unset( self::$connections[ $channel ] );
                unset( self::$timeouts[ $channel ] );
            }
        }

        return $cleaned_count;
    }

    /**
     * Automatic cleanup based on threshold
     *
     * Automatically cleans up inactive connections during WordPress shutdown
     * if connection count exceeds threshold or at most once per minute.
     * Logs cleanup activity when WP_DEBUG is enabled.
     *
     * @return void
     * @access public
     * @since  1.0.0
     *
     * @see    error_log()
     */
    public function auto_cleanup(): void {
        static $last_cleanup = 0;

        // Only run cleanup every minute or when threshold is reached
        if ( time() - $last_cleanup < 60 && count( self::$connections, COUNT_RECURSIVE ) < self::CLEANUP_THRESHOLD ) {
            return;
        }

        $cleaned = $this->cleanup_connections( false );
        $last_cleanup = time();

        if ( $cleaned > 0 && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
            \UltraguardPro\core\Logger::error(
                sprintf(
                'Ultraguard Pro: Auto-cleaned %d inactive connections',
                $cleaned
            )
            );
        }
    }

    /**
     * Get connection statistics
     *
     * Returns detailed statistics about active connections including
     * total connections, per-channel counts, and total channels.
     *
     * @return array{
     *     total_connections: int,
     *     channels: array<string, int>,
     *     total_channels: int
     * } Connection statistics
     * @access public
     * @since  1.0.0
     */
    public function get_connection_stats(): array {
        $stats = array(
            'total_connections' => 0,
            'channels'          => array(),
            'total_channels'    => 0,
        );

        foreach ( self::$connections as $channel => $clients ) {
            $client_count = count( $clients );
            $stats['channels'][ $channel ] = $client_count;
            $stats['total_connections'] += $client_count;
        }

        $stats['total_channels'] = count( $stats['channels'] );

        return $stats;
    }

    /**
     * Get registered channels
     *
     * Returns an array of all currently registered real-time update channels.
     *
     * @return string[] Array of channel names
     * @access public
     * @since  1.0.0
     */
    public function get_registered_channels(): array {
        return array_keys( self::$updates );
    }

    /**
     * Handle WordPress heartbeat for connection health
     *
     * Integrates with WordPress Heartbeat API to provide real-time manager
     * status information in heartbeat responses.
     *
     * @param  array $response Heartbeat response array
     * @param  array $data     Heartbeat data received
     * @return array           Modified heartbeat response
     * @access public
     * @since  1.0.0
     *
     * @see    apply_filters()
     */
    public function handle_heartbeat( array $response, array $data ): array {
        if ( isset( $data['ultraguard_realtime_heartbeat'] ) ) {
            $response['ultraguard_realtime_status'] = array(
                'connections' => $this->get_connection_stats(),
                'channels'    => $this->get_registered_channels(),
                'timestamp'   => time(),
            );
        }

        return $response;
    }

    /**
     * Store persistent broadcast message
     *
     * Stores broadcast messages persistently in WordPress options table
     * for retrieval after server restarts or cache clearing.
     *
     * @param  string $channel        Channel name
     * @param  array  $broadcast_data Complete broadcast data array
     * @return void
     * @access private
     * @since  1.0.0
     *
     * @see    get_option()
     * @see    update_option()
     */
    private function store_persistent_broadcast( string $channel, array $broadcast_data ): void {
        $persistent_key = 'ultraguard_persistent_broadcasts';
        $persistent_broadcasts = get_option( $persistent_key, array() );

        if ( ! isset( $persistent_broadcasts[ $channel ] ) ) {
            $persistent_broadcasts[ $channel ] = array();
        }

        $persistent_broadcasts[ $channel ] = $broadcast_data;

        // Keep only last 10 persistent broadcasts per channel
        if ( count( $persistent_broadcasts[ $channel ] ) > 10 ) {
            $persistent_broadcasts[ $channel ] = array_slice( $persistent_broadcasts[ $channel ], -10 );
        }

        update_option( $persistent_key, $persistent_broadcasts, false );
    }

    /**
     * Get broadcast messages for channel
     *
     * Retrieves broadcast messages for a specific channel that were sent
     * after the last update timestamp. Cleans old cache entries automatically.
     *
     * @param  string $channel     Channel name
     * @param  int    $last_update Last update timestamp (Unix timestamp)
     * @return array              Array of broadcast messages
     * @access private
     * @since  1.0.0
     */
    private function get_broadcasts( string $channel, int $last_update ): array {
        $broadcasts = array();
        $now = time();

        // Check memory cache first
        foreach ( self::$broadcast_cache as $cache_key => $broadcast ) {
            if ( strpos( $cache_key, 'broadcast_' . $channel . '_' ) === 0 ) {
                if ( $broadcast['timestamp'] > $last_update ) {
                    $broadcasts[] = $broadcast;
                }

                // Clean old cache entries (older than 5 minutes)
                if ( $now - $broadcast['timestamp'] > 300 ) {
                    unset( self::$broadcast_cache[ $cache_key ] );
                }
            }
        }

        return $broadcasts;
    }

    /**
     * Check rate limiting for client
     *
     * Implements rate limiting logic for client requests based on
     * per-minute request limits defined in channel configuration.
     *
     * @param  string $channel   Channel name
     * @param  string $client_id Client identifier
     * @return bool              True if request is allowed, false if rate limited
     * @access private
     * @since  1.0.0
     */
    private function check_rate_limit( string $channel, string $client_id ): bool {
        if ( ! $client_id || ! isset( self::$connections[ $channel ][ $client_id ] ) ) {
            return true;
        }

        $client_data = &self::$connections[ $channel ][ $client_id ];
        $rate_limit = self::$updates[ $channel ]['config']['rate_limit'] ?? 60;
        $now = time();

        // Reset rate count if minute has passed
        if ( $now - $client_data['rate_time'] >= 60 ) {
            $client_data['rate_count'] = 1;
            $client_data['rate_time'] = $now;
            return true;
        }

        // Check if rate limit exceeded
        if ( $client_data['rate_count'] >= $rate_limit ) {
            return false;
        }

        $client_data['rate_count']++;
        return true;
    }

    /**
     * Get sanitized POST parameter
     *
     * Safely retrieves and sanitizes a POST parameter with fallback to
     * default value. Only processes string values for security.
     *
     * @param  string $key     POST parameter key
     * @param  mixed  $default Default value if parameter not found
     * @return mixed           Sanitized value or default
     * @access private
     * @since  1.0.0
     *
     * @see    sanitize_text_field()
     * @see    wp_unslash()
     */
    private function get_sanitized_post_param( string $key, $default = '' ) {
        if ( ! isset( $_POST[ $key ] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Missing -- helper is only used after nonce verification in realtime AJAX handlers.
            return $default;
        }

        return is_string( $_POST[ $key ] ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.NonceVerification.Missing -- type check only; helper is only used after nonce verification.
            ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Missing -- helper is only used after nonce verification in realtime AJAX handlers.
            : $default;
    }

    /**
     * Check if request is valid
     *
     * Validates that the current request is a POST AJAX request with
     * POST data present.
     *
     * @return bool True if request is valid, false otherwise
     * @access private
     * @since  1.0.0
     */
    private function is_valid_request(): bool {
        return wp_doing_ajax() && isset( $_SERVER['REQUEST_METHOD'] ) &&
               'POST' === $_SERVER['REQUEST_METHOD'] && ! empty( $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- request-shape guard only; nonce is verified in action handlers.
    }

    /**
     * Verify nonce
     *
     * Verifies a WordPress nonce and sends error response if verification fails.
     * Terminates execution with JSON error on failure.
     *
     * @param  string $nonce     Nonce value to verify
     * @param  string $nonce_key Nonce action/key used when creating nonce
     * @return void
     * @access private
     * @since  1.0.0
     *
     * @see    wp_verify_nonce()
     * @see    $this->send_error()
     */
    private function verify_nonce( string $nonce, string $nonce_key ): void {
        if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, $nonce_key ) ) {
            $this->send_error( __( 'Security verification failed', 'ultraguard-security-suite' ), 403 );
        }
    }

    /**
     * Send JSON success response
     *
     * Sends a standardized JSON success response with timestamp and
     * success indicator. Wraps data in consistent response structure.
     *
     * @param  mixed $data Response data to send (will be converted to array)
     * @return void
     * @access private
     * @since  1.0.0
     *
     * @see    wp_send_json_success()
     */
    private function send_success( $data = null ): void {
        $response_data = is_array( $data ) ? $data : array( 'data' => $data );
        // Ensure timestamp is present for polling clients
        $response_data['timestamp'] = $response_data['timestamp'] ?? time();

        \UltraguardPro\support\AjaxResponseBuilder::make()
            ->success()
            ->mergeData( $response_data )
            ->send( 200 );
    }

    /**
     * Send JSON error response
     *
     * Sends a standardized JSON error response with error code, message,
     * timestamp, and success indicator. Includes additional data if provided.
     *
     * @param  string $message         Error message to display
     * @param  int    $code            HTTP status code for error
     * @param  array  $additional_data Optional additional error data
     * @return void
     * @access private
     * @since  1.0.0
     *
     * @see    wp_send_json_error()
     */
    private function send_error( string $message, int $code = 400, array $additional_data = array() ): void {
        $builder = \UltraguardPro\support\AjaxResponseBuilder::make()
            ->error( $message, (string) $code )
            ->addData( 'timestamp', time() );

        if ( ! empty( $additional_data ) ) {
            $builder->mergeData( $additional_data );
        }

        $builder->send( $code );
    }

    /**
     * Log error securely
     *
     * Logs errors to WordPress error log when WP_DEBUG is enabled.
     * Includes context information for easier debugging.
     *
     * @param  string     $context Error context/category
     * @param  \Exception $e       Exception object containing error details
     * @return void
     * @access private
     * @since  1.0.0
     *
     * @see    error_log()
     */
    private function log_error( string $context, \Exception $e ): void {
        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
            \UltraguardPro\core\Logger::error(
                sprintf(
                'Ultraguard Pro Real-time Manager Error (%s): %s',
                $context,
                $e->getMessage()
            )
            );
        }
    }

    /**
     * Get all updates (for debugging/admin)
     *
     * Returns all registered update channels and their configurations.
     * Primarily for debugging and administrative purposes.
     *
     * @return array All registered updates with callbacks and configs
     * @access public
     * @since  1.0.0
     */
    public function get_updates() {
        return self::$updates;
    }

    /**
     * Get all connections (for debugging/admin)
     *
     * Returns all active client connections across all channels.
     * Primarily for debugging and administrative monitoring.
     *
     * @return array All active client connections
     * @access public
     * @since  1.0.0
     */
    public function get_connections(): array {
        return self::$connections;
    }

    /**
     * Clear all caches (for testing/debugging)
     *
     * Resets all internal caches and connection data to empty states.
     * Useful for testing and debugging scenarios.
     *
     * @return void
     * @access public
     * @static
     * @since  1.0.0
     */
    public static function clear_cache(): void {
        self::$config_cache = array();
        self::$broadcast_cache = array();
        self::$connections = array();
        self::$timeouts = array();
    }

    /**
     * Prevent cloning
     *
     * Private clone method to prevent cloning of singleton instance.
     *
     * @return void
     * @access private
     */
    private function __clone() {}

    /**
     * Prevent unserialization
     *
     * Prevents unserialization of singleton instance for security.
     * Triggers WordPress doing_it_wrong notice if attempted.
     *
     * @return void
     * @access public
     * @since  1.0.0
     *
     * @see    _doing_it_wrong()
     */
    public function __wakeup() {
        _doing_it_wrong( __FUNCTION__, esc_html__( 'RealtimeManager cannot be unserialized.', 'ultraguard-security-suite' ), '1.0.0' );
    }
}
