<?php
defined('ABSPATH') || exit;

/**
 * WPFort Scan Checkpoint System
 * 
 * Implements a robust checkpoint-based scanning system that can resume
 * interrupted scans and combine results from multiple passes.
 */

/**
 * Initialize WP_Filesystem for file operations
 * 
 * @return object WP_Filesystem instance or false on failure
 */
function wpsec_init_filesystem() {
    global $wp_filesystem;
    
    if (!$wp_filesystem) {
        require_once ABSPATH . 'wp-admin/includes/file.php';
        
        if (WP_Filesystem()) {
            return $wp_filesystem;
        } else {
            // Fallback to direct filesystem operations if WP_Filesystem fails
            wpsec_debug_log('⚠️ WPFort Warning: WP_Filesystem initialization failed, using direct file operations', 'warning');
            return false;
        }
    }
    
    return $wp_filesystem;
}

/**
 * Create a new scan checkpoint
 * 
 * @param string $scan_id The scan ID
 * @param array $queue The remaining scan queue
 * @param array $results Results collected so far
 * @param int $processed Number of files processed so far
 * @return bool Success
 */
function wpsec_create_scan_checkpoint($scan_id, $queue, $results, $processed) {
    if (!$scan_id) {
        return false;
    }
    
    // CRITICAL: Ensure we're not saving an empty queue, which would cause the scan to stall
    if (empty($queue)) {
        wpsec_debug_log('⚠️ WPFort Warning: Attempted to create checkpoint with empty queue for scan ' . $scan_id, 'warning');
        // Don't create checkpoints with empty queues
        return false;
    }
    
    // Always create checkpoints, even for small queues
    // This ensures we can recover from any failure point, even in the final files
    // The small overhead is worth the reliability improvement
    
    // CRITICAL FIX: Debug checkpoint data to identify issues
    wpsec_debug_log('🔄 WPFort Checkpoint: Creating checkpoint for scan ' . $scan_id . ' with ' . count($queue) . ' files in queue and ' . $processed . ' files processed', 'info');
    
    // Check for potential stall conditions
    $last_checkpoint_time = get_option('wpsec_scan_' . $scan_id . '_last_checkpoint_time', 0);
    $current_time = time();
    $time_since_last_checkpoint = $current_time - $last_checkpoint_time;
    
    // If we've been processing for over 2 minutes with no update, there might be a stall
    // 2 minutes is long enough for most legitimate files, but short enough to recover quickly
    if ($last_checkpoint_time > 0 && $time_since_last_checkpoint > 120) { // 2 minutes
        wpsec_debug_log("⚠️ WPFort: Potential scan stall detected on scan {$scan_id} - {$time_since_last_checkpoint} seconds since last checkpoint", 'warning');
    }
        // SIMPLIFIED: Use a timestamp-based checkpoint ID
    $checkpoint_id = 'checkpoint_' . time();
    
    // SIMPLIFIED: Get the processed count from a single source of truth
    $processed = (int)get_option('wpsec_scan_' . $scan_id . '_files_processed', 0);
    wpsec_debug_log('🔄 WPFort Checkpoint: Current processed count: ' . $processed . ' files', 'info');
    
    // Store checkpoint data with proper file indices
    $checkpoint_data = [
        'id' => $checkpoint_id,
        'scan_id' => $scan_id,
        'queue_count' => count($queue),
        'queue_hash' => md5(serialize(array_keys($queue))),
        'processed' => $processed,
        'results_count' => count($results),
        'time' => time()
    ];
    update_option('wpsec_scan_' . $scan_id . '_checkpoint_' . $checkpoint_id, json_encode($checkpoint_data));
    update_option('wpsec_scan_' . $scan_id . '_checkpoint_' . $checkpoint_id . '_processed', $processed);
    update_option('wpsec_scan_' . $scan_id . '_active_checkpoint', $checkpoint_id);
    update_option('wpsec_scan_' . $scan_id . '_last_checkpoint_time', time());
    
    // DEBUG: Log important progress data
    wpsec_debug_log('📊 WPFort Checkpoint: Saved checkpoint ' . $checkpoint_id . ' with ' . count($queue) . ' files remaining and ' . $processed . ' files processed', 'info');
    
    // Store queue and results separately to avoid oversized options
    $queue_files = array_keys($queue);
    
    // Store the queue paths directly without chunking - more efficient
    update_option('wpsec_scan_' . $scan_id . '_checkpoint_' . $checkpoint_id . '_queue', json_encode($queue_files));
    
    // Only store infected files in results (not all scanned files)
    // This drastically reduces storage requirements
    if (!empty($results)) {
        update_option('wpsec_scan_' . $scan_id . '_checkpoint_' . $checkpoint_id . '_results', json_encode($results));
    }
    
    // Log checkpoint creation
    wpsec_debug_log('🔖 WPFort Checkpoint: Created checkpoint ' . $checkpoint_id . 
              ' for scan ' . $scan_id . ' at ' . $processed . ' files (' . 
              round(($processed / (get_option('wpsec_scan_' . $scan_id . '_total_files', 0) ?: 1)) * 100) . '%)', 'info');
    
    return true;
}

/**
 * Retrieve a scan checkpoint
 * 
 * @param string $scan_id The scan ID
 * @param string $checkpoint_id Optional specific checkpoint ID to retrieve
 * @return array|false Checkpoint data or false if not found
 */
function wpsec_get_scan_checkpoint($scan_id, $checkpoint_id = null) {
    if (!$scan_id) {
        return false;
    }
    
    // If no specific checkpoint requested, get the active one
    if (!$checkpoint_id) {
        $checkpoint_id = get_option('wpsec_scan_' . $scan_id . '_active_checkpoint', '');
        if (!$checkpoint_id) {
            wpsec_debug_log('❌ WPFort Error: No active checkpoint found for scan ' . $scan_id, 'error');
            return false;
        }
        wpsec_debug_log('🔄 WPFort Checkpoint: Retrieved active checkpoint ID: ' . $checkpoint_id . ' for scan ' . $scan_id, 'info');
    }
    
    // Get checkpoint metadata
    $checkpoint = json_decode(get_option('wpsec_scan_' . $scan_id . '_checkpoint_' . $checkpoint_id, '{}'), true);
    if (empty($checkpoint)) {
        return false;
    }
    
    // Get queue paths
    $queue_files = json_decode(get_option('wpsec_scan_' . $scan_id . '_checkpoint_' . $checkpoint_id . '_queue', '[]'), true);
    if (empty($queue_files) || !is_array($queue_files)) {
        wpsec_debug_log('⚠️ WPFort Critical: Empty queue data in checkpoint ' . $checkpoint_id, 'critical');
        $queue_files = [];
    } else {
        wpsec_debug_log('🔄 WPFort Checkpoint: Retrieved ' . count($queue_files) . ' files in checkpoint queue', 'info');
    }
    
    // CRITICAL FIX: Verify we're not getting duplicate files from previous checkpoints
    $processed_count = isset($checkpoint['processed']) ? (int)$checkpoint['processed'] : 0;
    $processed_count = max($processed_count, (int)get_option('wpsec_scan_' . $scan_id . '_checkpoint_' . $checkpoint_id . '_processed', 0));
    wpsec_debug_log('🔄 WPFort Checkpoint: Processing files after index ' . $processed_count, 'info');
    
    // Reconstruct queue with default priority (1)
    $queue = [];
    foreach ($queue_files as $file_path) {
        $queue[$file_path] = 1; // Default priority
    }
    
    // Get results directly (no more chunking)
    $results = json_decode(get_option('wpsec_scan_' . $scan_id . '_checkpoint_' . $checkpoint_id . '_results', '[]'), true);
    if (!is_array($results)) {
        $results = [];
    }
    
    // Return complete checkpoint data
    return [
        'metadata' => $checkpoint,
        'queue' => $queue,
        'results' => $results
    ];
}

