<?php
/**
 * Handles AJAX requests for the RANK AI plugin.
 */

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

class Rank_Ajax {

    /**
     * Initialize AJAX hooks.
     */
    public static function init() {
        // Debug logs AJAX handler
        add_action( 'wp_ajax_rank_get_debug_logs', array( __CLASS__, 'get_debug_logs' ) );
        // Action hooks for the API-based workflow
        add_action( 'wp_ajax_rank_start_processing', array( __CLASS__, 'ajax_start_processing' ) );
        add_action( 'wp_ajax_rank_delete_all_data', array( __CLASS__, 'ajax_delete_all_data' ) );
        add_action( 'wp_ajax_rank_get_fixes_data', array( __CLASS__, 'ajax_get_fixes_data' ) );
        add_action( 'wp_ajax_rank_get_all_fix_counts', array( __CLASS__, 'ajax_get_all_fix_counts' ) );
        add_action( 'wp_ajax_rank_prepare_health_issues', array( __CLASS__, 'ajax_prepare_health_issues' ) );
        add_action( 'wp_ajax_rank_get_batch_state', array( __CLASS__, 'ajax_get_batch_state' ) );
        add_action( 'wp_ajax_rank_record_decision', array( __CLASS__, 'ajax_record_decision' ) );
        add_action( 'wp_ajax_rank_schedule_tasks', array( __CLASS__, 'ajax_schedule_tasks' ) );
        add_action( 'wp_ajax_rank_get_health_issues_overview_optimized', array( __CLASS__, 'ajax_get_health_issues_overview_optimized' ) );
        add_action( 'wp_ajax_rank_clear_pending_decisions', array( __CLASS__, 'ajax_clear_pending_decisions' ) );
        add_action( 'wp_ajax_rank_update_fix_value', array( __CLASS__, 'ajax_update_fix_value' ) );
        add_action( 'wp_ajax_rank_check_already_fixed', array( __CLASS__, 'ajax_check_already_fixed' ) );
        add_action( 'wp_ajax_rank_toggle_debug', array( __CLASS__, 'ajax_toggle_debug' ) );
        add_action( 'wp_ajax_rank_export_category_csv', array( __CLASS__, 'ajax_export_category_csv' ) );
        
        
    }

    /**
     * Set up common AJAX request handling parameters
     *
     * @param int $timeout Optional timeout in seconds
     * @param bool $abort_on_disconnect Whether to stop execution when client disconnects (default: false)
     */
    private static function setup_ajax_request($timeout = 60, $abort_on_disconnect = false) {
        // Tell PHP to stop execution when the client disconnects if specified
        if ($abort_on_disconnect !== null) {
            ignore_user_abort($abort_on_disconnect);
        }
        
        // Set a reasonable timeout for this specific request if specified
        if ($timeout !== null) {
            set_time_limit($timeout);
        }
        
        // Close the session to prevent blocking
        if (session_status() === PHP_SESSION_ACTIVE) {
            session_write_close();
        }
    }

