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

/**
 * Helper function to get memory limit in bytes
 * 
 * @return int Memory limit in bytes
 */
/**
 * Advanced diagnostic function to capture scan state and performance data
 * 
 * @param string $scan_id The scan ID to diagnose
 * @return array Diagnostic data
 */
function wpsec_diagnose_scan_bottlenecks($scan_id) {
    if (!$scan_id) {
        return ['error' => 'No scan ID provided'];
    }
    
    // Get scan status and metrics
    $status = get_option('wpsec_scan_' . $scan_id . '_status', 'unknown');
    $start_time = get_option('wpsec_scan_' . $scan_id . '_start', 0);
    $last_update = get_option('wpsec_scan_' . $scan_id . '_last_progress_update', 0);
    $progress = get_option('wpsec_scan_' . $scan_id . '_progress', 0);
    $total_files = get_option('wpsec_scan_' . $scan_id . '_total_files', 0);
    $last_file = get_option('wpsec_scan_' . $scan_id . '_last_scanned_file', '');
    $last_index = get_option('wpsec_scan_' . $scan_id . '_last_scanned_index', 0);
    $chunk_size = get_option('wpsec_scan_' . $scan_id . '_chunk_size', 0);
    
    // Calculate time-based metrics
    $elapsed_time = time() - $start_time;
    $stall_time = time() - $last_update;
    $files_per_second = $progress > 0 && $elapsed_time > 0 ? round($progress / $elapsed_time, 2) : 0;
    
    // Server environment data
    $server_env = wpsec_detect_server_environment();
    
    // Check for potential bottlenecks
    $bottlenecks = [];
    
    // Memory usage issues
    if ($server_env['memory_available'] < 32) { // Less than 32MB available
        $bottlenecks[] = 'Low memory available: ' . $server_env['memory_available'] . 'MB';
    }
    
    // CPU load issues
    if ($server_env['cpu_load'] > 80) {
        $bottlenecks[] = 'High CPU load: ' . $server_env['cpu_load'] . '%';
    }
    
    // File processing rate issues
    if ($files_per_second < 5 && $progress > 100) { // Only check if we've processed some files
        $bottlenecks[] = 'Slow file processing rate: ' . $files_per_second . ' files/sec';
    }
    
    // Large file detection
    if ($last_file && file_exists($last_file)) {
        $file_size = filesize($last_file) / 1024 / 1024; // Size in MB
        if ($file_size > 5) { // If last file was over 5MB
            $bottlenecks[] = 'Large file processing: ' . $last_file . ' (' . round($file_size, 2) . 'MB)';
        }
    }
    
    // Check for problematic file types in the last file
    if ($last_file) {
        $extension = strtolower(pathinfo($last_file, PATHINFO_EXTENSION));
        $slow_extensions = ['sql', 'gz', 'zip', 'log', 'bak'];
        if (in_array($extension, $slow_extensions)) {
            $bottlenecks[] = 'Potentially slow file type: .' . $extension;
        }
    }
    
    // Check if we're stuck in an infinite loop
    $scanned_files = [];
    for ($i = 1; $i <= 3; $i++) {
        $prev_file = get_option('wpsec_scan_' . $scan_id . '_prev_file_' . $i, '');
        if ($prev_file) {
            $scanned_files[] = $prev_file;
        }
    }
    
    // Store current file for future comparisons
    update_option('wpsec_scan_' . $scan_id . '_prev_file_3', get_option('wpsec_scan_' . $scan_id . '_prev_file_2', ''));
    update_option('wpsec_scan_' . $scan_id . '_prev_file_2', get_option('wpsec_scan_' . $scan_id . '_prev_file_1', ''));
    update_option('wpsec_scan_' . $scan_id . '_prev_file_1', $last_file);
    
    // Check if we're scanning the same file repeatedly
    if (count(array_unique($scanned_files)) === 1 && !empty($scanned_files[0])) {
        $problem_file = $scanned_files[0];
        $bottlenecks[] = 'Possible infinite loop on file: ' . $problem_file;
        
        // Automatically blacklist the problematic file to prevent future loops
        $blacklist = get_option('wpsec_scan_blacklisted_files', array());
        if (!in_array($problem_file, $blacklist)) {
            $blacklist[] = $problem_file;
            update_option('wpsec_scan_blacklisted_files', $blacklist);
            wpsec_debug_log("⚠️ WPFort: Blacklisted problematic file causing infinite loop: {$problem_file}", 'warning');
        }
    }
    
    return [
        'scan_id' => $scan_id,
        'status' => $status,
        'progress' => $progress,
        'total_files' => $total_files,
        'percent_complete' => $total_files > 0 ? round(($progress / $total_files) * 100, 1) : 0,
        'elapsed_time' => $elapsed_time,
        'stall_time' => $stall_time,
        'files_per_second' => $files_per_second,
        'last_file' => $last_file,
        'last_index' => $last_index,
        'chunk_size' => $chunk_size,
        'server_environment' => $server_env,
        'bottlenecks' => $bottlenecks,
        'is_stalled' => $stall_time > 60, // Consider stalled if no updates for 60 seconds
        'memory_limit' => wpsec_get_memory_limit_bytes() / 1024 / 1024 . 'MB',
    ];
}