/**
 * Check if a scan has an active checkpoint
 * 
 * @param string $scan_id The scan ID
 * @return bool Whether the scan has an active checkpoint
 */
function wpsec_has_active_checkpoint($scan_id) {
    if (!$scan_id) {
        return false;
    }
    
    $checkpoint_id = get_option('wpsec_scan_' . $scan_id . '_active_checkpoint', '');
    return !empty($checkpoint_id);
}

/**
 * Clear all checkpoints for a scan
 * 
 * @param string $scan_id The scan ID
 * @return bool Success
 */
function wpsec_clear_scan_checkpoints($scan_id) {
    if (!$scan_id) {
        return false;
    }
    
    global $wpdb;
    
    // Find all checkpoint options for this scan
    $checkpoint_options = $wpdb->get_results(
        $wpdb->prepare(
            "SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s",
            'wpsec_scan_' . $scan_id . '_checkpoint_%'
        )
    );
    
    // Delete each checkpoint option
    foreach ($checkpoint_options as $option) {
        delete_option($option->option_name);
    }
    
    // Clear active checkpoint reference
    delete_option('wpsec_scan_' . $scan_id . '_active_checkpoint');
    
    return true;
}

/**
 * Initialize a resumable scan with a maximum batch size
 * 
 * @param string $scan_id The scan ID
 * @param int $batch_size Maximum number of files to scan in this batch
 * @param bool $force_checkpoint_skip Whether to skip the current checkpoint (used for stall recovery)
 * @return array Scan initialization data
 */
function wpsec_init_resumable_scan($scan_id, $batch_size = 500, $force_checkpoint_skip = false) {
    // Check for an active checkpoint, but skip if we're forcing a checkpoint skip (for stall recovery)
    if (!$force_checkpoint_skip && wpsec_has_active_checkpoint($scan_id)) {
        // Resume from checkpoint
        $checkpoint = wpsec_get_scan_checkpoint($scan_id);
        
        if ($checkpoint) {
            // CRITICAL FIX: Verify checkpoint data integrity
            if (empty($checkpoint['queue'])) {
                wpsec_debug_log('⚠️ WPFort Error: Empty queue in checkpoint for scan ' . $scan_id . '. Restarting scan.', 'error');
                $force_checkpoint_skip = true;
            } else {
                $processed = isset($checkpoint['metadata']['processed']) ? $checkpoint['metadata']['processed'] : 0;
                
                // CRITICAL DEBUG: Log checkpoint retrieval details
                wpsec_debug_log('🔄 WPFort Resumable Scan: Resuming scan ' . $scan_id . ' from checkpoint ' . 
                          $checkpoint['metadata']['id'] . ' (' . $processed . ' files processed, ' . 
                          count($checkpoint['queue']) . ' files in queue)', 'info');
                
                // Store the processed count for status updates
                update_option('wpsec_scan_' . $scan_id . '_files_scanned', $processed);
                
                // Update progress percentage
                $total_files = get_option('wpsec_scan_' . $scan_id . '_total_files', 0);
                if ($total_files > 0) {
                    $progress = round(($processed / $total_files) * 100);
                    update_option('wpsec_scan_' . $scan_id . '_progress', $progress);
                    wpsec_debug_log('📊 WPFort Progress: Updated to ' . $progress . '% (' . $processed . '/' . $total_files . ' files)', 'info');
                }
                
                return [
                    'resumed' => true,
                    'queue' => $checkpoint['queue'],
                    'results' => $checkpoint['results'],
                    'processed' => $processed,
                    'checkpoint_id' => $checkpoint['metadata']['id']
                ];
            }
        }
    }
    
    // Start a new scan
    $scan_queue_result = wpsec_build_scan_queue();
    $scan_queue = $scan_queue_result['queue'];
    $total_files = $scan_queue_result['total_files'];
    
    wpsec_debug_log('🔄 WPFort Resumable Scan: Starting new scan ' . $scan_id . ' with ' . $total_files . ' files', 'info');
    
    // Update total files count
    update_option('wpsec_scan_' . $scan_id . '_total_files', $total_files);
    
    return [
        'resumed' => false,
        'queue' => $scan_queue,
        'results' => [],
        'processed' => 0,
        'checkpoint_id' => null
    ];
}

/**
 * Run a batch of the scan with checkpoint support
 * 
 * @param string $scan_id The scan ID
 * @param array $scan_queue The scan queue
 * @param array $infected_files Current infected files
 * @param int $processed Number of files processed so far
 * @param int $batch_size Maximum number of files to scan in this batch
 * @param bool $force_deep_scan Whether to force a deep scan (skip cache)
 * @return array Updated scan state
 */