    /**
     * Verify nonce and user permissions for AJAX requests
     *
     * @param string $nonce_key The nonce key to verify (default: 'rank_ajax_nonce')
     * @param bool $require_admin Whether to require admin capabilities (default: true)
     * @return bool|WP_Error True if authorized, WP_Error otherwise
     */
    private static function verify_ajax_request($nonce_key = 'rank_ajax_nonce', $require_admin = true) {
        // Verify nonce
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), $nonce_key)) {
            return new WP_Error('invalid_nonce', 'Nonce verification failed.', array('status' => 403));
        }
        
        // Check user permissions if required
        if ($require_admin && !current_user_can('manage_options')) {
            return new WP_Error('insufficient_permissions', 'Permission denied.', array('status' => 403));
        }
        
        return true;
    }

    /**
     * Validate batch ID from request
     *
     * @param string $batch_id The batch ID to validate
     * @return bool|WP_Error True if valid, WP_Error otherwise
     */
    private static function validate_batch_id($batch_id) {
        if (empty($batch_id)) {
            return new WP_Error('missing_batch_id', 'Missing batch ID.', array('status' => 400));
        }
        
        if (strpos($batch_id, 'rank_batch_') !== 0) {
            return new WP_Error('invalid_batch_id', 'Invalid batch ID format.', array('status' => 400));
        }
        
        return true;
    }

    /**
     * Validate URL existence and check for redirects
     *
     * @param string $url The URL to validate
     * @param array $batch_data Reference to batch data for logging
     * @param string $row_key The current row key for decisions
     * @param array $new_ui_logs Optional array to store new UI logs
     * @return bool|array True if valid, array with error details otherwise
     */
    private static function validate_url($url, &$batch_data, $row_key, &$new_ui_logs = array()) {
        if (empty($url)) {
            return array(
                'valid' => false,
                'message' => 'Empty URL provided.'
            );
        }
        
        $url_validation = rank_validate_url($url);
        
        if (!$url_validation['exists']) {
            $log_message = '';
            
            if ($url_validation['redirects']) {
                $log_message = "🔄 Skipping URL as it redirects to another location: " . $url;
                $batch_data['decisions'][$row_key] = array(
                    'approved' => false,
                    'error'    => true,
                    'message'  => 'URL redirects to another location'
                );
            } else {
                $log_message = "🐞 Skipping URL as it does not exist on our website: " . $url;
                $batch_data['decisions'][$row_key] = array(
                    'approved' => false,
                    'error'    => true,
                    'message'  => 'URL does not exist on this WordPress site'
                );
            }
            
            $batch_data['ui_logs'][] = $log_message;
            if (is_array($new_ui_logs)) {
                $new_ui_logs[] = $log_message;
            }
            
            return array(
                'valid' => false,
                'message' => $log_message
            );
        }
        
        return true;
    }

    /**
     * Log error message to error_log with consistent format
     *
     * @param string $method The method where the error occurred
     * @param string $message The error message
     * @param mixed $context Optional additional context (error code, data, etc.)
     */
    private static function log_error($method, $message, $context = null) {
        $log_message = sprintf('RANK AJAX (%s): %s', $method, $message);
        
        if ($context !== null) {
            if (is_array($context) || is_object($context)) {
                $log_message .= ' | Context: ' . json_encode($context);
            } else {
                $log_message .= ' | Context: ' . $context;
            }
        }
        
        error_log($log_message);
    }
    
    /**
     * Send standardized JSON error response and log the error
     *
     * @param string $message Error message
     * @param int $status HTTP status code
     * @param array $data Additional data to include in response
     * @param string $method Optional method name for logging
     */
    private static function send_error($message, $status = 400, $data = array(), $method = null) {
        // Log the error
        if ($method !== null) {
            self::log_error($method, $message, ['status' => $status, 'data' => $data]);
        }
        
        // Send error response
        $response = array_merge(array('message' => $message), $data);
        wp_send_json_error($response, $status);
    }

    /**
     * Send standardized JSON success response
     *
     * @param string $message Success message
     * @param array $data Additional data to include in response
     */
    private static function send_success($message, $data = array()) {
        $response = array_merge(array('message' => $message), $data);
        wp_send_json_success($response);
    }
    
    /**
     * Get page URL and display path from issue data
     *
     * @param array $issue_item_data The issue data containing page information
     * @return array Array with 'page_url' and 'page_path' keys
     */
    private static function get_page_details_from_issue($issue_item_data) {
        $api_page_id = $issue_item_data['page_id'] ?? ($issue_item_data['page']['id'] ?? null);
        $wp_page_url = null;
        $wp_page_path_display = 'N/A';

        if ($api_page_id && class_exists('Rank_API_Helper') && method_exists('Rank_API_Helper', 'get_page_details_from_api_id')) {
            $page_details = Rank_API_Helper::get_page_details_from_api_id($api_page_id);
            if ($page_details && !empty($page_details['url'])) {
                $wp_page_url = $page_details['url'];
                $parsed_url = parse_url($wp_page_url);
                $wp_page_path_display = $parsed_url['path'] ?? $wp_page_url;
            }
        }
        
        return array(
            'page_url' => $wp_page_url,
            'page_path' => $wp_page_path_display
        );
    }
    
    /**
     * Build UI display string for an issue based on its type
     *
     * @param string $batch_api_issue_type The issue type for the batch
     * @param string $wp_page_path_display The page path for display
     * @param string $item_identifier_from_issue The item identifier (e.g., image URL)
     * @param string $key The row key for fallback display
     * @return string The formatted UI display string
     */
    private static function build_ui_display_string($batch_api_issue_type, $wp_page_path_display, $item_identifier_from_issue, $key) {
        if ($batch_api_issue_type === 'missing_alt_tags') {
            return ($wp_page_path_display !== 'N/A' ? esc_html($wp_page_path_display) . ' ' : '') . 'Image: ' . esc_html($item_identifier_from_issue ?? 'N/A');
        } elseif ($batch_api_issue_type === 'missing_meta_description') {
            return (esc_html($wp_page_path_display) ?? 'Page') . ' Missing meta description';
        } elseif ($batch_api_issue_type === 'missing_title_tag') {
            return (esc_html($wp_page_path_display) ?? 'Page') . ' Missing title tag';
        } else {
            // Generic display for other types
            $page_display_part = $wp_page_path_display !== 'N/A' ? esc_html($wp_page_path_display) : 'Item';
            $item_display_part = $item_identifier_from_issue ? esc_html($item_identifier_from_issue) : "Details for " . $key;
            return $page_display_part . ($item_identifier_from_issue ? ': ' . $item_display_part : '');
        }
    }
    
    /**
     * Get mapping between frontend category keys and database override types
     *
     * @return array Associative array mapping frontend keys to database types
     */
    private static function get_category_db_mapping() {
        return [
            // Meta description mappings
            'missing_meta_description' => 'meta_description',
            'short_meta_description'   => 'meta_description',
            'long_meta_description'    => 'meta_description',
            
            // Title tag mappings
            'missing_title_tag'        => 'title',
            'short_title_tag'          => 'title',
            'long_title_tag'           => 'title',
            
            // Image alt tag mappings
            'missing_alt_tags'         => 'image_alt',
            'image_alt'                => 'image_alt'
        ];
    }
    
    /**
     * Get mapping between frontend category keys and option names
     *
     * @return array Associative array mapping frontend keys to option names
     */
    private static function get_category_option_mapping() {
        return [
            // Internal links mappings
            'broken_internal_links'    => 'rank_internal_url_replacements',
            'broken_links_internal'    => 'rank_internal_url_replacements',
            
            // External links mappings
            'broken_external_links'    => 'rank_external_url_replacements',
            'broken_links_external'    => 'rank_external_url_replacements'
        ];
    }

    /**
     * Normalize URL for search comparison
     *
     * @param string $url The URL to normalize
     * @return string The normalized URL
     */
    private static function normalize_url_for_search($url) {
        if (empty($url)) {
            return '';
        }
        
        // Remove protocol and domain
        $normalized = preg_replace('/^https?:\/\/[^\/]+/', '', $url);
        
        // Ensure starts with /
        if (!empty($normalized) && $normalized[0] !== '/') {
            $normalized = '/' . $normalized;
        }
        
        // Remove trailing slash for consistency
        $normalized = rtrim($normalized, '/') ?: '/';
        
        return strtolower($normalized);
    }

    /**
     * AJAX handler for scheduling tasks from batch data.
     */
    public static function ajax_schedule_tasks() {
        // Set up AJAX request parameters
        self::setup_ajax_request();
        
        // Verify nonce and permissions
        $auth_check = self::verify_ajax_request();
        if (is_wp_error($auth_check)) {
            self::send_error($auth_check->get_error_message(), $auth_check->get_error_data('status'));
            return;
        }

        // Get and validate batch ID
        $batch_id = isset($_POST['batch_id']) ? sanitize_key($_POST['batch_id']) : '';
        $batch_validation = self::validate_batch_id($batch_id);
        if (is_wp_error($batch_validation)) {
            self::send_error($batch_validation->get_error_message(), $batch_validation->get_error_data('status'));
            return;
        }

        $batch_data = get_option( $batch_id );

        if ( false === $batch_data || ! is_array( $batch_data ) || !isset($batch_data['issues']) || empty( $batch_data['issues'] ) || !isset($batch_data['issue_type']) ) {
             self::log_error('ajax_schedule_tasks', 'Batch data not found, invalid, or empty');
             self::send_error('Batch data not found, invalid, or empty. Please prepare the batch again.', 400, [], 'ajax_schedule_tasks');
             return;
        }

        $issues = $batch_data['issues'];
        $issue_type = $batch_data['issue_type']; // Already checked isset above
        $action_group = 'rank-tasks-' . $batch_id;

        delete_transient( 'rank_log_' . $batch_id );

        $scheduled_count = 0;
        $skipped_count = 0; // Add counter for skipped rows
        
        foreach ( $issues as $index => $issue ) {
            // Skip issues that don't have decisions or were declined
            $row_key = $issue['rank_id']; // rank_id is the unique key for an issue in the batch
            if (!isset($batch_data['decisions'][$row_key]) ||
                !$batch_data['decisions'][$row_key]['approved']) {
                $skipped_count++;
                continue;
            }
            
            $decision = $batch_data['decisions'][$row_key];
            
            // Log the issue being processed

            // Validate the essential data exists
            // Page ID is crucial for context.
            if (!isset($issue['page_id']) || empty($issue['page_id'])) {
                $skipped_count++;
                continue;
            }
            
            // Extract page URL from the issue data (same logic as in ajax_get_batch_state)
            $api_page_full_url = $issue['page']['path_full'] ?? null;
            $page_context_url = $api_page_full_url ? esc_url($api_page_full_url) : null;
            
            // The specific URL of the item to be fixed (e.g., image URL)
            // Prefer 'issue_identifier' as it's directly tied to the issue, fallback to 'parameters.url'
            $original_item_url = $issue['issue_identifier'] ?? ($issue['parameters']['url'] ?? null);
            if (empty($original_item_url)) {
                $skipped_count++;
                continue;
            }
            // Store the URL as-is without encoding to preserve special characters
            $original_item_url = trim($original_item_url);


            $solution = isset($decision['solution']) ? trim($decision['solution']) : null;
            
            // Skip if solution is empty
            if (empty($solution)) {
                $skipped_count++;
                continue;
            }

            // If we reach here, validation passed for this issue
            try {
                $task_args = array(
                    'page_id'           => $issue['page_id'], // Contextual page ID
                    'object_id'         => $issue['page_id'], // Initial object_id, Rank_Processor will refine for images
                    'issue_type'        => $issue_type,       // The overall issue type for the batch
                    'solution'          => $solution,
                    'original_url'      => $original_item_url, // URL of the specific item (e.g. image)
                    'page_context_url'  => $page_context_url, // URL of the page where the issue was found
                    'row_key'           => $row_key,
                    'batch_id'          => $batch_id,
                );


                $action_id = as_enqueue_async_action(
                    'rank_process_single_item_hook', // Use the same hook as ajax_record_decision
                    array( $task_args ),             // Pass arguments as a single array
                    $action_group                    // Action group
                );

                if ( $action_id ) {
                    $scheduled_count++;
                } else {
                    $skipped_count++; // Consider it skipped if scheduling failed
                    // Failed to schedule task
                }
            } catch ( Exception $e ) {
                $skipped_count++;
                // Error scheduling task
            }
        }

        // Response message
        if ( $scheduled_count > 0 ) {
            $message = $scheduled_count . ' tasks scheduled successfully.';
            if ($skipped_count > 0) {
                $message .= ' ' . $skipped_count . ' items were skipped (check PHP error logs for details).';
            }
            wp_send_json_success( array( 'message' => $message ) );
        } else {
             $message = 'No tasks were scheduled.';
             if ($skipped_count > 0) {
                 $message .= ' All ' . $skipped_count . ' items were skipped (check PHP error logs for details).';
             } else {
                 $message .= ' The batch might be empty or contain no approved fixes.';
             }
             self::log_error('ajax_schedule_tasks', $message);
             self::send_error($message, 400, [], 'ajax_schedule_tasks');
             return;
        }
    }

    /**
     * AJAX handler for deleting all override data (custom table and URL replacement option).
     */
    public static function ajax_delete_all_data() {
        // Verify nonce and permissions
        $auth_check = self::verify_ajax_request();
        if (is_wp_error($auth_check)) {
            self::send_error($auth_check->get_error_message(), $auth_check->get_error_data('status'), [], 'ajax_start_processing');
            return;
        }

        global $wpdb;
        $table_name = $wpdb->prefix . RANK_TABLE_NAME;
        $table_truncated = false;
        $options_deleted = true; // Assume success unless one fails
        $db_error = ''; // Initialize error variable

        // Attempt to truncate the custom table
        // Ensure RANK_TABLE_NAME is defined
        if ( defined('RANK_TABLE_NAME') && !empty(RANK_TABLE_NAME) ) { // Basic check
            $result = $wpdb->query( "TRUNCATE TABLE `$table_name`" );
            if ( $result !== false ) {
                $table_truncated = true;
            } else {
                $db_error = $wpdb->last_error; // Capture error before next operation
            }
        } else {
             $db_error = 'RANK_TABLE_NAME constant not defined or invalid.';
             // Error in delete_all_overrides
        }

        // Initialize options_deleted as true (assume success)
        $options_deleted = true;
        
        // For internal URL replacements
        $internal_option = get_option('rank_internal_url_replacements', false);
        if ($internal_option !== false) {
            // Option exists, try to delete it
            if (!delete_option('rank_internal_url_replacements')) {
                $options_deleted = false;
                // Failed to delete rank_internal_url_replacements option
            }
        }
        
        // For external URL replacements
        $external_option = get_option('rank_external_url_replacements', false);
        if ($external_option !== false) {
            // Option exists, try to delete it
            if (!delete_option('rank_external_url_replacements')) {
                $options_deleted = false;
                // Failed to delete rank_external_url_replacements option
            }
        }

        if ( $table_truncated && $options_deleted ) {
            wp_send_json_success( array( 'message' => 'All RANK AI data deleted successfully.' ) );
        } else {
            $error_message = 'Error deleting data.';
            if (!$table_truncated) {
                $error_message .= ' Failed to truncate table.';
                if ($db_error) {
                     $error_message .= ' DB Error: ' . $db_error;
                }
            }
            if (!$options_deleted) {
                 $error_message .= ' Failed to delete one or more URL replacement options.';
            }
            self::log_error('ajax_delete_all_data', $error_message);
            self::send_error($error_message, 500, [], 'ajax_delete_all_data');
            return;
        }
    }

    /**
     * AJAX handler to initiate the processing for a given batch ID.
     * This confirms the batch is ready and allows the frontend to start polling for status.
     */
    public static function ajax_start_processing() {
        // Set up AJAX request parameters
        self::setup_ajax_request();
        
        // Verify nonce and permissions
        $auth_check = self::verify_ajax_request();
        if (is_wp_error($auth_check)) {
            self::send_error($auth_check->get_error_message(), $auth_check->get_error_data('status'));
            return;
        }

        // Get and validate batch ID
        $batch_id = isset($_POST['batch_id']) ? sanitize_key($_POST['batch_id']) : '';
        $batch_validation = self::validate_batch_id($batch_id);
        if (is_wp_error($batch_validation)) {
            self::send_error($batch_validation->get_error_message(), $batch_validation->get_error_data('status'));
            return;
        }
        
        // Verify batch exists
        $batch_data = get_option( $batch_id );
        if (!$batch_data) {
            self::log_error('ajax_start_processing', 'Batch not found: ' . $batch_id);
            self::send_error('Batch not found.', 404, [], 'ajax_start_processing');
            return;
        }

        // If checks pass, send success to allow JS polling to begin
        wp_send_json_success( 'Processing initiated. Status checks will begin.' );
    }

    /**
     * AJAX handler to fetch the total count of applied fixes for ALL categories.
     */
    public static function ajax_get_all_fix_counts() {
        // Verify nonce and permissions
        $auth_check = self::verify_ajax_request();
        if (is_wp_error($auth_check)) {
            self::send_error($auth_check->get_error_message(), $auth_check->get_error_data('status'), [], 'ajax_get_fixes_data');
            return;
        }

        $categories = rank_get_health_check_categories();
        $counts = [];
        global $wpdb;
        $table_name = $wpdb->prefix . RANK_TABLE_NAME;

        // Get mappings between frontend keys and database/option types
        $db_category_map = self::get_category_db_mapping();
        $option_name_map = self::get_category_option_mapping();

        // Define which categories use the DB vs Options
        $db_override_keys = array_keys($db_category_map);
        $option_override_keys = array_keys($option_name_map);

        foreach ( array_keys( $categories ) as $key ) {
            if ( in_array( $key, $db_override_keys ) ) {
                // Get the corresponding database type from the map
                $db_override_type = isset($db_category_map[$key]) ? $db_category_map[$key] : null;

                if ($db_override_type) {
                    // Get count from Custom Table using the mapped type AND issue_type
                    $count = $wpdb->get_var( $wpdb->prepare(
                        "SELECT COUNT(*) FROM `$table_name` WHERE override_type = %s AND (issue_type = %s OR (issue_type IS NULL AND %s = %s))",
                        $db_override_type,
                        $key,
                        $key,
                        $db_override_type
                    ) );
                    $counts[ $key ] = ( $count !== null ) ? absint( $count ) : 0;
                } else {
                     $counts[ $key ] = 0;
                }

            } elseif ( in_array( $key, $option_override_keys ) || isset($option_name_map[$key]) ) {
                // Get count from Options Table
                $option_name = isset($option_name_map[$key]) ? $option_name_map[$key] : null;
                
                if ($option_name) {
                    $rules = get_option( $option_name, [] );
                    $count = is_array( $rules ) ? count( $rules ) : 0;
                    
                    // Store the count under both the frontend key and the option key
                    $counts[ $key ] = $count;
                    
                    // If this is a broken_links_* key, also store under broken_*_links for backwards compatibility
                    if ($key === 'broken_links_internal') {
                        $counts['broken_internal_links'] = $count;
                    } elseif ($key === 'broken_links_external') {
                        $counts['broken_external_links'] = $count;
                    }
                    
                    // If this is a broken_*_links key, also store under broken_links_* for forwards compatibility
                    if ($key === 'broken_internal_links') {
                        $counts['broken_links_internal'] = $count;
                    } elseif ($key === 'broken_external_links') {
                        $counts['broken_links_external'] = $count;
                    }
                } else {
                    $counts[ $key ] = 0;
                }
            } else {
                // Handle potential future categories or unknown types
                $counts[ $key ] = 0;
            }
        }

        wp_send_json_success( $counts );
    }

    /**
     * AJAX handler to get data for the "Fixes Overview" tab.
     */
    public static function ajax_get_fixes_data() {
        // Verify nonce and permissions
        $auth_check = self::verify_ajax_request();
        if (is_wp_error($auth_check)) {
            self::send_error($auth_check->get_error_message(), $auth_check->get_error_data('status'));
            return;
        }

        $category_key = isset( $_POST['category'] ) ? sanitize_key( $_POST['category'] ) : null;
        $issue_type   = isset( $_POST['issue_type'] ) ? sanitize_key( $_POST['issue_type'] ) : $category_key;
        $page         = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
        $search_query = isset( $_POST['search_query'] ) ? sanitize_text_field( $_POST['search_query'] ) : '';
        $per_page     = 20; // Or make this configurable
        $offset       = ( $page - 1 ) * $per_page;

        if ( ! $category_key ) {
            self::log_error('ajax_get_fixes_data', 'Category not specified');
            self::send_error('Category not specified.', 400, [], 'ajax_get_fixes_data');
            return;
        }

        $results      = [];
        $total_items  = 0;
        
        // Dynamically determine if the category uses database overrides or option suggestions
        $all_issue_configs = rank_get_issue_type_config();
        $current_category_config = $all_issue_configs[$category_key] ?? null;

        if (!$current_category_config) {
            self::log_error('ajax_get_fixes_data', 'Unknown category key: ' . $category_key);
            self::send_error('Invalid category specified.', 400, [], 'ajax_get_fixes_data');
            return;
        }

        $handler_type = $current_category_config['handler_type'] ?? null;
        $db_override_type = null; // For database_override types

        if ($handler_type === 'database_override') {
            // Get database override type from mapping
            $db_category_map = self::get_category_db_mapping();
            $db_override_type = isset($db_category_map[$category_key]) ? $db_category_map[$category_key] : null;

            if ($db_override_type) {
                global $wpdb;
                $table_name = $wpdb->prefix . 'rank_seo_overrides';

                // Use the provided issue_type for filtering
                $issue_type_filter = $issue_type;
                
                // For 'image_alt' category key, use 'missing_alt_tags' as the issue_type filter if not specified
                if ($category_key === 'image_alt' && $issue_type === $category_key) {
                    $issue_type_filter = 'missing_alt_tags';
                }
                
                // Build search condition if search query is provided
                $search_condition = '';
                if (!empty($search_query)) {
                    $normalized_search = self::normalize_url_for_search($search_query);
                    $search_condition = $wpdb->prepare(
                        " AND (original_page_url LIKE %s OR original_page_url LIKE %s)",
                        '%' . $wpdb->esc_like($search_query) . '%',
                        '%' . $wpdb->esc_like($normalized_search) . '%'
                    );
                }
                
                $total_items_query = $wpdb->prepare(
                    "SELECT COUNT(DISTINCT override_id) FROM {$table_name} WHERE override_type = %s AND (issue_type = %s OR (issue_type IS NULL AND %s = %s))" . $search_condition,
                    $db_override_type,
                    $issue_type_filter,
                    $issue_type_filter,
                    $db_override_type
                );
                $total_items = (int) $wpdb->get_var( $total_items_query );

                $query = $wpdb->prepare(
                    "SELECT post_id, object_id, override_value, issue_type, original_page_url FROM {$table_name} WHERE override_type = %s AND (issue_type = %s OR (issue_type IS NULL AND %s = %s))" . $search_condition . " ORDER BY override_id DESC LIMIT %d OFFSET %d",
                    $db_override_type,
                    $issue_type_filter,
                    $issue_type_filter,
                    $db_override_type,
                    $per_page,
                    $offset
                );
                $db_results = $wpdb->get_results( $query );


                // Format results
                if ( $db_results ) {
                    foreach ( $db_results as $row ) {
                        // Use the stored original_page_url if available, otherwise fall back to get_permalink
                        $context_url = !empty($row->original_page_url) ? $row->original_page_url : ($row->post_id ? get_permalink( $row->post_id ) : null);
                        $item_details = [
                            'context' => $context_url ? esc_url($context_url) : 'Post ID: ' . $row->post_id,
                            'value'   => esc_html( $row->override_value ),
                            'post_id' => $row->post_id,
                            'object_id' => $row->object_id,
                            'issue_type' => isset($row->issue_type) ? $row->issue_type : $category_key,
                        ];
                        if ($db_override_type === 'image_alt' && $row->object_id > 0) {
                             $attachment_url = wp_get_attachment_url($row->object_id);
                             $item_details['image_src'] = $attachment_url ? esc_url($attachment_url) : 'Attachment ID: ' . $row->object_id;
                             // For images, show relative path + image ID for consistency
                             $relative_path = $context_url ? preg_replace('/^https?:\/\/[^\/]+/', '', $context_url) : ($row->post_id ? 'Post ID: ' . $row->post_id : 'Site-wide');
                             $item_details['context_text'] = $relative_path . ' (Image ID: ' . $row->object_id . ')';
                        } else {
                            // For standard SEO fixes, show relative path instead of title for consistency with broken links
                            $item_details['context_text'] = $context_url ? preg_replace('/^https?:\/\/[^\/]+/', '', $context_url) : ($row->post_id ? 'Post ID: ' . $row->post_id : 'N/A');
                        }
                         $results[] = $item_details;
                    }
                }
            }

        } elseif ( $handler_type === 'option_suggestion' ) {
            // Get option name from mapping
            $option_name_map = self::get_category_option_mapping();
            $option_name = isset($option_name_map[$category_key]) ? $option_name_map[$category_key] : null;

            if ($option_name) {
                $rules = get_option( $option_name, [] );
                
                // Apply search filter if search query is provided
                if (!empty($search_query) && is_array($rules)) {
                    $normalized_search = self::normalize_url_for_search($search_query);
                    $rules = array_filter($rules, function($new_url_data, $old_url) use ($search_query, $normalized_search) {
                        $landing_page_url = '';
                        
                        if (is_array($new_url_data)) {
                            $landing_page_url = $new_url_data['context'] ?? '';
                        }
                        
                        // Search in both the landing page URL and normalized version
                        return stripos($landing_page_url, $search_query) !== false ||
                               stripos(self::normalize_url_for_search($landing_page_url), $normalized_search) !== false;
                    }, ARRAY_FILTER_USE_BOTH);
                }
                
                $total_items = is_array( $rules ) ? count( $rules ) : 0;

                if ( is_array( $rules ) && $total_items > 0 ) {
                    // Manual pagination for option arrays
                    $paginated_rules = array_slice( $rules, $offset, $per_page, true );

                    foreach ( $paginated_rules as $old_url => $new_url_data ) {
                        // Assuming $new_url_data might be an array with 'new_url' and 'context'
                        $suggestion_value = '';
                        $landing_page_url = ''; // Will store the landing page URL
                        
                        if (is_array($new_url_data)) {
                            // Get the new URL (replacement)
                            if (isset($new_url_data['new_url'])) {
                                $suggestion_value = $new_url_data['new_url'];
                            }
                            
                            // Get the landing page URL (where the broken link was found)
                            if (isset($new_url_data['context'])) {
                                $landing_page_url = $new_url_data['context'];
                            } elseif (isset($new_url_data['context_post_id'])) {
                                // Legacy format with context_post_id
                                $context_post_id = absint($new_url_data['context_post_id']);
                                $page_title = get_the_title($context_post_id);
                                $page_link = get_permalink($context_post_id);
                                if ($page_title && $page_link) {
                                    $landing_page_url = $page_link;
                                }
                            }
                        } elseif (is_string($new_url_data)) {
                            $suggestion_value = $new_url_data; // Legacy format
                        }

                        $results[] = [
                            'context'      => $landing_page_url ?: $old_url, // Landing page URL (where the broken link was found)
                            'value'        => esc_html( $suggestion_value ), // New URL (replacement)
                            'context_text' => $old_url, // Old URL (the broken link)
                            'is_option'    => true,
                            'row_key'      => $new_url_data['row_key'] ?? '', // Include row_key for decoding in frontend
                        ];
                    }
                }
            }
        }

        // Prepare response data
        $response_data = [
            'items'        => $results,
            'total_items'  => $total_items,
            'per_page'     => $per_page,
            'current_page' => $page,
            'total_pages'  => $total_items > 0 ? ceil( $total_items / $per_page ) : 0,
            'category'     => $category_key,
            'issue_type'   => $issue_type,
        ];

        wp_send_json_success( $response_data );
    }

    /**
     * AJAX handler for preparing health issues for a specific category.
     * This fetches all items for the issue type and creates a batch.
     */
    public static function ajax_prepare_health_issues() {
        // Set up AJAX request parameters
        self::setup_ajax_request(25, false);
        
        // Verify nonce and permissions
        $auth_check = self::verify_ajax_request();
        if (is_wp_error($auth_check)) {
            self::send_error($auth_check->get_error_message(), $auth_check->get_error_data('status'), [], 'ajax_prepare_health_issues');
            return;
        }

        $issue_type = isset( $_POST['issue_type'] ) ? sanitize_key( $_POST['issue_type'] ) : '';
        if ( empty( $issue_type ) ) {
            self::log_error('ajax_prepare_health_issues', 'Issue type not specified');
            self::send_error('Issue type not specified.', 400, [], 'ajax_prepare_health_issues');
            return;
        }

        // Validate issue type
        $categories = rank_get_health_check_categories();
        if ( ! array_key_exists( $issue_type, $categories ) ) {
            self::log_error('ajax_prepare_health_issues', 'Invalid issue type: ' . $issue_type);
            self::send_error('Invalid issue type.', 400, [], 'ajax_prepare_health_issues');
            return;
        }

        // Check if this is a chunked request
        $chunk_size = 250; // amount of issues to be chunked together when processing items. Don't go above 250 (performance)
        $chunk_offset = isset( $_POST['chunk_offset'] ) ? absint( $_POST['chunk_offset'] ) : 0;
        $batch_id = isset( $_POST['batch_id'] ) ? sanitize_key( $_POST['batch_id'] ) : null;
        
        // If no batch_id provided, this is the first request - create batch
        if ( ! $batch_id ) {
            // Get available credits to determine preparation limit (only on first request)
            $credits_data = rank_get_ai_credits();
            $available_credits = 0;
            if (!is_wp_error($credits_data) && isset($credits_data['limit']) && isset($credits_data['usage'])) {
                $available_credits = max(0, intval($credits_data['limit']) - intval($credits_data['usage']));
                self::log_error('ajax_prepare_health_issues', "Credits fetched at batch creation: limit={$credits_data['limit']}, usage={$credits_data['usage']}, available={$available_credits}");
            } else {
                self::log_error('ajax_prepare_health_issues', "Failed to fetch credits or invalid format", ['credits_data' => $credits_data]);
            }
            
            // Generate a batch ID and create initial batch data
            $batch_id = 'rank_batch_' . uniqid();
            $batch_data = array(
                'issue_type' => $issue_type,
                'issues'     => array(), // Will be populated chunk by chunk
                'decisions'  => array(),
                'pending'    => 0,
                'complete'   => 0,
                'failed'     => 0,
                'ui_logs'    => array(),
                'created_at' => time(),
                'total_count' => 0, // Will be set from first chunk response
                'chunks_processed' => 0,
                'available_credits' => $available_credits, // Store available credits
                'effective_limit' => 0 // Will be calculated based on credits and max limit
            );
            
            update_option( $batch_id, $batch_data, false );
        }

        // Get existing batch data
        $batch_data = get_option( $batch_id );
        if ( ! $batch_data ) {
            self::send_error('Batch data not found.', 404, [], 'ajax_prepare_health_issues');
            return;
        }

        // Get domain identifier for chunked call
        $domain_identifier = rank_get_domain_identifier();
        if ( is_wp_error( $domain_identifier ) ) {
            self::send_error($domain_identifier->get_error_message(), 400, [], 'ajax_prepare_health_issues');
            return;
        }
        
        // Always use chunked function - every call returns both issues and total count
        $chunk_result = rank_get_issues_chunk( $issue_type, $chunk_size, $chunk_offset, $domain_identifier );
        
        if ( is_wp_error( $chunk_result ) ) {
            self::log_error('ajax_prepare_health_issues', 'Error getting issues chunk: ' . $chunk_result->get_error_message(), [
                'error_code' => $chunk_result->get_error_code(),
                'error_data' => $chunk_result->get_error_data()
            ]);
            self::send_error($chunk_result->get_error_message(), 400, [], 'ajax_prepare_health_issues');
            return;
        }
        
        // Every response includes both issues and total count
        $raw_issues_list = $chunk_result['issues'];
        $total_count = $chunk_result['total'];
        
        // Set total count if not already set
        if ( $batch_data['total_count'] === 0 ) {
            // Calculate effective limit: min(available_credits, 5000, actual_total)
            $max_limit = 5000;
            $available_credits = isset($batch_data['available_credits']) ? $batch_data['available_credits'] : 0;
            $effective_limit = min($total_count, $max_limit, $available_credits);
            
            self::log_error('ajax_prepare_health_issues', "Calculating effective limit: total_count={$total_count}, max_limit={$max_limit}, available_credits={$available_credits}, effective_limit={$effective_limit}");
            
            $batch_data['total_count'] = $total_count; // Store actual total from API
            $batch_data['effective_limit'] = $effective_limit; // Store the capped limit
            
            if ( $total_count === 0 ) {
                wp_send_json_success( array(
                    'batchId' => null,
                    'count' => 0,
                    'total_count' => 0,
                    'message' => 'No issues found for this category.'
                ) );
                return;
            }
        }

        // Get effective limit to check if we need to truncate this chunk
        $effective_limit = isset($batch_data['effective_limit']) ? $batch_data['effective_limit'] : $batch_data['total_count'];
        $current_count = count( $batch_data['issues'] );
        
        // Calculate how many more issues we can add without exceeding the limit
        $remaining_slots = $effective_limit - $current_count;
        
        // Get WordPress base path for filtering
        $wp_home_url = home_url();
        $wp_base_path = parse_url($wp_home_url, PHP_URL_PATH);
        $wp_host = parse_url($wp_home_url, PHP_URL_HOST);
        
        // Process this chunk and add to batch, but only up to the effective limit
        $chunk_issues_map = [];
        $added_count = 0;
        $skipped_count = 0;
        
        foreach ($raw_issues_list as $issue_item) {
            // Stop adding if we've reached the limit
            if ($added_count >= $remaining_slots) {
                break;
            }
            
            if (isset($issue_item['rank_id']) && !empty($issue_item['rank_id'])) {
                // Filter out URLs that don't belong to this WordPress installation
                $issue_url = null;
                if (isset($issue_item['page']['path_full'])) {
                    $issue_url = $issue_item['page']['path_full'];
                } elseif (isset($issue_item['page']['path'])) {
                    // Construct full URL from path if needed
                    $issue_url = home_url($issue_item['page']['path']);
                }
                
                // Validate URL belongs to this WordPress installation
                if ($issue_url) {
                    $url_host = parse_url($issue_url, PHP_URL_HOST);
                    $url_path = parse_url($issue_url, PHP_URL_PATH);
                    
                    // Check host match
                    if ($url_host !== $wp_host) {
                        $skipped_count++;
                        continue;
                    }
                    
                    // Check path match for subdirectory installations
                    if (!empty($wp_base_path) && $wp_base_path !== '/') {
                        $wp_base_path_normalized = rtrim($wp_base_path, '/');
                        $url_path_normalized = rtrim($url_path, '/');
                        
                        if (strpos($url_path_normalized, $wp_base_path_normalized) !== 0) {
                            $skipped_count++;
                            continue;
                        }
                    }
                }
                
                $chunk_issues_map[$issue_item['rank_id']] = $issue_item;
                $added_count++;
            }
        }
        
        // Log skipped items if any (only to debug log, not error log)
        if ($skipped_count > 0) {
            Rank_Processor::add_debug_log("Skipped {$skipped_count} URLs that don't belong to this WordPress installation (subdirectory mismatch)");
        }

        // Add chunk to batch data (only the items we actually added)
        $batch_data['issues'] = array_merge( $batch_data['issues'], $chunk_issues_map );
        $batch_data['pending'] = count( $batch_data['issues'] );
        $batch_data['chunks_processed']++;
        
        // Update batch data
        update_option( $batch_id, $batch_data, false );

        // Recalculate current count after adding
        $total_count = $batch_data['total_count'];
        $current_count = count( $batch_data['issues'] );
        
        // Stop when we reach the effective limit (min of credits, 5000, or total)
        $has_more = count( $raw_issues_list ) === $chunk_size && $current_count < $effective_limit;

        // Debug logging
        self::log_error('ajax_prepare_health_issues', "Chunk processed: offset={$chunk_offset}, chunk_size=" . count($chunk_issues_map) . ", current_count={$current_count}, effective_limit={$effective_limit}, total_count={$total_count}, has_more=" . ($has_more ? 'true' : 'false'));

        wp_send_json_success( array(
            'batchId' => $batch_id,
            'count' => $current_count,
            'total_count' => $effective_limit, // Return effective limit to frontend
            'actual_total' => $total_count, // Include actual total for reference
            'chunk_size' => count( $chunk_issues_map ),
            'next_offset' => $chunk_offset + $chunk_size,
            'has_more' => $has_more,
            'message' => $has_more ?
                "Prepared {$current_count} of {$effective_limit} issues..." :
                "Preparation complete. Found {$current_count} issues."
        ) );
    }

    /**
     * Get batch state and next issue to process
     */
    public static function ajax_get_batch_state() {
        // Set up AJAX request parameters
        self::setup_ajax_request();
        
        // Verify nonce and permissions
        $auth_check = self::verify_ajax_request();
        if (is_wp_error($auth_check)) {
            self::send_error($auth_check->get_error_message(), $auth_check->get_error_data('status'), [], 'ajax_get_batch_state');
            return;
        }

        // Get and validate batch ID
        $batch_id = isset($_POST['batchId']) ? sanitize_text_field($_POST['batchId']) : '';
        $batch_validation = self::validate_batch_id($batch_id);
        if (is_wp_error($batch_validation)) {
            self::send_error($batch_validation->get_error_message(), $batch_validation->get_error_data('status'), [], 'ajax_get_batch_state');
            return;
        }

        // Get batch data
        $batch_data = get_option( $batch_id );
        if ( ! $batch_data ) {
            self::log_error('ajax_get_batch_state', 'Batch not found: ' . $batch_id);
            self::send_error('Batch not found.', 404, [], 'ajax_get_batch_state');
            return;
        }


        // Create an array to store only new UI logs for this request
        $new_ui_logs = array();

        $next_row = null;
        $credits_exhausted_flag = false; // Flag to indicate if credits ran out during this call
        $api_error_400_halt_flag = false; // Flag for generic 400 API error
        $batch_status_message = '';
        
        // Now $batch_data['issues'] is an associative array keyed by rank_id (the complex key)
        foreach ( $batch_data['issues'] as $current_row_key => $issue ) {
            // Skip if we already made a decision on this one using the authoritative $current_row_key
            if ( isset( $batch_data['decisions'][$current_row_key] ) ) {
                continue;
            }
            // Extract page URL from the issue data before making the AI fix request
            $api_page_full_url = $issue['page']['path_full'] ?? null;
            $wp_page_url = $api_page_full_url ? esc_url($api_page_full_url) : null;
            
            // Check if the URL exists and doesn't redirect
            if (!empty($wp_page_url)) {
                $url_validation_result = self::validate_url($wp_page_url, $batch_data, $current_row_key, $new_ui_logs);
                if ($url_validation_result !== true) {
                    // Process only one URL per request, then break
                    break;
                }
            }
            
            // Extract page URL and path information before making the API call
            // Page context from the issue data
            $api_page_path = $issue['page']['path'] ?? null;
            $api_page_full_url = $issue['page']['path_full'] ?? null;
            $wp_page_url = $api_page_full_url ? esc_url($api_page_full_url) : null; // Prefer full URL if available
            $wp_page_path_display = $api_page_path ? esc_html(ltrim($api_page_path, '/')) : 'N/A';
            
            // Item identifier from the issue data (e.g. image URL, broken link URL)
            $item_identifier_from_issue = $issue['issue_identifier'] ?? null;
            if ($item_identifier_from_issue === ($issue['validator'] ?? null) && !filter_var($item_identifier_from_issue, FILTER_VALIDATE_URL)) {
                // If issue_identifier is just the validator type (e.g., "missing_meta_description") and not a URL, clear it for item-specific display.
                // The page itself is the item in this case.
                $item_identifier_from_issue = null;
            }
            
            // Get AI fix for this issue
            $ai_fix = rank_get_ai_fix( $current_row_key ); // domain_identifier is handled within rank_get_ai_fix

            if ( is_wp_error( $ai_fix ) ) {
                $error_code = $ai_fix->get_error_code();
                $error_message = $ai_fix->get_error_message();
                $error_data = $ai_fix->get_error_data();
                $http_status_code = null;

                if ( is_array( $error_data ) && isset( $error_data['status_code'] ) ) {
                    $http_status_code = (int) $error_data['status_code'];
                }
                
                // Get item name and page path for logging
                // Make sure we have the item identifier and page path before logging
                $item_name = isset($item_identifier_from_issue) && !empty($item_identifier_from_issue) ?
                    basename($item_identifier_from_issue) :
                    (isset($issue['issue_identifier']) ? basename($issue['issue_identifier']) : 'Unknown');
                
                $page_path = isset($wp_page_path_display) && !empty($wp_page_path_display) ?
                    $wp_page_path_display :
                    (isset($issue['page']['path']) ? ltrim($issue['page']['path'], '/') : 'Unknown page');
                
                if ( 'out_of_credits' === $error_code ) {
                    // AI credits exhausted
                    $log_message = 'AI Credits Exhausted while processing Image: ' . esc_html($item_name) . ' on /' . esc_html($page_path) . '. Cannot fetch further suggestions.';
                    self::log_error('ajax_get_batch_state', $log_message, ['error_code' => $error_code]);
                    
                    // Keep UI logs for user feedback
                    $ui_log_message = '⚠️ ' . $log_message;
                    $batch_data['ui_logs'][] = $ui_log_message;
                    $new_ui_logs[] = $ui_log_message;
                    // Mark current item as unable to process due to no credits
                    $batch_data['decisions'][$current_row_key] = array(
                        'approved'       => false,
                        'error'          => true,
                        'message'        => $error_message, // "AI credits exhausted."
                        'out_of_credits' => true
                    );
                    $credits_exhausted_flag = true;
                    $batch_status_message = 'AI credits exhausted. Processing stopped.';
                    // Do not prepare $next_row, and break the loop as we can't proceed.
                    break;
                } elseif ( $http_status_code === 400 && $error_code === 'api_error' ) {
                    // Handle generic 400 API errors (that are not 'out_of_credits')
                    // Make sure we're using the correct item name and page path
                    $log_message = 'API Error (400) for ' . esc_html($item_name) . ' on /' . esc_html($page_path) . ': ' . esc_html($error_message) . '. Processing halted.';
                    self::log_error('ajax_get_batch_state', $log_message, ['error_code' => $error_code, 'status_code' => $http_status_code]);
                    
                    // Keep UI logs for user feedback
                    $ui_log_message = '🐞 Error: ' . esc_html($item_name) . ' on /' . esc_html($page_path) . ': ' . esc_html($error_message);
                    $batch_data['ui_logs'][] = $ui_log_message;
                    $new_ui_logs[] = $ui_log_message;
                    $batch_data['decisions'][$current_row_key] = array(
                        'approved' => false,
                        'error'    => true,
                        'message'  => 'API Error (400): ' . $error_message
                    );
                    $api_error_400_halt_flag = true;
                    $batch_status_message = 'A critical API error (HTTP 400) occurred. Processing stopped.';
                    break; // Halt batch processing for this request
                } else {
                    // Handle other errors from rank_get_ai_fix (e.g., 5xx, connection issues, non-400 'api_error')
                    $log_message = 'Error: ' . esc_html($item_name) . ' on /' . esc_html($page_path) . ': ' . esc_html($error_message);
                    self::log_error('ajax_get_batch_state', $log_message, ['error_code' => $error_code, 'error_data' => $error_data]);
                    
                    // Keep UI logs for user feedback - format consistently for the JS parser
                    $ui_log_message = '🐞 Error: ' . esc_html($item_name) . ' on /' . esc_html($page_path) . ': ' . esc_html($error_message);
                    $batch_data['ui_logs'][] = $ui_log_message;
                    $new_ui_logs[] = $ui_log_message;
                    $batch_data['decisions'][$current_row_key] = array(
                        'approved' => false,
                        'error'    => true,
                        'message'  => $error_message
                    );
                    // For other errors, break the loop after processing one URL
                    break;
                }
            }
            
            // If $ai_fix is not a WP_Error, it should contain the 'content' key with the solution(s)
            // The structure of $ai_fix (successful response) is assumed to be:
            // { "content": [ { "solution_type": "...", "solution_value": "..." }, ... ] }
            // Or simply { "content": "single solution string" } - adapt as needed.
            
            $ai_solutions_for_current_row = null;
            
            // Process the AI fix content
            if ( isset( $ai_fix['content'] ) ) {
                if ( is_array( $ai_fix['content'] ) ) {
                    // Content is already an array of solutions
                    $ai_solutions_for_current_row = $ai_fix['content'];
                } elseif ( is_string( $ai_fix['content'] ) ) {
                    // If content is a string, wrap it in an array to match structure of multiple solutions
                    $ai_solutions_for_current_row = array(
                        array( 'solution_type' => 'text', 'solution_value' => $ai_fix['content'] )
                    );
                } else {
                    // Unexpected format for 'content' - try to convert to string
                    $content_string = is_object($ai_fix['content']) || is_array($ai_fix['content'])
                        ? json_encode($ai_fix['content'])
                        : (string)$ai_fix['content'];
                    
                    $ai_solutions_for_current_row = array(
                        array( 'solution_type' => 'text', 'solution_value' => $content_string )
                    );
                }
            } else {
                // This shouldn't happen with our updated rank_get_ai_fix function,
                // but let's handle it just in case
                
                // Try to extract any useful text from the response
                $fallback_content = "Image description";
                
                // Check for common fields that might contain useful text
                foreach (['alt_text', 'suggestion', 'text', 'message', 'description'] as $field) {
                    if (isset($ai_fix[$field]) && is_string($ai_fix[$field])) {
                        $fallback_content = $ai_fix[$field];
                        break;
                    }
                }
                
                $ai_solutions_for_current_row = array(
                    array( 'solution_type' => 'text', 'solution_value' => $fallback_content )
                );
                
                // Log this issue but continue processing
                $item_name = isset($item_identifier_from_issue) ? basename($item_identifier_from_issue) : 'Unknown';
                $page_path = isset($wp_page_path_display) ? $wp_page_path_display : 'Unknown page';
                $log_message = 'Warning for Image: ' . esc_html($item_name) . ' on /' . esc_html($page_path) . ': Using fallback content.';
                self::log_error('ajax_get_batch_state', $log_message);
                
                // Keep UI logs for user feedback
                $ui_log_message = '⚠️ ' . $log_message;
                $batch_data['ui_logs'][] = $ui_log_message;
                $new_ui_logs[] = $ui_log_message;
            }


            // If we are here, we have a valid $ai_solutions_for_current_row
            // Prepare the $next_row for the UI
            // This logic is similar to what's in ajax_record_decision for finding the next row,
            // but here we are populating the *current* row that was just fetched.

            $ui_url_display = 'N/A';
            $ui_issue_data = [ /* ... populate with page_url, image_url etc. from $issue ... */ ];
            
            // Extract page_url, image_url etc. from $issue (which is $batch_data['issues'][$current_row_key])
            $batch_api_issue_type = $batch_data['issue_type'] ?? null; // The overall type for this batch
            $current_item_api_validator = $issue['validator'] ?? $batch_api_issue_type; // More specific if available

            // Page context from the issue data
            $api_page_path = $issue['page']['path'] ?? null;
            $api_page_full_url = $issue['page']['path_full'] ?? null;
            $wp_page_url = $api_page_full_url ? esc_url($api_page_full_url) : null; // Prefer full URL if available
            $wp_page_path_display = $api_page_path ? esc_html(ltrim($api_page_path, '/')) : 'N/A';
            
            // Extract URL fragments from the API


            $ui_issue_data['page_url'] = $wp_page_url;
            $ui_issue_data['page_path'] = $wp_page_path_display; // Relative path for display

            // Item identifier from the issue data (e.g. image URL, broken link URL)
            $item_identifier_from_issue = $issue['issue_identifier'] ?? null;
            if ($item_identifier_from_issue === $current_item_api_validator && !filter_var($item_identifier_from_issue, FILTER_VALIDATE_URL)) {
                // If issue_identifier is just the validator type (e.g., "missing_meta_description") and not a URL, clear it for item-specific display.
                // The page itself is the item in this case.
                $item_identifier_from_issue = null; 
            }


            // Get the site URL to construct full URLs
            $site_url = site_url();
            
            // Ensure site URL ends with a slash
            if (substr($site_url, -1) !== '/') {
                $site_url .= '/';
            }
            
            // Construct a full URL from the path fragment
            $full_page_url = $wp_page_url;
            if (empty($full_page_url) && !empty($api_page_path)) {
                // Remove leading slash if present
                $path_for_url = ltrim($api_page_path, '/');
                $full_page_url = $site_url . $path_for_url;
            }
            
            switch ($current_item_api_validator) {
                case 'missing_alt_tags':
                    $ui_url_display = 'Image: ' . ($item_identifier_from_issue ? basename($item_identifier_from_issue) : 'Unknown') . ' on <a href="' . esc_url($full_page_url) . '" target="_blank">' . esc_html($wp_page_path_display) . '</a>';
                    $ui_issue_data['image_url'] = $item_identifier_from_issue;
                    $ui_issue_data['page_url'] = $full_page_url; // Store the full URL
                    break;
                case 'missing_meta_description':
                case 'short_meta_description':
                case 'long_meta_description':
                    $desc = '';
                    if ($current_item_api_validator === 'missing_meta_description') $desc = 'Missing meta description';
                    if ($current_item_api_validator === 'short_meta_description') $desc = 'Short meta description';
                    if ($current_item_api_validator === 'long_meta_description') $desc = 'Long meta description';
                    $ui_url_display = '<a href="' . esc_url($full_page_url) . '" target="_blank">' . esc_html($wp_page_path_display) . '</a> - ' . $desc;
                    $ui_issue_data['page_url'] = $full_page_url; // Store the full URL
                    break;
                case 'missing_title_tag':
                case 'short_title_tag':
                case 'long_title_tag':
                    $desc = '';
                    if ($current_item_api_validator === 'missing_title_tag') $desc = 'Missing title';
                    if ($current_item_api_validator === 'short_title_tag') $desc = 'Short title';
                    if ($current_item_api_validator === 'long_title_tag') $desc = 'Long title';
                    $ui_url_display = '<a href="' . esc_url($full_page_url) . '" target="_blank">' . esc_html($wp_page_path_display) . '</a> - ' . $desc;
                    $ui_issue_data['page_url'] = $full_page_url; // Store the full URL
                    break;
                case 'broken_links_external':
                case 'broken_links_internal':
                    $link_type = ($current_item_api_validator === 'broken_links_external') ? 'External' : 'Internal';
                    $ui_url_display = 'Broken ' . $link_type . ' Link: ' . ($item_identifier_from_issue ? esc_html($item_identifier_from_issue) : 'N/A') . ' on <a href="' . esc_url($full_page_url) . '" target="_blank">' . esc_html($wp_page_path_display) . '</a>';
                    if ($item_identifier_from_issue && filter_var($item_identifier_from_issue, FILTER_VALIDATE_URL)) {
                       $ui_issue_data['item_specific_url_generic'] = $item_identifier_from_issue; // The broken URL
                    }
                    $ui_issue_data['page_url'] = $full_page_url; // Store the full URL
                    break;
                default:
                    $page_display_part = '<a href="' . esc_url($full_page_url) . '" target="_blank">' . esc_html($wp_page_path_display) . '</a>';
                    $item_display_part = $item_identifier_from_issue ? esc_html($item_identifier_from_issue) : "Details for " . $current_row_key;
                    $ui_url_display = $page_display_part . ($item_identifier_from_issue ? ': ' . $item_display_part : '');
                    if ($item_identifier_from_issue && filter_var($item_identifier_from_issue, FILTER_VALIDATE_URL)) {
                       $ui_issue_data['item_specific_url_generic'] = $item_identifier_from_issue;
                    }
                    $ui_issue_data['page_url'] = $full_page_url; // Store the full URL
                    break;
            }

            // Prepare the next row data for the frontend
            
            $next_row = array(
                'rowKey'        => $current_row_key,
                'url'           => $ui_url_display,
                'solution'      => $ai_solutions_for_current_row, // This is the array of solutions
                'issue_data'    => $ui_issue_data
            );
            
            // Explicitly set skip_auto_approve to false for normal rows
            // This ensures the frontend knows this is not a continuation row
            if (!isset($next_row['issue_data']['skip_auto_approve'])) {
                $next_row['issue_data']['skip_auto_approve'] = false;
            }
            
            // We found an item to process.
            // The decision for this item will be made by the user via ajax_record_decision.
            
            // We've processed one URL (valid or invalid), so break the loop
            break;
        } // End foreach loop

        // Save batch data (potentially updated ui_logs and decisions for errors)
        update_option( $batch_id, $batch_data, false );

        $total_issues = isset($batch_data['issues']) && is_array($batch_data['issues']) ? count($batch_data['issues']) : 0;
        $processed_count = isset($batch_data['decisions']) ? count($batch_data['decisions']) : 0;

        // Fetch current credits information
        $credits_data = rank_get_ai_credits();
        $credits_info = array( 'remaining' => 0, 'total' => 0 );
        
        if ( !is_wp_error( $credits_data ) && is_array( $credits_data ) ) {
            if ( isset( $credits_data['limit'] ) && isset( $credits_data['usage'] ) ) {
                $credits_info['total'] = intval( $credits_data['limit'] );
                $credits_info['remaining'] = max( 0, intval( $credits_data['limit'] ) - intval( $credits_data['usage'] ) );
            }
        }

        // No need for dummy next_row logic since we're processing one URL at a time

        // Check if there are more URLs to process and find the next URL
        $more_to_process = false;
        $next_unprocessed_key = null;
        
        if ($processed_count < $total_issues) {
            foreach ($batch_data['issues'] as $key => $issue_data) {
                if (!isset($batch_data['decisions'][$key])) {
                    $more_to_process = true;
                    $next_unprocessed_key = $key;
                    break;
                }
            }
        }
        
        // If we have more to process but no next_row is set, create one for the next unprocessed URL
        if ($more_to_process && !$next_row && $next_unprocessed_key) {
            $next_issue = $batch_data['issues'][$next_unprocessed_key];
            
            // Extract basic information for the next row
            $api_page_full_url = $next_issue['page']['path_full'] ?? null;
            $wp_page_url = $api_page_full_url ? esc_url($api_page_full_url) : null;
            $api_page_path = $next_issue['page']['path'] ?? null;
            $wp_page_path_display = $api_page_path ? esc_html(ltrim($api_page_path, '/')) : 'N/A';
            
            // Create a special next_row that tells the frontend this is just for continuation
            // and should not be auto-approved or logged as a fix
            $next_row = array(
                'rowKey'        => $next_unprocessed_key,
                'url'           => 'Continue processing...',
                'solution'      => array('Continue processing...'),
                'issue_data'    => array(
                    'page_url' => $wp_page_url,
                    'page_path' => $wp_page_path_display,
                    'continue_processing' => true,
                    'skip_auto_approve' => true
                )
            );
        }

        $response_payload = array(
            'nextRow'        => $next_row,
            'ui_logs'        => $new_ui_logs, // Only send new logs generated during this request
            'totalCount'     => $total_issues,
            'processedCount' => $processed_count,
            'status'         => $more_to_process ? 'processing' : 'batch_complete', // Set status based on whether there are more URLs to process
            'message'        => $batch_status_message ?: ($next_row ? 'Next item fetched.' : ($more_to_process ? 'More items to process.' : 'No more items to process or batch complete.')),
            'credits'        => $credits_info, // Add credits information to the response
            'more_to_process' => $more_to_process, // Explicitly indicate if there are more URLs to process
            'force_continue' => $more_to_process, // Additional flag to force frontend to continue
            'is_continuation' => $next_row && isset($next_row['issue_data']['skip_auto_approve']) ? true : false // Flag to indicate this is a continuation request
        );

        if ( $api_error_400_halt_flag ) {
            $response_payload['status'] = 'api_error_400_halt'; // Specific status for JS
            // $response_payload['message'] is already set by $batch_status_message
        } elseif ( $credits_exhausted_flag ) {
            $response_payload['status'] = 'out_of_credits';
            // $response_payload['message'] is already set by $batch_status_message
        } elseif ( $total_issues === 0 ) {
            $response_payload['status'] = 'batch_empty';
            $response_payload['message'] = 'No issues found in this batch to process.';
        }


        
        wp_send_json_success( $response_payload );
    }

    /**
     * Record user decision (approve/decline)
     */
    public static function ajax_record_decision() {
        // Set up AJAX request parameters
        self::setup_ajax_request(10, false);
        
        // Verify nonce and permissions
        $auth_check = self::verify_ajax_request();
        if (is_wp_error($auth_check)) {
            self::send_error($auth_check->get_error_message(), $auth_check->get_error_data('status'), [], 'ajax_record_decision');
            return;
        }

        // Get and validate batch ID
        $batch_id = isset($_POST['batchId']) ? sanitize_key($_POST['batchId']) : null;
        $batch_validation = self::validate_batch_id($batch_id);
        if (is_wp_error($batch_validation)) {
            self::send_error($batch_validation->get_error_message(), $batch_validation->get_error_data('status'), [], 'ajax_record_decision');
            return;
        }
        
        // Get row key and approval status
        $row_key = isset($_POST['rowKey']) ? sanitize_text_field($_POST['rowKey']) : null;
        $approved = isset($_POST['approved']) ? filter_var($_POST['approved'], FILTER_VALIDATE_BOOLEAN) : false;
        
        if (!$row_key) {
            self::send_error('Missing row key.', 400, [], 'ajax_record_decision');
            return;
        }

        // Retrieve new POST variables expected from the frontend
        $page_url_from_post = isset($_POST['page_url']) ? esc_url_raw(trim(wp_unslash($_POST['page_url']))) : null;
        // Store item URLs as-is to preserve special characters
        $item_url_from_post = isset($_POST['item_url']) ? trim(wp_unslash($_POST['item_url'])) : null;
        $solution_from_post = isset($_POST['solution']) ? wp_unslash($_POST['solution']) : null;
        
        // Simple solution extraction
        if (is_string($solution_from_post)) {
            // Check if it's a serialized array
            if (is_serialized($solution_from_post)) {
                $unserialized = maybe_unserialize($solution_from_post);
                if (is_array($unserialized) && isset($unserialized['solution_value'])) {
                    $solution_from_post = $unserialized['solution_value'];
                }
            }
            $solution_from_post = trim($solution_from_post);
        } elseif (is_array($solution_from_post)) {
            // If it's an array with a single object (common case from logs)
            if (count($solution_from_post) === 1 && isset($solution_from_post[0]) && is_array($solution_from_post[0])) {
                if (isset($solution_from_post[0]['solution_value'])) {
                    $solution_from_post = $solution_from_post[0]['solution_value'];
                }
            }
            // If it's a direct array with solution_value
            elseif (isset($solution_from_post['solution_value'])) {
                $solution_from_post = $solution_from_post['solution_value'];
            }
            // If it's a simple array, use the first element
            elseif (isset($solution_from_post[0])) {
                $solution_from_post = $solution_from_post[0];
            }
            
            // Ensure it's a string and trim it
            if (is_string($solution_from_post)) {
                $solution_from_post = trim($solution_from_post);
            }
        }
        // $api_issue_type_from_post will be derived after fetching $current_issue_data or from POST

        $batch_data = get_option( $batch_id ); 


        if ( ! $batch_data || ! isset( $batch_data['issues'][$row_key] ) ) {
            self::log_error('ajax_record_decision', 'Batch data or specific issue not found', [
                'batch_id' => $batch_id,
                'row_key' => $row_key
            ]);
            self::send_error('Batch data or specific issue not found.', 404, [], 'ajax_record_decision');
            return;
        }

        // Ensure decisions and ui_logs are initialized as arrays
        if ( ! isset( $batch_data['decisions'] ) || ! is_array( $batch_data['decisions'] ) ) {
            $batch_data['decisions'] = array();
        }
        if ( ! isset( $batch_data['ui_logs'] ) || ! is_array( $batch_data['ui_logs'] ) ) {
            $batch_data['ui_logs'] = array();
        }

        $batch_data['decisions'][$row_key] = $approved; // Store simple approval status
        // For storing the actual solution if edited, the frontend should send it,
        // and it should be stored in $batch_data['decisions'][$row_key]['solution'] if needed for ajax_schedule_tasks
        if ($approved && $solution_from_post !== null) {
            if (!is_array($batch_data['decisions'][$row_key])) { // If it was just a boolean
                 $batch_data['decisions'][$row_key] = ['approved' => true];
            }
            $batch_data['decisions'][$row_key]['approved'] = true;
            $batch_data['decisions'][$row_key]['solution'] = $solution_from_post;
        } elseif (!$approved) {
             $batch_data['decisions'][$row_key] = ['approved' => false];
        }


        $current_issue_data = $batch_data['issues'][$row_key];
        $api_issue_type_from_post = isset($_POST['api_issue_type']) ? sanitize_key($_POST['api_issue_type']) : ($current_issue_data['validator'] ?? $batch_data['issue_type'] ?? null);


        $action_text = $approved ? 'approved' : 'declined';
        
        // Correctly access nested data for logging and task arguments
        $log_identifier_url = $page_url_from_post ?? $item_url_from_post ?? $row_key;
        $actual_issue_type_for_log = $api_issue_type_from_post ?? 'N/A';
        $actual_solution_for_log = $solution_from_post ?? 'N/A (Solution not provided in POST)';
        
        // Ensure solution is a string, trim it, and provide a default if empty after trimming.
        $actual_solution_for_log = is_string($actual_solution_for_log) ? trim($actual_solution_for_log) : 'N/A';
        if (empty($actual_solution_for_log)) {
            $actual_solution_for_log = 'N/A';
        }

        // Create detailed log message for server logs only (not for UI)
        $detailed_log_message = sprintf(
            'Item for %s (%s) %s. Solution: "%s"',
            $log_identifier_url,
            $actual_issue_type_for_log,
            $action_text,
            $actual_solution_for_log
        );
        
        // Log to server error log
        self::log_error('ajax_record_decision', $detailed_log_message);
        
        // No longer adding this message to UI logs
        if ( count( $batch_data['ui_logs'] ) > 500 ) { // Limit log size
            $batch_data['ui_logs'] = array_slice( $batch_data['ui_logs'], 0, 500 );
        }

        if ( $approved ) {
            if ( ! function_exists('as_enqueue_async_action') ) {
                $error_message = 'Error: Action Scheduler function as_enqueue_async_action() not available. Task not scheduled.';
                $batch_data['ui_logs'][] = $error_message; // Add to UI logs as well
                update_option( $batch_id, $batch_data, false ); // Save updated batch data
                self::log_error('ajax_record_decision', $error_message);
                self::send_error($error_message, 500, ['log' => $batch_data['ui_logs']], 'ajax_record_decision');
                return; // Ensure execution stops
            }

            // Check if the URL exists in WordPress before scheduling the task
            $wp_post_id = 0;
            $url_exists = false;
            $skip_reason = '';
            
            // Check if page URL is missing
            if (empty($page_url_from_post)) {
                $skip_reason = 'missing_page_url';
                $log_message = "Skipping item due to missing page URL: " . ($item_url_from_post ?? $row_key);
                array_unshift($batch_data['ui_logs'], $log_message);
                self::log_error('ajax_record_decision', $log_message);
                
                // Mark as skipped in batch data
                $batch_data['decisions'][$row_key] = [
                    'approved' => false,
                    'skipped' => true,
                    'skip_reason' => 'missing_page_url',
                    'message' => 'Missing page URL'
                ];
                
                // Save batch data with the new log message
                update_option($batch_id, $batch_data, false);
                
                // Find next row to process instead of stopping
                $next_row_for_ui = self::find_next_unprocessed_row($batch_data);
                
                // Return success but indicate the URL was skipped
                self::send_success('⚠️ Item skipped (missing page URL)', array(
                    'nextRow'        => $next_row_for_ui, // Include next row to continue processing
                    'totalCount'     => isset($batch_data['issues']) && is_array($batch_data['issues']) ? count($batch_data['issues']) : 0,
                    'processedCount' => isset($batch_data['decisions']) ? count($batch_data['decisions']) : 0,
                ));
                return;
            }
            
            // URL validation already done in ajax_get_batch_state() - no need to validate again
            // Prepare arguments for the scheduled task
            $task_args = array(
                'page_context_url' => $page_url_from_post, // URL of the page/post
                'item_specific_url'=> $item_url_from_post, // URL of the specific item (e.g., image)
                'api_issue_type'   => $api_issue_type_from_post, // e.g., 'missing_meta_description'
                'solution'         => $solution_from_post,   // The actual solution text
                'row_key'          => $row_key,
                'batch_id'         => $batch_id,
            );


            $action_id = as_enqueue_async_action(
                'rank_process_single_item_hook', // Ensure this hook matches what Rank_Processor expects
                array( $task_args ), // Pass $task_args directly as the single argument in the array
                'rank-ai-seo'
            );

            if ( $action_id ) {
                // Success - no action needed
            } else {
                
                // Log the error
                $error_message = sprintf('Failed to schedule task for item: %s', $row_key);
                self::log_error('ajax_record_decision', $error_message);
                
                // We still need to show errors in the UI
                $ui_error_message = sprintf(
                    'Error: Failed to schedule task for item: %s',
                    esc_html( $row_key )
                );
                array_unshift( $batch_data['ui_logs'], $ui_error_message );
            }
        }

        // Save batch data (decisions, logs)
        update_option( $batch_id, $batch_data, false );

        // Find next unprocessed row for the UI
        $next_row_for_ui = null;
        if (isset($batch_data['issues']) && is_array($batch_data['issues'])) {
            $batch_api_issue_type = $batch_data['issue_type'] ?? null; // Get the overall issue type for the batch

            foreach ( $batch_data['issues'] as $key => $issue_item_data ) { // $key is rank_id, $issue_item_data is the issue's own data
                if ( ! isset( $batch_data['decisions'][$key] ) ) { // Found an unprocessed item
                    
                    $next_ui_url_display = 'N/A'; // For the main display string in the UI
                    $next_ui_issue_data = [
                        'page_path' => null, // For display in UI
                        'page_url'  => null, // Full URL for processing
                        'image_url' => null  // Full image URL if applicable
                    ];

                    // Get page details using helper method
                    $page_details = self::get_page_details_from_issue($issue_item_data);
                    $next_ui_issue_data['page_url'] = $page_details['page_url']; // This is the crucial full URL
                    $next_ui_issue_data['page_path'] = $page_details['page_path']; // For UI text

                    // Get item identifier (e.g., image URL for alt tags, or specific element identifier)
                    $item_identifier_from_issue = $issue_item_data['issue_identifier'] ?? ($issue_item_data['parameters']['url'] ?? null);

                    $ai_solutions_for_next_row = $issue_item_data['solution'] ?? ['Solution not available'];
                    if (is_string($ai_solutions_for_next_row)) { // Ensure it's an array for the UI
                        $ai_solutions_for_next_row = [$ai_solutions_for_next_row];
                    }

                    // Build UI display string using helper method
                    $next_ui_url_display = self::build_ui_display_string($batch_api_issue_type, $page_details['page_path'], $item_identifier_from_issue, $key);
                    
                    // Set type-specific issue data
                    if ($batch_api_issue_type === 'missing_alt_tags') {
                        $next_ui_issue_data['image_url'] = $item_identifier_from_issue; // This is the item_url for alt tags
                    } elseif ($item_identifier_from_issue && filter_var($item_identifier_from_issue, FILTER_VALIDATE_URL) && $batch_api_issue_type !== 'missing_alt_tags') {
                        $next_ui_issue_data['item_specific_url_generic'] = $item_identifier_from_issue;
                    }

                    $next_row_for_ui = array(
                        'rowKey'        => $key,                         // The unique rank_id
                        'url'           => $next_ui_url_display,         // For UI display string
                        'solution'      => $ai_solutions_for_next_row,   // Array of solutions
                        'issue_data'    => $next_ui_issue_data           // Contains page_url, image_url etc. for JS
                    );
                    break; // Found the next row, exit loop
                }
            }
        }
        
        $total_issues = isset($batch_data['issues']) && is_array($batch_data['issues']) ? count($batch_data['issues']) : 0;
        $processed_count = isset($batch_data['decisions']) ? count($batch_data['decisions']) : 0;

        // Fetch current credits information
        $credits_data = rank_get_ai_credits();
        $credits_info = array( 'remaining' => 0, 'total' => 0 );
        
        if ( !is_wp_error( $credits_data ) && is_array( $credits_data ) ) {
            if ( isset( $credits_data['limit'] ) && isset( $credits_data['usage'] ) ) {
                $credits_info['total'] = intval( $credits_data['limit'] );
                $credits_info['remaining'] = max( 0, intval( $credits_data['limit'] ) - intval( $credits_data['usage'] ) );
            }
        }

        wp_send_json_success( array(
            'message'        => $approved ? '✅ Fixed' : '❌ Fix declined.',
            'nextRow'        => $next_row_for_ui, // Send the fully prepared next row for UI
            'totalCount'     => $total_issues,
            'processedCount' => $processed_count,
            'credits'        => $credits_info // Add credits information to the response
        ) );
        
    }

    

    /**
     * AJAX handler for optimized health issues overview using the new /overall endpoint.
     * This replaces the sequential processing with a single API call.
     */
    public static function ajax_get_health_issues_overview_optimized() {
        // Set up AJAX request parameters
        self::setup_ajax_request(30, false); // Increased timeout for the API call
        
        // Verify nonce and permissions
        $auth_check = self::verify_ajax_request();
        if (is_wp_error($auth_check)) {
            self::send_error($auth_check->get_error_message(), $auth_check->get_error_data('status'), [], 'ajax_get_health_issues_overview_optimized');
            return;
        }
        
        // Get optimized issue counts using the new endpoint
        $overview_data = rank_get_health_issues_overview_counts();
        
        if (is_wp_error($overview_data)) {
            self::log_error('ajax_get_health_issues_overview_optimized', 'Error fetching health issues overview: ' . $overview_data->get_error_message(), [
                'error_code' => $overview_data->get_error_code(),
                'error_data' => $overview_data->get_error_data()
            ]);
            self::send_error('Error fetching health issues overview: ' . $overview_data->get_error_message(), 500, [], 'ajax_get_health_issues_overview_optimized');
            return;
        }
        
        // Extract issues and crawled_at from the response
        $issues_data = isset($overview_data['issues']) ? $overview_data['issues'] : $overview_data;
        $crawled_at = isset($overview_data['crawled_at']) ? $overview_data['crawled_at'] : null;
        
        // Check if the crawl data is too old
        $is_data_too_old = rank_is_crawl_data_too_old($crawled_at);
        
        // Fetch AI credits information
        $credits_data = rank_get_ai_credits();
        
        if (is_wp_error($credits_data)) {
            self::log_error('ajax_get_health_issues_overview_optimized', 'Error fetching AI credits data: ' . $credits_data->get_error_message(), [
                'error_code' => $credits_data->get_error_code(),
                'error_data' => $credits_data->get_error_data()
            ]);
            self::send_error('Error fetching AI credits data: ' . $credits_data->get_error_message(), 500, [], 'ajax_get_health_issues_overview_optimized');
            return;
        }
        
        if (!is_array($credits_data)) {
            $credits_data = array('remaining' => 0, 'total' => 0, 'limit' => 0, 'usage' => 0);
        }
        
        $response_data = array(
            'issues' => $issues_data,
            'credits' => $credits_data,
            'issue_types' => array_keys($issues_data), // Send list of issue types from the response
            'crawled_at' => $crawled_at,
            'is_data_too_old' => $is_data_too_old
        );
        
        wp_send_json_success($response_data);
    }

    /**
     * AJAX handler to clear pending decisions for a batch.
     * This is called when a user clicks the Cancel button to reset the state.
     */
    public static function ajax_clear_pending_decisions() {
        // Set up AJAX request parameters
        self::setup_ajax_request(10, false);
        
        // Verify nonce and permissions
        $auth_check = self::verify_ajax_request();
        if (is_wp_error($auth_check)) {
            self::send_error($auth_check->get_error_message(), $auth_check->get_error_data('status'), [], 'ajax_clear_pending_decisions');
            return;
        }

        // Get and validate batch ID
        $batch_id = isset($_POST['batchId']) ? sanitize_key($_POST['batchId']) : '';
        $batch_validation = self::validate_batch_id($batch_id);
        if (is_wp_error($batch_validation)) {
            self::send_error($batch_validation->get_error_message(), $batch_validation->get_error_data('status'), [], 'ajax_clear_pending_decisions');
            return;
        }
        
        // Get the batch data
        $batch_data = get_option( $batch_id );
        
        if ( ! $batch_data ) {
            // If batch doesn't exist, nothing to clear
            wp_send_json_success( array( 'message' => 'No batch data found to clear.' ) );
            return;
        }
        
        // Reset the batch state
        if (isset($batch_data['decisions'])) {
            // Clear all decisions that haven't been processed yet
            $batch_data['decisions'] = array();
        }
        
        // Reset counters
        $batch_data['pending'] = isset($batch_data['issues']) ? count($batch_data['issues']) : 0;
        $batch_data['complete'] = 0;
        $batch_data['failed'] = 0;
        
        // Add a log entry
        if ( isset( $batch_data['ui_logs'] ) && is_array( $batch_data['ui_logs'] ) ) {
            $batch_data['ui_logs'][] = 'User canceled processing and reset all pending decisions.';
        } else {
            $batch_data['ui_logs'] = array( 'User canceled processing and reset all pending decisions.' );
        }
        
        // Cancel any scheduled tasks for this batch
        if (function_exists('as_unschedule_all_actions')) {
            $action_group = 'rank-tasks-' . $batch_id;
            as_unschedule_all_actions('rank_process_single_item_hook', array(), $action_group);
            
            if (isset($batch_data['ui_logs']) && is_array($batch_data['ui_logs'])) {
                $batch_data['ui_logs'][] = 'All scheduled tasks for this batch have been canceled.';
            }
        }
        
        // Save the updated batch data
        update_option( $batch_id, $batch_data, false );
        
        // Log the action
        self::log_error('ajax_clear_pending_decisions', 'User cleared pending decisions for batch: ' . $batch_id);
        
        wp_send_json_success( array(
            'message' => 'Pending decisions cleared successfully. All processing has been reset.',
            'batch_data' => $batch_data // Return the updated batch data for the frontend
        ) );
    }

    /**
     * AJAX handler for updating fix values
     */
    public static function ajax_update_fix_value() {
        // Set up AJAX request parameters
        self::setup_ajax_request(10, false);
        
        // Verify nonce and permissions
        $auth_check = self::verify_ajax_request();
        if (is_wp_error($auth_check)) {
            self::send_error($auth_check->get_error_message(), $auth_check->get_error_data('status'), [], 'ajax_update_fix_value');
            return;
        }
        
        // Get and sanitize parameters
        $category = isset($_POST['category']) ? sanitize_key($_POST['category']) : '';
        $value = isset($_POST['value']) ? sanitize_text_field($_POST['value']) : '';
        $is_option = isset($_POST['is_option']) && $_POST['is_option'] === 'true';
        
        if (empty($category) || $value === '') {
            self::send_error('Missing required parameters', 400, [], 'ajax_update_fix_value');
        }
        
        // Handle different types of fixes
        if ($is_option) {
            // Handle option-based fixes (broken links)
            $old_url = isset($_POST['old_url']) ? sanitize_text_field($_POST['old_url']) : '';
            if (empty($old_url)) {
                self::send_error('Missing old URL parameter', 400, [], 'ajax_update_fix_value');
            }
            
            // Get option name from mapping
            $option_name_map = self::get_category_option_mapping();
            $option_name = isset($option_name_map[$category]) ? $option_name_map[$category] : '';
            
            if (empty($option_name)) {
                self::send_error('Invalid category for option-based fix', 400, [], 'ajax_update_fix_value');
                return;
            }
            
            // Get current replacements
            $replacements = get_option($option_name, []);
            
            // Update the value
            if (is_array($replacements) && array_key_exists($old_url, $replacements)) {
                if (is_array($replacements[$old_url]) && isset($replacements[$old_url]['new_url'])) {
                    $replacements[$old_url]['new_url'] = $value;
                } else {
                    $replacements[$old_url] = $value;
                }
                
                update_option($option_name, $replacements, false);
                wp_send_json_success();
            } else {
                self::log_error('ajax_update_fix_value', 'Old URL not found in replacements: ' . $old_url);
                self::send_error('Old URL not found in replacements', 404, [], 'ajax_update_fix_value');
                return;
            }
        } else {
            // Handle database-based fixes
            $post_id = isset($_POST['post_id']) ? absint($_POST['post_id']) : 0;
            $object_id = isset($_POST['object_id']) ? absint($_POST['object_id']) : 0;
            
            if ($post_id <= 0) {
                self::send_error('Invalid post ID', 400, [], 'ajax_update_fix_value');
                return;
            }
            
            // Get database override type from mapping
            $db_category_map = self::get_category_db_mapping();
            $override_type = isset($db_category_map[$category]) ? $db_category_map[$category] : '';
            
            if (empty($override_type)) {
                self::send_error('Invalid category for database-based fix', 400, [], 'ajax_update_fix_value');
                return;
            }
            
            // First, get the existing record to preserve original_page_url and image_url
            global $wpdb;
            $table_name = $wpdb->prefix . RANK_TABLE_NAME;
            
            $existing_record = $wpdb->get_row($wpdb->prepare(
                "SELECT original_page_url, image_url FROM {$table_name} WHERE post_id = %d AND object_id = %d AND override_type = %s LIMIT 1",
                $post_id,
                $object_id,
                $override_type
            ));
            
            $original_page_url = $existing_record ? $existing_record->original_page_url : null;
            $image_url = $existing_record ? $existing_record->image_url : null;
            
            // Use the existing update_seo_data method with preserved URLs
            $result = Rank_Processor::update_seo_data($post_id, $object_id, $override_type, $value, $category, null, null, $image_url, $original_page_url);
            
            if ($result) {
                wp_send_json_success();
            } else {
                self::log_error('ajax_update_fix_value', 'Failed to update database value', [
                    'post_id' => $post_id,
                    'object_id' => $object_id,
                    'override_type' => $override_type,
                    'category' => $category
                ]);
                self::send_error('Failed to update database value', 500, [], 'ajax_update_fix_value');
                return;
            }
        }
    }

    /**
     * Helper method to find the next unprocessed row in a batch
     *
     * @param array $batch_data The batch data containing issues and decisions
     * @return array|null The next row data for UI, or null if no more rows to process
     */
    private static function find_next_unprocessed_row($batch_data) {
        if (!isset($batch_data['issues']) || !is_array($batch_data['issues'])) {
            return null;
        }
        
        $batch_api_issue_type = $batch_data['issue_type'] ?? null;
        
        foreach ($batch_data['issues'] as $key => $issue_item_data) {
            if (!isset($batch_data['decisions'][$key])) { // Found an unprocessed item
                $next_ui_url_display = 'N/A';
                $next_ui_issue_data = [
                    'page_path' => null,
                    'page_url'  => null,
                    'image_url' => null
                ];
                
                // Get page details using helper method
                $page_details = self::get_page_details_from_issue($issue_item_data);
                $next_ui_issue_data['page_url'] = $page_details['page_url'];
                $next_ui_issue_data['page_path'] = $page_details['page_path'];
                
                // Get item identifier
                $item_identifier_from_issue = $issue_item_data['issue_identifier'] ?? ($issue_item_data['parameters']['url'] ?? null);
                
                $ai_solutions_for_next_row = $issue_item_data['solution'] ?? ['Solution not available'];
                if (is_string($ai_solutions_for_next_row)) {
                    $ai_solutions_for_next_row = [$ai_solutions_for_next_row];
                }
                
                // Build UI display string using helper method
                $next_ui_url_display = self::build_ui_display_string($batch_api_issue_type, $page_details['page_path'], $item_identifier_from_issue, $key);
                
                // Set type-specific issue data
                if ($batch_api_issue_type === 'missing_alt_tags') {
                    $next_ui_issue_data['image_url'] = $item_identifier_from_issue;
                } elseif ($item_identifier_from_issue && filter_var($item_identifier_from_issue, FILTER_VALIDATE_URL) && $batch_api_issue_type !== 'missing_alt_tags') {
                    $next_ui_issue_data['item_specific_url_generic'] = $item_identifier_from_issue;
                }
                
                return array(
                    'rowKey'        => $key,
                    'url'           => $next_ui_url_display,
                    'solution'      => $ai_solutions_for_next_row,
                    'issue_data'    => $next_ui_issue_data
                );
            }
        }
        
        return null; // No more unprocessed rows
    }

    /**
     * AJAX handler to check how many issues in a batch have already been fixed
     */
    public static function ajax_check_already_fixed() {
        // Set up AJAX request parameters
        self::setup_ajax_request(15, false);
        
        // Verify nonce and permissions
        $auth_check = self::verify_ajax_request();
        if (is_wp_error($auth_check)) {
            self::send_error($auth_check->get_error_message(), $auth_check->get_error_data('status'), [], 'ajax_check_already_fixed');
            return;
        }

        // Get and validate batch ID
        $batch_id = isset($_POST['batchId']) ? sanitize_key($_POST['batchId']) : '';
        $batch_validation = self::validate_batch_id($batch_id);
        if (is_wp_error($batch_validation)) {
            self::send_error($batch_validation->get_error_message(), $batch_validation->get_error_data('status'), [], 'ajax_check_already_fixed');
            return;
        }

        // Get batch data
        $batch_data = get_option($batch_id);
        if (!$batch_data || !isset($batch_data['issues']) || !is_array($batch_data['issues'])) {
            self::send_error('Batch data not found or invalid.', 404, [], 'ajax_check_already_fixed');
            return;
        }

        $issues = $batch_data['issues'];
        $issue_type = $batch_data['issue_type'] ?? '';
        $total_issues = count($issues);
        
        if ($total_issues === 0) {
            wp_send_json_success(array(
                'already_fixed' => 0,
                'total_checked' => 0,
                'sample_size' => 0,
                'message' => 'No issues to check.'
            ));
            return;
        }

        // For performance, we'll sample up to 5000 issues for excellent accuracy while maintaining performance
        $sample_size = min($total_issues, 5000);

        // Get a representative sample of issues
        $sample_issues = array_slice($issues, 0, $sample_size, true);
        
        // Extract URLs and prepare for database check
        $urls_to_check = array();
        $url_to_key_map = array();
        
        foreach ($sample_issues as $key => $issue) {
            // Extract page URL from issue data
            $page_url = null;
            
            // Try different ways to get the page URL
            if (isset($issue['page']['path_full'])) {
                $page_url = $issue['page']['path_full'];
            } elseif (isset($issue['page']['path'])) {
                // Construct full URL from path
                $site_url = site_url();
                if (substr($site_url, -1) !== '/') {
                    $site_url .= '/';
                }
                $path = ltrim($issue['page']['path'], '/');
                $page_url = $site_url . $path;
            }
            
            if ($page_url) {
                $urls_to_check[] = $page_url;
                $url_to_key_map[$page_url] = $key;
            }
        }

        if (empty($urls_to_check)) {
            wp_send_json_success(array(
                'already_fixed' => 0,
                'total_checked' => 0,
                'sample_size' => $sample_size,
                'message' => 'No valid URLs found to check.'
            ));
            return;
        }

        // Check database for already fixed issues
        global $wpdb;
        
        // Ensure RANK_TABLE_NAME is defined
        if (!defined('RANK_TABLE_NAME') || empty(RANK_TABLE_NAME)) {
            wp_send_json_success(array(
                'already_fixed' => 0,
                'total_checked' => 0,
                'sample_size' => $sample_size,
                'message' => 'Database table not configured.'
            ));
            return;
        }
        
        $table_name = $wpdb->prefix . RANK_TABLE_NAME;
        
        // Get database override type from mapping
        $db_category_map = self::get_category_db_mapping();
        $db_override_type = isset($db_category_map[$issue_type]) ? $db_category_map[$issue_type] : null;
        
        $already_fixed_count = 0;
        
        if ($db_override_type && count($urls_to_check) > 0) {
            // Prepare placeholders for IN clause
            $placeholders = implode(',', array_fill(0, count($urls_to_check), '%s'));
            
            // Build query parameters
            $query_params = array_merge(
                array($db_override_type, $issue_type, $issue_type, $db_override_type),
                $urls_to_check
            );
            
            // Query to check for existing fixes
            $query = $wpdb->prepare(
                "SELECT DISTINCT original_page_url FROM `$table_name`
                 WHERE override_type = %s
                 AND (issue_type = %s OR (issue_type IS NULL AND %s = %s))
                 AND original_page_url IN ($placeholders)",
                $query_params
            );
            
            $fixed_urls = $wpdb->get_col($query);
            $already_fixed_count = is_array($fixed_urls) ? count($fixed_urls) : 0;
            
        } elseif (in_array($issue_type, array('broken_internal_links', 'broken_external_links', 'broken_links_internal', 'broken_links_external'))) {
            // Handle option-based fixes (broken links)
            $option_name_map = self::get_category_option_mapping();
            $option_name = isset($option_name_map[$issue_type]) ? $option_name_map[$issue_type] : null;
            
            if ($option_name) {
                $replacements = get_option($option_name, array());
                
                if (is_array($replacements)) {
                    // For broken links, we need to check if any of the broken URLs have been fixed
                    // Count unique broken URLs that have been fixed, not total occurrences
                    $unique_fixed_urls = array();
                    foreach ($sample_issues as $key => $issue) {
                        $broken_url = $issue['issue_identifier'] ?? null;
                        if ($broken_url && array_key_exists($broken_url, $replacements)) {
                            if (!in_array($broken_url, $unique_fixed_urls)) {
                                $unique_fixed_urls[] = $broken_url;
                            }
                        }
                    }
                    $already_fixed_count = count($unique_fixed_urls);
                }
            }
        }

        // Calculate estimated total based on sample
        $estimated_total_fixed = 0;
        if ($sample_size > 0 && $already_fixed_count > 0) {
            $fix_rate = $already_fixed_count / $sample_size;
            $estimated_total_fixed = round($fix_rate * $total_issues);
        }

        wp_send_json_success(array(
            'already_fixed' => $already_fixed_count,
            'total_checked' => count($urls_to_check),
            'sample_size' => $sample_size,
            'total_issues' => $total_issues,
            'estimated_total_fixed' => $estimated_total_fixed,
            'message' => $already_fixed_count > 0 ?
                "Found {$already_fixed_count} already fixed issues in sample of {$sample_size}." :
                "No already fixed issues found in sample."
        ));
    }

    /**
     * AJAX handler to get debug logs from WordPress debug.log file
     */
    public static function get_debug_logs() {
        // Verify nonce and permissions
        $auth_check = self::verify_ajax_request();
        if (is_wp_error($auth_check)) {
            self::send_error($auth_check->get_error_message(), $auth_check->get_error_data('status'), [], 'get_debug_logs');
            return;
        }

        // Use the custom debug log from Rank_Processor
        $log_data = Rank_Processor::get_debug_logs(200);
        
        if (!$log_data['file_info']['exists']) {
            wp_send_json_error(array(
                'message' => 'No debug logs found yet. Logs will appear here when RANK AI processing runs.',
                'logs' => ''
            ));
            return;
        }

        wp_send_json_success(array(
            'logs' => $log_data['logs'],
            'timestamp' => current_time('mysql'),
            'file_size' => size_format($log_data['file_info']['size']),
            'lines_shown' => $log_data['file_info']['lines_shown'],
            'total_lines' => $log_data['file_info']['lines']
        ));
    }

    /**
     * AJAX handler to toggle debug logging on/off
     */
    public static function ajax_toggle_debug() {
        // Verify nonce and permissions
        $auth_check = self::verify_ajax_request();
        if (is_wp_error($auth_check)) {
            self::send_error($auth_check->get_error_message(), $auth_check->get_error_data('status'), [], 'ajax_toggle_debug');
            return;
        }

        // Get the enabled state
        $enabled = isset($_POST['enabled']) ? intval($_POST['enabled']) : 0;
        
        // Update the option
        $result = update_option('rank_debug_enabled', (bool)$enabled, false);
        
        if ($result !== false) {
            wp_send_json_success(array(
                'message' => 'Debug logging ' . ($enabled ? 'enabled' : 'disabled'),
                'enabled' => (bool)$enabled
            ));
        } else {
            self::send_error('Failed to update debug setting', 500, [], 'ajax_toggle_debug');
        }
    }

    /**
     * AJAX handler for exporting category fixes to CSV
     */
    public static function ajax_export_category_csv() {
        // Verify nonce
        if (!isset($_GET['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['nonce'])), 'rank_ajax_nonce')) {
            wp_die('Security check failed', 'Unauthorized', array('response' => 403));
        }
        
        // Check permissions
        if (!current_user_can('manage_options')) {
            wp_die('Permission denied', 'Forbidden', array('response' => 403));
        }
        
        // Get and sanitize parameters
        $category = isset($_GET['category']) ? sanitize_key($_GET['category']) : '';
        $issue_type = isset($_GET['issue_type']) ? sanitize_key($_GET['issue_type']) : $category;
        
        if (empty($issue_type)) {
            wp_die('Missing required parameters', 'Bad Request', array('response' => 400));
        }
        
        global $wpdb;
        
        // Ensure RANK_TABLE_NAME is defined
        if (!defined('RANK_TABLE_NAME')) {
            wp_die('Database table not configured', 'Internal Server Error', array('response' => 500));
        }
        
        $table_name = $wpdb->prefix . RANK_TABLE_NAME;
        
        // Query all rows for this issue type
        $results = $wpdb->get_results($wpdb->prepare(
            "SELECT * FROM $table_name WHERE issue_type = %s ORDER BY created_at DESC",
            $issue_type
        ), ARRAY_A);
        
        // Set headers for CSV download
        header('Content-Type: text/csv; charset=utf-8');
        header('Content-Disposition: attachment; filename="rank-fixes-' . sanitize_file_name($issue_type) . '-' . date('Y-m-d') . '.csv"');
        header('Pragma: no-cache');
        header('Expires: 0');
        
        // Open output stream
        $output = fopen('php://output', 'w');
        
        // Add BOM for Excel UTF-8 compatibility
        fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF));
        
        if (!empty($results)) {
            // Write column headers
            fputcsv($output, array_keys($results[0]));
            
            // Write data rows
            foreach ($results as $row) {
                fputcsv($output, $row);
            }
        } else {
            // Write headers even if no data
            fputcsv($output, array('override_id', 'post_id', 'object_id', 'override_type', 'issue_type', 'override_value', 'created_at', 'updated_at', 'image_url', 'original_page_url'));
            fputcsv($output, array('No data found for this category'));
        }
        
        fclose($output);
        exit;
    }

}