/**
 * Check if a scan needs recovery and attempt to recover it
 * Called by the auto-recovery mechanism
 *
 * @param string $scan_id The scan ID to check
 * @return bool True if recovery was attempted
 */
function wpsec_check_scan_recovery($scan_id) {
    // Check if scan exists
    $status = get_option('wpsec_scan_' . $scan_id . '_status', '');
    if (empty($status)) {
        return false;
    }
    
    // Get current timestamp and scan data
    $now = time();
    $last_update = (int) get_option('wpsec_scan_' . $scan_id . '_last_update', 0);
    $resume_scheduled = (bool) get_option('wpsec_scan_' . $scan_id . '_resume_scheduled', false);
    $resume_position = (int) get_option('wpsec_scan_' . $scan_id . '_resume_position', 0);
    
    // Skip if scan completed
    if ($status === 'complete') {
        return false;
    }
    
    // Different recovery strategies based on status
    
    // Case 1: Scan is paused but resume not scheduled or didn't trigger
    if ($status === 'paused') {
        // If resume was scheduled but it's been more than 5 minutes, the scheduler failed
        if ($resume_scheduled && ($now - $last_update > 300)) {
            wpsec_debug_log("⚠️ WPFort: Scan {$scan_id} resume didn't trigger after 5+ minutes, forcing resume", 'warning');
            wpsec_resume_scan($scan_id, $resume_position);
            return true;
        }
        
        // Also check for backup transient in case WP Cron failed
        $transient_data = get_transient('wpsec_resume_scan_' . $scan_id);
        if ($transient_data !== false) {
            $resume_data = json_decode($transient_data, true);
            $transient_position = isset($resume_data['resume_from']) ? (int)$resume_data['resume_from'] : 0;
            wpsec_debug_log("⚠️ WPFort: Scan {$scan_id} has resume transient but no scheduled resume, forcing resume from position {$transient_position}", 'warning');
            wpsec_resume_scan($scan_id, $transient_position);
            return true;
        }
    }
    
    // Case 2: Scan is marked as scanning but hasn't updated in 60+ seconds (stalled)
    // OR scan progress hasn't actually increased in 30+ seconds despite last_update being fresh
    $last_progress_change = (int) get_option('wpsec_scan_' . $scan_id . '_last_progress_change', 0);
    $current_progress = (int) get_option('wpsec_scan_' . $scan_id . '_progress', 0);
    $progress_stalled = ($now - $last_progress_change > 30); // 30 seconds with no progress change
    
    if ($status === 'scanning' && (($now - $last_update > 60) || $progress_stalled)) {
        if ($progress_stalled) {
            wpsec_debug_log("⚠️ WPFort: Scan {$scan_id} appears stalled at {$current_progress}%, no progress change in 30+ seconds, forcing resume", 'warning');
        } else {
            wpsec_debug_log("⚠️ WPFort: Scan {$scan_id} appears stalled, no updates in 60+ seconds, forcing resume", 'warning');
        }
        wpsec_resume_scan($scan_id, $resume_position > 0 ? $resume_position : 0);
        return true;
    }
    
    // Case 3: Scan is 'resuming' but hasn't started scanning after 2+ minutes (stuck in resuming state)
    if ($status === 'resuming' && ($now - $last_update > 120)) {
        wpsec_debug_log("⚠️ WPFort: Scan {$scan_id} stuck in 'resuming' state for 2+ minutes, forcing resume", 'warning');
        update_option('wpsec_scan_' . $scan_id . '_status', 'paused'); // Reset status to allow resume
        wpsec_resume_scan($scan_id, $resume_position);
        return true;
    }
    
    return false;
}

