<?php
/**
 * Sentinel Logger Class
 * 
 * Core logging functionality and batch processing.
 */

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

class Sentinel_Logger {
    
    private static $queue = [];
    private static $batch_size = 50;
    private static $queue_option_key = 'sentinel_log_queue';
    private $batch_enabled = false;
    
    // Protection against recursive logging
    private static $currently_logging = false;
    private static $logging_depth = 0;

    public function __construct() {
        // Initialize batch logging hooks if enabled
        $this->init_batch_logging();
    }
    
    /**
     * Initialize batch logging functionality
     */
    private function init_batch_logging() {
        $settings = get_option('sentinel_log_management', array());
        
        // Set batch enabled flag based on settings
        $this->batch_enabled = !empty($settings['batch_logging_enabled']);
        
        if ($this->batch_enabled) {
            // Process queue on shutdown to catch any remaining logs
            add_action('shutdown', array($this, 'maybe_process_queue_on_shutdown'));
        }
    }
    

    
    /**
     * Log an event to the database or queue
     *
     * @param string $event_key The event key (must be registered)
     * @param array $data       Additional event data (optional)
     * @param int $user_id      User ID (optional)
     * @return bool             True on success, false on failure
     */
    public function log($event_key, $data = array(), $user_id = null) {
        // Prevent recursive logging and logging depth issues
        if (self::$currently_logging && self::$logging_depth > 2) {
            error_log('[Sentinel] Prevented recursive logging of: ' . $event_key);
            return false;
        }
        
        // Prevent database error logging during logging operations
        if (self::$currently_logging && in_array($event_key, array('wp_database_error', 'php_fatal_error', 'php_error'))) {
            error_log('[Sentinel] Prevented database error logging during active logging operation: ' . $event_key);
            return false;
        }
        
        // Increment logging depth
        self::$logging_depth++;
        
        // Get rate limiting settings
        $settings = get_option('sentinel_log_management', array());
        $rate_limiting_enabled = !empty($settings['rate_limiting_enabled']);
        
        // Check rate limits (unless bypassed)
        $bypass_rate_limit = isset($data['_bypass_rate_limit']) ? $data['_bypass_rate_limit'] : false;
        if ($rate_limiting_enabled && !$bypass_rate_limit) {
            $rate_limit_result = $this->check_rate_limits($event_key, $data, $user_id);
            if ($rate_limit_result !== true) {
                self::$logging_depth--;
                return $rate_limit_result; // Returns false or 'sampled'
            }
        }
        
        // Remove bypass flag from data before logging
        if (isset($data['_bypass_rate_limit'])) {
            unset($data['_bypass_rate_limit']);
        }
        
        // Set logging flag
        self::$currently_logging = true;
        
        try {
            // Check if batch logging is enabled
            if ($this->batch_enabled) {
                $result = $this->add_to_queue($event_key, $data, $user_id);
            } else {
                $result = $this->log_immediately($event_key, $data, $user_id);
            }
        } finally {
            // Always reset logging flag
            self::$currently_logging = false;
            self::$logging_depth--;
        }
        
        return $result;
    }
    
