<?php
/**
 * Handles frontend filtering for SEO data overrides (title, meta description, alt tags, content URLs).
 */

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

class Rank_Frontend_Filters {

    // Track if content was modified by the standard filters
    private static $content_modified = false;
    
    // Cache for image alt overrides by URL (per-request cache) - primary lookup method
    private static $page_image_url_cache = null;

    // Cache for title/meta overrides keyed by "{object_id}|{override_type}" (per-request)
    private static $page_seo_override_cache = null;
    
    // Cache: whether any overrides exist in the database at all (transient-backed)
    private static $has_any_overrides = null;
    
    // Cache: whether any URL replacements exist (transient-backed)
    private static $has_any_url_replacements = null;

    /**
     * Initialize frontend filter hooks.
     */
    public static function init() {
        // On frontend, skip all hooks if the plugin has no data at all (common for fresh installs)
        if ( ! is_admin() && ! wp_doing_ajax() && ! self::has_any_overrides() ) {
            return;
        }
        
        // Image Alt Tags - Multiple hooks for better coverage
        // wp_get_attachment_image_attributes runs early (priority 5) for direct WordPress image functions
        add_filter( 'wp_get_attachment_image_attributes', array( __CLASS__, 'filter_attachment_alt' ), 5, 2 );
        // the_content runs at priority 5 for standard posts/pages
        add_filter( 'the_content', array( __CLASS__, 'filter_content_images' ), 5, 1 );
        
        // Page Builder Specific Filters - run at priority 15 after builders process content
        add_filter( 'siteorigin_panels_render', array( __CLASS__, 'filter_content_images' ), 15, 1 ); // SiteOrigin Page Builder
        add_filter( 'elementor/frontend/the_content', array( __CLASS__, 'filter_content_images' ), 15, 1 ); // Elementor
        add_filter( 'fl_builder_before_render_shortcodes', array( __CLASS__, 'filter_content_images' ), 15, 1 ); // Beaver Builder
        add_filter( 'et_builder_render_layout', array( __CLASS__, 'filter_content_images' ), 15, 1 ); // Divi Builder
        
        add_filter( 'post_thumbnail_html', array( __CLASS__, 'filter_featured_image_alt' ), 5, 5 );
        add_filter( 'get_image_tag', array( __CLASS__, 'filter_image_tag_alt' ), 5, 6 );

        // Title Tags
        add_filter( 'pre_get_document_title', array( __CLASS__, 'filter_fallback_title' ), 5, 1 ); // Fallback
        add_filter( 'wpseo_title', array( __CLASS__, 'filter_seo_plugin_title' ), 15, 1 ); // Yoast
        add_filter( 'rank_math/frontend/title', array( __CLASS__, 'filter_seo_plugin_title' ), 15, 1 ); // Rank Math

        // Meta Descriptions
        add_filter( 'wpseo_metadesc', array( __CLASS__, 'filter_meta_description' ), 10, 1 ); // Yoast
        add_filter( 'rank_math/frontend/description', array( __CLASS__, 'filter_meta_description' ), 10, 1 ); // Rank Math
        add_action( 'wp_head', array( __CLASS__, 'add_meta_description_tag_fallback' ), 5 ); // Fallback output

        // --- Content URL Replacement Filters ---
        // Only register URL replacement hooks if URL replacements exist
        if ( self::has_any_url_replacements() ) {
            add_filter( 'the_content', array( __CLASS__, 'filter_content_urls' ), 10, 1 );
            add_filter( 'the_excerpt', array( __CLASS__, 'filter_content_urls' ), 10, 1 );
            
            // Use output buffering as a fallback for content not caught by filters
            add_action( 'template_redirect', array( __CLASS__, 'maybe_start_output_buffer' ) );
        }
        
        // Add hooks for JavaScript-based alt tag handling
        add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_alt_tag_script' ), 1 );
        add_action( 'wp_ajax_get_bulk_alt_tags', array( __CLASS__, 'handle_bulk_alt_tags_request' ) );
        add_action( 'wp_ajax_nopriv_get_bulk_alt_tags', array( __CLASS__, 'handle_bulk_alt_tags_request' ) );
    }
    