/**
 * Schedule a scan to resume after being paused
 * 
 * @param string $scan_id The scan ID to resume
 * @param int $resume_from The position to resume from
 * @return void
 */
function wpsec_schedule_scan_resume($scan_id, $resume_from) {
    // Get info about current server conditions to decide resume timing
    $server_env = wpsec_detect_server_environment();
    
    // Record scan as officially paused
    update_option('wpsec_scan_' . $scan_id . '_status', 'paused');
    update_option('wpsec_scan_' . $scan_id . '_resume_position', $resume_from);
    update_option('wpsec_scan_' . $scan_id . '_last_update', time());
    
    // Count how many times this scan has been paused already
    $resume_count = (int) get_option('wpsec_scan_' . $scan_id . '_resume_count', 0);
    update_option('wpsec_scan_' . $scan_id . '_resume_count', $resume_count + 1);
    
    // Adjust resume delay based on server load and previous pause count
    $base_delay = 30; // 30 seconds baseline
    
    // If server is under high load, increase delay
    if ($server_env['cpu_load'] > 80) {
        $base_delay = 60; // 1 minute
    }
    if ($server_env['cpu_load'] > 90) {
        $base_delay = 120; // 2 minutes
    }
    
    // Progressive backoff - increase delay for repeated pauses
    // But cap at 15 minutes to ensure scan eventually completes
    $backoff_multiplier = min(5, $resume_count);
    $resume_delay = min(900, $base_delay * $backoff_multiplier);
    
    // Clear any existing scheduled events for this scan to avoid duplicates
    $existing_timestamp = wp_next_scheduled('wpsec_resume_scan', [$scan_id, $resume_from]);
    if ($existing_timestamp) {
        wp_unschedule_event($existing_timestamp, 'wpsec_resume_scan', [$scan_id, $resume_from]);
    }
    
    // Schedule the resume using WP Cron - we don't rely heavily on this, but keep as a backup
    $scheduled = wp_schedule_single_event(time() + $resume_delay, 'wpsec_resume_scan', [$scan_id, $resume_from]);
    
    // Use transients as our PRIMARY resumption mechanism - much more reliable than WP Cron
    // This is critical for consistent operation across different hosting environments
    set_transient('wpsec_resume_scan_' . $scan_id, json_encode([
        'resume_from' => $resume_from,
        'scheduled_at' => time(),
        'resume_at' => time() + $resume_delay
    ]), $resume_delay + 300); // 5 minutes extra buffer
    
    // Store detailed resume information
    update_option('wpsec_scan_' . $scan_id . '_resume_scheduled', time());
    update_option('wpsec_scan_' . $scan_id . '_resume_position', $resume_from);
    update_option('wpsec_scan_' . $scan_id . '_resume_delay', $resume_delay);
    update_option('wpsec_scan_' . $scan_id . '_resume_expected_at', time() + $resume_delay);
    
    // Set scan status to paused - CRITICAL for UI feedback
    update_option('wpsec_scan_' . $scan_id . '_status', 'paused');
    
    wpsec_debug_log("🔄 WPFort: Scheduled scan {$scan_id} to resume from position {$resume_from} in {$delay} seconds", 'info');
    return $scheduled;
}