    /**
     * Add log entry to queue for batch processing
     */
    private function add_to_queue($event_key, $data = array(), $user_id = null) {
        // Get event registry
        $event_config = Sentinel_Events::get_event($event_key);
        if (!$event_config) {
            return false; // Not a valid event
        }
        
        // Gather log info
        $user_id = $user_id ?: get_current_user_id();
        $ip_address = isset($data['ip_address']) ? $data['ip_address'] : $this->get_client_ip();
        $user_agent = isset($data['user_agent']) ? $data['user_agent'] : ($_SERVER['HTTP_USER_AGENT'] ?? '');
        $url = isset($data['url']) ? $data['url'] : (is_admin() ? (admin_url($_SERVER['REQUEST_URI'] ?? '')) : ($_SERVER['REQUEST_URI'] ?? ''));
        $created_at = current_time('mysql');

        // Remove reserved keys from data
        $reserved = array('user_id', 'ip_address', 'user_agent', 'url', 'created_at');
        $event_data = array_diff_key($data, array_flip($reserved));

        // Prevent logging of stack traces or fatal errors as events
        $raw_data = wp_json_encode($data);
        if (stripos($raw_data, 'Fatal error') !== false || stripos($raw_data, 'Stack trace') !== false) {
            return false;
        }
        
        // Create queue entry
        $queue_entry = array(
            'event_key'   => $event_key,
            'category'    => $event_config['category'],
            'priority'    => $event_config['priority'],
            'user_id'     => $user_id,
            'ip_address'  => $ip_address,
            'user_agent'  => $user_agent,
            'url'         => $url,
            'data'        => !empty($event_data) ? wp_json_encode($event_data) : '{}',
            'created_at'  => $created_at,
            'queued_at'   => current_time('mysql')
        );
        
        // Get current queue
        $queue = get_option(self::$queue_option_key, array());
        $queue[] = $queue_entry;
        
        // Update queue
        update_option(self::$queue_option_key, $queue);
        
        // Check if we should process immediately due to queue size
        $settings = get_option('sentinel_log_management', array());
        $batch_size = !empty($settings['batch_size']) ? intval($settings['batch_size']) : 50;
        
        if (count($queue) >= $batch_size) {
            $this->process_queue();
        }
        
        return true;
    }
    
