<?php
/**
 * Handles processing of scheduled SEO update tasks via Action Scheduler.
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

class Rank_Processor {

    /**
     * Initialize processor hooks.
     */
    public static function init() {
        // Hook for tasks from manual approval (passes 1 array arg)
        add_action( 'rank_process_single_item_hook', array( __CLASS__, 'run_scheduled_task' ), 10, 1 );

        // Hook for tasks from bulk scheduling (passes 5 distinct args - assumed from original code)
        // This ensures 'rank_process_single_url' hook, if used by another process for bulk actions,
        // is also handled by run_scheduled_task with its expected 5 arguments.
        add_action( 'rank_process_single_url', array( __CLASS__, 'run_scheduled_task' ), 10, 5 );
    }

    /**
     * Runs the scheduled task.
     * This method can be called by Action Scheduler with different argument structures
     * based on the hook that scheduled it.
     *
     * @param mixed $arg1 First argument. Can be an array of task details or the first of 5 distinct args.
     * @param mixed $arg2 Optional. Second of 5 distinct args.
     * @param mixed $arg3 Optional. Third of 5 distinct args.
     * @param mixed $arg4 Optional. Fourth of 5 distinct args.
     * @param mixed $arg5 Optional. Fifth of 5 distinct args.
     */
    public static function run_scheduled_task( $arg1, $arg2 = null, $arg3 = null, $arg4 = null, $arg5 = null ) {
        // Debug logging: Entry point
        self::add_debug_log("=== RANK PROCESSOR START ===");
        self::add_debug_log("Arguments received: " . print_r(func_get_args(), true));
        self::add_debug_log("Timestamp: " . current_time('mysql'));
        
        try {

            $task_args = array();
            $is_five_arg_payload = false;
            $is_array_payload = false;

            // Determine argument structure
            if ( ! is_array( $arg1 ) && $arg5 !== null ) { // Check for 5 distinct arguments
                $is_five_arg_payload = true;
                // Argument mapping:
                // $arg1 = original_url (page context)
                // $arg2 = item_specific_url (e.g., image URL, or broken link URL)
                // $arg3 = api_issue_type
                // $arg4 = solution
                // $arg5 = batch_id
                $task_args = array(
                    'batch_id'         => $arg5,
                    'page_context_url' => $arg1, 
                    'item_specific_url'=> $arg2,
                    'api_issue_type'   => $arg3,
                    'solution'         => $arg4,
                    'original_url'     => $arg1, // Keep for backward compatibility if any logic relies on 'original_url' explicitly
                    'row_key'          => 'from_rank_process_single_url_' . md5(print_r(func_get_args(), true)),
                );
            }
            elseif ( is_array( $arg1 ) && isset( $arg1['row_key'] ) && isset( $arg1['api_issue_type'] ) && isset($arg1['page_context_url']) ) {
                $task_args = $arg1;
                $is_array_payload = true;
            }
            elseif ( func_num_args() === 5 && !is_array($arg1) ) {
                $task_args = array(
                    'page_id_raw'    => $arg2,
                    'object_id_raw'  => $arg5,
                    'api_issue_type' => $arg3,
                    'solution'       => $arg4,
                    'original_url'   => $arg1,
                    'row_key'        => 'from_rank_process_single_url_' . md5(print_r(func_get_args(), true)),
                );
            }
            elseif ( is_array( $arg1 ) && isset( $arg1['batch_id'] ) && isset( $arg1['issue_type'] ) ) {
                $task_args = $arg1;
                if (isset($task_args['issue_type'])) $is_array_payload = true;
            }
            else {
                error_log("Rank_Processor: run_scheduled_task called with unexpected arguments.");
                return;
            }

            $batch_id         = isset($task_args['batch_id']) ? sanitize_key($task_args['batch_id']) : null;
            $page_context_url = isset( $task_args['page_context_url'] ) ? esc_url_raw( $task_args['page_context_url'] ) : null;
            // Store item_specific_url as-is to preserve special characters
            $item_specific_url = isset( $task_args['item_specific_url'] ) ? $task_args['item_specific_url'] : null;
            $api_issue_type   = isset( $task_args['api_issue_type'] ) ? $task_args['api_issue_type'] : ($task_args['issue_type'] ?? null);
            $override_value   = isset( $task_args['solution'] ) ? $task_args['solution'] : null;
            
                        
            // Simple solution extraction
            if (is_string($override_value)) {
                // Check if it's a serialized array
                if (is_serialized($override_value)) {
                    $unserialized = maybe_unserialize($override_value);
                    if (is_array($unserialized) && isset($unserialized['solution_value'])) {
                        $override_value = $unserialized['solution_value'];
                    }
                }
            } elseif (is_array($override_value)) {
                // If it's an array with a single object (common case from logs)
                if (count($override_value) === 1 && isset($override_value[0]) && is_array($override_value[0])) {
                    if (isset($override_value[0]['solution_value'])) {
                        $override_value = $override_value[0]['solution_value'];
                    }
                }
                // If it's a direct array with solution_value
                elseif (isset($override_value['solution_value'])) {
                    $override_value = $override_value['solution_value'];
                }
                // If it's a simple array, use the first element
                elseif (isset($override_value[0])) {
                    $override_value = $override_value[0];
                }
            }
            $row_key          = isset( $task_args['row_key'] ) ? $task_args['row_key'] : 'unknown_row_key_' . uniqid();

            // Debug logging: Task arguments processed
            self::add_debug_log("=== TASK ARGS PROCESSED ===");
            self::add_debug_log("Batch ID: " . ($batch_id ?? 'NULL'));
            self::add_debug_log("Page Context URL: " . ($page_context_url ?? 'NULL'));
            self::add_debug_log("API Issue Type: " . ($api_issue_type ?? 'NULL'));
            self::add_debug_log("Override Value: " . print_r($override_value, true));
            self::add_debug_log("Row Key: " . ($row_key ?? 'NULL'));

            // Ensure critical task_args are present
            if (empty($api_issue_type)) {
                error_log("Rank_Processor: api_issue_type is missing. Row Key: {$row_key}. Task Args: " . print_r($task_args, true));
                if ($batch_id) {
                    self::add_log_to_transient( $batch_id, "Processor Error: Critical task data (api_issue_type) missing.", $row_key, 'error' );
                }
                return; // Cannot proceed
            }

            if (empty($page_context_url) && isset($task_args['original_url'])) {
                $page_context_url = esc_url_raw($task_args['original_url']);
            }
            // For missing_alt_tags, item_specific_url is the image URL.
            // For broken links, item_specific_url should be the broken URL itself.
            // The original logic for item_specific_url for missing_alt_tags might need to be conditional.
            // Debug logging: item_specific_url extraction
            self::add_debug_log("=== ITEM_SPECIFIC_URL EXTRACTION START ===");
            self::add_debug_log("Initial item_specific_url: " . ($item_specific_url ?? 'NULL'));
            self::add_debug_log("API issue type: " . ($api_issue_type ?? 'NULL'));
            self::add_debug_log("Has original_url in task_args: " . (isset($task_args['original_url']) ? 'YES' : 'NO'));
            if (isset($task_args['original_url'])) {
                self::add_debug_log("original_url value: " . $task_args['original_url']);
            }

            if (empty($item_specific_url)) {
                self::add_debug_log("item_specific_url is empty, attempting extraction...");
                
                // For missing_alt_tags, use original_url if it's different from page_context_url
                if (isset($task_args['original_url']) && $api_issue_type === 'missing_alt_tags') {
                    self::add_debug_log("Processing missing_alt_tags extraction");
                    if ($page_context_url !== $task_args['original_url'] || !isset($task_args['page_context_url'])) {
                        // Store the URL as-is without encoding
                        $item_specific_url = $task_args['original_url'];
                        self::add_debug_log("Set item_specific_url from original_url for alt tags: " . $item_specific_url);
                    }
                }
                // For broken links, first try to use original_url directly
                elseif (($api_issue_type === 'broken_links_external' || $api_issue_type === 'broken_links_internal')) {
                    self::add_debug_log("Processing broken links extraction");
                    
                    // First try to get it directly from original_url
                    if (isset($task_args['original_url'])) {
                        self::add_debug_log("Trying original_url: " . $task_args['original_url']);
                        // Store the URL as-is without encoding
                        $item_specific_url = $task_args['original_url'];
                        self::add_debug_log("Set item_specific_url from original_url: " . $item_specific_url);
                    }
                    // If still empty, try to extract from row_key
                    elseif (empty($item_specific_url) && !empty($row_key)) {
                        self::add_debug_log("original_url not available, trying row_key extraction");
                        self::add_debug_log("Row key: " . $row_key);
                        
                        // Try to decode the row_key if it looks like base64
                        // More lenient pattern that allows missing padding
                        if (preg_match('/^[a-zA-Z0-9\/\r\n+]+={0,2}$/', $row_key) || strlen($row_key) > 20) {
                            self::add_debug_log("Row key matches base64 pattern, attempting decode");
                            
                            // Fix base64 padding if needed
                            $row_key_padded = $row_key;
                            $mod = strlen($row_key_padded) % 4;
                            if ($mod) {
                                $row_key_padded .= str_repeat('=', 4 - $mod);
                                self::add_debug_log("Added padding to row_key: " . (4 - $mod) . " characters");
                            }
                            
                            // Don't use strict mode as it may reject valid base64
                            $decoded = base64_decode($row_key_padded);
                            
                            if ($decoded !== false && is_string($decoded) && !empty($decoded)) {
                                self::add_debug_log("Base64 decode successful: " . $decoded);
                                $json_data = json_decode($decoded, true);
                                
                                if ($json_data === null) {
                                    self::add_debug_log("JSON decode failed for decoded data");
                                } else {
                                    self::add_debug_log("JSON decode successful: " . print_r($json_data, true));
                                    
                                    // First try direct access to issue_identifier
                                    if (is_array($json_data) && isset($json_data['issue_identifier'])) {
                                        $item_specific_url = $json_data['issue_identifier'];
                                        self::add_debug_log("Found issue_identifier directly: " . $item_specific_url);
                                    }
                                    // Then try to parse the nested payload JSON
                                    elseif (is_array($json_data) && isset($json_data['payload'])) {
                                        self::add_debug_log("Found payload, attempting to parse: " . $json_data['payload']);
                                        $payload = json_decode($json_data['payload'], true);
                                        
                                        if ($payload === null) {
                                            self::add_debug_log("Payload JSON decode failed");
                                        } else {
                                            self::add_debug_log("Payload JSON decode successful: " . print_r($payload, true));
                                            if (is_array($payload) && isset($payload['issue_identifier'])) {
                                                $item_specific_url = $payload['issue_identifier'];
                                                self::add_debug_log("Found issue_identifier in payload: " . $item_specific_url);
                                            } else {
                                                self::add_debug_log("No issue_identifier found in payload");
                                            }
                                        }
                                    } else {
                                        self::add_debug_log("No issue_identifier or payload found in decoded JSON");
                                    }
                                }
                            } else {
                                self::add_debug_log("Base64 decode failed");
                            }
                        } else {
                            self::add_debug_log("Row key does not match base64 pattern");
                        }
                    } else {
                        self::add_debug_log("No original_url and no row_key available for extraction");
                    }
                }
            } else {
                self::add_debug_log("item_specific_url already has value, skipping extraction");
            }
            
            self::add_debug_log("Final item_specific_url after extraction: " . ($item_specific_url ?? 'NULL'));
            self::add_debug_log("=== ITEM_SPECIFIC_URL EXTRACTION END ===");
            
            // VALIDATION: For broken links, item_specific_url is REQUIRED
            // If we couldn't extract it, FAIL the task instead of storing corrupt data
            if (($api_issue_type === 'broken_links_external' || $api_issue_type === 'broken_links_internal') && empty($item_specific_url)) {
                $error_msg = "FATAL: Cannot process broken link - failed to extract broken URL from API response. This item has corrupt data and will be skipped.";
                self::add_debug_log($error_msg);
                throw new Exception($error_msg);
            }

            // Check if this specific item has already been fixed (safety net)
            // Primary check happens in ajax_get_batch_state to save AI credits
            // This is a secondary check in case something was fixed between batch prep and processing
            if (!empty($page_context_url) && !empty($api_issue_type)) {
                self::add_debug_log("=== SECONDARY ALREADY FIXED CHECK (PROCESSOR) ===");
                $already_fixed = rank_is_issue_already_fixed($api_issue_type, $page_context_url, $item_specific_url);
                self::add_debug_log("Secondary check result: " . ($already_fixed ? 'YES (skipping)' : 'NO (proceeding)'));
                
                if ($already_fixed) {
                    if ($batch_id) {
                        self::add_log_to_transient($batch_id, "✅ Item already fixed - skipping", $row_key, 'info');
                    }
                    self::add_debug_log("=== PROCESSOR SKIPPED - ALREADY FIXED ===");
                    return; // Skip processing this item
                }
            }

            $db_override_type = '';
            $needs_database_update = true; // Flag to determine if update_seo_data should be called

            switch ( $api_issue_type ) {
                case 'missing_meta_description':
                case 'short_meta_description':
                case 'long_meta_description':
                    $db_override_type = 'meta_description';
                    break;
                case 'missing_title_tag':
                case 'short_title_tag':
                case 'long_title_tag':
                    $db_override_type = 'title';
                    break;
                case 'missing_alt_tags':
                    $db_override_type = 'image_alt';
                    break;
                case 'broken_links_external':
                case 'broken_links_internal':
                    self::add_debug_log("=== BROKEN LINKS PROCESSING START ===");
                    $needs_database_update = false; // These will be handled by storing suggestions
                    
                    self::add_debug_log("item_specific_url before fallback extraction: " . ($item_specific_url ?? 'NULL'));
                    
                    // If we still don't have a broken URL but we have a row_key, try to use the row_key itself
                    // as a fallback since it might contain the broken URL in some format
                    if (empty($item_specific_url) && !empty($row_key) && strpos($row_key, 'http') !== false) {
                        self::add_debug_log("Attempting fallback URL extraction from row_key (contains http)");
                        // Extract URL-like string from row_key as a last resort
                        if (preg_match('/https?:\/\/[^\s\)"\']+/', $row_key, $matches)) {
                            $item_specific_url = $matches[0];
                            self::add_debug_log("Fallback extraction successful: " . $item_specific_url);
                        } else {
                            self::add_debug_log("Fallback regex extraction failed");
                        }
                    } else {
                        self::add_debug_log("Skipping fallback extraction - item_specific_url: " . ($item_specific_url ?? 'NULL') . ", row_key contains http: " . (!empty($row_key) && strpos($row_key, 'http') !== false ? 'YES' : 'NO'));
                    }
                    
                    self::add_debug_log("=== BROKEN LINKS STORAGE ATTEMPT ===");
                    self::add_debug_log("page_context_url: " . ($page_context_url ?? 'NULL'));
                    self::add_debug_log("override_value isset: " . (isset($override_value) ? 'YES' : 'NO'));
                    self::add_debug_log("override_value: " . ($override_value ?? 'NULL'));
                    self::add_debug_log("item_specific_url: " . ($item_specific_url ?? 'NULL'));
                    
                    if (!empty($page_context_url) && isset($override_value)) {
                        self::add_debug_log("Storage conditions met, proceeding with storage");
                        
                        // Store the suggestion even if item_specific_url is empty - we'll use a placeholder
                        $broken_url = !empty($item_specific_url) ? $item_specific_url : '[unknown broken URL]';
                        self::add_debug_log("broken_url for storage: " . $broken_url);
                        
                        $suggestion_message = sprintf(
                            "Fix for %s: On page '%s', change link from '%s' to '%s'.",
                            esc_html($api_issue_type),
                            esc_html($page_context_url),
                            esc_html($broken_url), // This is the broken URL or placeholder
                            esc_html($override_value)    // This is the suggested new URL
                        );
                        self::add_debug_log("suggestion_message: " . $suggestion_message);
                        
                        // Get the appropriate option name
                        $option_name = ($api_issue_type === 'broken_links_internal')
                            ? 'rank_internal_url_replacements'
                            : 'rank_external_url_replacements';
                        self::add_debug_log("option_name: " . $option_name);
                        
                        // Get current replacements
                        $replacements = get_option($option_name, []);
                        self::add_debug_log("Current replacements count: " . count($replacements));
                        
                        // If item_specific_url is empty, try to extract it from row_key
                        $actual_broken_url = $item_specific_url;
                        if (empty($actual_broken_url) && !empty($row_key)) {
                            // Try to decode the row_key to extract the issue_identifier
                            self::add_debug_log("item_specific_url is empty, attempting to decode row_key");
                            
                            // Fix base64 padding if needed
                            $row_key_padded = $row_key;
                            $mod = strlen($row_key_padded) % 4;
                            if ($mod) {
                                $row_key_padded .= str_repeat('=', 4 - $mod);
                            }
                            
                            // Don't use strict mode as it may reject valid base64
                            $decoded_base64 = base64_decode($row_key_padded);
                            if ($decoded_base64 === false || empty($decoded_base64)) {
                                self::add_debug_log("Base64 decode failed for row_key");
                            } else {
                                self::add_debug_log("Base64 decoded successfully, attempting JSON decode");
                                $decoded_data = json_decode($decoded_base64, true);
                                
                                if ($decoded_data && isset($decoded_data['payload'])) {
                                    self::add_debug_log("Successfully decoded row_key, extracting payload");
                                    $payload = json_decode($decoded_data['payload'], true);
                                    if ($payload && isset($payload['issue_identifier']) && !empty($payload['issue_identifier'])) {
                                        $actual_broken_url = $payload['issue_identifier'];
                                        self::add_debug_log("Successfully extracted broken URL from row_key: " . $actual_broken_url);
                                    } else {
                                        self::add_debug_log("Failed to extract issue_identifier from payload: " . print_r($payload, true));
                                    }
                                } else {
                                    self::add_debug_log("Failed to decode JSON or payload not found. Decoded data: " . print_r($decoded_data, true));
                                }
                            }
                        } else {
                            self::add_debug_log("Using item_specific_url directly: " . $actual_broken_url);
                        }
                        
                        // Create a unique composite key combining broken URL and context
                        // This ensures each broken URL + page context combination gets its own entry
                        if (!empty($actual_broken_url)) {
                            // Normalize URLs to handle encoding and trailing slashes consistently
                            $normalized_page_url = rank_normalize_url($page_context_url);
                            $normalized_broken_url = rank_normalize_url($actual_broken_url);
                            
                            // Use a composite key: broken_url + context hash
                            $context_hash = md5($normalized_page_url);
                            $key_to_use = $normalized_broken_url . '::context::' . $context_hash;
                            self::add_debug_log("Using composite key for broken URL + context: " . $key_to_use);
                        } else {
                            // Fallback to row_key if we still can't get the broken URL
                            $key_to_use = 'row_key_' . md5($row_key);
                            self::add_debug_log("Using row_key fallback: " . $key_to_use);
                        }
                        
                        // Add or update the replacement
                        $replacements[$key_to_use] = [
                            'new_url' => $override_value,
                            'context' => $page_context_url,
                            'row_key' => $row_key,
                            'broken_url' => $actual_broken_url // Store the actual broken URL (extracted from row_key if needed)
                        ];
                        
                        self::add_debug_log("Replacement data to store: " . print_r($replacements[$key_to_use], true));
                        
                        // Save the updated replacements
                        $update_result = update_option($option_name, $replacements, false);
                        self::add_debug_log("update_option result: " . ($update_result ? 'SUCCESS' : 'FAILED'));
                        self::add_debug_log("New replacements count: " . count($replacements));
                        
                        // Verify the data was stored
                        $verification = get_option($option_name, []);
                        self::add_debug_log("Verification - stored replacements count: " . count($verification));
                        if (isset($verification[$key_to_use])) {
                            self::add_debug_log("Verification - our key exists: YES");
                            self::add_debug_log("Verification - stored data: " . print_r($verification[$key_to_use], true));
                        } else {
                            self::add_debug_log("Verification - our key exists: NO");
                        }
                        
                        if ($batch_id) {
                            self::add_log_to_transient( $batch_id, $suggestion_message, $row_key, 'suggestion' );
                            self::add_debug_log("Added log to transient for batch: " . $batch_id);
                        }
                        
                        self::add_debug_log("=== BROKEN LINKS STORAGE SUCCESS ===");
                    } else {
                        self::add_debug_log("=== BROKEN LINKS STORAGE FAILED - CONDITIONS NOT MET ===");
                        $error_message = "[RANK PROCESSOR ERROR] Missing data for {$api_issue_type} suggestion. PageURL: '{$page_context_url}', BrokenURL: '{$item_specific_url}', SuggestedFix: '{$override_value}'. (Row: {$row_key}, Batch: {$batch_id})";
                        error_log($error_message);
                        self::add_debug_log("Error logged: " . $error_message);
                        
                        if ($batch_id) {
                            self::add_log_to_transient( $batch_id, "Error: Missing data for {$api_issue_type} suggestion.", $row_key, 'error' );
                        }
                    }
                    
                    self::add_debug_log("=== BROKEN LINKS PROCESSING END ===");
                    break;
                default:
                    $log_message = "[RANK PROCESSOR ERROR] Unknown API issue type: '{$api_issue_type}' for Row Key {$row_key}. Cannot map to DB override type.";
                    error_log( $log_message );
                    if ($batch_id) {
                        self::add_log_to_transient( $batch_id, "Processor Error: Unknown API issue type '{$api_issue_type}'.", $row_key, 'error' );
                    }
                    return; // Stop processing if type is unknown
            }

            if ( ! $needs_database_update ) {
                // For types like broken links, the action (recording suggestion) is done.
                // Log success for this item if no errors occurred during suggestion recording.
                // error_log("Rank_Processor: Successfully recorded suggestion for row_key '{$row_key}'.");
                // The main try-catch will handle overall success/failure logging for the task.
                // No specific "return" here, allow to proceed to batch completion checks etc. if any.
                // If add_log_to_transient already indicates success/error, that might be sufficient.
                // For now, we assume the task is "handled" if $needs_database_update is false.
            } else {
                // Proceed with database update logic for meta, title, alt tags
                // Debug logging: URL to Post ID conversion
                self::add_debug_log("=== URL TO POST ID CONVERSION ===");
                self::add_debug_log("Input URL: " . ($page_context_url ?? 'NULL'));
                
                // Check if we already have a post ID from the AJAX handler
                $wp_post_id = isset($task_args['wp_post_id']) ? intval($task_args['wp_post_id']) : 0;
                self::add_debug_log("Existing wp_post_id from task_args: " . $wp_post_id);
                
                // If we don't have a post ID yet, try to get it from the URL
                if ($wp_post_id <= 0 && !empty($page_context_url)) {
                    // First try the standard WordPress function
                    $wp_post_id = url_to_postid($page_context_url);
                    self::add_debug_log("Standard url_to_postid result: " . $wp_post_id);
                    
                    // Apply WPML correction to get the correct language-specific post_id
                    if ($wp_post_id > 0 && function_exists('apply_filters')) {
                        $wpml_post_id = apply_filters('wpml_object_id', $wp_post_id, 'post', true);
                        if ($wpml_post_id && $wpml_post_id != $wp_post_id) {
                            self::add_debug_log("WPML corrected post_id from $wp_post_id to $wpml_post_id");
                            $wp_post_id = $wpml_post_id;
                        }
                    }
                    
                    // If standard function fails, try our enhanced method for multilingual sites
                    if ($wp_post_id <= 0) {
                        $wp_post_id = self::enhanced_url_to_postid($page_context_url);
                        self::add_debug_log("Enhanced url_to_postid result: " . $wp_post_id);
                        
                        // Apply WPML correction to enhanced result as well
                        if ($wp_post_id > 0 && function_exists('apply_filters')) {
                            $wpml_post_id = apply_filters('wpml_object_id', $wp_post_id, 'post', true);
                            if ($wpml_post_id && $wpml_post_id != $wp_post_id) {
                                self::add_debug_log("WPML corrected enhanced post_id from $wp_post_id to $wpml_post_id");
                                $wp_post_id = $wpml_post_id;
                            }
                        }
                    }
                    
                    if ($wp_post_id <= 0) {
                        self::add_debug_log("wp_post_id is 0 - checking if we can proceed with synthetic ID generation");
                        
                        // For certain override types (image_alt, meta_description, title), we can proceed
                        // even with post_id = 0 because update_seo_data() will generate a synthetic ID
                        // based on the original_page_url. This is essential for homepage URLs and
                        // language-specific pages that don't have a traditional post_id.
                        $allowed_types_for_zero_post_id = array('image_alt', 'meta_description', 'title');
                        
                        if (!in_array($db_override_type, $allowed_types_for_zero_post_id, true)) {
                            self::add_debug_log("Rank_Processor: Failed to convert URL to post_id and override type doesn't support synthetic IDs.", 'error');
                            
                            $error_message = "URL does not belong to this WordPress installation (subdirectory mismatch): " . $page_context_url;
                            error_log("Rank_Processor: " . $error_message);
                            
                            if ($batch_id) {
                                self::add_log_to_transient($batch_id, "⚠️ Skipped: " . $error_message, $row_key, 'warning');
                            }
                            
                            return; // Stop processing this URL
                        }
                        
                        // If we have original_page_url, we can proceed - update_seo_data will generate synthetic ID
                        if (empty($page_context_url)) {
                            self::add_debug_log("Rank_Processor: Cannot proceed - no page_context_url for synthetic ID generation.", 'error');
                            
                            if ($batch_id) {
                                self::add_log_to_transient($batch_id, "⚠️ Skipped: Missing page context URL", $row_key, 'warning');
                            }
                            
                            return; // Stop processing if we can't determine the URL
                        }
                        
                        self::add_debug_log("Proceeding with wp_post_id = 0 - synthetic ID will be generated in update_seo_data()");
                    }
                }
                
                self::add_debug_log("Final wp_post_id: " . $wp_post_id);

                $object_id_to_use = 0; // Default for non-image types or if image not found

                if ( $db_override_type === 'image_alt' ) {
                    if ( ! empty( $item_specific_url ) ) {
                        $attachment_id = self::get_attachment_id_from_url( $item_specific_url );
                        if ( $attachment_id > 0 ) {
                            $object_id_to_use = $attachment_id;
                        } else {
                            // For theme images or other non-media library images, use object_id = 0
                            // and rely on the image_url field for lookups
                            $object_id_to_use = 0;
                            error_log("Rank_Processor: No attachment ID found for image. Using URL-based alt tag storage instead.");
                            if ($batch_id) {
                                self::add_log_to_transient( $batch_id, "Notice: Using URL-based storage for " . esc_html(basename($item_specific_url)), $row_key, 'info' );
                            }
                        }
                    } else {
                        error_log("Rank_Processor: Missing item_specific_url for image_alt.");
                         if ($batch_id) {
                            self::add_log_to_transient( $batch_id, "Processor Error: Missing image URL for alt tag.", $row_key, 'error' );
                        }
                        return; // Stop processing if item_specific_url is missing for alt tag
                    }
                } else {
                    // For non-image issues like meta_description, title, object_id is 0.
                    $object_id_to_use = 0;
                }

                // Final check for override_value before DB operation
                if ( $override_value === null ) {
                    error_log("Rank_Processor: Override value is null. Skipping update.");
                    if ($batch_id) {
                        self::add_log_to_transient( $batch_id, "Processor Error: Solution (override value) is missing for {$db_override_type}.", $row_key, 'error' );
                    }
                    return;
                }

                // Debug logging: Database update attempt
                self::add_debug_log("=== DATABASE UPDATE ATTEMPT ===");
                self::add_debug_log("wp_post_id: " . $wp_post_id);
                self::add_debug_log("object_id_to_use: " . $object_id_to_use);
                self::add_debug_log("db_override_type: " . $db_override_type);
                self::add_debug_log("override_value: " . print_r($override_value, true));
                self::add_debug_log("item_specific_url: " . ($item_specific_url ?? 'NULL'));
                self::add_debug_log("page_context_url: " . ($page_context_url ?? 'NULL'));
                self::add_debug_log("api_issue_type: " . ($api_issue_type ?? 'NULL'));
                self::add_debug_log("batch_id: " . ($batch_id ?? 'NULL'));
                self::add_debug_log("row_key: " . ($row_key ?? 'NULL'));
                
                $update_success = self::update_seo_data( $wp_post_id, $object_id_to_use, $db_override_type, $override_value, $api_issue_type, $batch_id, $row_key, $item_specific_url, $page_context_url );

                if ( $update_success ) {
                    // Clear the "already fixed" cache for this item so subsequent checks see the new data
                    if (function_exists('rank_clear_fixed_status_cache')) {
                        rank_clear_fixed_status_cache($api_issue_type, $page_context_url, $item_specific_url);
                    }
                    
                    if ($batch_id) {
                        $success_log_msg = ucfirst($db_override_type) . " updated successfully.";
                        if ($db_override_type === 'image_alt' && $item_specific_url) {
                             $success_log_msg = "Alt tag for image '" . basename($item_specific_url) . "' updated.";
                        }
                        self::add_log_to_transient( $batch_id, $success_log_msg, $row_key, 'success' );
                    }
                } else {
                    // add_log_to_transient is called within update_seo_data on failure
                }
            } // End of if ($needs_database_update) else block

            // Debug logging: Task completion
            self::add_debug_log("=== RANK PROCESSOR END ===");
            self::add_debug_log("Task completed successfully for row_key: " . ($row_key ?? 'NULL'));
            self::add_debug_log("Batch ID: " . ($batch_id ?? 'NULL'));

        } catch (Throwable $e) { // Throwable catches Errors and Exceptions in PHP 7+
            error_log("Rank_Processor: CRITICAL ERROR in run_scheduled_task. Error: " . $e->getMessage());
            if (isset($batch_id) && $batch_id && isset($row_key)) { // Ensure batch_id and row_key are available
                 self::add_log_to_transient( $batch_id, "CRITICAL Processor Error: " . $e->getMessage(), $row_key, 'critical' );
            }
            throw $e; // Re-throw to ensure Action Scheduler marks it as failed and handles retry logic if applicable.
        }
    }

    /**
     * Updates or inserts SEO override data into the custom table.
     * Uses $wpdb->replace for efficiency.
     *
     * @param int         $post_id        The ID of the post.
     * @param int|string  $object_id      Optional. The ID of the specific object (e.g., attachment ID) or relevant string. 0 for post-level overrides.
     * @param string      $override_type  The type of override (e.g., 'title', 'meta_description', 'image_alt').
     * @param string      $override_value The new value for the override.
     * @param string      $issue_type     Optional. The specific issue type (e.g., 'missing_meta_description', 'short_title_tag').
     * @param string      $batch_id       Optional. The batch ID for logging.
     * @param string      $row_key        Optional. The row key for logging.
     * @param string      $image_url      Optional. The URL of the image (for image_alt type).
     * @return bool True on success (insert/replace), false on error.
     */
    public static function update_seo_data( $post_id, $object_id, $override_type, $override_value, $issue_type = null, $batch_id = null, $row_key = null, $image_url = null, $original_page_url = null ) {
        global $wpdb;

        // Debug logging: Entry point of update_seo_data
        self::add_debug_log("=== UPDATE_SEO_DATA START ===");
        self::add_debug_log("Input params - post_id: $post_id, object_id: $object_id, override_type: $override_type");
        self::add_debug_log("override_value: " . print_r($override_value, true));
        self::add_debug_log("issue_type: " . ($issue_type ?? 'NULL'));
        self::add_debug_log("batch_id: " . ($batch_id ?? 'NULL'));
        self::add_debug_log("row_key: " . ($row_key ?? 'NULL'));
        self::add_debug_log("image_url: " . ($image_url ?? 'NULL'));
        self::add_debug_log("original_page_url: " . ($original_page_url ?? 'NULL'));

        // Ensure RANK_TABLE_NAME is defined
        if ( ! defined('RANK_TABLE_NAME') ) {
             self::add_debug_log('RANK Plugin Error: RANK_TABLE_NAME constant not defined in Rank_Processor::update_seo_data.');
             return false;
        }
        $table_name = $wpdb->prefix . RANK_TABLE_NAME;

        // Ensure $post_id is an integer
        $post_id = intval( $post_id );
        if ( $post_id < 0 ) {
            self::add_debug_log("Invalid Post ID for override type $override_type.");
            if ($batch_id) self::add_log_to_transient( $batch_id, "DB Error: Invalid Post ID $post_id for $override_type.", $row_key, 'error' );
            return false;
        }
        
        // For archive pages (post_id = 0), generate synthetic post_id based on URL
        // to prevent different archive pages from overwriting each other
        if ( $post_id === 0 && !empty( $original_page_url ) ) {
            // Use 2 billion as base to differentiate from image synthetic IDs (1 billion)
            // and ensure no conflicts with real post IDs
            $synthetic_post_id = 2000000000 + abs( crc32( $original_page_url ) );
            
            self::add_debug_log("Synthetic post_id generated: $synthetic_post_id for URL: $original_page_url");
            $post_id = $synthetic_post_id;
        }

        // Ensure $object_id is an integer. For non-image types, it should be 0.
        // For image_alt, it should be the attachment_id or a synthetic negative ID.
        $object_id = intval( $object_id );
        
        // For image_alt with no attachment ID (object_id = 0), generate a synthetic positive ID
        // based on the image filename to ensure uniqueness across different images on the same page
        if ( $override_type === 'image_alt' && $object_id === 0 && !empty( $image_url ) ) {
            // Extract the filename from the URL
            $filename = basename( $image_url );
            
            // Generate a positive ID based on the filename
            // Use 1 billion as a base to avoid conflicts with real attachment IDs
            $object_id = 1000000000 + abs( crc32( $filename ) );
        }
        
        // Only validate object_id if it's manually set to an invalid value
        // Real attachment IDs are positive and typically below 1 billion
        // Our synthetic IDs are above 1 billion
        if ( $override_type === 'image_alt' && $object_id <= 0 && empty( $image_url ) ) {
            self::add_debug_log( "Invalid Attachment ID for image_alt." );
            if ( $batch_id ) self::add_log_to_transient( $batch_id, "DB Error: Invalid Attachment ID $object_id for image_alt.", $row_key, 'error' );
            return false;
        }

        // Sanitize override_type (already mapped from api_issue_type)
        $allowed_types = array( 'meta_description', 'title', 'image_alt' );
        if ( ! in_array( $override_type, $allowed_types, true ) ) {
            self::add_debug_log("Invalid Override Type: $override_type.");
            if ($batch_id) self::add_log_to_transient( $batch_id, "DB Error: Invalid override type $override_type.", $row_key, 'error' );
            return false;
        }

        // Sanitize override_value based on type
        $sanitized_value = '';
        $value_format = '%s'; // Default to string
        // Make sure override_value is a string before sanitizing
        if (!is_string($override_value)) {
            if (is_array($override_value)) {
                // Convert array to string if needed
                $override_value = isset($override_value[0]) ? (string)$override_value[0] : '';
            } else {
                // Convert any other type to string
                $override_value = (string)$override_value;
            }
        }
        
        // Sanitize the value
        $sanitized_value = sanitize_text_field($override_value);

        $data = array(
            'post_id'        => $post_id,
            'object_id'      => $object_id, // Now using synthetic positive IDs (>1 billion) for images without attachment IDs
            'override_type'  => $override_type,
            'issue_type'     => $issue_type ?: $override_type, // Use issue_type if provided, otherwise fallback to override_type
            'override_value' => $sanitized_value,
        );
        
        // Store the original page URL if provided
        if (!empty($original_page_url)) {
            $data['original_page_url'] = $original_page_url;
        }
        
        // Always add image_url to data array for image_alt overrides
        if ($override_type === 'image_alt') {
            if (!empty($image_url)) {
                // Store the image URL as-is without encoding special characters
                $data['image_url'] = $image_url;
            } else {
                // Log warning but continue - the image_url is important for URL-based lookups
                self::add_debug_log("Warning: Empty image_url for image_alt override. This may affect frontend retrieval.");
            }
        }

        $format = array(
            '%d', // post_id
            '%d', // object_id
            '%s', // override_type
            '%s', // issue_type
            $value_format, // override_value
        );
        
        // Add format for original_page_url if it's in the data array
        if (isset($data['original_page_url'])) {
            $format[] = '%s'; // original_page_url
        }
        
        // Add format for image_url if it's in the data array
        if (isset($data['image_url'])) {
            $format[] = '%s'; // image_url
        }
        
        // If an override_id exists for this combination, $wpdb->replace will UPDATE it.
        // Otherwise, it will INSERT a new row.
        // This relies on a unique key on (post_id, object_id, override_type).
        // For short_meta_description, log the exact SQL that will be executed
        
        // Debug logging: Database operation preparation
        self::add_debug_log("=== DATABASE OPERATION ===");
        self::add_debug_log("Final data array: " . print_r($data, true));
        self::add_debug_log("Format array: " . print_r($format, true));
        self::add_debug_log("Table name: " . $table_name);
        
        // For alt tags, use image_url AND original_page_url as unique identifiers to prevent overwriting
        if ($override_type === 'image_alt' && isset($data['image_url'])) {
            self::add_debug_log("Using image_alt specific logic");
            // First check if an entry with this exact image_url AND original_page_url already exists
            // This matches the detection logic in rank_is_issue_already_fixed()
            if (!empty($data['original_page_url'])) {
                // Normalize the URL for comparison (same as detection)
                $normalized_url = rank_normalize_url($data['original_page_url']);
                $url_with_slash = $normalized_url . '/';
                
                $existing_id = $wpdb->get_var($wpdb->prepare(
                    "SELECT override_id FROM $table_name
                    WHERE (original_page_url = %s OR original_page_url = %s)
                    AND image_url = %s
                    AND override_type = 'image_alt'",
                    $normalized_url,
                    $url_with_slash,
                    $data['image_url']
                ));
            } else {
                // Fallback to post_id if original_page_url is not available (shouldn't happen)
                self::add_debug_log("Warning: No original_page_url available, falling back to post_id check");
                $existing_id = $wpdb->get_var($wpdb->prepare(
                    "SELECT override_id FROM $table_name
                    WHERE image_url = %s AND override_type = 'image_alt' AND post_id = %d",
                    $data['image_url'],
                    $post_id
                ));
            }
            
            self::add_debug_log("Existing ID check result: " . ($existing_id ?? 'NULL'));
            if (!empty($data['original_page_url'])) {
                self::add_debug_log("Checked using original_page_url: " . $data['original_page_url']);
            } else {
                self::add_debug_log("Checked using post_id: " . $post_id);
            }
            
            if ($existing_id) {
                // Update existing entry
                self::add_debug_log("Updating existing entry with ID: " . $existing_id);
                $where = array('override_id' => $existing_id);
                $where_format = array('%d');
                $result = $wpdb->update($table_name, $data, $where, $format, $where_format);
            } else {
                // Insert new entry
                self::add_debug_log("Inserting new image_alt entry");
                $result = $wpdb->insert($table_name, $data, $format);
            }
        } else {
            // For non-image entries (meta_description, title), also check by original_page_url
            // to prevent overwriting different language versions
            self::add_debug_log("Using meta/title specific logic");
            
            if (!empty($data['original_page_url'])) {
                // Normalize the URL for comparison (same as detection)
                $normalized_url = rank_normalize_url($data['original_page_url']);
                $url_with_slash = $normalized_url . '/';
                
                $existing_id = $wpdb->get_var($wpdb->prepare(
                    "SELECT override_id FROM $table_name
                    WHERE (original_page_url = %s OR original_page_url = %s)
                    AND override_type = %s",
                    $normalized_url,
                    $url_with_slash,
                    $data['override_type']
                ));
                
                self::add_debug_log("Existing ID check result: " . ($existing_id ?? 'NULL'));
                self::add_debug_log("Checked using original_page_url: " . $data['original_page_url']);
                
                if ($existing_id) {
                    // Update existing entry
                    self::add_debug_log("Updating existing entry with ID: " . $existing_id);
                    $where = array('override_id' => $existing_id);
                    $where_format = array('%d');
                    $result = $wpdb->update($table_name, $data, $where, $format, $where_format);
                } else {
                    // Insert new entry
                    self::add_debug_log("Inserting new meta/title entry");
                    $result = $wpdb->insert($table_name, $data, $format);
                }
            } else {
                // Fallback to REPLACE if no original_page_url (shouldn't happen with current code)
                self::add_debug_log("Warning: No original_page_url, using REPLACE fallback");
                $result = $wpdb->replace($table_name, $data, $format);
            }
        }
        
        // Debug logging: Database operation results
        self::add_debug_log("Database operation result: " . print_r($result, true));
        self::add_debug_log("WPDB last error: " . ($wpdb->last_error ?? 'NULL'));
        self::add_debug_log("WPDB last query: " . ($wpdb->last_query ?? 'NULL'));
        if ($result !== false) {
            self::add_debug_log("WPDB insert_id: " . $wpdb->insert_id);
        }

        if ( false === $result ) {
            $error_msg = "Failed to update/insert SEO data. WPDB Error: " . $wpdb->last_error;
            self::add_debug_log($error_msg);
            return false;
        }
        
        
        return true;
    }

    /**
     * Attempt to find the attachment ID for a given URL.
     *
     * @param string $url The URL of the attachment.
     * @return int The attachment ID, or 0 if not found.
     */
    public static function get_attachment_id_from_url( $url ) {
        global $wpdb;
        $attachment_id = 0;

        if ( empty( $url ) ) {
            return 0;
        }

        // Try the most reliable WordPress function first
        $attachment_id = attachment_url_to_postid( $url );
        if ( $attachment_id > 0 ) {
            return $attachment_id;
        }

        // Fallback logic if attachment_url_to_postid fails

        $upload_dir_paths = wp_upload_dir();
        if ( !isset($upload_dir_paths['baseurl']) ) {
            return 0;
        }
        $upload_baseurl = $upload_dir_paths['baseurl'];

        // Check if the FULL URL starts with the upload baseurl
        if ( strpos( $url, $upload_baseurl ) === 0 ) {
            // $relative_path is path relative to the uploads directory.
            // Example: $url = 'https://example.com/wp-content/uploads/2023/01/image.jpg'
            // $upload_baseurl = 'https://example.com/wp-content/uploads'
            // str_replace result: '/2023/01/image.jpg'
            // ltrim result: '2023/01/image.jpg'
            $relative_path = ltrim( str_replace( $upload_baseurl, '', $url ), '/' );

            if (empty($relative_path)) { // Should not happen if strpos matched and baseurl is not the full url
                 return 0;
            }

            $filename = basename( $relative_path );
            $directory = dirname( $relative_path );

            if ($directory === '.' || $directory === '') {
                $directory = '';
            } else {
                $directory .= '/';
            }

            // Use the exact relative path without normalization
            $sql_like_relative_path = '%' . $wpdb->esc_like( $relative_path );
            $attachment_id = (int) $wpdb->get_var( $wpdb->prepare(
                "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'attachment' AND guid LIKE %s",
                $sql_like_relative_path
            ) );
        } else {
        }
        
        if ($attachment_id > 0) {
        } else {
        }

        return $attachment_id;
    }
    
    /**
     * Enhanced URL to Post ID conversion that handles multilingual sites and edge cases
     * that the standard WordPress url_to_postid() function might miss.
     *
     * @param string $url The URL to convert to a post ID
     * @return int The post ID, or 0 if not found
     */
    public static function enhanced_url_to_postid( $url ) {
        global $wpdb;
        
        // If empty URL, return 0
        if ( empty( $url ) ) {
            return 0;
        }
        
        // Parse the URL to get its components
        $url_parts = parse_url( $url );
        if ( !isset( $url_parts['path'] ) ) {
            return 0;
        }
        
        // Get the path and remove trailing slash
        $path = rtrim( $url_parts['path'], '/' );
        
        // Handle multilingual URLs (e.g., /de/, /fr/, etc.)
        $path_parts = explode( '/', trim( $path, '/' ) );
        
        // Check if the first part is a language code (2 characters)
        $language_prefixes = array( 'de', 'fr', 'es', 'it', 'nl', 'da', 'sv', 'no', 'fi', 'pt', 'ru', 'pl', 'ja', 'zh', 'ko', 'ar' );
        $has_language_prefix = false;
        
        if ( count( $path_parts ) > 0 && strlen( $path_parts[0] ) === 2 && in_array( $path_parts[0], $language_prefixes ) ) {
            $has_language_prefix = true;
            // Remove language prefix for the query
            array_shift( $path_parts );
            $path_without_language = '/' . implode( '/', $path_parts );
        } else {
            $path_without_language = $path;
        }
        
        // Try to find post by path
        $sql = $wpdb->prepare(
            "SELECT ID FROM $wpdb->posts
            WHERE post_status = 'publish'
            AND (post_name = %s OR post_name = %s)
            LIMIT 1",
            trim( $path, '/' ),
            trim( $path_without_language, '/' )
        );
        
        $post_id = $wpdb->get_var( $sql );
        
        if ( $post_id ) {
            return (int) $post_id;
        }
        
        // If not found, try to match against the guid field
        $site_url = site_url();
        $sql = $wpdb->prepare(
            "SELECT ID FROM $wpdb->posts
            WHERE post_status = 'publish'
            AND guid = %s
            LIMIT 1",
            esc_url_raw( $url )
        );
        
        $post_id = $wpdb->get_var( $sql );
        
        if ( $post_id ) {
            return (int) $post_id;
        }
        
        // If still not found, try to match against post meta (for custom permalinks)
        $sql = $wpdb->prepare(
            "SELECT post_id FROM $wpdb->postmeta
            WHERE meta_key = '_wp_page_template'
            AND meta_value LIKE %s
            LIMIT 1",
            '%' . $wpdb->esc_like( basename( $path ) ) . '%'
        );
        
        $post_id = $wpdb->get_var( $sql );
        
        if ( $post_id ) {
            return (int) $post_id;
        }
        
        // Last resort: try to match against the slug in the database
        $slug = basename( $path );
        $sql = $wpdb->prepare(
            "SELECT ID FROM $wpdb->posts
            WHERE post_status = 'publish'
            AND post_name = %s
            LIMIT 1",
            $slug
        );
        
        $post_id = $wpdb->get_var( $sql );
        
        return (int) $post_id;
    }

    /**
     * Adds a log entry to a transient for batch processing feedback.
     *
     * @param string $batch_id   The batch ID to associate the log with.
     * @param string $message    The log message.
     * @param string $row_key    A unique identifier for the specific task.
     * @param string $status     The status of the log entry (success, error, warning, info, critical, suggestion).
     * @return bool              True if the log was added, false otherwise.
     */
    public static function add_log_to_transient($batch_id, $message, $row_key, $status = 'info') {
        if (empty($batch_id) || empty($message)) {
            return false;
        }

        $transient_key = 'rank_batch_log_' . $batch_id;
        $current_logs = get_transient($transient_key);
        
        if (!is_array($current_logs)) {
            $current_logs = array();
        }

        // Add the new log entry
        $current_logs[] = array(
            'time' => current_time('mysql'),
            'message' => $message,
            'row_key' => $row_key,
            'status' => $status
        );

        // Store for 24 hours (86400 seconds)
        return set_transient($transient_key, $current_logs, 86400);
    }

    /**
     * Check if the SEO overrides table exists in the database.
     *
     * @return bool True if the table exists, false otherwise
     */
    public static function check_table_exists() {
        global $wpdb;
        
        if (!defined('RANK_TABLE_NAME')) {
            return false;
        }
        
        $table_name = $wpdb->prefix . RANK_TABLE_NAME;
        $result = $wpdb->get_var("SHOW TABLES LIKE '$table_name'");
        
        return $result === $table_name;
    }

    /**
     * Check if Action Scheduler is properly configured and running.
     *
     * @return array Status information about Action Scheduler
     */
    public static function check_action_scheduler() {
        $status = [
            'exists' => class_exists('ActionScheduler'),
            'hooks_registered' => false
        ];
        
        if ($status['exists']) {
            // Check if our hooks are registered
            $status['hooks_registered'] = has_action('rank_process_single_item_hook') &&
                                         has_action('rank_process_single_url');
        }
        
        return $status;
    }

    /**
     * Check PHP version.
     *
     * @return array PHP version information
     */
    public static function check_php_version() {
        $current_version = phpversion();
        $recommended_version = '8.0.0';
        
        return [
            'current' => $current_version,
            'recommended' => $recommended_version,
            'is_recommended' => version_compare($current_version, $recommended_version, '>=')
        ];
    }

    /**
     * Perform a comprehensive system status check.
     *
     * @return array Status information about various system components
     */
    public static function system_status_check() {
        $status = [
            'table_exists' => self::check_table_exists(),
            'action_scheduler' => self::check_action_scheduler(),
            'php_version' => self::check_php_version(),
        ];
        
        return $status;
    }

    /**
     * Add a debug log entry to a custom log file
     *
     * @param string $message The message to log
     * @param string $level Log level (info, error, warning)
     */
    public static function add_debug_log($message, $level = 'info') {
        // Check if debug logging is enabled
        if (!get_option('rank_debug_enabled', false)) {
            return; // Skip logging if disabled
        }
        
        // Create logs directory if it doesn't exist
        $logs_dir = RANK_PLUGIN_DIR . 'logs/';
        if (!file_exists($logs_dir)) {
            wp_mkdir_p($logs_dir);
        }
        
        // Create .htaccess to protect the logs directory
        $htaccess_file = $logs_dir . '.htaccess';
        if (!file_exists($htaccess_file)) {
            $htaccess_content = "Order deny,allow\nDeny from all\n";
            file_put_contents($htaccess_file, $htaccess_content);
        }
        
        // Log file path
        $log_file = $logs_dir . 'rank-debug.log';
        
        // Format the log entry
        $timestamp = current_time('Y-m-d H:i:s');
        $formatted_message = "[{$timestamp}] [{$level}] {$message}" . PHP_EOL;
        
        // Write to log file (append mode)
        file_put_contents($log_file, $formatted_message, FILE_APPEND | LOCK_EX);
        
        // Keep log file size manageable (max 200KB)
        if (file_exists($log_file) && filesize($log_file) > 200 * 1024) {
            // Read last 100KB and rewrite file
            $content = file_get_contents($log_file);
            $content = substr($content, -100 * 1024); // Keep last 100KB
            // Find first complete line
            $first_newline = strpos($content, "\n");
            if ($first_newline !== false) {
                $content = substr($content, $first_newline + 1);
            }
            file_put_contents($log_file, $content, LOCK_EX);
        }
    }

    /**
     * Get debug logs from the custom log file
     *
     * @param int $lines Number of lines to retrieve (default: 200)
     * @return array Array with 'logs' and 'file_info'
     */
    public static function get_debug_logs($lines = 200) {
        $log_file = RANK_PLUGIN_DIR . 'logs/rank-debug.log';
        
        if (!file_exists($log_file)) {
            return array(
                'logs' => '',
                'file_info' => array(
                    'exists' => false,
                    'size' => 0,
                    'lines' => 0
                )
            );
        }
        
        // Read the file
        $content = file_get_contents($log_file);
        $all_lines = explode("\n", $content);
        
        // Get last N lines
        $log_lines = array_slice($all_lines, -$lines);
        $logs = implode("\n", $log_lines);
        
        return array(
            'logs' => $logs,
            'file_info' => array(
                'exists' => true,
                'size' => filesize($log_file),
                'lines' => count($all_lines),
                'lines_shown' => count($log_lines)
            )
        );
    }

}