/**
 * Resume a previously paused or stalled scan
 *
 * @param string $scan_id The ID of the scan to resume
 * @param int $resume_from The position to resume from 
 * @return bool True if scan was successfully resumed
 */
function wpsec_resume_scan($scan_id, $resume_from = 0) {
    // Check if scan exists and isn't already completed
    $status = get_option('wpsec_scan_' . $scan_id . '_status', '');
    
    if ($status === 'completed' || $status === 'failed') {
        wpsec_debug_log("⚠️ WPFort: Cannot resume scan {$scan_id} - status is {$status}", 'warning');
        return false;
    }
    
    // Get scan parameters
    $deep_scan = get_option('wpsec_scan_' . $scan_id . '_deep', false);
    $verbose = get_option('wpsec_scan_' . $scan_id . '_verbose', false);
    
    // Set status to scanning (not 'resuming' to avoid getting stuck)
    update_option('wpsec_scan_' . $scan_id . '_status', 'scanning');
    update_option('wpsec_scan_' . $scan_id . '_last_update', time());
    
    // Log resume attempts (for diagnostics)
    $resume_attempts = get_option('wpsec_scan_' . $scan_id . '_resume_attempts', 0);
    update_option('wpsec_scan_' . $scan_id . '_resume_attempts', $resume_attempts + 1);
    
    // Attempt to get the most reliable resume position
    // If a position was explicitly provided, use it
    // Otherwise try these sources in order of reliability:
    if ($resume_from <= 0) {
        // 1. First check for resume_position as it's the most accurate
        $resume_from = get_option('wpsec_scan_' . $scan_id . '_resume_position', 0);
        
        // 2. If that's not available, use files_scanned as backup
        if ($resume_from <= 0) {
            $resume_from = get_option('wpsec_scan_' . $scan_id . '_files_scanned', 0);
        }
        
        // 3. As final fallback, check the last_files_processed value
        if ($resume_from <= 0) {
            $resume_from = get_option('wpsec_scan_' . $scan_id . '_last_files_processed', 0);
        }
        
        // Log which source we're using
        wpsec_debug_log("🔍 WPFort: Using resume position {$resume_from} for scan {$scan_id}", 'info');
    }
    
    wpsec_debug_log("⚡ WPFort: Resuming scan {$scan_id} from position {$resume_from} (attempt #{$resume_attempts})", 'info');
    
    // Call the scan function with the resume position
    require_once dirname(__FILE__) . '/scan.php';
    // Run the scan directly
    wpsec_run_scan($scan_id, $deep_scan, $verbose, $resume_from);
    
    return true;
}

/**
 * This is intentionally left empty to fix the duplicate function declaration.
 * The actual implementation is at line 127.
 */

function wpsec_get_memory_limit_bytes() {
    $memory_limit = ini_get('memory_limit');
    
    if (preg_match('/^(\d+)(.)$/', $memory_limit, $matches)) {
        if ($matches[2] == 'G') {
            $memory_limit = $matches[1] * 1024 * 1024 * 1024;
        } else if ($matches[2] == 'M') {
            $memory_limit = $matches[1] * 1024 * 1024;
        } else if ($matches[2] == 'K') {
            $memory_limit = $matches[1] * 1024;
        }
    }
    
    return (int) $memory_limit;
}

/**
 * Release memory by clearing caches and forcing garbage collection
 * 
 * @return void
 */
function wpsec_release_memory() {
    // Clear various caches
    if (function_exists('wp_cache_flush')) {
        wp_cache_flush();
    }
    
    // Clear opcode cache if applicable
    if (function_exists('opcache_reset')) {
        @opcache_reset();
    }
    
    // Force immediate garbage collection
    if (function_exists('gc_collect_cycles')) {
        gc_collect_cycles();
    }
    
    // Attempt to reduce memory fragmentation
    $dummy = str_repeat('x', 1024 * 1024 * 10); // Allocate 10MB
    unset($dummy); // Release it to consolidate memory
}