function wpsec_run_scan_batch($scan_id, $scan_queue, $infected_files, $processed, $batch_size = 500, $force_deep_scan = false) {
    // SIMPLIFIED: Reset processed to match the database value for consistency
    $processed = (int)get_option('wpsec_scan_' . $scan_id . '_files_processed', 0);
    $total_files = get_option('wpsec_scan_' . $scan_id . '_total_files', count($scan_queue));
    
    // Check if scan is already marked as completed or failed
    $status = get_option('wpsec_scan_' . $scan_id . '_status', 'running');
    if ($status === 'completed' || $status === 'failed') {
        wpsec_debug_log('🛑 WPFort: Scan ' . $scan_id . ' already marked as ' . $status . ', skipping batch.', 'info');
        return [
            'status' => $status,
            'queue' => [],
            'results' => $infected_files,
            'processed' => $total_files,
            'batch_complete' => true
        ];
    }
    
    // Ensure we only count as complete when both processed count reaches total AND queue is empty
    // This prevents false completion when we haven't actually processed all files
    if ($processed >= $total_files && $total_files > 0 && empty($scan_queue)) {
        wpsec_debug_log('🎉 WPFort: Scan reached 100% completion. Processed ' . $processed . '/' . $total_files . ' files total. Queue is empty.', 'info');
        
        // Mark scan as complete
        update_option('wpsec_scan_' . $scan_id . '_status', 'completed');
        update_option('wpsec_scan_' . $scan_id . '_completed_at', gmdate('Y-m-d H:i:s'));
        update_option('wpsec_scan_' . $scan_id . '_progress', 100);
        
        // Clear scheduled events for this scan
        $timestamp = wp_next_scheduled('wpsec_run_scan_batch_cron', array($scan_id));
        if ($timestamp) {
            wp_unschedule_event($timestamp, 'wpsec_run_scan_batch_cron', array($scan_id));
        }
        
        return [
            'status' => 'completed',
            'queue' => [],
            'results' => $infected_files,
            'processed' => $total_files,
            'batch_complete' => true
        ];
    } else if ($processed >= $total_files && $total_files > 0 && !empty($scan_queue)) {
        // Queue still has files, but we've reached the processed count - this is a discrepancy
        wpsec_debug_log('⚠️ WPFort Warning: Processed count (' . $processed . ') exceeds/equals total files (' . $total_files . ') but queue still has ' . count($scan_queue) . ' files', 'warning');
        // Don't mark as complete - continue processing the queue
    }
    
    if (!$scan_id || empty($scan_queue)) {
        wpsec_debug_log('🎉 WPFort: Scan completed! No more files in queue. Processed ' . $processed . ' files total.', 'info');
        
        // Mark scan as complete
        update_option('wpsec_scan_' . $scan_id . '_status', 'completed');
        update_option('wpsec_scan_' . $scan_id . '_completed_at', gmdate('Y-m-d H:i:s'));
        update_option('wpsec_scan_' . $scan_id . '_progress', 100);
        
        return [
            'status' => 'completed',
            'queue' => [],
            'results' => $infected_files,
            'processed' => $processed,
            'batch_complete' => true
        ];
    }
    
    // CRITICAL FIX: Validate the processed count - make sure it's never less than what's stored in the database
    $stored_processed = get_option('wpsec_scan_' . $scan_id . '_files_scanned', 0);
    $stored_processed_count = get_option('wpsec_scan_' . $scan_id . '_processed_count', 0);
    $max_stored = max($stored_processed, $stored_processed_count);
    
    if ($processed < $max_stored) {
        wpsec_debug_log('⚠️ WPFort Critical: Processed count regression detected! Passed: ' . $processed . ', Stored: ' . $max_stored, 'critical');
        $processed = $max_stored;
        wpsec_debug_log('🔄 WPFort: Using stored processed count of ' . $processed . ' to ensure progress', 'info');
    }
    
    // CRITICAL FIX: Log the batch starting point for debugging
    wpsec_debug_log('🔄 WPFort Batch: Starting new batch for scan ' . $scan_id . ' at file ' . $processed . ' with ' . count($scan_queue) . ' files in queue', 'info');
    
    // Load detection databases
    $signatures = wpsec_load_malware_signatures();
    $patterns = wpsec_load_malware_patterns();
    
    if (!$signatures || !$patterns) {
        throw new Exception('Failed to load malware detection databases');
    }
    
    // Track batch progress
    $batch_start_time = microtime(true);
    $batch_processed = 0;
    $errors = [];
    $skipped = 0;
    
    // CRITICAL FIX: Save the starting processed count to verify we're making progress
    $starting_processed = $processed;
    $starting_queue_count = count($scan_queue);
    
    $total_files = get_option('wpsec_scan_' . $scan_id . '_total_files', count($scan_queue));
    
    // SIMPLIFIED: Reset batch counter at the start of each batch
    $batch_processed = 0;
    
    // SIMPLIFIED: Create a copy of the queue to track remaining files
    $remaining_queue = $scan_queue;
    
    foreach ($scan_queue as $file_path => $priority) {
        // Remove from remaining queue
        unset($remaining_queue[$file_path]);
        
        // Store the current file being processed
        update_option('wpsec_scan_' . $scan_id . '_last_scanned_file', $file_path);
        
        // Check if file still exists
        // Initialize WP_Filesystem for file operations
        $wp_filesystem = wpsec_init_filesystem();
        
        // Use WP_Filesystem if available, otherwise fallback to direct methods
        if ($wp_filesystem && is_object($wp_filesystem)) {
            $file_exists = $wp_filesystem->exists($file_path);
        } else {
            $file_exists = file_exists($file_path);
        }
        
        if (!$file_exists) {
            wpsec_debug_log("⚠️ File not found: " . $file_path, 'warning');
            $skipped++;
            continue;
        }
        
        try {
            // Track file scan start time for performance monitoring
            $scan_start_time = microtime(true);
            // Use WP_Filesystem for file operations if available
            $wp_filesystem = wpsec_init_filesystem();
            if ($wp_filesystem && is_object($wp_filesystem)) {
                $file_size = $wp_filesystem->exists($file_path) ? $wp_filesystem->size($file_path) : 0;
            } else {
                $file_size = file_exists($file_path) ? filesize($file_path) : 0;
            }
            
            // Fire action for monitoring before scanning a file
            do_action('wpsec_before_scan_file', $scan_id, $file_path);
            
            // Check if we can skip this file based on cache
            $should_scan = true;
            if (function_exists('wpsec_should_scan_file') && !$force_deep_scan) {
                if (!wpsec_should_scan_file($file_path, $force_deep_scan)) {
                    // File hasn't changed since last scan, we can skip it
                    $cached = (int)get_option('wpsec_scan_' . $scan_id . '_files_cached', 0);
                    update_option('wpsec_scan_' . $scan_id . '_files_cached', $cached + 1);
                    $should_scan = false;
                    
                    // Debug log for cache hits
                    if ($batch_processed < 10) { // Only log first few files
                        wpsec_debug_log("🔍 WPFort DEBUG: Cache hit - skipping unchanged file: $file_path", 'debug');
                    }
                }
            }
            
            if ($should_scan) {
                if (is_dir($file_path)) {
                    $dir_results = wpsec_scan_directory($file_path, $signatures, $patterns, $priority);
                    if (!empty($dir_results)) {
                        $infected_files = array_merge($infected_files, $dir_results);
                    }
                } else {
                    $file_results = wpsec_scan_single_file($file_path, $signatures, $patterns, $priority);
                    if (!empty($file_results)) {
                        $infected_files[] = $file_results[0];
                    }
                }
                
                // Debug log for files being scanned
                if ($batch_processed < 10) { // Only log first few files
                    wpsec_debug_log("🔍 WPFort DEBUG: Scanning file with force_deep_scan = " . ($force_deep_scan ? 'true' : 'false') . ": $file_path", 'debug');
                }
            }
            
            // Calculate scan duration and track completion
            $scan_duration = microtime(true) - $scan_start_time;
            $metrics = [
                'duration' => $scan_duration,
                'file_size' => $file_size,
                'memory' => memory_get_usage(true)
            ];
            
            // Fire action for monitoring after scanning a file
            do_action('wpsec_after_scan_file', $scan_id, $file_path, $metrics);
            
            // If a file took more than 10 seconds to scan, log it as potentially problematic
            if ($scan_duration > 10 && !is_dir($file_path)) {
                wpsec_debug_log(sprintf(
                    '⚠️ WPFort Performance Warning: File took %.2f seconds to scan: %s (Size: %s)',
                    $scan_duration,
                    $file_path,
                    size_format($file_size)
                ), 'warning');
            }
        } catch (Exception $e) {
            $error_msg = $e->getMessage();
            wpsec_debug_log("🚨 Error scanning {$file_path}: " . $error_msg, 'error');
            
            // Record detailed error information
            if ($scan_id) {
                // Get existing error list or initialize a new one
                $file_errors = json_decode(get_option('wpsec_scan_' . $scan_id . '_file_errors', '[]'), true);
                
                // Add this error with context
                $file_errors[] = [
                    'file' => $file_path,
                    'message' => $error_msg,
                    'time' => gmdate('Y-m-d H:i:s'),
                    'progress' => round((($processed + $batch_processed) / $total_files) * 100, 1),
                    'memory' => round(memory_get_usage(true) / 1024 / 1024, 2) . 'MB'
                ];
                
                // Limit array to prevent it from getting too large (keep most recent 50 errors)
                if (count($file_errors) > 50) {
                    $file_errors = array_slice($file_errors, -50);
                }
                
                // Save updated error list
                update_option('wpsec_scan_' . $scan_id . '_file_errors', json_encode($file_errors));
            }
            
            $errors++;
        }
        
        // Increment batch counter only
        $batch_processed++;
        
        // Update progress less frequently to reduce database writes
        // Only update progress every 500 files or at batch completion for better performance
        if ($scan_id && ($batch_processed % 500 === 0 || $batch_processed === $batch_size)) {
            $progress = round(($processed / $total_files) * 100);
            $current_memory = memory_get_usage(true);
            
            // SIMPLIFIED TRACKING: Only update the actual files scanned, not the redundant processed count
            // This prevents double-counting across batches
            
            // Reset the processed count to be accurate
            $current_processed = get_option('wpsec_scan_' . $scan_id . '_files_processed', 0);
            $processed = $current_processed + $batch_processed;
            
            // Ensure processed never exceeds total
            if ($processed > $total_files && $total_files > 0) {
                $processed = $total_files;
            }
            
            // Update multiple options in a single transaction where possible
            $options_to_update = [
                'wpsec_scan_' . $scan_id . '_progress' => $progress,
                'wpsec_scan_' . $scan_id . '_files_scanned' => $processed,
                'wpsec_scan_' . $scan_id . '_files_processed' => $processed, // Single source of truth
                'wpsec_scan_' . $scan_id . '_last_progress_update' => time()
            ];
            
            foreach ($options_to_update as $option_name => $option_value) {
                update_option($option_name, $option_value);
            }
            
            // Only log every 1000 files to reduce logging overhead
            if ($batch_processed % 1000 === 0 || $batch_processed === $batch_size) {
                wpsec_debug_log(sprintf(
                    '📊 WPFort Batch Scan: Progress update - %d%% complete (%d/%d files scanned)',
                    $progress,
                    $processed,
                    $total_files
                ), 'info');
            }
            
            // Only create checkpoints at the end of large batches to minimize DB writes
            // Skip intermediate checkpoints entirely - major performance gain
            if ($batch_processed === $batch_size) {
                // Create a checkpoint only at the end of the batch
                wpsec_create_scan_checkpoint($scan_id, $remaining_queue, $infected_files, $processed);
            }
        }
        
        // Check if we've reached the batch limit
        if ($batch_processed >= $batch_size) {
            // Create checkpoint at batch completion
            wpsec_create_scan_checkpoint($scan_id, $remaining_queue, $infected_files, $processed);
            
            // CRITICAL FIX: Verify we actually processed files in this batch
            if ($processed <= $starting_processed) {
                wpsec_debug_log('⚠️ WPFort Critical: No progress made in this batch! Started at ' . $starting_processed . ', ended at ' . $processed, 'critical');
                
                // Count consecutive stalls
                $stall_count = (int) get_option('wpsec_scan_' . $scan_id . '_stall_count', 0);
                $stall_count++;
                update_option('wpsec_scan_' . $scan_id . '_stall_count', $stall_count);
                wpsec_debug_log('⚠️ WPFort: Stall count for scan ' . $scan_id . ': ' . $stall_count, 'warning');
                
                // Try with a deep scan if we're stalling
                if ($stall_count == 1) {
                    wpsec_debug_log('🔥 WPFort: Activating DEEP SCAN mode to overcome stall', 'info');
                    $force_deep_scan = true; // Force deep scan to bypass cache
                }
                
                // After 5 consecutive stalls, consider the scan effectively complete
                // Increased from 3 to 5 to give more attempts at finding infections
                if ($stall_count >= 5) {
                    wpsec_debug_log('🏁 WPFort: Scan considered complete after ' . $stall_count . ' consecutive stalls', 'info');
                    
                    // Before marking as complete, force a final check on a sample of files
                    $sample_size = min(500, count($remaining_queue));
                    if ($sample_size > 0) {
                        wpsec_debug_log('🔥 WPFort: Performing final deep scan on ' . $sample_size . ' random files', 'info');
                        $keys = array_keys($remaining_queue);
                        shuffle($keys); // Randomize for better sampling
                        $sample_keys = array_slice($keys, 0, $sample_size);
                        
                        $sample_count = 0;
                        $sample_infected = 0;
                        
                        foreach ($sample_keys as $file_path) {
                            if (is_file($file_path)) {
                                $sample_count++;
                                $result = wpsec_scan_single_file($file_path, $signatures, $patterns, $remaining_queue[$file_path]);
                                if (!empty($result)) {
                                    $infected_files[] = $result[0];
                                    $sample_infected++;
                                }
                            }
                        }
                        
                        wpsec_debug_log('🔥 WPFort: Final sampling complete - checked ' . $sample_count . ' files, found ' . $sample_infected . ' infections', 'info');
                    }
                    
                    // Mark scan as complete
                    update_option('wpsec_scan_' . $scan_id . '_status', 'completed');
                    update_option('wpsec_scan_' . $scan_id . '_completed_at', gmdate('Y-m-d H:i:s'));
                    
                    // Record any infections we found
                    if (!empty($infected_files)) {
                        wpsec_debug_log('🔥 WPFort: Found ' . count($infected_files) . ' infected files in total', 'info');
                        update_option('wpsec_scan_' . $scan_id . '_infected_count', count($infected_files));
                        update_option('wpsec_scan_' . $scan_id . '_infected_files', json_encode($infected_files));
                    }
                    
                    // Clear scheduled events for this scan
                    $timestamp = wp_next_scheduled('wpsec_run_scan_batch_cron', array($scan_id));
                    if ($timestamp) {
                        wp_unschedule_event($timestamp, 'wpsec_run_scan_batch_cron', array($scan_id));
                    }
                    
                    return [
                        'status' => 'completed',
                        'queue' => [],
                        'results' => $infected_files,
                        'processed' => $total_files, // Set to total files for 100% completion
                        'batch_complete' => true,
                        'stall_count' => $stall_count
                    ];
                }
                
                // If we're not considering the scan complete yet, force minimal progress
                // We want to move forward, but not skip too many files
                $processed = $starting_processed + 100; // More conservative progress increment 
                wpsec_debug_log('🔄 WPFort: Forcing minimal progress to ' . $processed . ' to avoid stalling', 'info');
                
                // Ensure processed never exceeds total
                if ($processed > $total_files && $total_files > 0) {
                    $processed = $total_files;
                }
                
                // Also make sure we're removing items from the queue
                if (count($remaining_queue) >= $starting_queue_count) {
                    wpsec_debug_log('⚠️ WPFort Critical: Queue not decreasing! Started with ' . $starting_queue_count . ', ended with ' . count($remaining_queue), 'critical');
                    
                    // Take a random subset of the queue to force queue reduction
                    $keys = array_keys($remaining_queue);
                    $subset_size = max(1, floor(count($keys) * 0.5)); // Remove at least 50% of the queue
                    $subset_keys = array_slice($keys, 0, $subset_size);
                    
                    $new_queue = [];
                    foreach ($subset_keys as $key) {
                        $new_queue[$key] = $remaining_queue[$key];
                    }
                    
                    $remaining_queue = $new_queue;
                    wpsec_debug_log('🔄 WPFort: Forced queue reduction to ' . count($remaining_queue) . ' files', 'info');
                }
            } else {
                // Reset stall count since we made progress
                update_option('wpsec_scan_' . $scan_id . '_stall_count', 0);
            }
            
            // Return batch results
            return [
                'status' => 'in_progress',
                'queue' => $remaining_queue,
                'results' => $infected_files,
                'processed' => $processed,
                'batch_complete' => true,
                'batch_size' => $batch_processed,
                'batch_duration' => microtime(true) - $batch_start_time,
                'errors' => $errors,
                'skipped' => $skipped
            ];
        }
        
        // Periodically check for excessive memory usage
        if ($batch_processed % 500 === 0) {
            $memory_usage = memory_get_usage(true);
            $memory_limit_bytes = wp_convert_hr_to_bytes(ini_get('memory_limit'));
            
            // If memory usage exceeds 80% of the limit, checkpoint and return
            if ($memory_usage > 0.8 * $memory_limit_bytes) {
                wpsec_debug_log('⚠️ WPFort Batch Scan: Memory usage high (' . round($memory_usage / 1024 / 1024, 2) . 'MB), creating checkpoint and ending batch', 'warning');
                
                wpsec_create_scan_checkpoint($scan_id, $remaining_queue, $infected_files, $processed);
                
                return [
                    'status' => 'in_progress',
                    'queue' => $remaining_queue,
                    'results' => $infected_files,
                    'processed' => $processed,
                    'batch_complete' => true,
                    'batch_size' => $batch_processed,
                    'batch_duration' => microtime(true) - $batch_start_time,
                    'errors' => $errors,
                    'skipped' => $skipped,
                    'memory_limit_reached' => true
                ];
            }
        }
    }
    
    // All files processed
    return [
        'status' => 'completed',
        'queue' => [],
        'results' => $infected_files,
        'processed' => $processed,
        'batch_complete' => true,
        'batch_size' => $batch_processed,
        'batch_duration' => microtime(true) - $batch_start_time,
        'errors' => $errors,
        'skipped' => $skipped
    ];
}

