<?php
/**
 * Handles chunked checking of already-fixed issues to prevent gateway timeouts.
 * 
 * This class processes large batches of issues in small chunks to avoid
 * server timeouts when many items have already been fixed.
 */

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

class Rank_Already_Fixed_Checker {

    /**
     * Number of items to check per chunk
     */
    const CHUNK_SIZE = 100;

    /**
     * Maximum execution time per request in seconds
     */
    const MAX_EXECUTION_TIME = 8;
    
    /**
     * Check already fixed issues in chunks
     *
     * @param array $batch_data The batch data containing issues
     * @param int $offset Starting offset for this chunk
     * @param bool $return_keys Whether to return the keys of fixed items
     * @return array Response data with chunk results
     */
    public static function check_chunk($batch_data, $offset = 0, $return_keys = false) {
        $start_time = microtime(true);
        
        // Validate input
        if (!is_array($batch_data)) {
            return array(
                'already_fixed' => 0,
                'checked_in_chunk' => 0,
                'total_checked' => 0,
                'total_issues' => 0,
                'has_more' => false,
                'next_offset' => 0,
                'progress_percent' => 100,
                'fixed_keys' => array(),
                'message' => 'Invalid batch data.'
            );
        }
        
        $issues = isset($batch_data['issues']) && is_array($batch_data['issues']) ? $batch_data['issues'] : array();
        $issue_type = isset($batch_data['issue_type']) ? $batch_data['issue_type'] : '';
        $total_issues = count($issues);
        
        if ($total_issues === 0) {
            return array(
                'already_fixed' => 0,
                'checked_in_chunk' => 0,
                'total_checked' => 0,
                'total_issues' => 0,
                'has_more' => false,
                'next_offset' => 0,
                'progress_percent' => 100,
                'fixed_keys' => array(),
                'message' => 'No issues to check.'
            );
        }

        // Calculate chunk boundaries
        $chunk_start = max(0, intval($offset));
        $chunk_end = min($total_issues, $chunk_start + self::CHUNK_SIZE);
        $chunk_issues = array_slice($issues, $chunk_start, self::CHUNK_SIZE, true);
        
        // Check for already fixed issues - count actual items, not URLs
        $already_fixed_count = 0;
        $fixed_keys = array();
        
        if (!empty($chunk_issues)) {
            $result = self::check_fixed_status_by_items($chunk_issues, $issue_type, $start_time, $return_keys);
            if ($return_keys && is_array($result)) {
                $already_fixed_count = $result['count'];
                $fixed_keys = $result['keys'];
            } else {
                $already_fixed_count = $result;
            }
        }

        // Calculate progress
        $total_checked = $chunk_end;
        $has_more = $chunk_end < $total_issues;
        $next_offset = $has_more ? $chunk_end : 0;
        $progress_percent = $total_issues > 0 ? round(($total_checked / $total_issues) * 100) : 100;
        
        return array(
            'already_fixed' => $already_fixed_count,
            'checked_in_chunk' => count($chunk_issues),
            'total_checked' => $total_checked,
            'total_issues' => $total_issues,
            'has_more' => $has_more,
            'next_offset' => $next_offset,
            'progress_percent' => $progress_percent,
            'fixed_keys' => $fixed_keys,
            'message' => $has_more
                ? "Checked {$total_checked} of {$total_issues} issues..."
                : "Check complete: Found {$already_fixed_count} already fixed in {$total_checked} issues."
        );
    }

    /**
     * Extract page URL from issue data
     * 
     * @param array $issue Issue data
     * @return string|null Page URL or null
     */
    private static function extract_page_url($issue) {
        $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;
        }
        