/**
 * Create or retrieve scan state for resuming operations
 * 
 * @param string $scan_id The scan ID
 * @param bool $force_deep_scan Whether deep scanning is enabled
 * @return array The scan state
 */
function wpsec_get_or_create_scan_state($scan_id, $force_deep_scan = false) {
    // Check if we have a checkpoint to resume from
    $checkpoint = get_transient('wpsec_scan_' . $scan_id . '_checkpoint');
    
    if ($checkpoint) {
        wpsec_debug_log("📋 WPFort: Resuming scan from checkpoint - already scanned " . $checkpoint['files_scanned'] . " files", 'info');
        return $checkpoint;
    }
    
    // No checkpoint exists, create initial state
    $scan_queue_result = wpsec_build_scan_queue();
    $scan_queue = $scan_queue_result['queue'];
    $total_files = $scan_queue_result['total_files'];
    
    $state = array(
        'scan_id' => $scan_id,
        'force_deep_scan' => $force_deep_scan,
        'files_to_scan' => $scan_queue,
        'total_files' => $total_files,
        'files_scanned' => 0,
        'progress' => 0,
        'start_time' => time(),
        'last_index' => 0,
        'current_file' => '',
        'retried_files' => array(),
        'skipped_files' => array(),
        'infected_files' => array(),
        'scan_errors' => array(),
        'last_heartbeat' => time()
    );
    
    return $state;
}

/**
 * Save scan checkpoint to allow resuming
 * 
 * @param array $scan_state Current scan state
 * @return void
 */
function wpsec_save_scan_checkpoint($scan_state) {
    $scan_id = $scan_state['scan_id'];
    
    // Save core progress information
    update_option('wpsec_scan_' . $scan_id . '_checkpoint', json_encode($scan_state));
    update_option('wpsec_scan_' . $scan_id . '_checkpoint_time', time());
    update_option('wpsec_scan_' . $scan_id . '_files_scanned', $scan_state['files_scanned']);
    update_option('wpsec_scan_' . $scan_id . '_progress', $scan_state['progress']);
    update_option('wpsec_scan_' . $scan_id . '_last_file', $scan_state['current_file']);
    
    // Also save to a transient for faster access
    set_transient('wpsec_scan_' . $scan_id . '_checkpoint', $scan_state, 3600);
}

/**
 * Update scan progress in database and optionally send webhook
 * 
 * @param string $scan_id The scan ID
 * @param array $scan_state Current scan state
 * @return void
 */