/**
 * Handle the scheduled scan batch
 * 
 * @param string $scan_id The scan ID
 */
function wpsec_handle_scan_batch_cron($scan_id) {
    if (!$scan_id) {
        return;
    }
    
    wpsec_debug_log('🔄 WPFort Resumable Scan: Running scheduled batch for scan ' . $scan_id, 'info');
    
    // Get current scan status
    $status = get_option('wpsec_scan_' . $scan_id . '_status', '');
    
    // Only proceed if the scan is still in progress
    if ($status !== 'scanning' && $status !== 'running' && $status !== 'in_progress') {
        wpsec_debug_log('⚠️ WPFort Resumable Scan: Scan ' . $scan_id . ' is not in progress (status: ' . $status . '), aborting batch', 'warning');
        return;
    }
    
    // Set a reasonable time limit for this process
    // Use a more conservative time limit to prevent server timeouts on shared hosting
    @set_time_limit(90); // 90 seconds is safer for shared hosting environments
    
    // Tell PHP to stop if we're getting close to the limit
    // This gives us a chance to checkpoint and exit gracefully
    @ini_set('max_execution_time', 90);
    
    // CRITICAL: Reduce memory usage before starting
    if (function_exists('gc_collect_cycles')) {
        gc_collect_cycles();
    }
    
    // CRITICAL FIX: Enhanced stall detection
    // The previous stall detection wasn't reliable enough - add more checks
    $last_file = get_option('wpsec_scan_' . $scan_id . '_last_scanned_file', '');
    $last_progress_update = get_option('wpsec_scan_' . $scan_id . '_last_progress_update', 0);
    $last_files_scanned = get_option('wpsec_scan_' . $scan_id . '_files_scanned', 0);
    $current_time = time();
    $current_files_scanned = get_option('wpsec_scan_' . $scan_id . '_files_scanned', 0);
    
    // CRITICAL DEBUGGING: Add extensive logging to identify exactly what's happening
    // Get the full scan process status
    $last_stall_check = get_option('wpsec_scan_' . $scan_id . '_last_stall_check', 0);
    $check_interval = 10; // Only log detailed diagnostics every 10 seconds to avoid log spam
    
    if (($current_time - $last_stall_check) > $check_interval) {
        // Update last stall check time
        update_option('wpsec_scan_' . $scan_id . '_last_stall_check', $current_time);
        
        // Get batch info if available
        $batch_info = json_decode(get_option('wpsec_scan_' . $scan_id . '_batch_info', '{}'), true);
        $checkpoint_info = json_decode(get_option('wpsec_scan_' . $scan_id . '_checkpoint_info', '{}'), true);
        
        // Log extremely detailed diagnostics
        wpsec_debug_log(sprintf(
            '🔍 DETAILED STALL CHECK - Scan: %s, Time since update: %ds, Last file: %s, Files processed: %d, Files change: %d, Memory: %sMB, Batch size: %d',
            $scan_id,
            ($current_time - $last_progress_update),
            $last_file,
            $current_files_scanned,
            ($current_files_scanned - $last_files_scanned),
            round(memory_get_usage(true) / 1024 / 1024, 2),
            isset($batch_info['size']) ? $batch_info['size'] : 0
        ), 'info');
        
        // Check if the file exists and log its details
        // Use WP_Filesystem for file operations if available
        $wp_filesystem = wpsec_init_filesystem();
        
        if (($wp_filesystem && is_object($wp_filesystem) && $wp_filesystem->exists($last_file)) || 
            (!$wp_filesystem && file_exists($last_file))) {
            
            // Get file size using WP_Filesystem or fallback
            if ($wp_filesystem && is_object($wp_filesystem)) {
                $file_size = $wp_filesystem->size($last_file);
                // Unfortunately WP_Filesystem doesn't have a direct equivalent to fileperms
                // so we'll use the direct function for this specific operation
                $file_perms = substr(sprintf('%o', fileperms($last_file)), -4);
            } else {
                $file_size = filesize($last_file);
                $file_perms = substr(sprintf('%o', fileperms($last_file)), -4);
            }
            $file_type = pathinfo($last_file, PATHINFO_EXTENSION);
            wpsec_debug_log(sprintf(
                '📄 FILE DETAILS - Path: %s, Size: %s, Permissions: %s, Type: %s',
                $last_file,
                size_format($file_size),
                $file_perms,
                $file_type
            ), 'info');
        } else {
            wpsec_debug_log('⚠️ STALL CHECK - Last file does not exist: ' . $last_file, 'warning');
        }
    }
    
    // If we've been on the same file for over 30 seconds with no progress, it might be stuck
    // Reduced from 60 seconds to 30 seconds for faster detection
    if ($last_progress_update > 0 && 
        (($current_time - $last_progress_update) > 30 || 
         ($last_files_scanned === $current_files_scanned && ($current_time - $last_progress_update) > 15))) {
        $stall_file = get_option('wpsec_scan_' . $scan_id . '_stall_file', '');

        // If we're stuck on the same file
        if ($stall_file === $last_file) {
            // This is a confirmed stall on the same file
            wpsec_debug_log('🚨 WPFort Resumable Scan: Detected stalled file: ' . $last_file, 'info');

            // Check file size - handle large files with special care
            // Use WP_Filesystem for file operations if available
            $wp_filesystem = wpsec_init_filesystem();
            
            if ($wp_filesystem && is_object($wp_filesystem)) {
                $file_size = $wp_filesystem->exists($last_file) ? $wp_filesystem->size($last_file) : 0;
            } else {
                $file_size = file_exists($last_file) ? filesize($last_file) : 0;
            }
            $file_type = pathinfo($last_file, PATHINFO_EXTENSION);

            // Large file detection (over 1MB) - these might need special handling
            if ($file_size > 1048576) { // 1MB
                wpsec_debug_log("📄 LARGE FILE DETECTED: {$last_file} - Size: " . size_format($file_size), 'info');

                // For large files, mark them for special scanning rather than skipping
                // We NEVER want to skip files that could be malicious
                $large_files = get_option('wpsec_scan_large_files', []);
                $large_files[] = [
                    'file_path' => $last_file,
                    'file_size' => $file_size,
                    'file_type' => $file_type,
                    'date_detected' => gmdate('Y-m-d H:i:s')
                ];
                update_option('wpsec_scan_large_files', $large_files);

                // Flag this file for detailed security review
                wpsec_debug_log("⚠️ SECURITY NOTICE: Large file marked for detailed review: {$last_file}", 'warning');

                // Report it in the scan results
                $infected_files = json_decode(get_option('wpsec_scan_' . $scan_id . '_infected_files', '[]'), true);

                // Add it as a potential issue (but not confirmed malware)
                $infected_files[] = [
                    'file_path' => $last_file,
                    'threat_score' => 2, // Low threat score, just flagging it
                    'confidence' => 30,
                    'detections' => [
                        [
                            'type' => 'performance',
                            'name' => 'Large File Scan Timeout',
                            'severity' => 'info',
                            'confidence' => 100,
                            'description' => 'This file is unusually large and caused timeout issues during scanning. While not necessarily malicious, large files that cause timeouts should be manually reviewed.'
                        ]
                    ],
                    'context' => [
                        'type' => pathinfo($last_file, PATHINFO_EXTENSION),
                        'file_size' => $file_size,
                        'risk_level' => 'low'
                    ],
                    'scan_time' => time(),
                    'file_size' => $file_size,
                    'extension' => pathinfo($last_file, PATHINFO_EXTENSION)
                ];

                update_option('wpsec_scan_' . $scan_id . '_infected_files', json_encode($infected_files));
            }

            // For all stalled files, record the info for diagnostics
            $stall_patterns = json_decode(get_option('wpsec_scan_' . $scan_id . '_stall_patterns', '[]'), true);

            $stall_patterns[] = [
                'file' => $last_file,
                'occurrences' => isset($stall_patterns[$last_file]) ? $stall_patterns[$last_file]['occurrences'] + 1 : 1,
                'type' => $file_type,
                'size' => $file_size
            ];

            update_option('wpsec_scan_' . $scan_id . '_stall_patterns', json_encode(array_slice($stall_patterns, -10))); // Keep last 10

            // Update progress counters to continue the scan
            $processed = (int)get_option('wpsec_scan_' . $scan_id . '_files_scanned', 0);
            $processed++;
            $skipped = (int)get_option('wpsec_scan_' . $scan_id . '_files_skipped', 0);
            $skipped++;
            update_option('wpsec_scan_' . $scan_id . '_files_scanned', $processed);
            update_option('wpsec_scan_' . $scan_id . '_files_skipped', $skipped);
            update_option('wpsec_scan_' . $scan_id . '_last_progress_update', $current_time);

            // Rather than permanently excluding the file, mark it for special handling
            $special_handling_files = get_option('wpsec_special_handling_files', []);
            if (!in_array($last_file, $special_handling_files)) {
                $special_handling_files[] = $last_file;
                update_option('wpsec_special_handling_files', $special_handling_files);
                wpsec_debug_log('🔍 WPFort: Added file to special handling list: ' . $last_file, 'info');
            }

            // Clear stall file marker
            delete_option('wpsec_scan_' . $scan_id . '_stall_file');
        } else {
            // First time we've seen this potential stall, mark it
            update_option('wpsec_scan_' . $scan_id . '_stall_file', $last_file);
            wpsec_debug_log('⚠️ WPFort Resumable Scan: Potential stall detected on file: ' . $last_file, 'warning');
        }
    }

    
    try {
        // If we detected a stall, we need to reconstruct the queue
        $stall_file = get_option('wpsec_scan_' . $scan_id . '_last_scanned_file', '');
        $force_checkpoint_skip = false;
        
        // Check if we have a stalled file situation
        // Use WP_Filesystem for file operations if available
        $wp_filesystem = wpsec_init_filesystem();
        
        $file_exists = false;
        if ($wp_filesystem && is_object($wp_filesystem)) {
            $file_exists = $wp_filesystem->exists($stall_file);
        } else {
            $file_exists = file_exists($stall_file);
        }
        
        if (!empty($stall_file) && $file_exists) {
            // CRITICAL FIX: More aggressive stall detection and recovery
            wpsec_debug_log('💥 WPFort CRITICAL: Stall detected on file: ' . $stall_file, 'critical');
            
            // Flag the file as suspicious (not automatically skipped)
            // Store it in a separate list for security review
            $slow_scan_files = get_option('wpsec_scan_slow_files', []);
            $already_logged = false;
            foreach ($slow_scan_files as $item) {
                if (isset($item['file_path']) && $item['file_path'] === $stall_file) {
                    $already_logged = true;
                    break;
                }
            }
            
            if (!$already_logged) {
                // Add file information for security review
                $slow_scan_files[] = [
                    'file_path' => $stall_file,
                    'type' => 'stall',
                    'file_size' => (function() use ($stall_file) {
                        // Use WP_Filesystem for file operations if available
                        $wp_filesystem = wpsec_init_filesystem();
                        
                        if ($wp_filesystem && is_object($wp_filesystem)) {
                            return $wp_filesystem->exists($stall_file) ? $wp_filesystem->size($stall_file) : 0;
                        } else {
                            return file_exists($stall_file) ? filesize($stall_file) : 0;
                        }
                    })(),
                    'date_added' => gmdate('Y-m-d H:i:s'),
                    'scan_id' => $scan_id
                ];
                update_option('wpsec_scan_slow_files', $slow_scan_files);
                wpsec_debug_log('🔍 WPFort: Added to suspicious slow files list for security review: ' . $stall_file, 'info');
            }
            
            // To prevent the scan from hanging indefinitely, still skip this checkpoint
            // but flag it as needing further security review
            wpsec_debug_log('⚠️ WPFort: Skipping checkpoint with suspicious file. This file needs security review: ' . $stall_file, 'warning');
            $force_checkpoint_skip = true;
        }
        
        // Initialize from checkpoint - if we're forcing a skip due to stall, pass flag to ignore current checkpoint
        // Use a much smaller batch size (500 files) for better reliability on shared hosting
        // Smaller batches process more reliably without timeouts on restricted environments
        // CRITICAL FIX: Ensure that the checkpoint retrieval is working properly
        // Add extensive logging to track the checkpoint retrieval process
        $has_checkpoint = wpsec_has_active_checkpoint($scan_id);
        wpsec_debug_log('🔄 WPFort Checkpoint Debug: Has active checkpoint: ' . ($has_checkpoint ? 'YES' : 'NO') . ' for scan ' . $scan_id, 'debug');
        
        if ($has_checkpoint && !$force_checkpoint_skip) {
            $checkpoint_id = get_option('wpsec_scan_' . $scan_id . '_active_checkpoint', '');
            wpsec_debug_log('🔄 WPFort Checkpoint Debug: Active checkpoint ID: ' . $checkpoint_id, 'debug');
        }
        
        $scan_data = wpsec_init_resumable_scan($scan_id, 500, $force_checkpoint_skip);
        
        // Get the original force_deep_scan parameter from scan settings
        $force_deep_scan = get_option('wpsec_scan_' . $scan_id . '_force_deep_scan', false);
        
        // Log the force_deep_scan value for debugging
        wpsec_debug_log("🔍 WPFort DEBUG: Resuming scan with force_deep_scan = " . ($force_deep_scan ? 'true' : 'false'), 'debug');
        
        // SIMPLIFIED: Use a single source of truth for processed count
        $processed = (int)get_option('wpsec_scan_' . $scan_id . '_files_processed', 0);
        wpsec_debug_log('🔄 WPFort Progress Debug: Using processed count: ' . $processed, 'debug');
        
        // Run the batch
        $batch_results = wpsec_run_scan_batch(
            $scan_id,
            $scan_data['queue'],
            $scan_data['results'],
            $processed,
            500, // Batch size
            $force_deep_scan // Use the retrieved force_deep_scan parameter
        );
        
        // If there are more files to scan, schedule the next batch
        if ($batch_results['status'] === 'in_progress' && !empty($batch_results['queue'])) {
            if (wpsec_schedule_next_scan_batch($scan_id)) {
                // Successfully scheduled
                wpsec_debug_log('✅ WPFort Scan Batch: Successfully scheduled next batch for scan ' . $scan_id, 'info');
                
                // CRITICAL FIX: Update scan status endpoint with latest progress
                $total_files = get_option('wpsec_scan_' . $scan_id . '_total_files', 0);
                $progress = ($total_files > 0) ? round(($batch_results['processed'] / $total_files) * 100) : 0;
                
                // Update scan status options for the REST API to read
                update_option('wpsec_scan_' . $scan_id . '_files_scanned', $batch_results['processed']);
                update_option('wpsec_scan_' . $scan_id . '_processed_count', $batch_results['processed']); // Add redundant storage
                update_option('wpsec_scan_' . $scan_id . '_progress', $progress);
                
                wpsec_debug_log('📊 WPFort Progress: Updated status endpoint to ' . $progress . '% (' . $batch_results['processed'] . '/' . $total_files . ' files)', 'info');
            } else {
                // Failed to schedule
                wpsec_debug_log('❌ WPFort Scan Batch: Failed to schedule next batch for scan ' . $scan_id, 'error');
                
                // Try to diagnose the issue
                if (!function_exists('wp_schedule_single_event')) {
                    wpsec_debug_log('❌ WPFort Error: wp_schedule_single_event function not available', 'error');
                }
            }
        } else {
            // Scan is complete
            wpsec_debug_log('✅ WPFort Resumable Scan: Scan ' . $scan_id . ' completed successfully', 'info');
            
            // Process final results
            $filtered_results = array_filter($batch_results['results'], function($file) {
                if (!isset($file['threat_score']) || !isset($file['confidence'])) {
                    return false;
                }
                // Consider both threat score and confidence with adjusted thresholds
                return ($file['threat_score'] >= 2 && $file['confidence'] >= 40) || 
                       ($file['threat_score'] >= 4) || 
                       ($file['confidence'] >= 80);
            });
            
            // Update final status
            update_option('wpsec_scan_' . $scan_id . '_status', 'completed');
            update_option('wpsec_scan_' . $scan_id . '_end', time());
            update_option('wpsec_scan_' . $scan_id . '_duration', time() - get_option('wpsec_scan_' . $scan_id . '_start', time()));
            update_option('wpsec_scan_' . $scan_id . '_progress', 100);
            update_option('wpsec_scan_' . $scan_id . '_files_scanned', $batch_results['processed']);
            update_option('wpsec_scan_' . $scan_id . '_infected_files', json_encode(array_values($filtered_results)));
            update_option('wpsec_scan_' . $scan_id . '_infected_count', count($filtered_results));
            
            // Clear checkpoints
            wpsec_clear_scan_checkpoints($scan_id);
            
            // Send completion webhook
            if (function_exists('wpsec_send_scan_complete_webhook')) {
                wpsec_send_scan_complete_webhook($scan_id);
            }
        }
    } catch (Exception $e) {
        wpsec_debug_log('🚨 WPFort Resumable Scan: Error in batch: ' . $e->getMessage(), 'error');
        
        // Record critical error
        $error_message = $e->getMessage();
        $trace = $e->getTraceAsString();
        
        // Check if a scan has been running too long
        $status = get_option('wpsec_scan_' . $scan_id . '_status', '');
        if ($status === 'running') {
            $started_at = get_option('wpsec_scan_' . $scan_id . '_started_at', '');
            $last_progress = get_option('wpsec_scan_' . $scan_id . '_last_progress_update', 0);
            $current_time = time();
            
            // If no progress in 5 minutes, consider scan stalled
            if ($last_progress > 0 && ($current_time - $last_progress) > 300) { // 5 minutes
                wpsec_debug_log('⚠️ WPFort Monitor: Scan ' . $scan_id . ' appears stalled - no progress in ' . ($current_time - $last_progress) . ' seconds', 'warning');
                
                // Check if stall count exceeds threshold
                $stall_count = (int) get_option('wpsec_scan_' . $scan_id . '_stall_count', 0);
                if ($stall_count >= 3) {
                    wpsec_debug_log('🏁 WPFort Monitor: Scan ' . $scan_id . ' has stalled ' . $stall_count . ' times, marking as completed', 'info');
                    
                    // Get total files
                    $total_files = get_option('wpsec_scan_' . $scan_id . '_total_files', 0);
                    
                    // Mark scan as complete
                    update_option('wpsec_scan_' . $scan_id . '_status', 'completed');
                    update_option('wpsec_scan_' . $scan_id . '_completed_at', gmdate('Y-m-d H:i:s'));
                    update_option('wpsec_scan_' . $scan_id . '_progress', 100);
                    update_option('wpsec_scan_' . $scan_id . '_files_processed', $total_files);
                    update_option('wpsec_scan_' . $scan_id . '_files_scanned', $total_files);
                    
                    // Clear scheduled events for this scan
                    $timestamp = wp_next_scheduled('wpsec_run_scan_batch_cron', array($scan_id));
                    if ($timestamp) {
                        wp_unschedule_event($timestamp, 'wpsec_run_scan_batch_cron', array($scan_id));
                    }
                } else {
                    // Try to recover by forcing a new batch
                    wpsec_schedule_next_scan_batch($scan_id);
                }
            }
            
            // If scan has been running for more than 24 hours, consider it failed
            if ($started_at && strtotime($started_at) < ($current_time - 86400)) { // 24 hours
                wpsec_debug_log('⚠️ WPFort Monitor: Scan ' . $scan_id . ' has been running for more than 24 hours, marking as failed', 'warning');
                update_option('wpsec_scan_' . $scan_id . '_status', 'failed');
                update_option('wpsec_scan_' . $scan_id . '_failed_at', gmdate('Y-m-d H:i:s'));
            }
        }
        
        $critical_error = [
            'message' => $error_message,
            'trace' => $trace,
            'last_file' => get_option('wpsec_scan_' . $scan_id . '_last_scanned_file', 'Unknown'),
            'progress' => get_option('wpsec_scan_' . $scan_id . '_progress', 0),
            'time' => gmdate('Y-m-d H:i:s'),
            'memory_usage' => round(memory_get_usage(true) / 1024 / 1024, 2) . 'MB'
        ];
        
        update_option('wpsec_scan_' . $scan_id . '_status', 'error');
        update_option('wpsec_scan_' . $scan_id . '_error', $error_message);
        update_option('wpsec_scan_' . $scan_id . '_critical_error', json_encode($critical_error));
        
        // Send failed webhook
        if (function_exists('wpsec_send_scan_failed_webhook')) {
            wpsec_send_scan_failed_webhook($scan_id, $error_message);
        }
    }
}

/**
 * Schedule the next batch of a scan
 * 
 * @param string $scan_id The scan ID
 * @return bool Success
 */
function wpsec_schedule_next_scan_batch($scan_id) {
    if (!$scan_id) {
        return false;
    }
    
    // Clear any existing schedules for this scan to prevent duplicates
    wp_clear_scheduled_hook('wpsec_run_scan_batch_cron', [$scan_id]);
    
    // Schedule the next batch to run immediately (no delay)
    $scheduled = wp_schedule_single_event(time(), 'wpsec_run_scan_batch_cron', [$scan_id]);
    
    wpsec_debug_log('🕒 WPFort Resumable Scan: Scheduled next batch for scan ' . $scan_id, 'info');
    
    return $scheduled;
}

// Register cron action
add_action('wpsec_run_scan_batch_cron', 'wpsec_handle_scan_batch_cron');
?>