    /**
     * Check if any SEO overrides exist in the database at all.
     * Uses a transient (5 min TTL) to avoid querying the DB on every page load.
     * The transient is invalidated when the processor saves new overrides.
     *
     * @return bool True if overrides exist, false if the DB table is empty.
     */
    private static function has_any_overrides() {
        if ( self::$has_any_overrides !== null ) {
            return self::$has_any_overrides;
        }
        
        $cached = get_transient('rank_has_overrides');
        if ( $cached !== false ) {
            self::$has_any_overrides = ( $cached === 'yes' );
            return self::$has_any_overrides;
        }
        
        // No cached value — do a lightweight DB check
        if ( ! defined('RANK_TABLE_NAME') ) {
            self::$has_any_overrides = false;
            return false;
        }
        
        global $wpdb;
        $table_name = $wpdb->prefix . RANK_TABLE_NAME;
        
        // Check if the table exists first (handles fresh installs before activation)
        $table_exists = $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_name ) );
        if ( ! $table_exists ) {
            self::$has_any_overrides = false;
            set_transient('rank_has_overrides', 'no', 5 * MINUTE_IN_SECONDS);
            return false;
        }
        
        // Quick existence check — SELECT 1 LIMIT 1 is the cheapest possible query
        $exists = $wpdb->get_var("SELECT 1 FROM $table_name LIMIT 1");
        
        self::$has_any_overrides = ( $exists !== null );
        set_transient('rank_has_overrides', self::$has_any_overrides ? 'yes' : 'no', 5 * MINUTE_IN_SECONDS);
        
        return self::$has_any_overrides;
    }
    
    /**
     * Check if any URL replacements exist.
     * Uses a transient (5 min TTL) to avoid calling get_option on every page load.
     *
     * @return bool True if URL replacements exist.
     */
    private static function has_any_url_replacements() {
        if ( self::$has_any_url_replacements !== null ) {
            return self::$has_any_url_replacements;
        }
        
        $cached = get_transient('rank_has_url_replacements');
        if ( $cached !== false ) {
            self::$has_any_url_replacements = ( $cached === 'yes' );
            return self::$has_any_url_replacements;
        }
        
        $internal = get_option('rank_internal_url_replacements', []);
        $external = get_option('rank_external_url_replacements', []);
        
        $has = ( ! empty($internal) || ! empty($external) );
        
        self::$has_any_url_replacements = $has;
        set_transient('rank_has_url_replacements', $has ? 'yes' : 'no', 5 * MINUTE_IN_SECONDS);
        
        return self::$has_any_url_replacements;
    }
    
    /**
     * Invalidate the frontend filter caches.
     * Call this when overrides are saved/deleted in the processor.
     */
    public static function invalidate_caches() {
        delete_transient('rank_has_overrides');
        delete_transient('rank_has_url_replacements');
        self::$has_any_overrides = null;
        self::$has_any_url_replacements = null;
    }
    
    /**
     * Start output buffering only if needed
     */
    public static function maybe_start_output_buffer() {
        // Only apply on frontend
        if (is_admin()) {
            return;
        }
        
        // Start the output buffer - we'll check if it's needed in the callback
        ob_start(array(__CLASS__, 'process_output_buffer'));
    }
    
    /**
     * Process the output buffer to replace URLs and add alt tags in the final HTML
     * This catches both URL replacements and images that lazy loading plugins may have re-rendered
     */
    public static function process_output_buffer($buffer) {
        // Get the current page URL
        $current_url = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
        if (empty($current_url)) {
            return $buffer;
        }
        
        // Get the full current URL
        $protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? "https" : "http";
        $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
        $full_current_url = $protocol . "://" . $host . $current_url;
        
        $modified_buffer = $buffer;
        
        // First, handle URL replacements (original functionality)
        if (!self::$content_modified) {
            $replacements = self::get_url_replacements_for_page($full_current_url);
            
            if (!empty($replacements)) {
                foreach ($replacements as $old_url => $new_url) {
                    $temp_buffer = str_replace($old_url, $new_url, $modified_buffer);
                    
                    if ($temp_buffer === $modified_buffer) {
                        $href_pattern = '/href=["\']' . preg_quote($old_url, '/') . '["\']/i';
                        $href_replacement = 'href="' . $new_url . '"';
                        $temp_buffer = preg_replace($href_pattern, $href_replacement, $modified_buffer);
                    }
                    
                    if ($temp_buffer === $modified_buffer) {
                        $decoded_old_url = urldecode($old_url);
                        if ($decoded_old_url !== $old_url) {
                            $temp_buffer = str_replace($decoded_old_url, $new_url, $modified_buffer);
                            
                            if ($temp_buffer === $modified_buffer) {
                                $href_pattern = '/href=["\']' . preg_quote($decoded_old_url, '/') . '["\']/i';
                                $href_replacement = 'href="' . $new_url . '"';
                                $temp_buffer = preg_replace($href_pattern, $href_replacement, $modified_buffer);
                            }
                        }
                    }
                    
                    $modified_buffer = $temp_buffer;
                }
            }
        }
        
        // Second, handle alt-tag processing for lazy-loaded images
        self::load_page_image_overrides();
        
        if (!empty(self::$page_image_url_cache)) {
            rank_debug_log('Output buffer: Processing final HTML for alt-tags');
            
            $modified_count = 0;
            
            if (preg_match_all('/<img[^>]+>/i', $modified_buffer, $matches)) {
                foreach ($matches[0] as $img_tag) {
                    $has_alt = preg_match('/alt=["\']([^"\']*)["\']/i', $img_tag, $alt_match);
                    
                    if (!$has_alt || empty($alt_match[1])) {
                        if (preg_match('/src=["\']([^"\']+)["\']/i', $img_tag, $src_match)) {
                            $image_url = $src_match[1];
                            
                            if (isset(self::$page_image_url_cache[$image_url])) {
                                $alt_text = self::$page_image_url_cache[$image_url];
                                
                                if ($has_alt) {
                                    $new_img_tag = preg_replace(
                                        '/alt=["\'][^"\']*["\']/i',
                                        'alt="' . esc_attr($alt_text) . '"',
                                        $img_tag
                                    );
                                } else {
                                    $new_img_tag = str_replace('<img ', '<img alt="' . esc_attr($alt_text) . '" ', $img_tag);
                                }
                                
                                $modified_buffer = str_replace($img_tag, $new_img_tag, $modified_buffer);
                                $modified_count++;
                                rank_debug_log('Output buffer: Applied alt to ' . $image_url);
                            }
                        }
                    }
                }
            }
            
            if ($modified_count > 0) {
                rank_debug_log('Output buffer: Modified ' . $modified_count . ' images');
            }
        }
        
        return $modified_buffer;
    }
    
    /**
     * Get URL replacements specific to the current page
     * This optimizes performance by only loading relevant replacements
     */
    private static function get_url_replacements_for_page($page_url) {
        $replacements = array();
        
        // Normalize the current page URL for consistent comparison (handles encoding and trailing slashes)
        $normalized_page_url = rank_normalize_url($page_url);
        
        // Get internal URL replacements
        $internal_replacements = get_option('rank_internal_url_replacements', []);
        $internal_count = is_array($internal_replacements) ? count($internal_replacements) : 0;
        
        if (is_array($internal_replacements)) {
            // First, quickly check if this page has any replacements
            $has_replacements_for_page = false;
            $quick_check_count = 0;
            foreach ($internal_replacements as $old_url => $data) {
                $quick_check_count++;
                if (is_array($data) && isset($data['context'])) {
                    // Normalize stored context for comparison
                    $normalized_context = rank_normalize_url($data['context']);
                    if ($normalized_context === $normalized_page_url) {
                        $has_replacements_for_page = true;
                        break;
                    }
                }
            }
            
            // Only process if this page has replacements
            if ($has_replacements_for_page) {
                foreach ($internal_replacements as $old_url => $data) {
                    // Only include replacements relevant to this page
                    if (is_array($data) && isset($data['new_url']) && isset($data['context'])) {
                        // Normalize stored context for comparison
                        $normalized_context = rank_normalize_url($data['context']);
                        // If this replacement is for the current page or has no specific context
                        if ($normalized_context === $normalized_page_url || empty($data['context'])) {
                            // Extract the actual broken URL from composite key or use broken_url field
                            $broken_url = isset($data['broken_url']) ? $data['broken_url'] : $old_url;
                            
                            // If it's a composite key, extract the broken URL part
                            if (strpos($broken_url, '::context::') !== false) {
                                $broken_url = explode('::context::', $broken_url)[0];
                            }
                            
                            $replacements[$broken_url] = $data['new_url'];
                        }
                    }
                }
            }
        }
        
        // Get external URL replacements (same optimization)
        $external_replacements = get_option('rank_external_url_replacements', []);
        $external_count = is_array($external_replacements) ? count($external_replacements) : 0;
        
        if (is_array($external_replacements)) {
            // First, quickly check if this page has any replacements
            $has_replacements_for_page = false;
            $quick_check_count = 0;
            foreach ($external_replacements as $old_url => $data) {
                $quick_check_count++;
                if (is_array($data) && isset($data['context'])) {
                    // Normalize stored context for comparison
                    $normalized_context = rank_normalize_url($data['context']);
                    if ($normalized_context === $normalized_page_url) {
                        $has_replacements_for_page = true;
                        break;
                    }
                }
            }
            
            // Only process if this page has replacements
            if ($has_replacements_for_page) {
                foreach ($external_replacements as $old_url => $data) {
                    // Only include replacements relevant to this page
                    if (is_array($data) && isset($data['new_url']) && isset($data['context'])) {
                        // Normalize stored context for comparison
                        $normalized_context = rank_normalize_url($data['context']);
                        // If this replacement is for the current page or has no specific context
                        if ($normalized_context === $normalized_page_url || empty($data['context'])) {
                            // Extract the actual broken URL from composite key or use broken_url field
                            $broken_url = isset($data['broken_url']) ? $data['broken_url'] : $old_url;
                            
                            // If it's a composite key, extract the broken URL part
                            if (strpos($broken_url, '::context::') !== false) {
                                $broken_url = explode('::context::', $broken_url)[0];
                            }
                            
                            $replacements[$broken_url] = $data['new_url'];
                        }
                    }
                }
            }
        }
        
        // Only log when there are actual replacements to avoid log spam
        if (!empty($replacements)) {
            rank_debug_log("URL Replacements: Found " . count($replacements) . " for page (from " . ($internal_count + $external_count) . " total in DB)");
        }
        
        return $replacements;
    }

    /**
     * Load all image alt overrides for the current page into cache.
     * This runs only once per page request to eliminate N+1 queries.
     */
    private static function load_page_image_overrides() {
        // Return early if cache is already loaded
        if ( self::$page_image_url_cache !== null ) {
            return;
        }
        
        global $wpdb;
        
        // Ensure RANK_TABLE_NAME is defined before using it
        if ( ! defined('RANK_TABLE_NAME') ) {
            error_log('RANK Plugin Error: RANK_TABLE_NAME constant not defined in Rank_Frontend_Filters.');
            self::$page_image_url_cache = array();
            return;
        }
        
        $table_name = $wpdb->prefix . RANK_TABLE_NAME;
        $post_id = get_queried_object_id();
        
        // Get current URL
        $protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? "https" : "http";
        $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
        $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
        $current_url = $protocol . "://" . $host . $uri;
        
        // Initialize cache
        self::$page_image_url_cache = array();
        
        // Single optimized query: Get all image URL overrides for this specific page URL only
        // No post_id fallback to prevent cross-page contamination (important for WPML/multilingual)
        
        // Create URL variations to handle trailing slash, protocol (http/https), and
        // www / non-www host differences (the API may store a different host variant
        // than the one visitors are served).
        $url_variations = self::build_page_url_variations( $current_url );
        $placeholders = implode( ', ', array_fill( 0, count( $url_variations ), '%s' ) );
        
        $sql = $wpdb->prepare(
            "SELECT image_url, override_value
             FROM $table_name
             WHERE original_page_url IN ($placeholders)
             AND override_type = 'image_alt'
             AND image_url IS NOT NULL
             ORDER BY override_id DESC",
            ...$url_variations
        );
        
        $results = $wpdb->get_results( $sql );
        
        // Cache results (first match wins)
        foreach ( $results as $row ) {
            // Normalize URL to protocol-relative format for consistent matching
            $normalized_url = self::normalize_image_url($row->image_url);
            if ( ! isset( self::$page_image_url_cache[$normalized_url] ) ) {
                self::$page_image_url_cache[$normalized_url] = $row->override_value;
                rank_debug_log('Cached override for image URL: ' . $row->image_url);
            }
        }
        
        // Debug: Log results
        rank_debug_log('Loaded ' . count(self::$page_image_url_cache) . ' image URL overrides from ' . count($results) . ' database rows');
        
        // If no results, log the SQL query for debugging
        if (empty($results)) {
            rank_debug_log('⚠️ No image overrides found in database for this page');
        }
    }

    /**
     * Load title and meta description overrides for the current page into cache.
     * Runs once per request to avoid duplicate DB queries from separate filters.
     */
    private static function load_page_seo_overrides() {
        if ( self::$page_seo_override_cache !== null ) {
            return;
        }

        self::$page_seo_override_cache = array();

        if ( ! defined( 'RANK_TABLE_NAME' ) ) {
            error_log( 'RANK Plugin Error: RANK_TABLE_NAME constant not defined in Rank_Frontend_Filters.' );
            return;
        }

        global $wpdb;
        $table_name = $wpdb->prefix . RANK_TABLE_NAME;

        $protocol = ( isset( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] === 'on' ) ? 'https' : 'http';
        $host     = isset( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '';
        $uri      = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '';
        $uri      = strtok( $uri, '?' );
        $current_url = $protocol . '://' . $host . $uri;

        $url_variations = self::build_page_url_variations( $current_url );
        $placeholders   = implode( ', ', array_fill( 0, count( $url_variations ), '%s' ) );

        $sql = $wpdb->prepare(
            "SELECT object_id, override_type, override_value
             FROM $table_name
             WHERE original_page_url IN ($placeholders)
             AND override_type IN ('title', 'meta_description')
             ORDER BY override_id DESC",
            ...$url_variations
        );

        $results = $wpdb->get_results( $sql );

        foreach ( $results as $row ) {
            $cache_key = absint( $row->object_id ) . '|' . sanitize_key( $row->override_type );
            if ( ! isset( self::$page_seo_override_cache[ $cache_key ] ) ) {
                self::$page_seo_override_cache[ $cache_key ] = $row->override_value;
            }
        }

        rank_debug_log( 'Loaded ' . count( self::$page_seo_override_cache ) . ' title/meta overrides from ' . count( $results ) . ' database rows' );
    }

    /**
     * Get override value from the database.
     * Title and meta description use a per-request page cache (see load_page_seo_overrides).
     *
     * @param int    $post_id       Post ID context.
     * @param int    $object_id     Specific object ID (e.g., attachment ID), 0 for post-level.
     * @param string $override_type The type of override ('title', 'meta_description', 'image_alt').
     * @return string|null The override value or null if not found.
     */
    public static function get_override_value( $post_id, $object_id, $override_type ) {
        $override_type = sanitize_key( $override_type );

        if ( in_array( $override_type, array( 'title', 'meta_description' ), true ) ) {
            self::load_page_seo_overrides();
            $cache_key = absint( $object_id ) . '|' . $override_type;
            return isset( self::$page_seo_override_cache[ $cache_key ] )
                ? self::$page_seo_override_cache[ $cache_key ]
                : null;
        }

        // Non-cached override types (if any) — direct lookup
        global $wpdb;

        if ( ! defined( 'RANK_TABLE_NAME' ) ) {
             error_log( 'RANK Plugin Error: RANK_TABLE_NAME constant not defined in Rank_Frontend_Filters.' );
             return null;
        }
        $table_name = $wpdb->prefix . RANK_TABLE_NAME;

        $protocol = ( isset( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] === 'on' ) ? 'https' : 'http';
        $host     = isset( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '';
        $uri      = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '';
        $uri      = strtok( $uri, '?' );
        $current_url = $protocol . '://' . $host . $uri;

        $url_variations = self::build_page_url_variations( $current_url );
        $placeholders   = implode( ', ', array_fill( 0, count( $url_variations ), '%s' ) );

        $query_args = array_merge(
            $url_variations,
            array( absint( $object_id ), $override_type )
        );

        $sql = $wpdb->prepare(
            "SELECT override_value
             FROM $table_name
             WHERE original_page_url IN ($placeholders)
             AND object_id = %d
             AND override_type = %s
             ORDER BY override_id DESC
             LIMIT 1",
            ...$query_args
        );

        return $wpdb->get_var( $sql );
    }

    /**
     * Build all URL variations for a given page URL so frontend lookups match
     * stored original_page_url values regardless of trailing slash, protocol
     * (http/https), or host www-prefix.
     *
     * The Morningscore API may report (and therefore store) a page under a host
     * variant (e.g. www.example.com) that differs from the host visitors are
     * actually served (e.g. example.com, after a www redirect). Without matching
     * both host variants, overrides saved against one host never match the other.
     *
     * @param string $current_url The current page URL.
     * @return array Unique list of URL variations to match against original_page_url.
     */
    private static function build_page_url_variations( $current_url ) {
        $base_without_slash = rtrim( $current_url, '/' );
        
        // Start with the current host variant (slash + protocol variations)
        $hosts = array( $base_without_slash );
        
        // Add the opposite www / non-www host variant of the same URL
        $parts = wp_parse_url( $base_without_slash );
        if ( ! empty( $parts['host'] ) ) {
            $host = $parts['host'];
            if ( strpos( $host, 'www.' ) === 0 ) {
                $alt_host = substr( $host, 4 ); // strip www.
            } else {
                $alt_host = 'www.' . $host;     // add www.
            }
            // Swap only the host portion, preserve scheme/path/query
            $alt_url = preg_replace(
                '#^(https?://)' . preg_quote( $host, '#' ) . '#i',
                '$1' . $alt_host,
                $base_without_slash,
                1
            );
            if ( $alt_url && $alt_url !== $base_without_slash ) {
                $hosts[] = $alt_url;
            }
        }
        
        // For each host variant, generate slash + protocol variations
        $variations = array();
        foreach ( $hosts as $host_url ) {
            $with_slash = $host_url . '/';
            $without_slash = $host_url;
            
            $variations[] = $with_slash;
            $variations[] = $without_slash;
            $variations[] = str_replace( 'https://', 'http://', $with_slash );
            $variations[] = str_replace( 'https://', 'http://', $without_slash );
        }
        
        return array_values( array_unique( $variations ) );
    }

    /**
     * Filter post content to replace specific URLs.
     *
     * @param string $content The original post content.
     * @return string The modified post content.
     */
    public static function filter_content_urls($content) {
        // Skip empty content
        if (empty($content)) {
            return $content;
        }
        
        // Get the current page URL
        $protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? "https" : "http";
        $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
        $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
        $current_url = $protocol . "://" . $host . $uri;
        
        // Get URL replacements for this specific page
        $replacements = self::get_url_replacements_for_page($current_url);
        
        // If no replacements found, return content unchanged
        if (empty($replacements)) {
            return $content;
        }
        
        $updated_content = $content;
        $content_changed = false;
        $total_replacements_made = 0;
        
        // Pre-compute all URL variations for efficient batch processing
        $url_variations = array();
        foreach ($replacements as $old_url => $new_url) {
            $variations = array($old_url); // Start with original
            
            // Add decoded variations if different
            $decoded = urldecode($old_url);
            if ($decoded !== $old_url) {
                $variations[] = $decoded;
            }
            
            $raw_decoded = rawurldecode($old_url);
            if ($raw_decoded !== $old_url && $raw_decoded !== $decoded) {
                $variations[] = $raw_decoded;
            }
            
            // Store all variations pointing to the same replacement
            foreach ($variations as $variation) {
                $url_variations[$variation] = array(
                    'new_url' => $new_url,
                    'original' => $old_url,
                    'type' => ($variation === $old_url) ? 'original' : 'decoded'
                );
            }
        }
        
        // Perform efficient batch replacements
        foreach ($url_variations as $search_url => $data) {
            $new_url = $data['new_url'];
            $original_url = $data['original'];
            $type = $data['type'];
            
            // Try exact string replacement
            $temp_content = str_replace($search_url, $new_url, $updated_content);
            if ($temp_content !== $updated_content) {
                $updated_content = $temp_content;
                $content_changed = true;
                $total_replacements_made++;
                continue;
            }
            
            // Try href pattern replacement
            $href_pattern = '/href=["\']' . preg_quote($search_url, '/') . '["\']/i';
            $href_replacement = 'href="' . $new_url . '"';
            $temp_content = preg_replace($href_pattern, $href_replacement, $updated_content);
            if ($temp_content !== $updated_content) {
                $updated_content = $temp_content;
                $content_changed = true;
                $total_replacements_made++;
                continue;
            }
        }
        
        // Only try expensive flexible matching if no replacements were made
        if ($total_replacements_made === 0 && !empty($replacements)) {
            foreach ($replacements as $old_url => $new_url) {
                $url_parts = parse_url($old_url);
                if (isset($url_parts['path']) && !empty($url_parts['path'])) {
                    $path_part = ltrim($url_parts['path'], '/');
                    
                    // Create a flexible pattern that handles mixed encoding
                    $flexible_pattern = str_replace(['%20', '+'], ['\s+', '\s+'], preg_quote($path_part, '/'));
                    $href_pattern = '/href=["\'][^"\']*' . $flexible_pattern . '[^"\']*["\']/i';
                    
                    if (preg_match($href_pattern, $updated_content)) {
                        $temp_content = preg_replace($href_pattern, 'href="' . $new_url . '"', $updated_content);
                        if ($temp_content !== $updated_content) {
                            $updated_content = $temp_content;
                            $content_changed = true;
                            $total_replacements_made++;
                            break; // Stop after first flexible match to avoid over-replacement
                        }
                    }
                }
            }
        }
        
        // Only log when replacements are actually made or when debugging specific issues
        if ($total_replacements_made > 0) {
            rank_debug_log("URL Replacements: " . $total_replacements_made . " successful replacements made in content");
        } else if (!empty($replacements)) {
            // Only log failures when there were expected replacements but none worked
            rank_debug_log("URL Replacements: 0 matches found for " . count($replacements) . " expected replacements");
        }
        
        // If content was changed, mark it so output buffer knows not to process it again
        if ($content_changed) {
            self::$content_modified = true;
        }
        
        return $updated_content;
    }

    /**
     * Filter image attributes to add alt text override if empty.
     * Now uses cached data to avoid N+1 queries.
     *
     * @param array $attr Attributes for the image tag.
     * @param WP_Post $attachment Attachment object.
     * @return array Modified attributes.
     */
    public static function filter_attachment_alt( $attr, $attachment ) {
        // URL-based lookup is the primary method (matches how data is stored)
        if ( isset($attr['src']) ) {
            // Load cache once per request
            self::load_page_image_overrides();
            
            // Lookup by image URL
            $alt_text = self::get_alt_by_image_url( $attr['src'] );
            
            if ( $alt_text ) {
                $attr['alt'] = $alt_text;
            }
        }
        return $attr;
    }
    
    /**
     * Normalize an image URL to a canonical key for consistent matching.
     *
     * The Morningscore crawler may store a different variant of an image than the
     * one rendered on the page. Common differences:
     *  - protocol (http vs https)
     *  - host www vs non-www
     *  - WordPress responsive size suffix (e.g. -670x515 before the extension)
     *  - an optimizer extension appended to the real one (e.g. .jpg.webp)
     *
     * We strip all of these so the stored image_url and the rendered <img src>
     * resolve to the same key (the size-agnostic, protocol/host-agnostic original).
     *
     * @param string $url The URL to normalize.
     * @return string The canonical matching key.
     */
    private static function normalize_image_url( $url ) {
        if ( empty( $url ) ) {
            return '';
        }
        
        // Strip protocol to get a protocol-relative URL (//host/path)
        if ( strpos( $url, 'https:' ) === 0 ) {
            $url = substr( $url, 6 );
        } elseif ( strpos( $url, 'http:' ) === 0 ) {
            $url = substr( $url, 5 );
        }
        
        // Strip a leading www. from the host (handles www vs non-www variants).
        // Matches //www.host/... at the very start of the protocol-relative URL.
        $url = preg_replace( '#^//www\.#i', '//', $url, 1 );
        
        // Strip an optimizer extension appended after the real image extension
        // (e.g. image.jpg.webp -> image.jpg, image.png.avif -> image.png).
        $url = preg_replace( '#\.(jpe?g|png|gif)\.(webp|avif)$#i', '.$1', $url );
        
        // Strip the WordPress responsive size suffix before the extension
        // (e.g. image-670x515.jpg -> image.jpg) so any rendered size matches the
        // stored variant regardless of which size the crawler captured.
        $url = preg_replace( '#-\d+x\d+(\.(?:jpe?g|png|gif|webp|avif))$#i', '$1', $url );
        
        return $url;
    }
    
    /**
     * Get alt text override by image URL from cache.
     *
     * @param string $image_url The image URL to lookup.
     * @return string|null The alt text or null if not found.
     */
    private static function get_alt_by_image_url( $image_url ) {
        self::load_page_image_overrides();
        // Normalize URL to protocol-relative format for lookup
        $normalized = self::normalize_image_url( $image_url );
        return isset( self::$page_image_url_cache[$normalized] )
            ? self::$page_image_url_cache[$normalized]
            : null;
    }
    
    /**
     * Filter post content to add alt tags to images.
     *
     * @param string $content The post content.
     * @return string Modified content with alt tags.
     */
    public static function filter_content_images( $content ) {
        if ( empty( $content ) || is_admin() ) {
            return $content;
        }
        
        // Find all img tags without alt or with empty alt
        if ( ! preg_match_all( '/<img[^>]+>/i', $content, $matches ) ) {
            return $content;
        }
        
        $modified_count = 0;
        foreach ( $matches[0] as $img_tag ) {
            // Check if alt is missing or empty
            $has_alt = preg_match( '/alt=["\']([^"\']*)["\']/i', $img_tag, $alt_match );
            
            if ( ! $has_alt || empty( $alt_match[1] ) ) {
                // Extract src attribute
                if ( preg_match( '/src=["\']([^"\']+)["\']/i', $img_tag, $src_match ) ) {
                    $image_url = $src_match[1];
                    $alt_text = self::get_alt_by_image_url( $image_url );
                    
                    if ( $alt_text ) {
                        // Add or replace alt attribute
                        if ( $has_alt ) {
                            $new_img_tag = preg_replace(
                                '/alt=["\'][^"\']*["\']/i',
                                'alt="' . esc_attr( $alt_text ) . '"',
                                $img_tag
                            );
                        } else {
                            $new_img_tag = str_replace( '<img ', '<img alt="' . esc_attr( $alt_text ) . '" ', $img_tag );
                        }
                        
                        $content = str_replace( $img_tag, $new_img_tag, $content );
                        $modified_count++;
                    }
                }
            }
        }
        
        // Mark content as modified so output buffer knows we processed it
        if ($modified_count > 0) {
            self::$content_modified = true;
        }
        
        return $content;
    }
    
    /**
     * Filter featured image HTML to add alt tag.
     *
     * @param string $html              The post thumbnail HTML.
     * @param int    $post_id           The post ID.
     * @param int    $post_thumbnail_id The attachment ID.
     * @param string $size              The image size.
     * @param array  $attr              Additional attributes.
     * @return string Modified HTML with alt tag.
     */
    public static function filter_featured_image_alt( $html, $post_id, $post_thumbnail_id, $size, $attr ) {
        // URL-based lookup (simplified)
        if ( empty( $html ) ) {
            return $html;
        }
        
        // Extract image URL and lookup alt text
        if ( preg_match( '/src=["\']([^"\']+)["\']/i', $html, $src_match ) ) {
            self::load_page_image_overrides();
            $alt_text = self::get_alt_by_image_url( $src_match[1] );
            
            if ( $alt_text ) {
                // Replace or add alt attribute
                if ( preg_match( '/alt=["\']([^"\']*)["\']/i', $html ) ) {
                    $html = preg_replace(
                        '/alt=["\'][^"\']*["\']/i',
                        'alt="' . esc_attr( $alt_text ) . '"',
                        $html
                    );
                } else {
                    $html = str_replace( '<img ', '<img alt="' . esc_attr( $alt_text ) . '" ', $html );
                }
            }
        }
        
        return $html;
    }
    
    /**
     * Filter image tag to add alt text.
     *
     * @param string $html  The image HTML.
     * @param int    $id    The attachment ID.
     * @param string $alt   The alt text.
     * @param string $title The title attribute.
     * @param string $align The alignment.
     * @param string $size  The image size.
     * @return string Modified HTML with alt tag.
     */
    public static function filter_image_tag_alt( $html, $id, $alt, $title, $align, $size ) {
        // URL-based lookup (simplified)
        if ( empty( $html ) ) {
            return $html;
        }
        
        // Extract image URL and lookup alt text
        if ( preg_match( '/src=["\']([^"\']+)["\']/i', $html, $src_match ) ) {
            self::load_page_image_overrides();
            $alt_text = self::get_alt_by_image_url( $src_match[1] );
            
            if ( $alt_text ) {
                // Replace or add alt attribute
                if ( preg_match( '/alt=["\']([^"\']*)["\']/i', $html ) ) {
                    $html = preg_replace(
                        '/alt=["\'][^"\']*["\']/i',
                        'alt="' . esc_attr( $alt_text ) . '"',
                        $html
                    );
                } else {
                    $html = str_replace( '<img ', '<img alt="' . esc_attr( $alt_text ) . '" ', $html );
                }
            }
        }
        
        return $html;
    }

    /**
     * Fallback function to filter the document title if major SEO plugins are inactive.
     *
     * @param string $title The original title.
     * @return string The original or overridden title.
     */
    public static function filter_fallback_title( $title ) {
        if ( defined('WPSEO_VERSION') || defined('RANK_MATH_VERSION') ) {
            return $title;
        }

        $post_id = get_queried_object_id();
        if ( $post_id > 0 ) {
            $title_override = self::get_override_value( $post_id, 0, 'title' );
            if ( $title_override !== null ) {
                return esc_html( $title_override );
            }
        }
        return $title;
    }

    /**
     * Callback function to potentially override the title generated by SEO plugins.
     *
     * @param string $title The original title generated by the SEO plugin.
     * @return string The original or overridden title.
     */
    public static function filter_seo_plugin_title( $title ) {
        $post_id = get_queried_object_id();
        if ( $post_id > 0 ) {
            $title_override = self::get_override_value( $post_id, 0, 'title' );
            if ( $title_override !== null ) {
                return esc_html( $title_override );
            }
        }
        return $title;
    }

    /**
     * Callback function to potentially override the meta description generated by SEO plugins.
     *
     * @param string $description The original description.
     * @return string The original or overridden description.
     */
    public static function filter_meta_description( $description ) {
        $post_id = get_queried_object_id();
        if ( $post_id > 0 ) {
            $desc_override = self::get_override_value( $post_id, 0, 'meta_description' );
            if ( $desc_override !== null ) {
                // Use the override value directly (sanitized on save)
                return $desc_override;
            }
        }
        return $description;
    }

    /**
     * Fallback function to output the meta description tag directly if no known SEO plugin is active.
     */
    public static function add_meta_description_tag_fallback() {
        if ( defined('WPSEO_VERSION') || defined('RANK_MATH_VERSION') || defined('AIOSEO_VERSION') || defined('SEOPRESS_VERSION') ) {
            return;
        }
        
        $post_id = get_queried_object_id();
        if ( ! $post_id ) {
            return;
        }

        $desc_override = self::get_override_value( $post_id, 0, 'meta_description' );

        if ( $desc_override !== null ) {
            echo '<meta name="description" content="' . esc_attr( $desc_override ) . '" />' . "\n";
        }
    }
    
    /**
     * Enqueue the JavaScript for handling alt tags.
     * Script is set to load in the head with defer attribute for faster execution.
     */
    public static function enqueue_alt_tag_script() {
        // Only enqueue on frontend
        if ( is_admin() ) {
            return;
        }
        
        // Path to the JavaScript file
        $js_file = plugin_dir_url( dirname( __FILE__ ) ) . 'assets/js/rank-alt.js';
        
        // Register and enqueue the script in head with defer
        wp_register_script(
            'rank-alt-tags',
            $js_file,
            array(),
            '1.4.1', // Updated version - performance fix for forced reflows
            false // Load in head instead of footer
        );
        
        // Add defer attribute for non-blocking execution
        add_filter( 'script_loader_tag', function( $tag, $handle ) {
            if ( 'rank-alt-tags' === $handle ) {
                return str_replace( ' src', ' defer src', $tag );
            }
            return $tag;
        }, 10, 2 );
        
        wp_enqueue_script( 'rank-alt-tags' );
        
        // Get the current post ID
        $post_id = get_queried_object_id();
        
        // Add the AJAX URL, post ID, and current URL to the script
        wp_localize_script( 'rank-alt-tags', 'rankAltTags', array(
            'ajaxUrl' => admin_url( 'admin-ajax.php' ),
            'postId' => $post_id,
            // Current URL is obtained directly in JavaScript using window.location.href
        ));
    }
    
    /**
     * Handle AJAX request for bulk alt tags.
     */
    public static function handle_bulk_alt_tags_request() {
        // Get the array of image URLs
        $urls = isset($_POST['urls']) ? json_decode(stripslashes($_POST['urls']), true) : [];
        
        if (empty($urls) || !is_array($urls)) {
            wp_send_json(array(
                'success' => false,
                'message' => 'No URLs provided'
            ));
            return;
        }
        
        $alt_tags = array();
        
        // Get post ID from the request, fallback to get_queried_object_id() if not provided
        $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : get_queried_object_id();
        
        // Get current page URL from the request
        $current_page_url = isset($_POST['current_url']) ? esc_url_raw($_POST['current_url']) : '';
        
        // If not provided, try to construct it (fallback)
        if (empty($current_page_url)) {
            $protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? "https" : "http";
            $current_page_url = $protocol . "://" . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
        }
        
        global $wpdb;
        
        // Check if RANK_TABLE_NAME is defined
        if (!defined('RANK_TABLE_NAME')) {
            wp_send_json(array(
                'success' => false,
                'message' => 'Configuration error: Table name not defined'
            ));
            return;
        }
        
        $table_name = $wpdb->prefix . RANK_TABLE_NAME;
        
        // No debug info initialization needed
        
        // Process all URLs for this page
        if (!empty($urls) && !empty($current_page_url)) {
            // Match the page URL against all variations (slash, http/https, www)
            // so overrides stored under a different host variant still match.
            $page_url_variations = self::build_page_url_variations( $current_page_url );
            $page_placeholders = implode( ',', array_fill( 0, count( $page_url_variations ), '%s' ) );
            
            // Load every image_alt override for this page, then match in PHP using
            // normalized (size/host/protocol-agnostic) keys. This is more robust than
            // an exact SQL match because the rendered <img src> is frequently a
            // different size/host variant than the stored image_url.
            $sql = $wpdb->prepare(
                "SELECT image_url, override_value
                 FROM $table_name
                 WHERE override_type = 'image_alt'
                 AND image_url IS NOT NULL
                 AND original_page_url IN ($page_placeholders)
                 ORDER BY override_id DESC",
                ...$page_url_variations
            );
            
            $results = $wpdb->get_results($sql);
            
            // Build a normalized-key => alt map (first match wins, newest first)
            $normalized_map = array();
            foreach ($results as $row) {
                $key = self::normalize_image_url($row->image_url);
                if (!isset($normalized_map[$key])) {
                    $normalized_map[$key] = $row->override_value;
                }
            }
            
            // For each page <img src> sent by JS, look up by its normalized key and
            // return the alt keyed by the ORIGINAL src so the JS imageMap matches.
            foreach ($urls as $src) {
                $key = self::normalize_image_url($src);
                if (isset($normalized_map[$key])) {
                    $alt_tags[$src] = $normalized_map[$key];
                }
            }
        }
        
        wp_send_json(array(
            'success' => true,
            'alt_tags' => $alt_tags
        ));
    }
    
}