function wpsec_update_scan_progress($scan_id, $scan_state) {
    if (!$scan_id || !is_array($scan_state)) {
        return;
    }
    
    // Extract state variables
    $processed = $scan_state['processed'] ?? 0;
    $position = $scan_state['position'] ?? 0;
    $total = $scan_state['total'] ?? 0;
    $infected = $scan_state['infected'] ?? 0;
    $skipped = $scan_state['skipped'] ?? 0;
    $cached = $scan_state['cached'] ?? 0;
    $whitelist = $scan_state['whitelist'] ?? 0;
    $errors = $scan_state['errors'] ?? 0;
    
    // Track when progress percentage actually changes
    $old_progress = (int) get_option('wpsec_scan_' . $scan_id . '_progress', 0);
    $new_progress = $total > 0 ? round(($processed / $total) * 100) : 0;
    
    // Update progress in database
    update_option('wpsec_scan_' . $scan_id . '_progress', $new_progress);
    update_option('wpsec_scan_' . $scan_id . '_files_scanned', $processed);
    update_option('wpsec_scan_' . $scan_id . '_last_progress_update', time());
    
    // Critical: Update timestamp when actual progress changes
    // This is essential for accurate stall detection
    if ($new_progress != $old_progress || $processed > get_option('wpsec_scan_' . $scan_id . '_last_files_processed', 0)) {
        update_option('wpsec_scan_' . $scan_id . '_last_progress_change', time());
        update_option('wpsec_scan_' . $scan_id . '_last_files_processed', $processed);
        if ($new_progress != $old_progress) {
            wpsec_debug_log("📈 WPFort: Scan progress changed: {$old_progress}% → {$new_progress}%", 'info');
        }
    }
    
    // Log memory usage
    $memory_usage = memory_get_usage(true);
    update_option('wpsec_scan_' . $scan_id . '_memory_usage', $memory_usage);
    
    // Update scan diagnostics
    $memory_limit = wpsec_get_memory_limit_bytes();
    $memory_percent = round(($memory_usage / $memory_limit) * 100, 1);
    
    $diagnostics = array(
        'memory_usage' => size_format($memory_usage),
        'memory_limit' => ini_get('memory_limit'),
        'memory_percent' => $memory_percent . '%',
        'load_average' => function_exists('sys_getloadavg') ? implode(', ', sys_getloadavg()) : 'N/A',
        'last_scanned_file' => $scan_state['current_file'],
        'file_size' => file_exists($scan_state['current_file']) ? size_format(filesize($scan_state['current_file'])) : 'N/A',
        'file_type' => pathinfo($scan_state['current_file'], PATHINFO_EXTENSION),
        'progress' => $scan_state['progress'],
        'files_scanned' => $scan_state['files_scanned'],
        'total_files' => $scan_state['total_files'],
        'time_since_update' => 0,
        'timestamp' => gmdate('Y-m-d H:i:s')
    );
    
    update_option('wpsec_scan_' . $scan_id . '_diagnostics', json_encode($diagnostics));
    
    // Send webhook if applicable
    if (function_exists('wpsec_send_scan_progress_webhook')) {
        $last_webhook_time = get_option('wpsec_scan_' . $scan_id . '_last_webhook_time', 0);
        
        // Only send webhook every 30 seconds
        if ((time() - $last_webhook_time) >= 30) {
            wpsec_send_scan_progress_webhook($scan_id);
            update_option('wpsec_scan_' . $scan_id . '_last_webhook_time', time());
        }
    }
    
    wpsec_debug_log(sprintf('📊 WPFort Status Debug: %d/%d = %d%%', 
        $scan_state['files_scanned'], 
        $scan_state['total_files'], 
        $scan_state['progress']
    ), 'debug');
}

/**
 * Check if scan control flags (pause or cancel) have been set
 * 
 * @param string $scan_id The scan ID
 * @return bool True if scan should stop processing
 */
function wpsec_check_scan_control_flags($scan_id) {
    $should_pause = get_option('wpsec_scan_' . $scan_id . '_pause', false);
    $should_cancel = get_option('wpsec_scan_' . $scan_id . '_cancel', false);
    
    if ($should_pause) {
        wpsec_debug_log("⏸️ WPFort: Scan paused by external control flag", 'info');
        update_option('wpsec_scan_' . $scan_id . '_status', 'paused');
        return true;
    }
    
    if ($should_cancel) {
        wpsec_debug_log("🛑 WPFort: Scan cancelled by external control flag", 'info');
        update_option('wpsec_scan_' . $scan_id . '_status', 'cancelled');
        return true;
    }
    
    return false;
}

/**
 * Log a file that failed to be scanned after retries
 * 
 * @param string $file_path Path to the file
 * @param string $scan_id The scan ID
 * @return void
 */
function wpsec_log_failed_file($file_path, $scan_id) {
    wpsec_debug_log("❌ WPFort: Failed to scan file after retries: " . basename($file_path), 'error');
    
    $failed_files = get_option('wpsec_scan_' . $scan_id . '_failed_files', array());
    $failed_files[] = array(
        'file' => $file_path,
        'size' => file_exists($file_path) ? filesize($file_path) : 0,
        'type' => pathinfo($file_path, PATHINFO_EXTENSION),
        'timestamp' => time()
    );
    
    update_option('wpsec_scan_' . $scan_id . '_failed_files', $failed_files);
}