        return $page_url;
    }

    /**
     * Check if items have already been fixed (counts actual items, not just URLs)
     *
     * @param array $chunk_issues Issues in current chunk
     * @param string $issue_type Type of issue
     * @param float $start_time Start time for timeout checking
     * @return int Count of already fixed items
     */
    private static function check_fixed_status_by_items($chunk_issues, $issue_type, $start_time, $return_keys = false) {
        global $wpdb;
        
        // 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;
        $fixed_keys = array();
        
        if ($db_override_type) {
            // Check database for fixes - count individual items
            $result = self::check_database_fixes_by_items($chunk_issues, $db_override_type, $issue_type, $start_time, $return_keys);
            if ($return_keys && is_array($result)) {
                $already_fixed_count = $result['count'];
                $fixed_keys = $result['keys'];
            } else {
                $already_fixed_count = $result;
            }
            
        } elseif (in_array($issue_type, array('broken_internal_links', 'broken_external_links', 'broken_links_internal', 'broken_links_external'))) {
            // Check option-based fixes (broken links) - count individual items
            $result = self::check_option_fixes($chunk_issues, $issue_type, $return_keys);
            if ($return_keys && is_array($result)) {
                $already_fixed_count = $result['count'];
                $fixed_keys = $result['keys'];
            } else {
                $already_fixed_count = $result;
            }
        }
        
        if ($return_keys) {
            return array(
                'count' => $already_fixed_count,
                'keys' => $fixed_keys
            );
        }
        
        return $already_fixed_count;
    }

    /**
     * Check database for already fixed issues - counts individual items
     *
     * @param array $chunk_issues Issues to check
     * @param string $db_override_type Database override type
     * @param string $issue_type Issue type
     * @param float $start_time Start time for timeout checking
     * @return int Count of fixed items
     */
    private static function check_database_fixes_by_items($chunk_issues, $db_override_type, $issue_type, $start_time, $return_keys = false) {
        global $wpdb;
        
        if (!defined('RANK_TABLE_NAME') || empty(RANK_TABLE_NAME)) {
            return 0;
        }
        
        $table_name = $wpdb->prefix . RANK_TABLE_NAME;
        $fixed_count = 0;
        $fixed_keys = array();
        
        // For alt tags, we need to check each image individually
        if ($db_override_type === 'image_alt') {
            foreach ($chunk_issues as $key => $issue) {
                // Check timeout
                if ((microtime(true) - $start_time) > self::MAX_EXECUTION_TIME) {
                    break;
                }
                
                $page_url = self::extract_page_url($issue);
                $image_url = $issue['issue_identifier'] ?? null;
                
                if (!$page_url || !$image_url) {
                    continue;
                }
                
                // Check if this specific image on this page has been fixed
                $exists = $wpdb->get_var($wpdb->prepare(
                    "SELECT COUNT(*) FROM `$table_name`
                     WHERE override_type = %s
                     AND image_url = %s
                     AND original_page_url = %s
                     LIMIT 1",
                    $db_override_type,
                    $image_url,
                    $page_url
                ));
                
                if ($exists && $exists > 0) {
                    $fixed_count++;
                    if ($return_keys) {
                        $fixed_keys[] = $key;
                    }
                }
            }
        } else {
            // For meta descriptions and titles, check by page URL
            foreach ($chunk_issues as $key => $issue) {
                // Check timeout
                if ((microtime(true) - $start_time) > self::MAX_EXECUTION_TIME) {
                    break;
                }
                
                $page_url = self::extract_page_url($issue);
                
                if (!$page_url) {
                    continue;
                }
                
                // Check if this page has been fixed for this override type
                $exists = $wpdb->get_var($wpdb->prepare(
                    "SELECT COUNT(*) FROM `$table_name`
                     WHERE override_type = %s
                     AND original_page_url = %s
                     LIMIT 1",
                    $db_override_type,
                    $page_url
                ));
                
                if ($exists && $exists > 0) {
                    $fixed_count++;
                    if ($return_keys) {
                        $fixed_keys[] = $key;
                    }
                }
            }
        }
        
        if ($return_keys) {
            return array(
                'count' => $fixed_count,
                'keys' => $fixed_keys
            );
        }
        
        return $fixed_count;
    }

    /**
     * Check option-based fixes for broken links - counts individual items
     *
     * @param array $chunk_issues Issues to check
     * @param string $issue_type Issue type
     * @return int Count of fixed items (individual occurrences, not unique URLs)
     */
    private static function check_option_fixes($chunk_issues, $issue_type, $return_keys = false) {
        $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) {
            return 0;
        }
        
        $replacements = get_option($option_name, array());
        
        if (!is_array($replacements) || empty($replacements)) {
            return 0;
        }
        
        // Count each fixed item individually (not unique URLs)
        $fixed_count = 0;
        $fixed_keys = array();
        
        foreach ($chunk_issues as $key => $issue) {
            $broken_url = $issue['issue_identifier'] ?? null;
            $page_url = self::extract_page_url($issue);
            
            if (!$broken_url || !$page_url) {
                continue;
            }
            
            // Use the same normalization function as the storage code
            if (function_exists('rank_normalize_url')) {
                $normalized_page_url = rank_normalize_url($page_url);
                $normalized_broken_url = rank_normalize_url($broken_url);
            } else {
                $normalized_page_url = $page_url;
                $normalized_broken_url = $broken_url;
            }
            
            // Check using the same composite key format as storage
            // Format: broken_url::context::md5(page_url)
            $context_hash = md5($normalized_page_url);
            $composite_key = $normalized_broken_url . '::context::' . $context_hash;
            
            // Check if this specific broken URL + page context has a replacement
            if (isset($replacements[$composite_key])) {
                $fixed_count++;
                if ($return_keys) {
                    $fixed_keys[] = $key;
                }
            }
        }
        
        if ($return_keys) {
            return array(
                'count' => $fixed_count,
                'keys' => $fixed_keys
            );
        }
        
        return $fixed_count;
    }

    /**
     * Check if a single issue is already fixed
     *
     * @param array $issue The issue to check
     * @param string $issue_type The issue type
     * @return bool True if fixed, false otherwise
     */
    public static function is_issue_fixed($issue, $issue_type) {
        global $wpdb;
        
        // 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;
        
        if ($db_override_type) {
            // Check database for this specific issue
            if (!defined('RANK_TABLE_NAME') || empty(RANK_TABLE_NAME)) {
                return false;
            }
            
            $table_name = $wpdb->prefix . RANK_TABLE_NAME;
            $page_url = self::extract_page_url($issue);
            
            if (!$page_url) {
                return false;
            }
            
            if ($db_override_type === 'image_alt') {
                $image_url = $issue['issue_identifier'] ?? null;
                if (!$image_url) {
                    return false;
                }
                
                $exists = $wpdb->get_var($wpdb->prepare(
                    "SELECT COUNT(*) FROM `$table_name`
                     WHERE override_type = %s
                     AND image_url = %s
                     AND original_page_url = %s
                     LIMIT 1",
                    $db_override_type,
                    $image_url,
                    $page_url
                ));
                
                return ($exists && $exists > 0);
            } else {
                // Meta descriptions and titles
                $exists = $wpdb->get_var($wpdb->prepare(
                    "SELECT COUNT(*) FROM `$table_name`
                     WHERE override_type = %s
                     AND original_page_url = %s
                     LIMIT 1",
                    $db_override_type,
                    $page_url
                ));
                
                return ($exists && $exists > 0);
            }
        } elseif (in_array($issue_type, array('broken_internal_links', 'broken_external_links', 'broken_links_internal', 'broken_links_external'))) {
            // Check 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) {
                return false;
            }
            
            $replacements = get_option($option_name, array());
            if (!is_array($replacements) || empty($replacements)) {
                return false;
            }
            
            $broken_url = $issue['issue_identifier'] ?? null;
            $page_url = self::extract_page_url($issue);
            
            if (!$broken_url || !$page_url) {
                return false;
            }
            
            // Use the same normalization function as the storage code
            if (function_exists('rank_normalize_url')) {
                $normalized_page_url = rank_normalize_url($page_url);
                $normalized_broken_url = rank_normalize_url($broken_url);
            } else {
                $normalized_page_url = $page_url;
                $normalized_broken_url = $broken_url;
            }
            
            // Check using the same composite key format as storage
            $context_hash = md5($normalized_page_url);
            $composite_key = $normalized_broken_url . '::context::' . $context_hash;
            
            return isset($replacements[$composite_key]);
        }
        
        return false;
    }

    /**
     * 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 array(
            // 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 array(
            // 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'
        );
    }
}