    /**
     * Log immediately to database (non-batch mode)
     */
    private function log_immediately($event_key, $data = array(), $user_id = null) {
        global $wpdb;

        // Get event registry
        $event_config = Sentinel_Events::get_event($event_key);
        if (!$event_config) {
            return false; // Not a valid event
        }

        // Gather log info
        $user_id = $user_id ?: get_current_user_id();
        $ip_address = isset($data['ip_address']) ? $data['ip_address'] : $this->get_client_ip();
        $user_agent = isset($data['user_agent']) ? $data['user_agent'] : ($_SERVER['HTTP_USER_AGENT'] ?? '');
        $url = isset($data['url']) ? $data['url'] : (is_admin() ? (admin_url($_SERVER['REQUEST_URI'] ?? '')) : ($_SERVER['REQUEST_URI'] ?? ''));
        $created_at = current_time('mysql');

        // Remove reserved keys from data
        $reserved = array('user_id', 'ip_address', 'user_agent', 'url', 'created_at');
        $event_data = array_diff_key($data, array_flip($reserved));

        // Prevent logging of stack traces or fatal errors as events
        $raw_data = wp_json_encode($data);
        if (stripos($raw_data, 'Fatal error') !== false || stripos($raw_data, 'Stack trace') !== false) {
            return false;
        }

        // Check if table exists before trying to insert
        $table = $wpdb->prefix . 'sentinel_logs';
        if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
            // Table doesn't exist - try to create it
            Sentinel_Migration::ensure_tables_exist();
            
            // Check again
            if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
                // Still doesn't exist - return false to trigger fallback
                return false;
            }
        }

        // Insert into database
        $result = $wpdb->insert(
            $table,
            array(
                'event_key'   => $event_key,
                'category'    => $event_config['category'],
                'priority'    => $event_config['priority'],
                'user_id'     => $user_id,
                'ip_address'  => $ip_address,
                'user_agent'  => $user_agent,
                'url'         => $url,
                'data'        => !empty($event_data) ? wp_json_encode($event_data) : '{}',
                'created_at'  => $created_at,
            ),
            array(
                '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%s', '%s'
            )
        );

        // If database error occurred, don't try to log it again to prevent feedback loop
        if (!$result && !empty($wpdb->last_error)) {
            error_log('[Sentinel] Database error during immediate logging (suppressed to prevent feedback loop): ' . $wpdb->last_error);
        }

        // Fire a hook for other devs
        if ($result) {
            do_action('sentinel_event_logged', $event_key, $event_data, $user_id);
        }

        return (bool) $result;
    }
    
    /**
     * Process the log queue - write queued entries to database
     */
    public function process_queue() {
        global $wpdb;
        
        // Get current queue
        $queue = get_option(self::$queue_option_key, array());
        
        if (empty($queue)) {
            return 0; // Nothing to process
        }
        
        // Check if table exists
        $table = $wpdb->prefix . 'sentinel_logs';
        if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
            // Table doesn't exist - try to create it
            Sentinel_Migration::ensure_tables_exist();
            
            // Check again
            if ($wpdb->get_var("SHOW TABLES LIKE '$table'") != $table) {
                // Still doesn't exist - keep queue for later
                error_log('[Sentinel] Cannot process queue: logs table missing');
                return 0;
            }
        }
        
        $processed = 0;
        $failed = 0;
        
        // Process each entry with safeguards
        foreach ($queue as $index => $entry) {
            // Add a small delay every 10 entries to prevent database overload
            if ($index > 0 && $index % 10 === 0) {
                usleep(50000); // 50ms pause to ease database strain
            }
            
            $result = $wpdb->insert(
                $table,
                array(
                    'event_key'   => $entry['event_key'],
                    'category'    => $entry['category'],
                    'priority'    => $entry['priority'],
                    'user_id'     => $entry['user_id'],
                    'ip_address'  => $entry['ip_address'],
                    'user_agent'  => $entry['user_agent'],
                    'url'         => $entry['url'],
                    'data'        => $entry['data'],
                    'created_at'  => $entry['created_at'],
                ),
                array(
                    '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%s', '%s'
                )
            );
            
            if ($result) {
                $processed++;
                // Fire hook for each processed entry
                $event_data = !empty($entry['data']) ? json_decode($entry['data'], true) : array();
                do_action('sentinel_event_logged', $entry['event_key'], $event_data, $entry['user_id']);
            } else {
                $failed++;
                // Don't log database errors during batch processing to prevent feedback loops
                if (!empty($wpdb->last_error)) {
                    error_log('[Sentinel] Failed to process queue entry (DB error suppressed to prevent feedback loop): ' . $entry['event_key']);
                } else {
                    error_log('[Sentinel] Failed to process queue entry: ' . $wpdb->last_error);
                }
            }
        }
        
        // Clear the processed queue
        delete_option(self::$queue_option_key);
        
        // Log batch processing results
        if ($processed > 0 || $failed > 0) {
            error_log("[Sentinel] Batch processed: {$processed} success, {$failed} failed");
        }
        
        return $processed;
    }
    
    /**
     * Maybe process queue on shutdown if it's getting large
     */
    public function maybe_process_queue_on_shutdown() {
        $queue = get_option(self::$queue_option_key, array());
        $settings = get_option('sentinel_log_management', array());
        $batch_size = !empty($settings['batch_size']) ? intval($settings['batch_size']) : 50;
        
        // Process if queue is at 75% of batch size to prevent overflow
        if (count($queue) >= ($batch_size * 0.75)) {
            $this->process_queue();
        }
    }
    
    /**
     * Get queue statistics
     */
    public static function get_queue_stats() {
        $queue = get_option(self::$queue_option_key, array());
        $settings = get_option('sentinel_log_management', array());
        $batch_size = !empty($settings['batch_size']) ? intval($settings['batch_size']) : 50;
        
        return array(
            'queue_size' => count($queue),
            'batch_size' => $batch_size,
            'queue_percent' => $batch_size > 0 ? round((count($queue) / $batch_size) * 100, 1) : 0,
            'next_scheduled' => wp_next_scheduled('sentinel_process_log_queue')
        );
    }
    
    /**
     * Static log method for backward compatibility
     */
    public static function log_static($event_key, $data = array(), $user_id = null) {
        $logger = new self();
        return $logger->log($event_key, $data, $user_id);
    }
    
    /**
     * Get client IP address with smart detection and privacy controls
     */
    private function get_client_ip() {
        // Check if IP logging is enabled in settings
        $settings = get_option('sentinel_log_management', array());
        $ip_logging_enabled = !empty($settings['log_ip_addresses']);
        
        // If IP logging is disabled, return null
        if (!$ip_logging_enabled) {
            return null;
        }
        
        // Smart IP detection - check headers in priority order
        $ip_headers = array(
            'HTTP_CF_CONNECTING_IP',     // CloudFlare
            'HTTP_CLIENT_IP',            // Proxy
            'HTTP_X_FORWARDED_FOR',      // Load balancer/proxy
            'HTTP_X_FORWARDED',          // Proxy
            'HTTP_X_CLUSTER_CLIENT_IP',  // Cluster
            'HTTP_FORWARDED_FOR',        // Proxy
            'HTTP_FORWARDED',            // Proxy
            'HTTP_X_REAL_IP',            // Nginx proxy
            'REMOTE_ADDR'                // Standard
        );
        
        foreach ($ip_headers as $header) {
            if (!empty($_SERVER[$header])) {
                $ip_list = explode(',', $_SERVER[$header]);
                
                foreach ($ip_list as $ip) {
                    $ip = trim($ip);
                    
                    // Validate IP address
                    if ($this->is_valid_ip($ip)) {
                        return $ip;
                    }
                }
            }
        }
        
        // Fallback - return null if no valid IP found
        return null;
    }
    
    /**
     * Validate IP address (IPv4 and IPv6)
     */
    private function is_valid_ip($ip) {
        // Basic IP validation
        if (!filter_var($ip, FILTER_VALIDATE_IP)) {
            return false;
        }
        
        // Exclude private and reserved IP ranges for better security
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
            return true;
        }
        
        // For development/testing, allow private IPs
        if (defined('WP_DEBUG') && WP_DEBUG) {
            return true;
        }
        
        // In production, we might want to allow private IPs from trusted proxies
        // For now, allow all valid IPs
        return true;
    }

    /**
     * Check rate limits with graceful degradation
     */
    private function check_rate_limits($event_key, $data, $user_id) {
        $settings = get_option('sentinel_log_management', array());
        $per_minute_limit = intval($settings['rate_limit_per_minute'] ?? 100);
        $per_hour_limit = intval($settings['rate_limit_per_hour'] ?? 1000);
        
        // Get event priority for tier-based limits
        $priority = 'info'; // default
        if (class_exists('Sentinel_Events')) {
            $event_config = Sentinel_Events::get_event($event_key);
            if ($event_config && isset($event_config['priority'])) {
                $priority = $event_config['priority'];
            }
        }
        
        // Critical events ALWAYS get through
        if ($priority === 'critical') {
            error_log('[Sentinel] Critical event bypassing rate limits: ' . $event_key);
            return true;
        }
        
        // Apply priority-based limit multipliers
        $priority_multipliers = array(
            'error' => 1.0,
            'warning' => 0.7,
            'info' => 0.5
        );
        $multiplier = $priority_multipliers[$priority] ?? 0.5;
        $effective_per_minute = floor($per_minute_limit * $multiplier);
        $effective_per_hour = floor($per_hour_limit * $multiplier);
        
        // Get current counters
        $minute_key = 'sentinel_rate_limit_minute_' . date('Y-m-d-H-i');
        $hour_key = 'sentinel_rate_limit_hour_' . date('Y-m-d-H');
        
        $minute_count = intval(get_transient($minute_key) ?: 0);
        $hour_count = intval(get_transient($hour_key) ?: 0);
        
        // Check if we're over limits
        $minute_over_limit = $minute_count >= $effective_per_minute;
        $hour_over_limit = $hour_count >= $effective_per_hour;
        
        // Warning system (80% and 90% thresholds)
        $this->check_rate_limit_warnings($minute_count, $effective_per_minute, $hour_count, $effective_per_hour);
        
        // If over limit, apply rate limiting behavior
        if ($minute_over_limit || $hour_over_limit) {
            return $this->apply_rate_limiting_behavior($event_key, $priority, $minute_count, $effective_per_minute, $hour_count, $effective_per_hour);
        }
        
        // Increment counters
        set_transient($minute_key, $minute_count + 1, 70); // 70 seconds to avoid edge cases
        set_transient($hour_key, $hour_count + 1, 3700); // 1 hour + 100 seconds
        
        return true; // Allow the log
    }
    
    /**
     * Check if we should warn about approaching rate limits
     */
    private function check_rate_limit_warnings($minute_count, $minute_limit, $hour_count, $hour_limit) {
        static $warnings_sent = array();
        
        // Check minute warnings
        $minute_percent = $minute_limit > 0 ? ($minute_count / $minute_limit) * 100 : 0;
        if ($minute_percent >= 80 && $minute_percent < 90) {
            $warning_key = 'minute_80_' . date('Y-m-d-H-i');
            if (!isset($warnings_sent[$warning_key])) {
                $this->log_rate_limit_warning('approaching', 'per_minute', $minute_count, $minute_limit);
                $warnings_sent[$warning_key] = true;
            }
        } elseif ($minute_percent >= 90) {
            $warning_key = 'minute_90_' . date('Y-m-d-H-i');
            if (!isset($warnings_sent[$warning_key])) {
                $this->log_rate_limit_warning('critical', 'per_minute', $minute_count, $minute_limit);
                $warnings_sent[$warning_key] = true;
            }
        }
        
        // Check hour warnings
        $hour_percent = $hour_limit > 0 ? ($hour_count / $hour_limit) * 100 : 0;
        if ($hour_percent >= 80 && $hour_percent < 90) {
            $warning_key = 'hour_80_' . date('Y-m-d-H');
            if (!isset($warnings_sent[$warning_key])) {
                $this->log_rate_limit_warning('approaching', 'per_hour', $hour_count, $hour_limit);
                $warnings_sent[$warning_key] = true;
            }
        } elseif ($hour_percent >= 90) {
            $warning_key = 'hour_90_' . date('Y-m-d-H');
            if (!isset($warnings_sent[$warning_key])) {
                $this->log_rate_limit_warning('critical', 'per_hour', $hour_count, $hour_limit);
                $warnings_sent[$warning_key] = true;
            }
        }
    }
    
    /**
     * Apply rate limiting behavior when over limits
     */
    private function apply_rate_limiting_behavior($event_key, $priority, $minute_count, $minute_limit, $hour_count, $hour_limit) {
        static $degradation_counters = array();
        static $exceeded_logged = array();
        
        // Get user's chosen behavior
        $settings = get_option('sentinel_log_management', array());
        $behavior = $settings['rate_limit_behavior'] ?? 'graceful_degradation';
        
        // Debug log the behavior being applied
        error_log('[Sentinel] Rate limit exceeded - applying behavior: ' . $behavior);
        
        // Log rate limit exceeded (only once per minute/hour)
        $time_window = $minute_count >= $minute_limit ? 'per_minute' : 'per_hour';
        $current_count = $minute_count >= $minute_limit ? $minute_count : $hour_count;
        $current_limit = $minute_count >= $minute_limit ? $minute_limit : $hour_limit;
        
        $exceeded_key = $time_window . '_' . date($time_window === 'per_minute' ? 'Y-m-d-H-i' : 'Y-m-d-H');
        if (!isset($exceeded_logged[$exceeded_key])) {
            $this->log_rate_limit_exceeded($event_key, $time_window, $current_count, $current_limit, $behavior);
            $exceeded_logged[$exceeded_key] = true;
        }
        
        // Apply the chosen behavior
        switch ($behavior) {
            case 'graceful_degradation':
                return $this->apply_graceful_degradation($event_key, $time_window, $minute_count, $hour_count);
                
            case 'hard_blocking':
                return $this->apply_hard_blocking($event_key, $time_window, $current_count, $current_limit);
                
            case 'priority_only':
                return $this->apply_priority_only($event_key, $priority, $time_window, $current_count, $current_limit);
                
            default:
                return $this->apply_graceful_degradation($event_key, $time_window, $minute_count, $hour_count);
        }
    }
    
    /**
     * Apply graceful degradation: sample every 10th event
     */
    private function apply_graceful_degradation($event_key, $time_window, $minute_count, $hour_count) {
        static $degradation_counters = array();
        
        // Smart sampling: Allow every 10th event through
        $counter_key = $time_window . '_degradation_' . date($time_window === 'per_minute' ? 'Y-m-d-H-i' : 'Y-m-d-H');
        $degradation_count = intval($degradation_counters[$counter_key] ?? 0);
        $degradation_counters[$counter_key] = $degradation_count + 1;
        
        // Every 10th event gets through
        if ($degradation_count % 10 === 0) {
            // Debug log
            error_log('[Sentinel] Graceful degradation - sampling event #' . $degradation_count . ': ' . $event_key);
            
            // Increment the original counters since we're allowing this through
            $minute_key = 'sentinel_rate_limit_minute_' . date('Y-m-d-H-i');
            $hour_key = 'sentinel_rate_limit_hour_' . date('Y-m-d-H');
            set_transient($minute_key, $minute_count + 1, 70);
            set_transient($hour_key, $hour_count + 1, 3700);
            return 'sampled'; // Special return value to indicate this was sampled
        }
        
        // Debug log every 5th blocked event to avoid spam
        if ($degradation_count % 5 === 0) {
            error_log('[Sentinel] Graceful degradation - blocking event #' . $degradation_count . ': ' . $event_key);
        }
        
        return false; // Block this event
    }
    
    /**
     * Apply hard blocking: completely stop logging
     */
    private function apply_hard_blocking($event_key, $time_window, $current_count, $current_limit) {
        // Debug log
        error_log('[Sentinel] Hard blocking applied - event blocked: ' . $event_key);
        
        // Hard blocking - no events get through
        return false;
    }
    
    /**
     * Apply priority only: only allow critical and error events
     */
    private function apply_priority_only($event_key, $priority, $time_window, $current_count, $current_limit) {
        // Only allow critical and error events through
        if (in_array($priority, array('critical', 'error'))) {
            // Debug log
            error_log('[Sentinel] Priority only - allowing ' . $priority . ' event: ' . $event_key);
            
            // Increment the original counters since we're allowing this through
            $minute_key = 'sentinel_rate_limit_minute_' . date('Y-m-d-H-i');
            $hour_key = 'sentinel_rate_limit_hour_' . date('Y-m-d-H');
            
            $minute_count = intval(get_transient($minute_key) ?: 0);
            $hour_count = intval(get_transient($hour_key) ?: 0);
            
            set_transient($minute_key, $minute_count + 1, 70);
            set_transient($hour_key, $hour_count + 1, 3700);
            return 'priority_allowed'; // Special return value to indicate this was allowed due to priority
        }
        
        // Debug log
        error_log('[Sentinel] Priority only - blocking ' . $priority . ' event: ' . $event_key);
        
        return false; // Block all other events
    }
    
    /**
     * Log rate limit warning (bypasses rate limiting)
     */
    private function log_rate_limit_warning($severity, $time_window, $current_count, $limit) {
        $data = array(
            'severity' => $severity,
            'time_window' => $time_window,
            'current_count' => $current_count,
            'limit' => $limit,
            'percentage' => round(($current_count / $limit) * 100, 1),
            '_bypass_rate_limit' => true
        );
        
        $this->log('rate_limit_warning', $data);
    }
    
    /**
     * Log rate limit exceeded (bypasses rate limiting)
     */
    private function log_rate_limit_exceeded($trigger_event, $time_window, $current_count, $limit, $behavior) {
        $behavior_descriptions = array(
            'graceful_degradation' => 'Graceful degradation (sampling every 10th event)',
            'hard_blocking' => 'Hard blocking (no events logged)',
            'priority_only' => 'Priority only (critical/error events only)'
        );
        
        $data = array(
            'trigger_event' => $trigger_event,
            'time_window' => $time_window,
            'current_count' => $current_count,
            'limit' => $limit,
            'behavior' => $behavior,
            'behavior_description' => $behavior_descriptions[$behavior] ?? 'Unknown behavior',
            '_bypass_rate_limit' => true
        );
        
        $this->log('rate_limit_exceeded', $data);
    }
} 