/**
 * Log a file that took too long to scan for performance optimization
 * 
 * @param string $file_path Path to the file
 * @param float $duration Duration in seconds
 * @param string $scan_id The scan ID
 * @return void
 */
function wpsec_log_slow_file_scan($file_path, $duration, $scan_id) {
    wpsec_debug_log(sprintf("⏱️ WPFort: Slow file scan: %s took %.2f seconds", basename($file_path), $duration), 'info');
    
    $slow_files = get_option('wpsec_scan_' . $scan_id . '_slow_files', array());
    $slow_files[] = array(
        'file' => $file_path,
        'size' => file_exists($file_path) ? filesize($file_path) : 0,
        'type' => pathinfo($file_path, PATHINFO_EXTENSION),
        'duration' => $duration,
        'timestamp' => time()
    );
    
    // Sort by duration descending and keep only top 20
    usort($slow_files, function($a, $b) {
        return $b['duration'] <=> $a['duration'];
    });
    
    $slow_files = array_slice($slow_files, 0, 20);
    
    update_option('wpsec_scan_' . $scan_id . '_slow_files', $slow_files);
}

/**
 * Finish a scan and record completion statistics
 * 
 * @param string $scan_id The scan ID
 * @param string $status Final status (complete, error, paused)
 * @param string $message Completion message
 * @return void
 */
function wpsec_finish_scan($scan_id, $status, $message) {
    $end_time = time();
    $start_time = get_option('wpsec_scan_' . $scan_id . '_start', $end_time);
    $duration = $end_time - $start_time;
    
    // Update final status
    update_option('wpsec_scan_' . $scan_id . '_status', $status);
    update_option('wpsec_scan_' . $scan_id . '_end', $end_time);
    update_option('wpsec_scan_' . $scan_id . '_duration', $duration);
    update_option('wpsec_scan_' . $scan_id . '_message', $message);
    
    // Calculate final stats
    $files_scanned = get_option('wpsec_scan_' . $scan_id . '_files_scanned', 0);
    $scan_rate = $duration > 0 ? round($files_scanned / $duration, 2) : 0;
    
    update_option('wpsec_scan_' . $scan_id . '_scan_rate', $scan_rate);
    update_option('wpsec_last_scan_duration', $duration);
    
    wpsec_debug_log(sprintf("🏁 WPFort: Scan %s completed with status: %s | Duration: %d seconds | Files scanned: %d | Rate: %.2f files/sec",
        $scan_id, $status, $duration, $files_scanned, $scan_rate
    ), 'info');
    
    // Send final webhook if applicable
    if (function_exists('wpsec_send_scan_complete_webhook')) {
        wpsec_send_scan_complete_webhook($scan_id);
    }
    
    // Clear timeout check if scheduled
    wp_clear_scheduled_hook('wpsec_check_scan_timeout', array($scan_id));
    
    // Clear checkpoint for completed scans
    if ($status === 'complete') {
        delete_transient('wpsec_scan_' . $scan_id . '_checkpoint');
    }
}

/**
 * Helper function to scan a file with timeout protection and retry capability
 * 
 * @param string $file_path Path to the file being scanned
 * @param array $scan_state Current scan state
 * @param array $options Scanning options
 * @return array Updated scan state with scan results
 */
function wpsec_scan_file_with_recovery($file_path, $scan_state, $options) {
    $max_retries = isset($options['max_retries']) ? $options['max_retries'] : 3;
    $retry_delay = isset($options['retry_delay']) ? $options['retry_delay'] : 5; // seconds
    $timeout_threshold = isset($options['timeout_threshold']) ? $options['timeout_threshold'] : 30; // seconds
    $force_deep_scan = isset($options['force_deep_scan']) ? $options['force_deep_scan'] : false;
    $verbose_logging = isset($options['verbose_logging']) ? $options['verbose_logging'] : false;
    
    // Initialize local retry counter for this specific file
    $retries = 0;
    $start_time = microtime(true);
    $result = false;
    
    // Get signature and pattern databases
    global $signatures, $patterns, $advanced_definitions;
    
    // Save current memory usage for comparison
    $initial_memory = memory_get_usage();
    
    while ($retries < $max_retries && $result === false) {
        // Set a time limit for this specific operation
        // This creates a safety net within PHP itself
        if (function_exists('set_time_limit')) {
            @set_time_limit(60); // Reset time limit for each file
        }
        
        // Check if we should pause first (for retries)
        if ($retries > 0) {
            wpsec_debug_log("🔄 WPFort Debug - Retry #$retries for file: " . basename($file_path) . " - Pausing for $retry_delay seconds", 'debug');
            
            // Force garbage collection before pausing
            wpsec_release_memory();
            
            // Store checkpoint in database to allow external resume if process dies
            wpsec_save_scan_checkpoint($scan_state);
            
            // Pause to let resources recover
            sleep($retry_delay);
        }
        
        try {
            // Start a timer for this operation
            $operation_start = microtime(true);
            
            // Determine the priority of the file (from the scan queue)
            $priority = isset($scan_state['files_to_scan'][$file_path]) ? $scan_state['files_to_scan'][$file_path] : 5; // Default priority
            
            // Perform the actual file scan
            if (is_dir($file_path)) {
                if ($verbose_logging) {
                    wpsec_debug_log("📁 WPFort: Scanning directory: " . $file_path, 'info');
                }
                $scan_results = wpsec_scan_directory($file_path, $signatures, $patterns, $priority);
            } else {
                if ($verbose_logging) {
                    wpsec_debug_log("📄 WPFort: Scanning file: " . $file_path, 'info');
                }
                $scan_results = wpsec_scan_single_file($file_path, $signatures, $patterns, $priority);
            }
            
            // If we got a valid result (empty array is valid for clean files)
            if (is_array($scan_results)) {
                $result = true;
                
                // If there are infections, add them to the scan state
                if (!empty($scan_results)) {
                    $scan_state['infected_files'] = array_merge($scan_state['infected_files'], $scan_results);
                }
            }
            
            $operation_duration = microtime(true) - $operation_start;
            
            // If scan took too long, log it as a potential bottleneck
            if ($operation_duration > 10) { // 10 seconds is quite long for a single file
                wpsec_log_slow_file_scan($file_path, $operation_duration, $scan_state['scan_id']);
            }
            
        } catch (Exception $e) {
            wpsec_debug_log("⚠️ WPFort Error - Exception during scan of " . basename($file_path) . ": " . $e->getMessage(), 'error');
            $result = false;
            
            // Record the error
            $scan_state['scan_errors'][] = array(
                'file' => $file_path,
                'error' => $e->getMessage(),
                'time' => time()
            );
        }
        
        // Check if we should retry
        if ($result === false) {
            $retries++;
            // Update scan state with retry information
            $scan_state['retried_files'][$file_path] = $retries;
            
            // Release memory
            wpsec_release_memory();
        }
    }
    
    // If all retries failed, log this file for later analysis
    if ($result === false && $retries >= $max_retries) {
        wpsec_log_failed_file($file_path, $scan_state['scan_id']);
        // Update scan state to mark this file as skipped
        $scan_state['skipped_files'][] = $file_path;
    }
    
    // Check memory usage and log if there's a significant increase
    $current_memory = memory_get_usage();
    $memory_diff = $current_memory - $initial_memory;
    if ($memory_diff > 1048576) { // 1MB increase
        wpsec_debug_log("📈 WPFort Debug - Memory increased by " . round($memory_diff/1048576, 2) . "MB after scanning " . basename($file_path), 'debug');
    }
    
    return $scan_state;
}
