<?php

// phpcs:disable WordPress.DB.SlowDBQuery
/**
 * Image Optimization Module
 *
 * Generates WebP and AVIF versions of images for better performance
 *
 * @package ProRank\SEO\Modules\Performance
 * @since   1.0.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Modules\Performance;

use ProRank\SEO\Core\Compatibility\PerformanceCompatibilityRegistry;
use ProRank\SEO\Modules\BaseModule;
use ProRank\SEO\Core\RestApi\ImageOptimizationController;
use ProRank\SEO\Core\ImageOptimization\OptimizationEngine;
use ProRank\SEO\Core\LicenseManager;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}


/**
 * ImageOptimizationModule class
 */


class ImageOptimizationModule extends BaseModule {

    /**
     * Supported image formats
     *
     * @var array
     */
    private const SUPPORTED_FORMATS = ['jpg', 'jpeg', 'png'];

    /**
     * Already-modern formats that can be recompressed during explicit force jobs.
     *
     * @var array<int, string>
     */
    private const FORCE_REOPTIMIZABLE_FORMATS = ['webp', 'avif'];

    /**
     * MIME types that can be optimized by this module.
     *
     * @var array<int, string>
     */
    private const SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/png'];

    /**
     * MIME types that are already modern image originals and do not need conversion.
     *
     * @var array<int, string>
     */
    private const MODERN_ORIGINAL_MIME_TYPES = ['image/webp', 'image/avif', 'image/jxl'];

    /**
     * WebP quality default
     *
     * @var int
     */
    private const DEFAULT_WEBP_QUALITY = 75;

    /**
     * AVIF quality default
     *
     * @var int
     */
    private const DEFAULT_AVIF_QUALITY = 65;

    /**
     * JPEG XL quality default
     *
     * @var int
     */
    private const DEFAULT_JXL_QUALITY = 75;

    /**
     * Additional responsive widths that help browsers choose a closer match
     * when builders emit overly coarse core image candidates.
     *
     * @var array<int, int>
     */
    private const RESPONSIVE_SUBSIZES = [360, 400, 480, 540, 640, 720, 960];

    /**
     * Default number of times a failed attachment can be retried during bulk optimization.
     *
     * @var int
     */
    private const DEFAULT_BULK_RETRY_LIMIT = 3;

    /**
     * Number of early images to force eager loading for (to reduce above-fold CLS).
     *
     * @var int
     */
    private const SKIP_LAZY_IMAGES = 10;

    /**
     * Maximum upload-URL attachment lookups allowed during one frontend render.
     *
     * attachment_url_to_postid() can be expensive on large media libraries. Keep
     * URL fallback bounded so builder-heavy pages cannot turn first render into
     * hundreds of synchronous database scans.
     *
     * @var int
     */
    private const MAX_ATTACHMENT_URL_LOOKUPS_PER_REQUEST = 40;

    /**
     * Maximum responsive-size files generated during one frontend render.
     *
     * Missing sub-sizes are useful, but generating many images plus WebP/AVIF
     * variants during the first uncached request can timeout shared hosting.
     *
     * @var int
     */
    private const MAX_RUNTIME_RESPONSIVE_GENERATIONS_PER_REQUEST = 4;

    /**
     * Output-buffer image counter for early-loading heuristics.
     *
     * @var int
     */
    private int $buffer_image_counter = 0;

    /**
     * Attachment-image counter for request-level LCP prioritization.
     *
     * @var int
     */
    private int $attachment_image_counter = 0;

    /**
     * In-request cache for resolved image dimensions by source URL.
     *
     * @var array<string, array{0:int,1:int}>
     */
    private array $dimension_lookup_cache = [];

    /**
     * In-request cache for attachment alt text lookups.
     *
     * @var array<int, string>
     */
    private array $alt_text_lookup_cache = [];

    /**
     * In-request cache for attachment IDs resolved from upload URLs.
     *
     * @var array<string, int>
     */
    private array $attachment_id_lookup_cache = [];

    /**
     * Number of upload-URL attachment lookups attempted this request.
     *
     * @var int
     */
    private int $attachment_url_lookup_count = 0;

    /**
     * Number of responsive-size generation attempts made this request.
     *
     * @var int
     */
    private int $runtime_responsive_generation_count = 0;

    /**
     * In-request cache for runtime variant generation attempts.
     *
     * @var array<string, bool>
     */
    private array $runtime_variant_cache = [];

    /**
     * Parsed request/image exclusion patterns.
     *
     * @var array<int, string>|null
     */
    private ?array $exclude_path_patterns = null;

    /**
     * Compression types
     *
     * @var array
     */
    private const COMPRESSION_TYPES = [
        'aggressive' => [
            'jpeg_quality' => 70,
            'webp_quality' => 65,
            'avif_quality' => 55,
            'jxl_quality' => 60,
        ],
        'lossy' => [
            'jpeg_quality' => 80,
            'webp_quality' => 75,
            'avif_quality' => 65,
            'jxl_quality' => 75,
        ],
        'glossy' => [
            'jpeg_quality' => 85,
            'webp_quality' => 82,
            'avif_quality' => 75,
            'jxl_quality' => 82,
        ],
        'lossless' => [
            'jpeg_quality' => 100,
            'webp_quality' => 100,
            'avif_quality' => 100,
            'jxl_quality' => 100,
        ],
    ];

    /**
     * Quality values used for responsive sub-size candidates (360/400/480/540/640w etc.).
     *
     * Sub-sizes are small display candidates; browsers pick the closest-fit
     * descriptor, so they can tolerate more aggressive compression than the
     * original upload. Lossless is intentionally absent — the guard in
     * get_subsize_quality() keeps lossless profiles at 100 for every size.
     *
     * @var array<string, array<string, int>>
     */
    private const SUBSIZE_COMPRESSION_TYPES = [
        'aggressive' => [
            'jpeg_quality' => 58,
            'webp_quality' => 50,
            'avif_quality' => 33,
            'jxl_quality' => 50,
        ],
        'lossy' => [
            'jpeg_quality' => 68,
            'webp_quality' => 60,
            'avif_quality' => 40,
            'jxl_quality' => 62,
        ],
        'glossy' => [
            'jpeg_quality' => 76,
            'webp_quality' => 70,
            'avif_quality' => 50,
            'jxl_quality' => 72,
        ],
    ];

    /**
     * Get cloud optimization providers
     *
     * @return array
     */
    private function getCloudProviders(): array {
        return [
            'local' => [
                'name' => 'Local Server',
                'endpoint' => null,
                'features' => ['webp', 'avif', 'jxl'],
            ],
        ];
    }

    /**
     * Resolve a verified JWT token for cloud image operations.
     *
     * @return string|null
     */
    private function get_cloud_auth_token(): ?string {
        return null;
    }

    /**
     * Build cloud auth headers from JWT token.
     *
     * @param string $auth_token
     * @return array
     */
    private function build_cloud_auth_headers(string $auth_token): array {
        $site_url = get_site_url();
        $site_domain = wp_parse_url($site_url, PHP_URL_HOST) ?: '';

        return [
            'X-Site-Url' => $site_url,
            'X-Site-Domain' => $site_domain,
            'Authorization' => 'Bearer ' . $auth_token,
        ];
    }

    /**
     * Constructor
     */
    public function __construct() {
        $this->slug = 'image_optimization';
        $this->name = 'Advanced Image Optimization';
        $this->description = 'Next-gen image optimization with WebP, AVIF, PDF support and AI-powered features';
        $this->feature_tier = 'free';
        $this->parent_slug = 'performance';
    }

    /**
     * Override settings lookup to prefer the dedicated option store.
     *
     * REST controllers persist settings into the `prorank_image_optimization_settings`
     * option. To keep compatibility with the SettingsManager, check that option first
     * and fall back to the base implementation when not present.
     *
     * @param string $key     Setting key.
     * @param mixed  $default Default value.
     * @return mixed
     */
    protected function get_setting(string $key, $default = null) {
        $option_settings = get_option('prorank_image_optimization_settings', []);

        if (is_array($option_settings) && array_key_exists($key, $option_settings)) {
            return $option_settings[$key];
        }

        return parent::get_setting($key, $default);
    }

    /**
     * Initialize module hooks
     *
     * @return void
     */
    public function init_hooks(): void {
        // Always register bulk/background optimization handlers (triggered by REST API)
        // These must be outside should_run() check so bulk optimization works
        add_action('prorank_bulk_optimize_images', [$this, 'process_bulk_optimization']);
        add_action('prorank_optimize_image', [$this, 'process_single_image_optimization']);
        add_action('prorank_bulk_optimization_continue', [$this, 'handle_bulk_optimization_continue'], 10, 2);

        // Check if module should run for other hooks
        if (!$this->should_run()) {
            return;
        }

        $scaled_sizes_enabled = $this->should_enable_scaled_image_sizes();

        if ($scaled_sizes_enabled) {
            add_action('init', [$this, 'register_image_sizes']);
        }

        $frontend_delivery_context = $this->is_frontend_optimization_context();
        $full_optimization_runtime = $this->has_full_optimization_runtime();
        $intrinsic_dimensions_enabled = $this->should_enable_intrinsic_dimensions();
        $autofill_alt_enabled = $frontend_delivery_context && $this->get_setting('autofill_missing_alt', true);
        $global_image_buffer_registered = false;

        // Auto-fill missing alt text from media library metadata.
        if ($autofill_alt_enabled) {
            add_filter('wp_get_attachment_image_attributes', [$this, 'autofill_attachment_image_alt_attribute'], 14, 3);
            add_filter('the_content', [$this, 'autofill_missing_alt_text'], 97);
            add_filter('the_excerpt', [$this, 'autofill_missing_alt_text'], 97);
            add_filter('widget_text', [$this, 'autofill_missing_alt_text'], 97);
            add_filter('widget_custom_html', [$this, 'autofill_missing_alt_text'], 97);
        }

        // Backfill intrinsic dimensions/fetchpriority for attachment-based images
        // (covers builders/widgets that bypass the_content filters).
        if ($frontend_delivery_context && $intrinsic_dimensions_enabled) {
            add_filter('wp_get_attachment_image_attributes', [$this, 'optimize_attachment_image_attributes'], 15, 3);
            add_filter('the_content', [$this, 'ensure_content_image_dimensions'], 98);
            add_filter('the_excerpt', [$this, 'ensure_content_image_dimensions'], 98);
            add_filter('widget_text', [$this, 'ensure_content_image_dimensions'], 98);
            add_filter('widget_custom_html', [$this, 'ensure_content_image_dimensions'], 98);
            add_action('wp_footer', [$this, 'print_intrinsic_dimension_restorer'], 100);
        }

        if ($frontend_delivery_context && !$full_optimization_runtime && ($intrinsic_dimensions_enabled || $scaled_sizes_enabled)) {
            add_action('template_redirect', [$this, 'start_dimension_buffer'], 1);
            $global_image_buffer_registered = true;
        }

        if (!$full_optimization_runtime) {
            if ($autofill_alt_enabled && !$global_image_buffer_registered) {
                add_action('template_redirect', [$this, 'start_alt_text_buffer'], 1);
            }
            return;
        }

        // Hook into image upload for auto-optimization
        if ($this->get_setting('optimize_on_upload', true)) {
            add_filter('wp_generate_attachment_metadata', [$this, 'generate_optimized_images'], 10, 2);
        }

        // Hook to delete optimized images when original is deleted
        add_action('delete_attachment', [$this, 'delete_optimized_images']);

        // Hook for PNG to JPEG conversion
        if ($this->get_setting('png_to_jpeg', false)) {
            add_filter('wp_generate_attachment_metadata', [$this, 'convert_png_to_jpeg'], 9, 2);
        }

        // Hook for CMYK to RGB conversion
        if ($this->get_setting('cmyk_to_rgb', true)) {
            add_filter('wp_generate_attachment_metadata', [$this, 'convert_cmyk_to_rgb'], 8, 2);
        }

        // Hook for EXIF data management
        if ($this->get_setting('remove_exif', true)) {
            add_filter('wp_generate_attachment_metadata', [$this, 'manage_exif_data'], 7, 2);
        }

        // Hook for PDF optimization
        if ($this->get_setting('optimize_pdf', false)) {
            add_filter('wp_generate_attachment_metadata', [$this, 'optimize_pdf'], 10, 2);
        }

        // Hook for retina image generation
        if ($this->get_setting('generate_retina', false)) {
            add_filter('wp_generate_attachment_metadata', [$this, 'generate_retina_images'], 11, 2);
        }

        // Hook to backfill any WordPress-registered subsizes that are missing
        if ($this->get_setting('auto_generate_missing_sizes', false)) {
            add_filter('wp_generate_attachment_metadata', [$this, 'fill_missing_subsizes'], 12, 2);
        }

        // Configurable threshold for WordPress to rescale oversized originals
        if ((int) $this->get_setting('oversized_threshold', 0) > 0) {
            add_filter('big_image_size_threshold', [$this, 'filter_big_image_threshold'], 10, 4);
        }

        // Hook for AI training protection
        if ($this->get_setting('no_ai_training', false)) {
            add_action('add_attachment', [$this, 'add_ai_training_protection']);
            if ($frontend_delivery_context) {
                add_filter('wp_get_attachment_image_attributes', [$this, 'add_noai_attributes'], 10, 3);
            }
        }

        // Hook for processing failed optimization queue
        add_action('prorank_retry_failed_optimizations', [$this, 'process_failed_optimization_queue']);

        // Get delivery method
        $delivery_method = $this->get_setting('delivery_method', 'picture_hooks');

        // 'picture' should use output buffering to catch ALL images (including Elementor)
        // 'picture_hooks' only works for images in the_content, which Elementor bypasses
        if ($delivery_method === 'picture') {
            $delivery_method = 'picture_global';
        }

        if ($frontend_delivery_context && $delivery_method === 'picture_hooks') {
            // Filter to convert img to picture element (only for hooks mode)
            add_filter('wp_get_attachment_image', [$this, 'convert_to_picture_element'], 10, 5);

            // Filter for post content images
            add_filter('the_content', [$this, 'convert_content_images'], 99);

            // Filter for excerpt
            add_filter('the_excerpt', [$this, 'convert_content_images'], 99);

            // Filter for widget text
            add_filter('widget_text', [$this, 'convert_content_images'], 99);

            // Filter for custom HTML widget
            add_filter('widget_custom_html', [$this, 'convert_content_images'], 99);
        } elseif ($frontend_delivery_context && $delivery_method === 'picture_global') {
            // Process entire output buffer - this catches ALL images including Elementor
            // Don't add wp_get_attachment_image filter to avoid double-wrapping
            add_action('template_redirect', [$this, 'start_output_buffer'], 1);
            $global_image_buffer_registered = true;
        } elseif ($frontend_delivery_context && $delivery_method === 'accept_header') {
            // Server-side Accept header detection - replaces URLs directly
            // Works for CSS backgrounds and all images, not just img tags
            add_action('template_redirect', [$this, 'start_accept_header_buffer'], 1);
            $global_image_buffer_registered = true;
        }

        if ($this->should_manage_htaccess_delivery_layer($delivery_method)) {
            // Own direct URL / CSS background delivery too. Picture mode only rewrites
            // markup, so stale .htaccess rules can otherwise keep serving WebP first.
            add_action('init', [$this, 'setup_htaccess_rules']);
        }

        if ($autofill_alt_enabled && !$global_image_buffer_registered) {
            add_action('template_redirect', [$this, 'start_alt_text_buffer'], 1);
        }

        // Client Hints support (response headers for DPR/Width/Viewport)
        if ($frontend_delivery_context && $this->get_setting('client_hints_enabled', false)) {
            add_action('send_headers', [$this, 'send_client_hints_headers']);
        }

        // AJAX handler for bulk conversion
        add_action('wp_ajax_prorank_convert_images', [$this, 'handle_bulk_conversion']);

        // Note: prorank_optimize_image action is registered above should_run() check
        // to ensure Action Scheduler actions work even when module conditionals fail

        // Add rewrite rules for serving optimized images
        add_action('init', [$this, 'add_rewrite_rules']);

        // Schedule background optimization cron if enabled
        if ($this->get_setting('background_mode', false)) {
            add_action('init', [$this, 'schedule_background_optimization']);
            add_action('prorank_background_image_optimization', [$this, 'run_background_optimization']);
        }

        // Custom media folders admin menu
        if ($this->get_setting('custom_media_folders', false)) {
            add_action('admin_menu', [$this, 'add_custom_media_menu']);
        }

        // CDN integration
        $cdn_delivery_enabled = !empty($this->get_setting('cdn_url', ''))
            && (
                $this->get_setting('cdn_enabled', false)
                || $delivery_method === 'cdn'
            );

        if ($frontend_delivery_context && $cdn_delivery_enabled) {
            add_filter('wp_get_attachment_url', [$this, 'rewrite_cdn_url'], 10, 2);
            add_filter('wp_calculate_image_srcset', [$this, 'rewrite_srcset_cdn'], 10, 5);
            add_filter('the_content', [$this, 'rewrite_content_cdn_urls'], 100);
        }
    }

    /**
     * Restrict HTML/image delivery mutations to real frontend page renders.
     *
     * Builder/admin/REST/AJAX contexts can persist transformed markup back into
     * stored content, which must never happen.
     *
     * @return bool
     */
    private function is_frontend_optimization_context(): bool {
        if (is_admin() || wp_doing_ajax() || wp_doing_cron()) {
            return false;
        }

        if ((defined('REST_REQUEST') && REST_REQUEST) || (defined('XMLRPC_REQUEST') && XMLRPC_REQUEST)) {
            return false;
        }

        if (defined('WP_CLI') && WP_CLI) {
            return false;
        }

        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
            return false;
        }

        return true;
    }

    /**
     * Optimize attachment image attributes at render time.
     *
     * Ensures width/height exist for CLS stability and adds a conservative
     * LCP hint (fetchpriority=high + eager) on the first eligible content image.
     *
     * @param array        $attr       Image attributes.
     * @param \WP_Post|int $attachment Attachment object or ID.
     * @param string|array $size       Requested size.
     * @return array
     */
    public function optimize_attachment_image_attributes(array $attr, $attachment, $size): array {
        if (!$this->is_frontend_optimization_context()) {
            return $attr;
        }

        $width = isset($attr['width']) ? (int) $attr['width'] : 0;
        $height = isset($attr['height']) ? (int) $attr['height'] : 0;

        $attachment_id = 0;
        if ($attachment instanceof \WP_Post) {
            $attachment_id = (int) $attachment->ID;
        } elseif (is_numeric($attachment)) {
            $attachment_id = (int) $attachment;
        }

        if (($width <= 0 || $height <= 0) && $attachment_id > 0) {
            $image_data = wp_get_attachment_image_src($attachment_id, $size ?: 'full');
            if (is_array($image_data)) {
                $resolved_width = isset($image_data[1]) ? (int) $image_data[1] : 0;
                $resolved_height = isset($image_data[2]) ? (int) $image_data[2] : 0;
                if ($resolved_width > 0 && $resolved_height > 0) {
                    $attr['width'] = $resolved_width;
                    $attr['height'] = $resolved_height;
                }
            }
        }

        $attr = $this->append_intrinsic_dimension_data_attrs($attr);

        if (!$this->is_lcp_fetchpriority_enabled()) {
            return $attr;
        }

        $class_value = strtolower((string) ($attr['class'] ?? ''));
        $alt_value = strtolower((string) ($attr['alt'] ?? ''));
        $src_value = strtolower((string) ($attr['src'] ?? ''));
        $is_branding_asset = false;
        foreach (['logo', 'site-icon', 'favicon', 'avatar'] as $needle) {
            if (
                ($class_value !== '' && strpos($class_value, $needle) !== false)
                || ($alt_value !== '' && strpos($alt_value, $needle) !== false)
                || ($src_value !== '' && strpos($src_value, $needle) !== false)
            ) {
                $is_branding_asset = true;
                break;
            }
        }

        if ($is_branding_asset) {
            return $attr;
        }

        if ($this->should_defer_fetchpriority_assignment_to_lcp_modules()) {
            return $attr;
        }

        if ($this->attachment_image_counter === 0) {
            if (empty($attr['fetchpriority'])) {
                $attr['fetchpriority'] = 'high';
            }
            if (($attr['loading'] ?? '') === 'lazy') {
                $attr['loading'] = 'eager';
            }
        }
        $this->attachment_image_counter++;

        return $attr;
    }

    /**
     * Check if fetchpriority hints are enabled through asset optimization settings.
     *
     * @return bool
     */
    private function is_lcp_fetchpriority_enabled(): bool {
        $settings = get_option('prorank_asset_optimization_settings', []);
        if (!is_array($settings)) {
            return false;
        }

        foreach (['lcp_fetchpriority_enabled', 'lcp_optimizer_enabled', 'lcp_optimize_first_image'] as $key) {
            $value = $settings[$key] ?? false;
            if ($value === true || $value === 1 || $value === '1' || $value === 'true' || $value === 'yes' || $value === 'on') {
                return true;
            }
        }

        return false;
    }

    /**
     * Let dedicated LCP modules own priority selection when a known target exists.
     *
     * @return bool
     */
    private function should_defer_fetchpriority_assignment_to_lcp_modules(): bool {
        if (!empty($GLOBALS['prorank_lcp_image_url']) && is_string($GLOBALS['prorank_lcp_image_url'])) {
            return true;
        }

        return $this->get_known_lcp_image_url_for_current_request() !== '';
    }

    /**
     * Resolve the current request's known LCP asset from preload, optimizer, or beacon data.
     *
     * @return string
     */
    private function get_known_lcp_image_url_for_current_request(): string {
        $preloaded = $GLOBALS['prorank_preloaded_urls'] ?? [];
        if (is_array($preloaded)) {
            foreach ($preloaded as $entry) {
                if (!is_string($entry) || trim($entry) === '') {
                    continue;
                }

                $candidate = trim((string) explode('|', $entry, 2)[0]);
                if (preg_match('/\.(avif|webp|jpe?g|png|gif|svg)(\?|$)/i', $candidate) === 1) {
                    return $candidate;
                }
            }
        }

        $optimizer_data = get_option('prorank_lcp_optimizer_data', []);
        if (is_array($optimizer_data)) {
            $page_key = $this->get_current_lcp_page_template_key();
            $entry = $page_key !== '' ? ($optimizer_data[$page_key] ?? null) : null;
            if (is_array($entry) && !empty($entry['url']) && is_string($entry['url'])) {
                return esc_url_raw($entry['url']);
            }
        }

        $beacon_data = get_option('prorank_lcp_beacon_data', []);
        if (is_array($beacon_data)) {
            $pattern = $this->get_current_lcp_url_pattern();
            $entry = $pattern !== '' ? ($beacon_data[$pattern] ?? null) : null;
            if (is_array($entry)) {
                $background = isset($entry['background_image']) && is_string($entry['background_image']) ? esc_url_raw($entry['background_image']) : '';
                if ($background !== '') {
                    return $background;
                }

                $src = isset($entry['src']) && is_string($entry['src']) ? esc_url_raw($entry['src']) : '';
                if ($src !== '') {
                    return $src;
                }
            }
        }

        return '';
    }

    /**
     * Match the LCP optimizer page-key scheme for the current request.
     *
     * @return string
     */
    private function get_current_lcp_page_template_key(): string {
        if (is_front_page()) {
            return 'front_page';
        }
        if (is_home()) {
            return 'blog_home';
        }
        if (is_singular('post')) {
            return 'single_post';
        }
        if (is_singular('page')) {
            $template = get_page_template_slug();
            return 'page_' . ($template ?: 'default');
        }
        if (is_singular()) {
            return 'single_' . get_post_type();
        }
        if (is_archive()) {
            if (is_category()) {
                return 'archive_category';
            }
            if (is_tag()) {
                return 'archive_tag';
            }
            if (is_author()) {
                return 'archive_author';
            }
            return 'archive_' . (get_post_type() ?: 'post');
        }
        if (is_search()) {
            return 'search';
        }
        if (is_404()) {
            return '404';
        }

        return 'other';
    }

    /**
     * Match the beacon module URL-pattern scheme for the current request.
     *
     * @return string
     */
    private function get_current_lcp_url_pattern(): string {
        $url = $this->get_current_request_url();
        if ($url === '') {
            return '';
        }

        $parsed = wp_parse_url($url);
        $path = $parsed['path'] ?? '/';
        $path = trim((string) $path, '/');
        if ($path === '') {
            return 'home';
        }

        $path = preg_replace('/\/\d+\//', '/*/', $path);
        $path = preg_replace('/\/\d+$/', '/*', $path);
        $parts = explode('/', $path);
        $parts = array_slice($parts, 0, 3);

        return implode('/', $parts);
    }

    /**
     * Build the current request URL for beacon-style lookup.
     *
     * @return string
     */
    private function get_current_request_url(): string {
        $protocol = is_ssl() ? 'https' : 'http';
        $host = $_SERVER['HTTP_HOST'] ?? '';
        $uri = $_SERVER['REQUEST_URI'] ?? '';

        if (!is_string($host) || !is_string($uri) || $host === '') {
            return '';
        }

        return $protocol . '://' . $host . $uri;
    }

    /**
     * Check if module should run
     *
     * @return bool
     */
    private function should_run(): bool {
        // Check if module is enabled
        if (!$this->is_enabled()) {
            return false;
        }

        if (\ProRank\SEO\Plugin::is_elementor_runtime_request()) {
            return false;
        }

        return $this->has_full_optimization_runtime()
            || $this->should_enable_intrinsic_dimensions()
            || $this->should_enable_scaled_image_sizes();
    }

    /**
     * Whether intrinsic dimensions support should run on the frontend.
     *
     * This can be enabled explicitly as a standalone low-risk feature, or
     * implicitly when the full image optimization runtime is active.
     *
     * @return bool
     */
    private function should_enable_intrinsic_dimensions(): bool {
        return $this->get_setting('image_dimensions_enabled', false)
            || $this->has_full_optimization_runtime();
    }

    /**
     * Whether responsive sub-sizes should be registered even when modern
     * format delivery is disabled.
     *
     * @return bool
     */
    private function should_enable_scaled_image_sizes(): bool {
        return $this->get_setting('serve_scaled_images', true)
            || $this->get_setting('enable_responsive_images', true);
    }

    /**
     * Determine whether the full image optimization runtime should boot.
     *
     * @return bool
     */
    private function has_full_optimization_runtime(): bool {
        $cdn_requested = !empty($this->get_setting('cdn_url', ''))
            && (
                $this->get_setting('cdn_enabled', false)
                || $this->get_setting('delivery_method', 'picture') === 'cdn'
            );

        return $this->get_setting('webp_enabled', false) ||
               $this->get_setting('avif_enabled', false) ||
               $this->get_setting('jxl_enabled', false) ||
               $this->get_setting('optimize_on_upload', false) ||
               $this->get_setting('background_mode', false) ||
               $cdn_requested;
    }

    /**
     * Backfill missing alt text in generic content/html strings.
     *
     * @param string $content
     * @return string
     */
    public function autofill_missing_alt_text(string $content): string {
        if ($content === '' || stripos($content, '<img') === false) {
            return $content;
        }

        return (string) preg_replace_callback(
            '/<img\b[^>]*>/i',
            fn(array $matches): string => $this->autofill_missing_alt_in_img_tag((string) ($matches[0] ?? '')),
            $content
        ) ?: $content;
    }

    /**
     * Fill empty attachment-rendered alt attributes from media metadata.
     *
     * @param array<string, mixed> $attr
     * @param \WP_Post|int        $attachment
     * @param string|int[]        $size
     * @return array<string, mixed>
     */
    public function autofill_attachment_image_alt_attribute(array $attr, $attachment, $size): array {
        $current_alt = isset($attr['alt']) ? trim((string) $attr['alt']) : '';
        if ($current_alt !== '') {
            return $attr;
        }

        $attachment_id = $attachment instanceof \WP_Post ? (int) $attachment->ID : (int) $attachment;
        $alt = $this->get_attachment_alt_text($attachment_id);
        if ($alt !== '') {
            $attr['alt'] = $alt;
        }

        return $attr;
    }

    /**
     * Start a lightweight output buffer that only backfills missing image alt text.
     *
     * @return void
     */
    public function start_alt_text_buffer(): void {
        if (!$this->is_frontend_optimization_context()) {
            return;
        }

        ob_start([$this, 'process_alt_text_buffer']);
    }

    /**
     * Process raw HTML and backfill missing image alt text on local media images.
     *
     * @param string $buffer
     * @return string
     */
    public function process_alt_text_buffer(string $buffer): string {
        if (!$this->is_frontend_optimization_context() || stripos($buffer, '<img') === false) {
            return $buffer;
        }

        if (!preg_match('/<html/i', $buffer) || is_admin() || $this->is_current_request_excluded()) {
            return $buffer;
        }

        return $this->autofill_missing_alt_text($buffer);
    }

    /**
     * Backfill a single img tag when it points at an attachment with media alt text.
     *
     * @param string $img_html
     * @return string
     */
    private function autofill_missing_alt_in_img_tag(string $img_html): string {
        if ($img_html === '' || stripos($img_html, '<img') === false) {
            return $img_html;
        }

        if ($this->image_tag_has_non_empty_alt($img_html)) {
            return $img_html;
        }

        $attachment_id = $this->resolve_attachment_id_from_img_tag($img_html);
        if ($attachment_id <= 0) {
            return $img_html;
        }

        $alt = $this->get_attachment_alt_text($attachment_id);
        if ($alt === '') {
            return $img_html;
        }

        return $this->replace_or_insert_alt_attribute($img_html, $alt);
    }

    /**
     * Check whether an img tag already has meaningful alt text.
     *
     * @param string $img_html
     * @return bool
     */
    private function image_tag_has_non_empty_alt(string $img_html): bool {
        if (preg_match('/(?<![\w:-])alt\s*=\s*(["\'])(.*?)\1/i', $img_html, $match)) {
            return trim(html_entity_decode((string) ($match[2] ?? ''), ENT_QUOTES, 'UTF-8')) !== '';
        }

        if (preg_match('/(?<![\w:-])alt\s*=\s*([^\s>]+)/i', $img_html, $match)) {
            return trim(html_entity_decode((string) ($match[1] ?? ''), ENT_QUOTES, 'UTF-8')) !== '';
        }

        return false;
    }

    /**
     * Resolve an attachment ID from image classes or upload URL.
     *
     * @param string $img_html
     * @return int
     */
    private function resolve_attachment_id_from_img_tag(string $img_html): int {
        $attributes = $this->extract_img_attributes($img_html);
        if ($attributes === '') {
            return 0;
        }

        $attachment_id = $this->extract_attachment_id_from_img_attributes($attributes);
        if ($attachment_id > 0) {
            return $attachment_id;
        }

        $src = $this->extract_attribute_value($attributes, 'src');
        if ($src === '') {
            $src = $this->extract_attribute_value($attributes, 'data-src');
        }
        if ($src === '') {
            $src = $this->extract_attribute_value($attributes, 'data-lazy-src');
        }
        if ($src === '' || stripos($src, 'data:') === 0) {
            return 0;
        }

        $src = html_entity_decode($src, ENT_QUOTES, 'UTF-8');
        $src = preg_replace('/[#?].*$/', '', $src) ?: $src;
        $upload_dir = wp_get_upload_dir();
        $upload_url = (string) ($upload_dir['baseurl'] ?? '');
        if ($upload_url !== '' && strpos($src, $upload_url) === false) {
            return 0;
        }

        return $this->lookup_attachment_id_by_upload_url($src);
    }

    /**
     * Resolve an attachment ID from a local uploads URL with per-request bounds.
     *
     * @param string $src Local upload URL without query/hash.
     * @return int
     */
    private function lookup_attachment_id_by_upload_url(string $src): int {
        if ($src === '' || !function_exists('attachment_url_to_postid')) {
            return 0;
        }

        $cache_key = strtolower($src);
        if (array_key_exists($cache_key, $this->attachment_id_lookup_cache)) {
            return $this->attachment_id_lookup_cache[$cache_key];
        }

        if ($this->attachment_url_lookup_count >= self::MAX_ATTACHMENT_URL_LOOKUPS_PER_REQUEST) {
            $this->attachment_id_lookup_cache[$cache_key] = 0;
            return 0;
        }

        $candidates = [];
        $clean_src = preg_replace('/-\d+x\d+(?=\.(?:avif|webp|jxl|jpe?g|png|gif)$)/i', '', $src);
        if (is_string($clean_src) && $clean_src !== $src) {
            // Sub-sized URLs are common in builder markup. Try the parent URL
            // first to avoid an unnecessary failing lookup.
            $candidates[] = $clean_src;
        }
        $candidates[] = $src;
        $candidates = array_values(array_unique($candidates));

        foreach ($candidates as $candidate) {
            if ($this->attachment_url_lookup_count >= self::MAX_ATTACHMENT_URL_LOOKUPS_PER_REQUEST) {
                break;
            }

            $this->attachment_url_lookup_count++;
            $attachment_id = (int) attachment_url_to_postid($candidate);
            if ($attachment_id > 0) {
                $this->attachment_id_lookup_cache[$cache_key] = $attachment_id;
                $this->attachment_id_lookup_cache[strtolower($candidate)] = $attachment_id;
                return $attachment_id;
            }
        }

        $this->attachment_id_lookup_cache[$cache_key] = 0;
        return 0;
    }

    /**
     * Read attachment alt text with a per-request cache.
     *
     * @param int $attachment_id
     * @return string
     */
    private function get_attachment_alt_text(int $attachment_id): string {
        if ($attachment_id <= 0) {
            return '';
        }

        if (array_key_exists($attachment_id, $this->alt_text_lookup_cache)) {
            return $this->alt_text_lookup_cache[$attachment_id];
        }

        $alt = trim(wp_strip_all_tags((string) get_post_meta($attachment_id, '_wp_attachment_image_alt', true)));
        $this->alt_text_lookup_cache[$attachment_id] = $alt;

        return $alt;
    }

    /**
     * Replace an empty alt attribute or insert one when it is absent.
     *
     * @param string $img_html
     * @param string $alt
     * @return string
     */
    private function replace_or_insert_alt_attribute(string $img_html, string $alt): string {
        $escaped = esc_attr($alt);
        $quoted_pattern = '/(?<![\w:-])alt\s*=\s*(["\'])(.*?)\1/i';
        if (preg_match($quoted_pattern, $img_html)) {
            return preg_replace($quoted_pattern, 'alt="' . $escaped . '"', $img_html, 1) ?: $img_html;
        }

        $unquoted_pattern = '/(?<![\w:-])alt\s*=\s*[^\s>]*/i';
        if (preg_match($unquoted_pattern, $img_html)) {
            return preg_replace($unquoted_pattern, 'alt="' . $escaped . '"', $img_html, 1) ?: $img_html;
        }

        return preg_replace('/<img\b/i', '<img alt="' . $escaped . '"', $img_html, 1) ?: $img_html;
    }

    /**
     * Backfill intrinsic dimensions in generic content/html strings.
     *
     * This mirrors the low-risk image-dimension approach used by competitors:
     * mutate only local <img> tags and leave delivery/format rewriting off.
     *
     * @param string $content
     * @return string
     */
    public function ensure_content_image_dimensions(string $content): string {
        if (!$this->is_frontend_optimization_context() || stripos($content, '<img') === false) {
            return $content;
        }

        return preg_replace_callback(
            '/<img\b[^>]*>/i',
            fn(array $matches): string => $this->ensure_intrinsic_dimensions($matches[0]),
            $content
        ) ?: $content;
    }

    /**
     * Start a lightweight output buffer that only backfills intrinsic dimensions.
     *
     * @return void
     */
    public function start_dimension_buffer(): void {
        if (!$this->is_frontend_optimization_context()) {
            return;
        }

        ob_start([$this, 'process_dimension_buffer']);
    }

    /**
     * Process raw HTML and backfill intrinsic dimensions on local images.
     *
     * @param string $buffer
     * @return string
     */
    public function process_dimension_buffer(string $buffer): string {
        if (stripos($buffer, '<img') === false) {
            return $buffer;
        }

        return $this->refresh_buffer_inline_images($buffer);
    }

    /**
     * Refresh inline image markup from live attachment metadata.
     *
     * This keeps builder-saved HTML in sync when image sub-sizes or srcset
     * candidates change after the original markup was stored.
     *
     * @param string $img_html
     * @return string
     */
    private function refresh_inline_image_markup(string $img_html, string $sizes_override = ''): string {
        $img_html = $this->autofill_missing_alt_in_img_tag($img_html);
        $img_html = $this->ensure_intrinsic_dimensions($img_html);

        if (!$this->should_enable_scaled_image_sizes()) {
            return $img_html;
        }

        $attributes = $this->extract_img_attributes($img_html);
        if ($attributes === '') {
            return $img_html;
        }

        $resolved_from_url = false;
        $attachment_id = $this->extract_attachment_id_from_img_attributes($attributes);
        if ($attachment_id <= 0) {
            // Elementor (e-image-base) and other builders frequently omit the
            // wp-image-<id> class. Fall back to resolving the attachment from the
            // upload URL so those images still receive ProRank responsive
            // sub-sizes (and WebP/AVIF) instead of the legacy WordPress ladder.
            $attachment_id = $this->resolve_attachment_id_from_img_tag($img_html);
            $resolved_from_url = $attachment_id > 0;
        }
        if ($attachment_id <= 0) {
            return $img_html;
        }

        $size_hint = $this->extract_size_hint_from_img_attributes($attributes);
        $sizes = $sizes_override !== '' ? $sizes_override : wp_get_attachment_image_sizes($attachment_id, $size_hint);
        $sizes = is_string($sizes) ? $sizes : '';
        if ($sizes_override === '') {
            $builder_fallback_sizes = $this->get_builder_fallback_sizes_for_url_resolved_image($attributes);
            if ($sizes === '' && $resolved_from_url) {
                $sizes = $builder_fallback_sizes;
            } elseif (
                $builder_fallback_sizes !== ''
                && (
                    $this->sizes_value_is_intrinsic_full_width($sizes, $this->get_attachment_source_width($attachment_id))
                    || $this->sizes_value_is_broad_large($sizes)
                )
            ) {
                $sizes = $builder_fallback_sizes;
            }
        }

        // Builder images (e.g. Elementor flex containers) often emit a broad
        // sizes="100vw" even when shown in a much smaller slot. For below-fold
        // images whose rendered slot is well under the source width, constrain
        // the advertised sizes so the browser stops over-selecting candidates.
        if ($sizes_override === '') {
            $constrained_sizes = $this->maybe_constrain_sizes_to_render_width(
                $img_html,
                $attributes,
                $sizes,
                $attachment_id
            );
            if ($constrained_sizes !== '') {
                $sizes = $constrained_sizes;
            }
        }

        $skip_delivery_rewrite = $this->should_skip_runtime_image_rewrite($img_html, $attributes);

        $srcset = wp_get_attachment_image_srcset($attachment_id, $size_hint);
        if (is_string($srcset) && $srcset !== '') {
            if ($resolved_from_url && $sizes === '') {
                return $img_html;
            }

            if (!$skip_delivery_rewrite) {
                $srcset = $this->augment_srcset_with_runtime_responsive_subsizes(
                    $srcset,
                    $attachment_id,
                    $sizes
                );
            }
            $srcset = $this->prune_oversized_srcset_candidates($srcset, $img_html);

            // Below-fold mobile images: cap candidates to the mobile slot when
            // it is cache-safe to do so. Never empties the srcset (the smallest
            // candidate is always kept).
            if ($this->mobile_srcset_constraint_is_safe()) {
                $mobile_srcset = $this->get_mobile_constrained_srcset(
                    $srcset,
                    $sizes,
                    $attributes
                );
                if ($mobile_srcset !== '') {
                    $srcset = $mobile_srcset;
                }
            }

            $img_html = $this->replace_or_insert_attribute($img_html, 'srcset', $srcset);
        }

        if ($sizes !== '') {
            $img_html = $this->replace_or_insert_attribute($img_html, 'sizes', $sizes);
        }

        return $img_html;
    }

    /**
     * Safe sizes fallback for builder images resolved from URL rather than a
     * WordPress image class. Only lazy, non-LCP images get this treatment; if we
     * cannot provide a sizes value, the URL-resolved srcset rewrite is skipped.
     *
     * @param string $attributes Image attributes.
     * @return string
     */
    private function get_builder_fallback_sizes_for_url_resolved_image(string $attributes): string {
        $loading = strtolower($this->extract_attribute_value($attributes, 'loading'));
        if ($loading === 'eager' || ($loading !== '' && $loading !== 'lazy')) {
            return '';
        }
        if (strtolower($this->extract_attribute_value($attributes, 'fetchpriority')) === 'high') {
            return '';
        }

        $class = strtolower($this->extract_attribute_value($attributes, 'class'));
        $looks_like_builder = $class !== '' && (
            strpos($class, 'e-image-base') !== false
            || strpos($class, 'elementor') !== false
            || strpos($class, 'swiper-slide-image') !== false
        );
        $looks_like_standard_large = preg_match('/\bwp-image-\d+\b/i', $class)
            && (
                strpos($class, 'attachment-large') !== false
                || strpos($class, 'size-large') !== false
                || strpos($class, 'attachment-full') !== false
                || strpos($class, 'size-full') !== false
            );
        if (!$looks_like_builder && !$looks_like_standard_large && stripos($attributes, 'data-e-type=') === false) {
            return '';
        }

        return (string) apply_filters(
            'prorank_image_builder_fallback_sizes',
            '(max-width: 767px) 400px, (max-width: 1200px) 50vw, 800px'
        );
    }

    private function sizes_value_is_intrinsic_full_width(string $sizes, int $source_width): bool {
        $sizes = trim(html_entity_decode($sizes, ENT_QUOTES, 'UTF-8'));
        if ($sizes === '' || stripos($sizes, '100vw') === false) {
            return false;
        }

        if (preg_match('/^\(max-width:\s*(\d+)px\)\s*100vw,\s*(\d+)px$/i', $sizes, $match)) {
            $breakpoint = (int) ($match[1] ?? 0);
            $fallback   = (int) ($match[2] ?? 0);

            if ($breakpoint > 0 && $fallback > 0 && $breakpoint === $fallback) {
                return $source_width <= 0 || abs($breakpoint - $source_width) <= 4;
            }
        }

        if (preg_match('/(?:^|,)\s*(\d+)px\s*$/i', $sizes, $match)) {
            $fallback = (int) ($match[1] ?? 0);
            return $source_width > 0 && abs($fallback - $source_width) <= 4;
        }

        return false;
    }

    private function sizes_value_is_broad_large(string $sizes): bool {
        $sizes = trim(html_entity_decode($sizes, ENT_QUOTES, 'UTF-8'));
        if ($sizes === '' || stripos($sizes, '100vw') === false) {
            return false;
        }

        if (preg_match('/max-width:\s*(\d+)px/i', $sizes, $match) && (int) ($match[1] ?? 0) >= 640) {
            return true;
        }

        if (preg_match('/(?:^|,)\s*(\d+)px\s*$/i', $sizes, $match) && (int) ($match[1] ?? 0) >= 640) {
            return true;
        }

        return false;
    }

    /**
     * Constrain an over-broad "100vw" sizes attribute to the rendered slot for
     * below-fold (lazy) builder images whose display width is well under the
     * source width. Eager / LCP / hero images are never touched.
     *
     * @param string $img_html      Image tag.
     * @param string $attributes    Pre-extracted attribute string.
     * @param string $sizes         Current sizes value.
     * @param int    $attachment_id Resolved attachment ID.
     * @return string Empty when no safe constraint applies.
     */
    private function maybe_constrain_sizes_to_render_width(string $img_html, string $attributes, string $sizes, int $attachment_id): string {
        if (strtolower($this->extract_attribute_value($attributes, 'loading')) !== 'lazy') {
            return '';
        }
        if (strtolower($this->extract_attribute_value($attributes, 'fetchpriority')) === 'high') {
            return '';
        }
        // Only narrow genuinely broad sizes; respect explicit narrow values.
        if ($sizes !== '' && stripos($sizes, '100vw') === false) {
            return '';
        }

        [$render_width] = $this->extract_dimensions_from_img_tag($img_html);
        if ($render_width <= 0) {
            return '';
        }

        // Guard against builders that emit the intrinsic width as the width
        // attribute: only constrain when the rendered width is clearly a
        // smaller slot than the source image.
        $source_width = $this->get_attachment_source_width($attachment_id);
        if ($source_width > 0 && $render_width >= (int) floor($source_width * 0.6)) {
            return '';
        }

        return (string) apply_filters(
            'prorank_image_constrained_sizes',
            sprintf('(max-width: %1$dpx) 100vw, %1$dpx', $render_width),
            $render_width,
            $attachment_id
        );
    }

    /**
     * Source (full) width for an attachment, 0 when unavailable.
     *
     * @param int $attachment_id Attachment ID.
     * @return int
     */
    private function get_attachment_source_width(int $attachment_id): int {
        if ($attachment_id <= 0 || !function_exists('wp_get_attachment_metadata')) {
            return 0;
        }
        $metadata = wp_get_attachment_metadata($attachment_id);
        return is_array($metadata) ? max(0, (int) ($metadata['width'] ?? 0)) : 0;
    }

    /**
     * Whether it is cache-safe to apply a mobile-only srcset constraint on this
     * request. Only on real mobile requests, and only when the page cache will
     * not serve the mobile markup to desktop (mobile_separate on, or caching off).
     *
     * @return bool
     */
    private function mobile_srcset_constraint_is_safe(): bool {
        if (!function_exists('wp_is_mobile') || !wp_is_mobile()) {
            return false;
        }
        if (!(bool) apply_filters('prorank_image_mobile_srcset_cap', true)) {
            return false;
        }

        $cache = get_option('prorank_cache_settings', []);
        if (!is_array($cache)) {
            return true;
        }
        $cache_active = !empty($cache['cache_enabled']) || !empty($cache['enabled']);
        if (!$cache_active) {
            return true; // no shared HTML cache to fragment.
        }
        return !empty($cache['mobile_separate']);
    }

    /**
     * Extract attachment ID from an image class list.
     *
     * @param string $attributes
     * @return int
     */
    private function extract_attachment_id_from_img_attributes(string $attributes): int {
        if (!preg_match('/\bwp-image-(\d+)\b/i', $attributes, $matches)) {
            return 0;
        }

        return (int) ($matches[1] ?? 0);
    }

    /**
     * Infer the requested size name from image classes.
     *
     * @param string $attributes
     * @return string
     */
    private function extract_size_hint_from_img_attributes(string $attributes): string {
        if (!preg_match('/\bsize-([a-z0-9_-]+)\b/i', $attributes, $matches)) {
            return 'full';
        }

        $size = strtolower((string) ($matches[1] ?? 'full'));
        return $size !== '' ? $size : 'full';
    }

    /**
     * Replace or insert an HTML attribute on an img tag.
     *
     * @param string $img_html
     * @param string $attribute
     * @param string $value
     * @return string
     */
    private function replace_or_insert_attribute(string $img_html, string $attribute, string $value): string {
        $escaped = esc_attr($value);
        $pattern = '/\b' . preg_quote($attribute, '/') . '\s*=\s*(["\']).*?\1/i';

        if (preg_match($pattern, $img_html)) {
            return (string) preg_replace($pattern, $attribute . '="' . $escaped . '"', $img_html, 1);
        }

        return preg_replace('/<img\b/i', '<img ' . $attribute . '="' . $escaped . '"', $img_html, 1) ?: $img_html;
    }

    /**
     * Drop oversized srcset candidates for explicitly-sized lazy images.
     *
     * Browsers on high-DPR mobile screens can choose a full-size candidate even
     * when the markup requested a small rendered image. That keeps WebP delivery
     * working but still triggers PageSpeed "larger than displayed dimensions"
     * diagnostics. For lazy images with a concrete width attribute, cap srcset
     * candidates to the requested rendered width plus conservative tolerance.
     *
     * @param string $srcset   Candidate list.
     * @param string $img_html Current img tag.
     * @return string
     */
    private function prune_oversized_srcset_candidates(string $srcset, string $img_html): string {
        if (!$this->get_setting('serve_scaled_images', true) && !$this->get_setting('enable_responsive_images', true)) {
            return $srcset;
        }

        $attributes = $this->extract_img_attributes($img_html);
        if (strtolower($this->extract_attribute_value($attributes, 'loading')) !== 'lazy') {
            return $srcset;
        }

        if (strtolower($this->extract_attribute_value($attributes, 'fetchpriority')) === 'high') {
            return $srcset;
        }

        [$render_width] = $this->extract_dimensions_from_img_tag($img_html);
        if ($render_width <= 0) {
            return $srcset;
        }

        $max_candidate_width = (int) apply_filters(
            'prorank_image_srcset_prune_max_width',
            max($render_width + 64, (int) ceil($render_width * 1.35)),
            $render_width,
            $img_html,
            $srcset
        );

        $sizes_width = $this->estimate_source_size_width_from_img_tag($img_html);
        if ($sizes_width > 0) {
            $max_candidate_width = min(
                $max_candidate_width,
                max($sizes_width + 29, (int) ceil($sizes_width * 1.05))
            );
        }

        if ($max_candidate_width < $render_width && $sizes_width <= 0) {
            return $srcset;
        }

        $parts = array_filter(array_map('trim', explode(',', $srcset)));
        if (count($parts) < 2) {
            return $srcset;
        }

        $kept = [];
        $dropped = 0;
        foreach ($parts as $part) {
            if (!preg_match('/^(\S+)\s+(\d+)w$/', $part, $matches)) {
                $kept[] = $part;
                continue;
            }

            $candidate_width = (int) ($matches[2] ?? 0);
            if ($candidate_width <= 0 || $candidate_width <= $max_candidate_width) {
                $kept[] = $part;
                continue;
            }

            $dropped++;
        }

        if ($dropped === 0 || empty($kept)) {
            return $srcset;
        }

        return implode(', ', $kept);
    }

    /**
     * Add missing ProRank responsive sub-sizes to a srcset at render time.
     *
     * WordPress only includes sizes that exist in attachment metadata. If
     * ProRank's finer responsive sizes were registered after an image upload,
     * older media can still expose a coarse 300/600/768 ladder and trigger
     * PageSpeed "larger than displayed dimensions" warnings. Generate the
     * missing intermediate files once and add them to the current srcset.
     *
     * @param string $srcset        Existing candidate list.
     * @param int    $attachment_id Attachment ID.
     * @param string $sizes         Sizes attribute for this render.
     * @return string
     */
    private function augment_srcset_with_runtime_responsive_subsizes(string $srcset, int $attachment_id, string $sizes): string {
        if (
            $attachment_id <= 0
            || (!$this->get_setting('serve_scaled_images', true) && !$this->get_setting('enable_responsive_images', true))
            || !function_exists('wp_get_attachment_metadata')
            || !function_exists('get_attached_file')
            || !function_exists('wp_get_attachment_url')
            || !function_exists('image_make_intermediate_size')
        ) {
            return $srcset;
        }

        $metadata = wp_get_attachment_metadata($attachment_id);
        if (!is_array($metadata)) {
            return $srcset;
        }

        $source_path = (string) get_attached_file($attachment_id);
        $attachment_url = (string) wp_get_attachment_url($attachment_id);
        if ($source_path === '' || $attachment_url === '' || !file_exists($source_path)) {
            return $srcset;
        }

        $source_width = (int) ($metadata['width'] ?? 0);
        if ($source_width <= 0) {
            $dimensions = @getimagesize($source_path);
            $source_width = is_array($dimensions) ? (int) ($dimensions[0] ?? 0) : 0;
        }
        if ($source_width <= 0) {
            return $srcset;
        }

        $existing_widths = $this->extract_srcset_width_map($srcset);
        $target_widths = $this->get_responsive_subsize_widths_for_sizes($sizes, $source_width);
        if (empty($target_widths)) {
            return $srcset;
        }

        $changed = false;
        $base_url = trailingslashit(dirname($attachment_url));
        $base_dir = trailingslashit(dirname($source_path));

        foreach ($target_widths as $target_width) {
            if ($target_width <= 0 || $target_width >= $source_width) {
                continue;
            }

            $size_data = $this->ensure_responsive_subsize($source_path, $attachment_id, $metadata, $target_width);
            if (isset($existing_widths[$target_width])) {
                continue;
            }

            if (empty($size_data['file']) || empty($size_data['width'])) {
                continue;
            }

            $candidate_width = (int) $size_data['width'];
            if ($candidate_width <= 0 || isset($existing_widths[$candidate_width])) {
                continue;
            }

            $candidate_path = $base_dir . $size_data['file'];
            if (!file_exists($candidate_path)) {
                continue;
            }

            $candidate_url = $base_url . $size_data['file'];
            $existing_widths[$candidate_width] = esc_url($candidate_url) . ' ' . $candidate_width . 'w';
            $changed = true;
        }

        if (!$changed) {
            return $srcset;
        }

        ksort($existing_widths, SORT_NUMERIC);
        return implode(', ', array_values($existing_widths));
    }

    /**
     * Ensure a single intermediate responsive size exists for an attachment.
     *
     * @param string              $source_path   Original image file path.
     * @param int                 $attachment_id Attachment ID.
     * @param array<string,mixed> $metadata      Attachment metadata.
     * @param int                 $width         Target width.
     * @return array<string,mixed>|null
     */
    private function ensure_responsive_subsize(string $source_path, int $attachment_id, array &$metadata, int $width): ?array {
        $size_name = 'prorank-responsive-' . $width;
        $base_dir = trailingslashit(dirname($source_path));

        if (!empty($metadata['sizes'][$size_name]) && is_array($metadata['sizes'][$size_name])) {
            $existing = $metadata['sizes'][$size_name];
            if (!empty($existing['file']) && file_exists($base_dir . $existing['file'])) {
                $existing_path = $base_dir . $existing['file'];
                $this->maybe_generate_runtime_responsive_formats($existing_path, $attachment_id, $size_name);

                return $existing;
            }
        }

        if (!$this->can_generate_runtime_responsive_asset()) {
            return null;
        }

        // Apply sub-size JPEG quality via WordPress's jpeg_quality filter so
        // image_make_intermediate_size() uses the correct aggressive quality.
        $subsize_jpeg_quality = $this->get_jpeg_quality(true);
        $jpeg_quality_callback = static function () use ($subsize_jpeg_quality): int {
            return $subsize_jpeg_quality;
        };
        add_filter('jpeg_quality', $jpeg_quality_callback);
        $generated = image_make_intermediate_size($source_path, $width, 0, false);
        remove_filter('jpeg_quality', $jpeg_quality_callback);
        if (!is_array($generated) || empty($generated['file']) || empty($generated['width'])) {
            return null;
        }

        if (function_exists('wp_check_filetype')) {
            $filetype = wp_check_filetype((string) $generated['file']);
            if (!empty($filetype['type'])) {
                $generated['mime-type'] = $filetype['type'];
            }
        }

        $metadata['sizes'][$size_name] = $generated;
        if (function_exists('wp_update_attachment_metadata')) {
            wp_update_attachment_metadata($attachment_id, $metadata);
        }

        $generated_path = $base_dir . $generated['file'];
        if (file_exists($generated_path)) {
            $this->generate_runtime_responsive_formats($generated_path, $attachment_id, $size_name);
        }

        return $generated;
    }

    /**
     * Generate modern-format variants for an existing responsive sub-size when
     * request limits allow it.
     *
     * @param string $path          File path.
     * @param int    $attachment_id Attachment ID.
     * @param string $size_name     Size name.
     * @return void
     */
    private function maybe_generate_runtime_responsive_formats(string $path, int $attachment_id, string $size_name): void {
        $needs_webp = $this->get_setting('webp_enabled', false);
        $needs_avif = $this->get_setting('avif_enabled', false) && $this->is_avif_supported();
        if (!$needs_webp && !$needs_avif) {
            return;
        }

        if (!$this->can_generate_runtime_responsive_asset()) {
            return;
        }

        $this->generate_runtime_responsive_formats($path, $attachment_id, $size_name);
    }

    /**
     * Generate modern-format variants for a responsive sub-size.
     *
     * @param string $path          File path.
     * @param int    $attachment_id Attachment ID.
     * @param string $size_name     Size name.
     * @return void
     */
    private function generate_runtime_responsive_formats(string $path, int $attachment_id, string $size_name): void {
        if ($this->get_setting('webp_enabled', false)) {
            $this->generate_webp($path, $attachment_id, $size_name);
        }

        if ($this->get_setting('avif_enabled', false) && $this->is_avif_supported()) {
            $this->generate_avif($path, $attachment_id, $size_name);
        }
    }

    /**
     * Whether another responsive image file may be generated in this request.
     *
     * @return bool
     */
    private function can_generate_runtime_responsive_asset(): bool {
        $is_admin = function_exists('is_admin') && is_admin();
        $is_ajax = function_exists('wp_doing_ajax') && wp_doing_ajax();
        $is_cron = function_exists('wp_doing_cron') && wp_doing_cron();
        if ($is_admin || $is_ajax || $is_cron) {
            return true;
        }

        if ($this->runtime_responsive_generation_count >= self::MAX_RUNTIME_RESPONSIVE_GENERATIONS_PER_REQUEST) {
            return false;
        }

        $this->runtime_responsive_generation_count++;
        return true;
    }

    /**
     * Parse a srcset into a width-keyed map.
     *
     * @param string $srcset Candidate list.
     * @return array<int,string>
     */
    private function extract_srcset_width_map(string $srcset): array {
        $map = [];
        foreach (array_filter(array_map('trim', explode(',', $srcset))) as $part) {
            if (!preg_match('/^(\S+)\s+(\d+)w$/', $part, $matches)) {
                continue;
            }

            $width = (int) ($matches[2] ?? 0);
            if ($width > 0) {
                $map[$width] = $part;
            }
        }

        return $map;
    }

    /**
     * Choose useful responsive widths for the current sizes string.
     *
     * @param string $sizes        Sizes attribute.
     * @param int    $source_width Original image width.
     * @return array<int,int>
     */
    private function get_responsive_subsize_widths_for_sizes(string $sizes, int $source_width): array {
        $widths = self::RESPONSIVE_SUBSIZES;
        $slot_width = $this->estimate_source_size_width_from_sizes($sizes);

        if ($slot_width > 0) {
            foreach ([$slot_width, $slot_width - 90, $slot_width - 170] as $derived_width) {
                $derived_width = (int) round($derived_width / 20) * 20;
                if ($derived_width >= 240) {
                    $widths[] = $derived_width;
                }
            }
        }

        $widths = array_values(array_unique(array_map('intval', $widths)));
        $widths = array_values(array_filter(
            $widths,
            static fn(int $width): bool => $width >= 240 && $width < $source_width
        ));
        sort($widths, SORT_NUMERIC);

        return $widths;
    }

    /**
     * Estimate the effective source-size width from an image tag's sizes attr.
     *
     * @param string $img_html Image tag.
     * @return int
     */
    private function estimate_source_size_width_from_img_tag(string $img_html): int {
        $attributes = $this->extract_img_attributes($img_html);
        return $this->estimate_source_size_width_from_sizes($this->extract_attribute_value($attributes, 'sizes'));
    }

    /**
     * Estimate the largest slot width described by a sizes string.
     *
     * @param string $sizes Sizes attribute.
     * @return int
     */
    private function estimate_source_size_width_from_sizes(string $sizes): int {
        $sizes = trim(html_entity_decode($sizes, ENT_QUOTES, 'UTF-8'));
        if ($sizes === '') {
            return 0;
        }

        $candidates = [];
        $lengths_only = preg_replace('/\([^)]*\)/', '', $sizes) ?: $sizes;
        if (preg_match_all('/(\d+(?:\.\d+)?)px/i', $lengths_only, $px_matches)) {
            foreach ($px_matches[1] as $px) {
                $candidates[] = (int) ceil((float) $px);
            }
        }

        if (preg_match_all('/max-width:\s*(\d+)px\)[^,]*?(\d+(?:\.\d+)?)vw/i', $sizes, $vw_matches, PREG_SET_ORDER)) {
            foreach ($vw_matches as $match) {
                $viewport = (int) ($match[1] ?? 0);
                $vw = (float) ($match[2] ?? 0);
                if ($viewport > 0 && $vw > 0) {
                    $candidates[] = (int) ceil($viewport * ($vw / 100));
                }
            }
        }

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

        return max(0, max($candidates));
    }

    /**
     * Build a smaller mobile-only srcset for lazy below-fold content images.
     *
     * @param string $srcset     Full responsive srcset.
     * @param string $sizes      Sizes attribute.
     * @param string $attributes Image attributes.
     * @return string
     */
    private function get_mobile_constrained_srcset(string $srcset, string $sizes, string $attributes): string {
        $loading = strtolower($this->extract_attribute_value($attributes, 'loading'));
        if ($loading !== '' && $loading !== 'lazy') {
            return '';
        }

        if (strtolower($this->extract_attribute_value($attributes, 'fetchpriority')) === 'high') {
            return '';
        }

        $cap = $this->estimate_mobile_source_size_width_from_sizes($sizes);
        if ($cap <= 0) {
            return '';
        }

        return $this->filter_srcset_to_max_width($srcset, $cap);
    }

    /**
     * Estimate the mobile slot width from the first max-width/vw sizes clause.
     *
     * @param string $sizes Sizes attribute.
     * @return int
     */
    private function estimate_mobile_source_size_width_from_sizes(string $sizes): int {
        $sizes = trim(html_entity_decode($sizes, ENT_QUOTES, 'UTF-8'));
        if ($sizes === '') {
            return 0;
        }

        if (preg_match('/max-width:\s*(\d+)px\)[^,]*?(\d+(?:\.\d+)?)px/i', $sizes, $match)) {
            $slot = (float) ($match[2] ?? 0);
            if ($slot > 0) {
                return (int) ceil($slot);
            }
        }

        if (preg_match('/max-width:\s*(\d+)px\)[^,]*?(\d+(?:\.\d+)?)vw/i', $sizes, $match)) {
            $viewport = (int) ($match[1] ?? 0);
            $vw = (float) ($match[2] ?? 0);
            if ($viewport > 0 && $vw > 0) {
                return (int) ceil($viewport * ($vw / 100));
            }
        }

        if (preg_match('/(?:^|,)\s*(\d+(?:\.\d+)?)px\s*$/i', $sizes, $match)) {
            return (int) ceil((float) ($match[1] ?? 0));
        }

        return 0;
    }

    /**
     * Keep only srcset candidates up to a maximum width, preserving fallback.
     *
     * @param string $srcset    Candidate list.
     * @param int    $max_width Maximum width.
     * @return string
     */
    private function filter_srcset_to_max_width(string $srcset, int $max_width): string {
        if ($max_width <= 0) {
            return '';
        }

        $candidates = $this->extract_srcset_width_map($srcset);
        if (empty($candidates)) {
            return '';
        }

        ksort($candidates, SORT_NUMERIC);
        $kept = [];
        foreach ($candidates as $width => $candidate) {
            if ((int) $width <= $max_width) {
                $kept[(int) $width] = $candidate;
            }
        }

        if (empty($kept)) {
            $first_width = array_key_first($candidates);
            if ($first_width !== null) {
                $kept[(int) $first_width] = $candidates[$first_width];
            }
        }

        return implode(', ', array_values($kept));
    }

    /**
     * Refresh all inline img tags in an HTML buffer.
     *
     * @param string $buffer
     * @return string
     */
    private function refresh_buffer_inline_images(string $buffer): string {
        if (!preg_match_all('/<img\b[^>]*>/i', $buffer, $matches, PREG_OFFSET_CAPTURE)) {
            return $buffer;
        }

        $rebuilt = '';
        $cursor = 0;

        foreach ($matches[0] as $match) {
            $img_html = (string) ($match[0] ?? '');
            $offset = (int) ($match[1] ?? 0);

            if ($img_html === '') {
                continue;
            }

            $rebuilt .= substr($buffer, $cursor, $offset - $cursor);

            // Premium Addons/Elementor carousel wrappers can sit far ahead of
            // later item images once template markup is expanded. Keep the
            // lookback generous so all items inherit the same responsive sizes.
            $context_start = max(0, $offset - 20000);
            $context = substr($buffer, $context_start, $offset - $context_start);
            $sizes_override = $this->detect_known_builder_sizes_from_context($context);

            $rebuilt .= $this->refresh_inline_image_markup($img_html, $sizes_override);
            $cursor = $offset + strlen($img_html);
        }

        $rebuilt .= substr($buffer, $cursor);
        return $rebuilt;
    }

    /**
     * Detect a known builder/container sizes formula from the surrounding HTML.
     *
     * @param string $context
     * @return string
     */
    private function detect_known_builder_sizes_from_context(string $context): string {
        if ($context === '' || stripos($context, 'premium-carousel-wrapper') === false) {
            return '';
        }

        if (!preg_match_all('/premium-carousel-wrapper[^>]+data-settings=(["\'])(.*?)\1/is', $context, $matches, PREG_SET_ORDER)) {
            return '';
        }

        $last = end($matches);
        if (!is_array($last) || empty($last[2])) {
            return '';
        }

        $settings_json = html_entity_decode((string) $last[2], ENT_QUOTES, 'UTF-8');
        $settings = json_decode($settings_json, true);
        if (!is_array($settings)) {
            return '';
        }

        $wrapper_pos = strripos($context, 'premium-carousel-wrapper');
        $boxed_pos = $wrapper_pos !== false ? strripos(substr($context, 0, $wrapper_pos), 'elementor-section-boxed') : false;
        $is_boxed_layout = $wrapper_pos !== false && $boxed_pos !== false && ($wrapper_pos - $boxed_pos) < 5000;

        return $this->build_premium_carousel_sizes_string($settings, $is_boxed_layout);
    }

    /**
     * Build a responsive sizes string for Premium Addons carousel images.
     *
     * @param array<string,mixed> $settings
     * @return string
     */
    private function build_premium_carousel_sizes_string(array $settings, bool $is_boxed_layout = false): string {
        $slides_mobile = max(1, (int) ($settings['slidesMob'] ?? $settings['slidesToShow'] ?? 1));
        $slides_tablet = max(1, (int) ($settings['slidesTab'] ?? $settings['slidesDesk'] ?? $settings['slidesToShow'] ?? $slides_mobile));
        $slides_desktop = max(1, (int) ($settings['slidesDesk'] ?? $settings['slidesToShow'] ?? $slides_tablet));

        $mobile_break = max(480, (int) ($settings['mobileBreak'] ?? 768));
        $tablet_break = max($mobile_break + 1, (int) ($settings['tabletBreak'] ?? 1025));
        $desktop_size = $this->format_viewport_fraction($slides_desktop);

        if ($is_boxed_layout && $slides_desktop > 1) {
            $desktop_size = $this->estimate_boxed_carousel_slide_size($slides_desktop);
        }

        return sprintf(
            '(max-width: %1$dpx) %2$s, (max-width: %3$dpx) %4$s, %5$s',
            $mobile_break - 1,
            $this->format_viewport_fraction($slides_mobile),
            $tablet_break - 1,
            $this->format_viewport_fraction($slides_tablet),
            $desktop_size
        );
    }

    /**
     * Estimate a desktop slide width for carousels inside boxed Elementor sections.
     *
     * @param int $slides
     * @return string
     */
    private function estimate_boxed_carousel_slide_size(int $slides): string {
        $slides = max(1, $slides);
        $container_width = (int) get_option('elementor_container_width', 1140);
        if ($container_width <= 0) {
            $container_width = 1140;
        }

        $available_width = max(320, $container_width - 20);
        $slide_width = (int) floor($available_width / $slides);

        return max(160, $slide_width) . 'px';
    }

    /**
     * Convert a slide count into a viewport-width size token.
     *
     * @param int $slides
     * @return string
     */
    private function format_viewport_fraction(int $slides): string {
        $slides = max(1, $slides);
        if ($slides === 1) {
            return '100vw';
        }

        $vw = 100 / $slides;
        $formatted = rtrim(rtrim(number_format($vw, 2, '.', ''), '0'), '.');
        return $formatted . 'vw';
    }

    /**
     * Print a tiny runtime restorer for lazy widgets that strip width/height.
     *
     * @return void
     */
    public function print_intrinsic_dimension_restorer(): void {
        if (!$this->is_frontend_optimization_context()) {
            return;
        }

        echo "<script>(function(){var dimensionMap=new Map();var remember=function(img){if(!(img instanceof HTMLImageElement))return;var w=img.getAttribute('width')||img.dataset.prorankWidth;var h=img.getAttribute('height')||img.dataset.prorankHeight;if(!w||!h)return;[img.getAttribute('src'),img.getAttribute('data-src'),img.currentSrc].forEach(function(key){if(key)dimensionMap.set(key,{w:w,h:h});});};var resolve=function(img){remember(img);var w=img.getAttribute('width')||img.dataset.prorankWidth;var h=img.getAttribute('height')||img.dataset.prorankHeight;if(w&&h)return{w:w,h:h};var key=img.currentSrc||img.getAttribute('src')||img.getAttribute('data-src');return key?dimensionMap.get(key)||null:null;};var apply=function(img){if(!(img instanceof HTMLImageElement))return;var dims=resolve(img);if(!dims)return;if(!img.hasAttribute('width'))img.setAttribute('width',dims.w);if(!img.hasAttribute('height'))img.setAttribute('height',dims.h);if(!img.style.aspectRatio)img.style.aspectRatio=dims.w+' / '+dims.h;};var scan=function(root){(root||document).querySelectorAll('img').forEach(function(img){remember(img);apply(img);});};if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',function(){scan(document);},{once:true});}else{scan(document);}var observer=new MutationObserver(function(mutations){mutations.forEach(function(mutation){if(mutation.type==='attributes'&&mutation.target&&mutation.target.tagName==='IMG'){apply(mutation.target);}mutation.addedNodes.forEach(function(node){if(!node||node.nodeType!==1)return;if(node.tagName==='IMG'){apply(node);}if(node.querySelectorAll){node.querySelectorAll('img').forEach(function(img){apply(img);});}});});});observer.observe(document.documentElement,{subtree:true,childList:true,attributes:true,attributeFilter:['src','data-src','width','height','class']});})();</script>";
    }

    /**
     * Get normalized exclusion patterns from settings.
     *
     * @return array<int, string>
     */
    private function get_exclude_path_patterns(): array {
        if ($this->exclude_path_patterns !== null) {
            return $this->exclude_path_patterns;
        }

        $raw = (string) $this->get_setting('exclude_paths', '');
        if ($raw === '') {
            $this->exclude_path_patterns = [];
            return $this->exclude_path_patterns;
        }

        $this->exclude_path_patterns = array_values(
            array_filter(
                array_map(
                    'trim',
                    preg_split('/\r\n|\r|\n/', $raw) ?: []
                ),
                static fn(string $pattern): bool => $pattern !== ''
            )
        );

        return $this->exclude_path_patterns;
    }

    /**
     * Check whether a URL/path matches a configured exclusion pattern.
     *
     * @param string $value URL or path.
     * @return bool
     */
    private function matches_exclude_path(string $value): bool {
        $value = trim($value);
        if ($value === '') {
            return false;
        }

        $candidates = [$value];
        $parsed_path = (string) wp_parse_url($value, PHP_URL_PATH);
        if ($parsed_path !== '') {
            $candidates[] = $parsed_path;
        }

        foreach ($this->get_exclude_path_patterns() as $pattern) {
            foreach ($candidates as $candidate) {
                if ($candidate !== '' && stripos($candidate, $pattern) !== false) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Check whether the current frontend request is excluded.
     *
     * @return bool
     */
    private function is_current_request_excluded(): bool {
        $request_uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '';
        if ($request_uri === '') {
            return false;
        }

        return $this->matches_exclude_path($request_uri) || $this->matches_exclude_path(home_url($request_uri));
    }

    /**
     * Generate missing runtime variants for a local uploads URL when needed.
     *
     * This covers site-rendered images that are not registered media attachments.
     *
     * @param string $source_url Source uploads URL.
     * @return void
     */
    private function ensure_runtime_optimized_variants(string $source_url): void {
        $normalized_url = preg_replace('/[#?].*$/', '', trim($source_url)) ?: trim($source_url);
        if ($normalized_url === '' || isset($this->runtime_variant_cache[$normalized_url])) {
            return;
        }

        $this->runtime_variant_cache[$normalized_url] = true;

        if ($this->matches_exclude_path($normalized_url)) {
            return;
        }

        $source_path = $this->map_upload_url_to_path($normalized_url);
        if ($source_path === '' || !file_exists($source_path)) {
            return;
        }

        $extension = strtolower(pathinfo($source_path, PATHINFO_EXTENSION));
        if (!in_array($extension, self::SUPPORTED_FORMATS, true)) {
            return;
        }

        if ($this->get_setting('webp_enabled', false) && !$this->optimized_file_exists($this->get_optimized_url($normalized_url, 'webp'))) {
            $this->generate_webp($source_path, 0);
        }

        if ($this->get_setting('avif_enabled', false) && $this->can_generate_avif() && !$this->optimized_file_exists($this->get_optimized_url($normalized_url, 'avif'))) {
            $this->generate_avif($source_path, 0);
        }

        if ($this->get_setting('jxl_enabled', false) && $this->can_generate_jxl() && !$this->optimized_file_exists($this->get_optimized_url($normalized_url, 'jxl'))) {
            $this->generate_jxl($source_path, 0);
        }
    }

    /**
     * Generate optimized images when attachment is created
     *
     * @param array $metadata      Attachment metadata
     * @param int   $attachment_id Attachment ID
     * @return array Modified metadata
     */
    public function generate_optimized_images(array $metadata, int $attachment_id): array {
        // Try to increase limits for image processing on shared hosting
        @ini_set('memory_limit', '512M');
        @set_time_limit(120);

        // Check if it's an image
        if (!wp_attachment_is_image($attachment_id)) {
            return $metadata;
        }

        // Get file info
        $file_path = get_attached_file($attachment_id);
        if (!$file_path || !file_exists($file_path)) {
            return $metadata;
        }

        $force_reoptimization = $this->is_force_reoptimization_job_active();

        // Check if format is supported
        $extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
        $is_force_modern_source = $force_reoptimization && in_array($extension, self::FORCE_REOPTIMIZABLE_FORMATS, true);
        if (!in_array($extension, self::SUPPORTED_FORMATS, true) && !$is_force_modern_source) {
            return $metadata;
        }

        // Check if file should be excluded
        if ($this->should_exclude_file($file_path, $attachment_id)) {
            return $metadata;
        }

        // Store original file size before any optimization
        $original_size = filesize($file_path);
        update_post_meta($attachment_id, '_prorank_original_size', $original_size);

        // Track format generation status
        $webp_generated = false;
        $avif_generated = false;
        $total_savings = 0;
        $original_source_saved = 0;
        $subsize_source_saved = 0;
        $modern_source_saved = 0;

        // Backup original if enabled
        if ($this->get_setting('backup_originals', true) && !$this->backup_original_image($file_path, $attachment_id)) {
            update_post_meta($attachment_id, '_prorank_optimization_failed', '1');
            update_post_meta($attachment_id, '_prorank_optimization_failure_reason', 'backup_original_failed');
            return $metadata;
        }

        // Optimize original + sizes with advanced engine.
        // Skip on local-only mode (free plugin) — GD re-compression saves very little
        // and uses a lot of memory on shared hosting.
        if ($this->should_optimize_original_source()) {
            $source_optimization = $this->optimize_original_and_sizes($file_path, $attachment_id, $metadata);
            $metadata = $source_optimization['metadata'] ?? $metadata;
            $original_source_saved = (int) ($source_optimization['original_saved_bytes'] ?? 0);
            $total_savings = max($total_savings, $original_source_saved);

            if (!$this->source_file_matches_extension($file_path)) {
                $this->restore_source_file_from_backup($file_path, $attachment_id);
                $original_source_saved = 0;
                $total_savings = 0;
                update_post_meta($attachment_id, '_prorank_optimization_failed', '1');
                update_post_meta($attachment_id, '_prorank_optimization_failure_reason', 'source_mime_mismatch_after_original_optimization');
            }
        }

        if ($force_reoptimization) {
            $subsize_source_saved = $this->recompress_generated_jpeg_subsizes($file_path, $attachment_id, $metadata);
            $modern_source_saved = $this->recompress_modern_original_and_subsizes($file_path, $attachment_id, $metadata);
            $total_savings += $subsize_source_saved + $modern_source_saved;
        }

        // Generate WebP if enabled
        // Determine if running in constrained environment (shared hosting, free plugin local mode)
        $is_local_mode = ($this->get_optimization_method() === 'local');

        // Generate WebP if enabled
        if ($this->get_setting('webp_enabled', false)) {
            $webp_success = $this->generate_webp($file_path, $attachment_id);
            if ($webp_success) {
                $webp_generated = true;
                $webp_path = $this->get_optimized_path($file_path, 'webp');
                if (file_exists($webp_path)) {
                    $webp_size = filesize($webp_path);
                    if ($webp_size < $original_size) {
                        $total_savings = max($total_savings, $original_size - $webp_size);
                    }
                }
            }

            // Generate WebP for sizes. Force re-optimization intentionally
            // revisits existing derivatives even on local-mode shared hosting.
            if (($force_reoptimization || !$is_local_mode) && !empty($metadata['sizes'])) {
                $base_dir = dirname($file_path);
                foreach ($metadata['sizes'] as $size => $size_data) {
                    if ($this->should_exclude_file($file_path, $attachment_id, $size)) {
                        continue;
                    }
                    $size_path = $base_dir . '/' . $size_data['file'];
                    if (file_exists($size_path)) {
                        if (strtolower((string) pathinfo($size_path, PATHINFO_EXTENSION)) === 'webp') {
                            $saved = $this->recompress_modern_file_at_quality($size_path, 'webp', $this->get_modern_format_quality('webp', $size));
                            if ($saved > 0) {
                                $webp_generated = true;
                                $total_savings += $saved;
                            }
                            continue;
                        }

                        $webp_path = $this->get_optimized_path($size_path, 'webp');
                        $before_webp_size = file_exists($webp_path) ? (int) filesize($webp_path) : 0;
                        if ($this->generate_webp($size_path, $attachment_id, $size)) {
                            $webp_generated = true;
                            if ($before_webp_size > 0 && file_exists($webp_path)) {
                                $after_webp_size = (int) filesize($webp_path);
                                if ($after_webp_size > 0 && $after_webp_size < $before_webp_size) {
                                    $total_savings += $before_webp_size - $after_webp_size;
                                }
                            }
                        }
                    }
                }
            }
        }

        // Generate AVIF if enabled. Normal local mode still avoids GD AVIF on
        // frontend/upload paths, but explicit force jobs run in capped cron batches.
        if (($force_reoptimization || !$is_local_mode) && $this->get_setting('avif_enabled', false) && $this->is_avif_supported()) {
            $avif_success = $this->generate_avif($file_path, $attachment_id);
            if ($avif_success) {
                $avif_generated = true;
                $avif_path = $this->get_optimized_path($file_path, 'avif');
                if (file_exists($avif_path)) {
                    $avif_size = filesize($avif_path);
                    if ($avif_size < $original_size) {
                        $total_savings = max($total_savings, $original_size - $avif_size);
                    }
                }
            }

            if (!empty($metadata['sizes'])) {
                $base_dir = dirname($file_path);
                foreach ($metadata['sizes'] as $size => $size_data) {
                    if ($this->should_exclude_file($file_path, $attachment_id, $size)) {
                        continue;
                    }
                    $size_path = $base_dir . '/' . $size_data['file'];
                    if (file_exists($size_path)) {
                        if (strtolower((string) pathinfo($size_path, PATHINFO_EXTENSION)) === 'avif') {
                            $saved = $this->recompress_modern_file_at_quality($size_path, 'avif', $this->get_modern_format_quality('avif', $size));
                            if ($saved > 0) {
                                $avif_generated = true;
                                $total_savings += $saved;
                            }
                            continue;
                        }

                        $avif_path = $this->get_optimized_path($size_path, 'avif');
                        $before_avif_size = file_exists($avif_path) ? (int) filesize($avif_path) : 0;
                        if ($this->generate_avif($size_path, $attachment_id, $size)) {
                            $avif_generated = true;
                            if ($before_avif_size > 0 && file_exists($avif_path)) {
                                $after_avif_size = (int) filesize($avif_path);
                                if ($after_avif_size > 0 && $after_avif_size < $before_avif_size) {
                                    $total_savings += $before_avif_size - $after_avif_size;
                                }
                            }
                        }
                    }
                }
            }
        }

        // Generate JPEG XL if enabled (skip in local mode — requires binary tools)
        $jxl_generated = false;
        if (($force_reoptimization || !$is_local_mode) && $this->get_setting('jxl_enabled', false) && $this->can_generate_jxl()) {
            $jxl_success = $this->generate_jxl($file_path, $attachment_id);
            if ($jxl_success) {
                $jxl_generated = true;
                $jxl_path = $this->get_optimized_path($file_path, 'jxl');
                if (file_exists($jxl_path)) {
                    $jxl_size = filesize($jxl_path);
                    if ($jxl_size < $original_size) {
                        $total_savings = max($total_savings, $original_size - $jxl_size);
                    }
                }
            }

            if (!empty($metadata['sizes'])) {
                $base_dir = dirname($file_path);
                foreach ($metadata['sizes'] as $size => $size_data) {
                    if ($this->should_exclude_file($file_path, $attachment_id, $size)) {
                        continue;
                    }
                    $size_path = $base_dir . '/' . $size_data['file'];
                    if (file_exists($size_path)) {
                        $jxl_path = $this->get_optimized_path($size_path, 'jxl');
                        $before_jxl_size = file_exists($jxl_path) ? (int) filesize($jxl_path) : 0;
                        if ($this->generate_jxl($size_path, $attachment_id, $size)) {
                            $jxl_generated = true;
                            if ($before_jxl_size > 0 && file_exists($jxl_path)) {
                                $after_jxl_size = (int) filesize($jxl_path);
                                if ($after_jxl_size > 0 && $after_jxl_size < $before_jxl_size) {
                                    $total_savings += $before_jxl_size - $after_jxl_size;
                                }
                            }
                        }
                    }
                }
            }
        }

        $has_real_optimization = $webp_generated || $avif_generated || $jxl_generated || $original_source_saved > 0 || $subsize_source_saved > 0 || $modern_source_saved > 0 || $total_savings > 0;

        update_post_meta($attachment_id, '_prorank_bytes_saved', $total_savings);
        update_post_meta($attachment_id, '_prorank_original_source_bytes_saved', $original_source_saved);
        update_post_meta($attachment_id, '_prorank_subsize_source_bytes_saved', $subsize_source_saved);
        update_post_meta($attachment_id, '_prorank_modern_source_bytes_saved', $modern_source_saved);
        update_post_meta($attachment_id, '_prorank_webp_generated', $webp_generated ? '1' : '');
        update_post_meta($attachment_id, '_prorank_avif_generated', $avif_generated ? '1' : '');
        update_post_meta($attachment_id, '_prorank_jxl_generated', $jxl_generated ? '1' : '');

        if ($has_real_optimization) {
            update_post_meta($attachment_id, '_prorank_optimized', '1');
            update_post_meta($attachment_id, '_prorank_optimization_date', current_time('mysql'));
            $this->clear_attachment_no_gain_state($attachment_id);
            delete_post_meta($attachment_id, '_prorank_optimization_failed');
            delete_post_meta($attachment_id, '_prorank_optimization_failure_reason');
            delete_post_meta($attachment_id, '_prorank_optimization_attempts');
        } else {
            $this->mark_attachment_no_gain($attachment_id, 'no_effective_output');
        }

        return $metadata;
    }

    /**
     * Optimize original image and all generated sizes using the advanced engine.
     *
     * @param string $file_path     Original file path.
     * @param int    $attachment_id Attachment ID.
     * @param array  $metadata      Attachment metadata.
     * @return void
     */
    private function optimize_original_and_sizes(string $file_path, int $attachment_id, array $metadata): array {
        $engine = new OptimizationEngine();
        $options = [
            'compression_type' => $this->get_setting('compression_type', 'glossy'),
            'smart_compression' => $this->get_setting('smart_compression', true),
            'jpegli_enabled' => $this->get_setting('jpegli_enabled', false),
            'png_pipeline' => $this->get_setting('png_pipeline', false),
            'png_quality' => (int) $this->get_setting('png_quality', 80),
            'optimization_method' => $this->get_optimization_method(),
        ];

        $starting_size = file_exists($file_path) ? (int) filesize($file_path) : 0;
        $resize_result = $this->maybe_resize_original_source($file_path, $attachment_id, $metadata);
        $metadata = $resize_result['metadata'] ?? $metadata;

        $original_result = $engine->optimize_file_path($file_path, $options);
        $final_size = file_exists($file_path) ? (int) filesize($file_path) : $starting_size;
        $original_saved_bytes = max(0, $starting_size - $final_size);

        if (!empty($original_result['success']) && !empty($original_result['method'])) {
            update_post_meta($attachment_id, '_prorank_optimization_method', (string) $original_result['method']);
        } elseif (!empty($resize_result['bytes_saved'])) {
            update_post_meta($attachment_id, '_prorank_optimization_method', 'resize');
        }

        if (!empty($metadata['sizes'])) {
            $base_dir = dirname($file_path);
            foreach ($metadata['sizes'] as $size => $size_data) {
                if ($this->should_exclude_file($file_path, $attachment_id, $size)) {
                    continue;
                }
                $size_path = $base_dir . '/' . $size_data['file'];
                if (file_exists($size_path)) {
                    $engine->optimize_file_path($size_path, $options);
                }
            }
        }

        return [
            'original_saved_bytes' => $original_saved_bytes,
            'original_success' => $original_saved_bytes > 0 || !empty($original_result['success']),
            'metadata' => $metadata,
        ];
    }

    /**
     * Free plugin image optimization is local-only.
     *
     * @return string
     */
    private function get_optimization_method(): string {
        return 'local';
    }

    /**
     * Whether the pipeline should intentionally optimize the original attachment URL in place.
     *
     * @return bool
     */
    private function should_optimize_original_source(): bool {
        return (bool) $this->get_setting('optimize_original_source', false)
            || $this->should_resize_large_originals();
    }

    /**
     * Whether large original attachments should be downscaled in place.
     *
     * @return bool
     */
    private function should_resize_large_originals(): bool {
        return (bool) $this->get_setting('resize_large_enabled', false)
            && (int) $this->get_setting('resize_max_width', 0) > 0
            && (int) $this->get_setting('resize_max_height', 0) > 0;
    }

    /**
     * Detect an active explicit force-reoptimization job.
     *
     * @return bool
     */
    private function is_force_reoptimization_job_active(): bool {
        $job_id = (string) get_option('prorank_bulk_job_active', '');
        if ($job_id === '') {
            return false;
        }

        $job_data = get_option('prorank_bulk_job_' . $job_id, []);
        if (!is_array($job_data) || empty($job_data)) {
            return false;
        }

        $status = (string) ($job_data['status'] ?? '');
        if (in_array($status, ['completed', 'stopped', 'failed'], true)) {
            return false;
        }

        return !empty($job_data['force_reoptimization']);
    }

    /**
     * Recompress generated JPEG sub-sizes during explicit force jobs.
     *
     * @param string $file_path     Original file path.
     * @param int    $attachment_id Attachment ID.
     * @param array  $metadata      Attachment metadata.
     * @return int Bytes saved.
     */
    private function recompress_generated_jpeg_subsizes(string $file_path, int $attachment_id, array $metadata): int {
        if (empty($metadata['sizes']) || !is_array($metadata['sizes'])) {
            return 0;
        }

        $quality = $this->get_jpeg_quality(true);
        $base_dir = dirname($file_path);
        $bytes_saved = 0;

        foreach ($metadata['sizes'] as $size => $size_data) {
            if (!is_array($size_data) || empty($size_data['file']) || !is_string($size_data['file'])) {
                continue;
            }

            $size = is_string($size) ? $size : (string) $size;
            if ($this->should_exclude_file($file_path, $attachment_id, $size)) {
                continue;
            }

            $size_path = $base_dir . '/' . $size_data['file'];
            $bytes_saved += $this->recompress_jpeg_file_at_quality($size_path, $quality);
        }

        return $bytes_saved;
    }

    /**
     * Recompress already-modern originals and same-format generated sub-sizes.
     *
     * @param string $file_path     Original file path.
     * @param int    $attachment_id Attachment ID.
     * @param array  $metadata      Attachment metadata.
     * @return int Bytes saved.
     */
    private function recompress_modern_original_and_subsizes(string $file_path, int $attachment_id, array $metadata): int {
        $bytes_saved = 0;
        $extension = strtolower((string) pathinfo($file_path, PATHINFO_EXTENSION));

        if (in_array($extension, self::FORCE_REOPTIMIZABLE_FORMATS, true)) {
            $bytes_saved += $this->recompress_modern_file_at_quality(
                $file_path,
                $extension,
                $this->get_modern_format_quality($extension, '')
            );
        }

        if (empty($metadata['sizes']) || !is_array($metadata['sizes'])) {
            return $bytes_saved;
        }

        $base_dir = dirname($file_path);
        foreach ($metadata['sizes'] as $size => $size_data) {
            if (!is_array($size_data) || empty($size_data['file']) || !is_string($size_data['file'])) {
                continue;
            }

            $size = is_string($size) ? $size : (string) $size;
            if ($this->should_exclude_file($file_path, $attachment_id, $size)) {
                continue;
            }

            $size_path = $base_dir . '/' . $size_data['file'];
            $size_extension = strtolower((string) pathinfo($size_path, PATHINFO_EXTENSION));
            if (!in_array($size_extension, self::FORCE_REOPTIMIZABLE_FORMATS, true)) {
                continue;
            }

            $bytes_saved += $this->recompress_modern_file_at_quality(
                $size_path,
                $size_extension,
                $this->get_modern_format_quality($size_extension, $size)
            );
        }

        return $bytes_saved;
    }

    /**
     * Re-encode a generated JPEG file at the configured sub-size quality.
     *
     * @param string $path    JPEG path.
     * @param int    $quality JPEG quality.
     * @return int Bytes saved.
     */
    private function recompress_jpeg_file_at_quality(string $path, int $quality): int {
        $extension = strtolower((string) pathinfo($path, PATHINFO_EXTENSION));
        if (!in_array($extension, ['jpg', 'jpeg'], true) || !file_exists($path)) {
            return 0;
        }

        $before = (int) @filesize($path);
        if ($before <= 0) {
            return 0;
        }

        $backup = @tempnam(sys_get_temp_dir(), 'prorank-jpg-');
        if (!is_string($backup) || $backup === '' || !@copy($path, $backup)) {
            return 0;
        }

        $success = false;

        if (extension_loaded('imagick') && class_exists('Imagick')) {
            try {
                $imagick = new \Imagick($path);
                $imagick->setImageFormat('jpeg');
                $imagick->setImageCompressionQuality($quality);
                $success = (bool) $imagick->writeImage($path);
                $imagick->destroy();
            } catch (\Throwable $e) {
                $success = false;
            }
        }

        if (!$success && function_exists('imagecreatefromjpeg') && function_exists('imagejpeg')) {
            $image = @imagecreatefromjpeg($path);
            if ($image) {
                $success = (bool) @imagejpeg($image, $path, $quality);
                imagedestroy($image);
            }
        }

        clearstatcache(true, $path);
        $after = file_exists($path) ? (int) @filesize($path) : 0;
        if ($success && $after > 0 && $after < $before) {
            \ProRank\SEO\Core\FilesystemHelper::delete_file($backup);
            return $before - $after;
        }

        @copy($backup, $path);
        \ProRank\SEO\Core\FilesystemHelper::delete_file($backup);
        clearstatcache(true, $path);

        return 0;
    }

    /**
     * Re-encode an already-modern WebP/AVIF file in place.
     *
     * @param string $path    File path.
     * @param string $format  webp or avif.
     * @param int    $quality Target quality.
     * @return int Bytes saved.
     */
    private function recompress_modern_file_at_quality(string $path, string $format, int $quality): int {
        $format = strtolower($format);
        if (!in_array($format, self::FORCE_REOPTIMIZABLE_FORMATS, true) || !file_exists($path)) {
            return 0;
        }

        $extension = strtolower((string) pathinfo($path, PATHINFO_EXTENSION));
        if ($extension !== $format) {
            return 0;
        }

        $before = (int) @filesize($path);
        if ($before <= 0) {
            return 0;
        }

        $backup = @tempnam(sys_get_temp_dir(), 'prorank-modern-');
        if (!is_string($backup) || $backup === '' || !@copy($path, $backup)) {
            return 0;
        }

        $success = false;

        if (extension_loaded('imagick') && class_exists('Imagick')) {
            try {
                $imagick = new \Imagick($path);
                $imagick->setImageFormat($format);
                $imagick->setImageCompressionQuality($quality);
                $success = (bool) $imagick->writeImage($path);
                $imagick->destroy();
            } catch (\Throwable $e) {
                $success = false;
            }
        }

        if (!$success && $format === 'webp' && function_exists('imagecreatefromwebp') && function_exists('imagewebp')) {
            $image = @imagecreatefromwebp($path);
            if ($image) {
                $success = (bool) @imagewebp($image, $path, $quality);
                imagedestroy($image);
            }
        }

        if (!$success && $format === 'avif' && function_exists('imagecreatefromavif') && function_exists('imageavif')) {
            $image = @imagecreatefromavif($path);
            if ($image) {
                $success = (bool) @imageavif($image, $path, $quality);
                imagedestroy($image);
            }
        }

        clearstatcache(true, $path);
        $after = file_exists($path) ? (int) @filesize($path) : 0;
        if ($success && $after > 0 && $after < $before) {
            \ProRank\SEO\Core\FilesystemHelper::delete_file($backup);
            return $before - $after;
        }

        @copy($backup, $path);
        \ProRank\SEO\Core\FilesystemHelper::delete_file($backup);
        clearstatcache(true, $path);

        return 0;
    }

    /**
     * Resolve target quality for WebP/AVIF originals and sub-sizes.
     *
     * @param string $format webp or avif.
     * @param string $size   WordPress size key; empty means original.
     * @return int
     */
    private function get_modern_format_quality(string $format, string $size = ''): int {
        $format = strtolower($format);
        $quality_key = $format . '_quality';
        $default = $format === 'avif' ? self::DEFAULT_AVIF_QUALITY : self::DEFAULT_WEBP_QUALITY;
        $compression_type = $this->get_setting('compression_type', 'lossy');

        if (!$this->get_setting('smart_compression', true)) {
            return (int) $this->get_setting($quality_key, $default);
        }

        if ($size !== '') {
            $quality = $this->get_subsize_quality($compression_type, $quality_key, $default);
            return (int) apply_filters('prorank_subsize_' . $format . '_quality', $quality, '', $size);
        }

        return self::COMPRESSION_TYPES[$compression_type][$quality_key] ?? $default;
    }

    /**
     * Check that in-place source optimization did not change the file format.
     *
     * @param string $file_path Source file path.
     * @return bool
     */
    private function source_file_matches_extension(string $file_path): bool {
        if (!file_exists($file_path)) {
            return false;
        }

        $expected = [
            'jpg' => 'image/jpeg',
            'jpeg' => 'image/jpeg',
            'png' => 'image/png',
            'gif' => 'image/gif',
            'webp' => 'image/webp',
            'avif' => 'image/avif',
        ];

        $extension = strtolower((string) pathinfo($file_path, PATHINFO_EXTENSION));
        if (!isset($expected[$extension])) {
            return true;
        }

        $image_info = @getimagesize($file_path);
        $actual_mime = is_array($image_info) ? (string) ($image_info['mime'] ?? '') : '';

        return $actual_mime === '' || $actual_mime === $expected[$extension];
    }

    /**
     * Restore the original source from backup after a failed in-place optimize.
     *
     * @param string $file_path     Source file path.
     * @param int    $attachment_id Attachment ID.
     * @return bool
     */
    private function restore_source_file_from_backup(string $file_path, int $attachment_id): bool {
        $backup_path = (string) get_post_meta($attachment_id, '_prorank_backup_path', true);
        if ($backup_path === '' || !file_exists($backup_path)) {
            return false;
        }

        return (bool) @copy($backup_path, $file_path);
    }

    /**
     * Resize oversized originals before format optimization so raw attachment URLs can shrink too.
     *
     * @param string $file_path     Original file path.
     * @param int    $attachment_id Attachment ID.
     * @param array  $metadata      Attachment metadata.
     * @return array<string,mixed>
     */
    private function maybe_resize_original_source(string $file_path, int $attachment_id, array $metadata): array {
        if (!$this->should_resize_large_originals() || !file_exists($file_path)) {
            return [
                'bytes_saved' => 0,
                'metadata' => $metadata,
            ];
        }

        $size = wp_getimagesize($file_path);
        if (!$size || empty($size[0]) || empty($size[1])) {
            return [
                'bytes_saved' => 0,
                'metadata' => $metadata,
            ];
        }

        $max_width = max(100, (int) $this->get_setting('resize_max_width', 2560));
        $max_height = max(100, (int) $this->get_setting('resize_max_height', 2560));
        if ((int) $size[0] <= $max_width && (int) $size[1] <= $max_height) {
            return [
                'bytes_saved' => 0,
                'metadata' => $metadata,
            ];
        }

        if (!function_exists('wp_get_image_editor')) {
            require_once ABSPATH . 'wp-admin/includes/image.php';
        }
        if (!function_exists('wp_tempnam')) {
            $file_api = ABSPATH . 'wp-admin/includes/file.php';
            if (file_exists($file_api)) {
                require_once $file_api;
            }
        }

        $temp_backup = function_exists('wp_tempnam')
            ? wp_tempnam(basename($file_path))
            : tempnam(sys_get_temp_dir(), 'prorank_resize_');
        if (!$temp_backup || !@copy($file_path, $temp_backup)) {
            if ($temp_backup) {
                \ProRank\SEO\Core\FilesystemHelper::delete_file($temp_backup);
            }
            return [
                'bytes_saved' => 0,
                'metadata' => $metadata,
            ];
        }

        $editor = wp_get_image_editor($file_path);
        if (is_wp_error($editor)) {
            \ProRank\SEO\Core\FilesystemHelper::delete_file($temp_backup);
            return [
                'bytes_saved' => 0,
                'metadata' => $metadata,
            ];
        }

        $filetype = wp_check_filetype($file_path);
        $mime_type = $filetype['type'] ?? '';
        $result = $editor->resize($max_width, $max_height, false);
        if (is_wp_error($result)) {
            @copy($temp_backup, $file_path);
            \ProRank\SEO\Core\FilesystemHelper::delete_file($temp_backup);
            return [
                'bytes_saved' => 0,
                'metadata' => $metadata,
            ];
        }

        $saved = $editor->save($file_path, $mime_type !== '' ? $mime_type : null);
        if (is_wp_error($saved)) {
            @copy($temp_backup, $file_path);
            \ProRank\SEO\Core\FilesystemHelper::delete_file($temp_backup);
            return [
                'bytes_saved' => 0,
                'metadata' => $metadata,
            ];
        }

        clearstatcache(true, $file_path);
        $original_size = (int) filesize($temp_backup);
        $new_size = file_exists($file_path) ? (int) filesize($file_path) : $original_size;
        if ($new_size >= $original_size) {
            @copy($temp_backup, $file_path);
            \ProRank\SEO\Core\FilesystemHelper::delete_file($temp_backup);
            return [
                'bytes_saved' => 0,
                'metadata' => $metadata,
            ];
        }

        \ProRank\SEO\Core\FilesystemHelper::delete_file($temp_backup);
        $metadata = $this->sync_resized_original_metadata($metadata, $file_path);
        wp_update_attachment_metadata($attachment_id, $metadata);

        return [
            'bytes_saved' => max(0, $original_size - $new_size),
            'metadata' => $metadata,
        ];
    }

    /**
     * Keep attachment metadata aligned after shrinking the original file in place.
     *
     * @param array  $metadata  Existing attachment metadata.
     * @param string $file_path Original file path.
     * @return array
     */
    private function sync_resized_original_metadata(array $metadata, string $file_path): array {
        $size = wp_getimagesize($file_path);
        if (!$size || empty($size[0]) || empty($size[1])) {
            return $metadata;
        }

        $full_width = (int) $size[0];
        $full_height = (int) $size[1];
        $metadata['width'] = $full_width;
        $metadata['height'] = $full_height;

        if (!empty($metadata['sizes']) && is_array($metadata['sizes'])) {
            $base_dir = dirname($file_path);
            foreach ($metadata['sizes'] as $size_name => $size_data) {
                $size_width = (int) ($size_data['width'] ?? 0);
                $size_height = (int) ($size_data['height'] ?? 0);
                if ($size_width > $full_width || $size_height > $full_height) {
                    if (!empty($size_data['file'])) {
                        \ProRank\SEO\Core\FilesystemHelper::delete_file($base_dir . '/' . $size_data['file']);
                    }
                    unset($metadata['sizes'][$size_name]);
                }
            }
        }

        return $metadata;
    }

    /**
     * Send Client Hints headers to enable DPR/Width/Viewport hints.
     *
     * @return void
     */
    public function send_client_hints_headers(): void {
        if (is_admin() || headers_sent()) {
            return;
        }

        $hints = [];
        if ($this->get_setting('client_hints_dpr', true)) {
            $hints[] = 'DPR';
        }
        if ($this->get_setting('client_hints_width', true)) {
            $hints[] = 'Width';
        }
        if ($this->get_setting('client_hints_viewport', true)) {
            $hints[] = 'Viewport-Width';
        }
        if ($this->get_setting('client_hints_save_data', false)) {
            $hints[] = 'Save-Data';
        }

        if (!$hints) {
            return;
        }

        header('Accept-CH: ' . implode(', ', $hints));
        header('Accept-CH-Lifetime: 86400');
        header('Vary: ' . implode(', ', array_merge(['Accept'], $hints)), false);
    }

    /**
     * Generate WebP version of image
     *
     * @param string $source_path   Source image path
     * @param int    $attachment_id Attachment ID
     * @param string $size          Image size name
     * @return bool Success status
     */
    private function generate_webp(string $source_path, int $attachment_id, string $size = ''): bool {
        if (!$this->is_supported_source_path($source_path)) {
            return false;
        }

        // Check if GD or Imagick supports WebP
        if (!$this->can_generate_webp()) {
            return false;
        }

        $webp_path = $this->get_optimized_path($source_path, 'webp');

        // Skip reusable output, but do not trust stale/lossless or pre-profile
        // WebP derivatives that remain byte-heavy for their dimensions. Those
        // keep PageSpeed image-delivery audits red after a quality-table change.
        if (file_exists($webp_path)) {
            if (!$this->should_regenerate_existing_optimized_variant($webp_path, $source_path, $size)) {
                return true;
            }

            \ProRank\SEO\Core\FilesystemHelper::delete_file($webp_path);
        }

        // Create directory if needed
        $dir = dirname($webp_path);
        if (!is_dir($dir)) {
            wp_mkdir_p($dir);
        }

        // Get quality setting based on compression type
        $compression_type = $this->get_setting('compression_type', 'lossy');
        $is_subsize = $size !== '';
        $is_responsive_subsize = strpos($size, 'prorank-responsive-') === 0;

        // Override with custom quality if SmartCompression is disabled
        if (!$this->get_setting('smart_compression', true)) {
            $quality = (int) $this->get_setting('webp_quality', self::DEFAULT_WEBP_QUALITY);
        } elseif ($is_subsize) {
            // Any generated sub-size can tolerate more aggressive compression than originals.
            $quality = $this->get_subsize_quality($compression_type, 'webp_quality', self::DEFAULT_WEBP_QUALITY);
            $quality = (int) apply_filters('prorank_subsize_webp_quality', $quality, $source_path, $size);
            if ($is_responsive_subsize) {
                $quality = (int) apply_filters('prorank_responsive_subsize_webp_quality', $quality, $source_path, $size);
            }
        } else {
            $quality = self::COMPRESSION_TYPES[$compression_type]['webp_quality'] ?? self::DEFAULT_WEBP_QUALITY;
        }

        $quality = $this->adjust_smart_format_quality($source_path, 'webp', (int) $quality);

        // Try to generate WebP
        $success = false;

        if (extension_loaded('imagick') && class_exists('Imagick')) {
            try {
                $imagick = new \Imagick($source_path);
                $imagick->setImageFormat('webp');
                $imagick->setImageCompressionQuality($quality);
                $success = $imagick->writeImage($webp_path);
                $imagick->destroy();
            } catch (\Exception $e) {
                // Fall back to GD
            }
        }

        if (!$success && function_exists('imagewebp')) {
            $image = $this->load_image_gd($source_path);
            if ($image) {
                $success = imagewebp($image, $webp_path, $quality);
                imagedestroy($image);
            }
        }

        if ($success && file_exists($webp_path) && filesize($webp_path) === 0) {
            \ProRank\SEO\Core\FilesystemHelper::delete_file($webp_path);
            return false;
        }

        // Size validation: WebP must be smaller than source, otherwise discard
        if ($success && file_exists($webp_path)) {
            $source_size = filesize($source_path);
            $webp_size = filesize($webp_path);
            if ($webp_size >= $source_size) {
                \ProRank\SEO\Core\FilesystemHelper::delete_file($webp_path);
                return false;
            }
        }

        if ($success && $attachment_id > 0) {
            // Store metadata
            $meta_key = '_prorank_webp_path' . ($size ? '_' . $size : '');
            update_post_meta($attachment_id, $meta_key, $webp_path);

            // Update statistics
            $this->update_optimization_stats($source_path, $webp_path, 'webp');
        }

        return $success;
    }

    /**
     * Optimize image via cloud service with retry mechanism
     *
     * @param string $source_path Source image path
     * @param string $target_path Target image path
     * @param string $format Target format (webp/avif)
     * @param int $quality Quality setting
     * @return bool Success status
     */
    private function optimize_via_cloud(string $source_path, string $target_path, string $format, int $quality): bool {
        // Check license tier / credits for cloud optimization
        $plugin = \ProRank\SEO\Plugin::get_instance();
        if (!$plugin || !$plugin->is_initialized()) {
            return false;
        }

        $license_manager = $plugin->license();
        $quota_status = $this->get_quota_status();
        if (($quota_status['status'] ?? '') !== 'active') {
            prorank_log('[ProRank Image Optimization] Cloud optimization unavailable: ' . ($quota_status['message'] ?? 'No active credits'));
            return false;
        }

        $provider = $this->get_setting('cloud_provider', 'prorank');
        $providers = $this->getCloudProviders();
        $endpoint = $providers[$provider]['endpoint'] ?? null;

        if (!$endpoint) {
            return false;
        }

        $auth_token = $this->get_cloud_auth_token();
        if (!$auth_token) {
            prorank_log('[ProRank Image Optimization] No valid JWT token available for cloud optimization');
            return false;
        }

        // Prepare request metadata
        $headers = $this->build_cloud_auth_headers($auth_token);

        // Add parameters
        $params = [
            'format' => $format,
            'quality' => (string) $quality,
            'compression_type' => $this->get_setting('compression_type', 'lossy'),
            'smart_compression' => $this->get_setting('smart_compression', true) ? '1' : '0',
        ];

        // Retry mechanism with exponential backoff
        $max_retries = 3;
        $retry_delay = 1000; // Start with 1 second
        $attempt = 0;
        $success = false;
        $last_error = '';
        $optimized_image = null;

        while ($attempt < $max_retries && !$success) {
            $attempt++;

            // Make the request (streaming for larger files, fallback for compatibility)
            $request_result = $this->send_cloud_multipart_request(
                $endpoint,
                $source_path,
                $headers,
                $params,
                30 + ($attempt * 10) // Increase timeout with each retry
            );

            if (!$request_result['ok']) {
                $last_error = $request_result['error'] ?: 'Cloud request failed';

                // Log error for monitoring
                prorank_log(sprintf(
                    '[ProRank Image Optimization] Cloud request failed (attempt %d/%d): %s',
                    $attempt,
                    $max_retries,
                    $last_error
                ));

                // If not last attempt, wait before retry
                if ($attempt < $max_retries) {
                    usleep($retry_delay * 1000); // Convert to microseconds
                    $retry_delay *= 2; // Exponential backoff
                }
                continue;
            }

            $response_code = (int) $request_result['status_code'];
            $response_body = (string) $request_result['body'];
            $response_headers = is_array($request_result['headers'] ?? null) ? $request_result['headers'] : [];

            // Success
            if ($response_code === 200) {
                $optimized_image = $response_body;
                $success = true;
                break;
            }

            $decoded_error = json_decode($response_body, true);
            $quota_error_detected =
                is_array($decoded_error)
                && (
                    isset($decoded_error['available_credits'])
                    || isset($decoded_error['remaining'])
                    || isset($decoded_error['remaining_quota'])
                    || isset($decoded_error['total_available'])
                    || isset($decoded_error['code'])
                )
                && (
                    stripos((string) ($decoded_error['error'] ?? $decoded_error['message'] ?? ''), 'quota') !== false
                    || stripos((string) ($decoded_error['error'] ?? $decoded_error['message'] ?? ''), 'credit') !== false
                    || in_array((string) ($decoded_error['code'] ?? ''), ['quota_exhausted', 'credits_required'], true)
                );

            // Quota exhausted or purchased credits unavailable - don't retry.
            if (($response_code === 402 || $response_code === 429) && $quota_error_detected) {
                $license_status = $license_manager->get_license_status_snapshot();
                $tier = $license_status['tier'] ?? $license_manager->get_tier();
                $normalized_tier = strpos($tier, '_plus') !== false ? str_replace('_plus', '', $tier) : $tier;
                if (!$license_manager->has_valid_license_status()) {
                    $normalized_tier = 'free';
                }
                $period = $this->resolve_quota_period(
                    $license_status,
                    $normalized_tier === 'free' && (bool) $this->get_setting('cloud_opt_in', false),
                    $this->normalize_opt_in_timestamp($this->get_setting('cloud_opt_in_at', null))
                );

                $quota_message = (string) ($decoded_error['message'] ?? $decoded_error['error'] ?? __('Cloud credits are not available', 'prorank-seo'));
                set_transient('prorank_image_quota_exceeded', [
                    'message' => $quota_message,
                    'remaining' => 0,
                    'next_reset' => $decoded_error['next_reset'] ?? gmdate('c', $period['end']),
                ], HOUR_IN_SECONDS);

                prorank_log(sprintf(
                    '[ProRank Image Optimization] Cloud quota exhausted: %s',
                    $quota_message
                ));

                $last_error = $quota_message;
                break;
            }

            // Rate limiting - wait longer
            if ($response_code === 429) {
                $retry_after_value = $response_headers['retry-after'] ?? '';
                if (is_array($retry_after_value)) {
                    $retry_after_value = reset($retry_after_value);
                }
                $retry_after_seconds = is_numeric($retry_after_value) ? (int) $retry_after_value : 0;
                $wait_time = $retry_after_seconds > 0 ? $retry_after_seconds * 1000000 : $retry_delay * 2000;

                prorank_log(sprintf(
                    '[ProRank Image Optimization] Rate limited (attempt %d/%d), waiting %d seconds',
                    $attempt,
                    $max_retries,
                    $wait_time / 1000000
                ));

                if ($attempt < $max_retries) {
                    usleep($wait_time);
                    $retry_delay = $wait_time / 1000; // Update delay for next iteration
                }
                continue;
            }

            // Quota exceeded - don't retry
            if ($response_code === 402) {
                $error_body = $response_body;
                $error_data = json_decode($error_body, true);
                $license_status = $license_manager->get_license_status_snapshot();
                $tier = $license_status['tier'] ?? $license_manager->get_tier();
                $normalized_tier = strpos($tier, '_plus') !== false ? str_replace('_plus', '', $tier) : $tier;
                if (!$license_manager->has_valid_license_status()) {
                    $normalized_tier = 'free';
                }
                $period = $this->resolve_quota_period(
                    $license_status,
                    $normalized_tier === 'free' && (bool) $this->get_setting('cloud_opt_in', false),
                    $this->normalize_opt_in_timestamp($this->get_setting('cloud_opt_in_at', null))
                );

                // Store quota status for display
                set_transient('prorank_image_quota_exceeded', [
                    'message' => $error_data['message'] ?? 'Yearly quota exceeded',
                    'remaining' => $error_data['remaining'] ?? 0,
                    'next_reset' => $error_data['next_reset'] ?? gmdate('c', $period['end']),
                ], HOUR_IN_SECONDS);

                prorank_log(sprintf(
                    '[ProRank Image Optimization] Quota exceeded: %s',
                    $error_data['message'] ?? 'Yearly quota exceeded'
                ));
                $last_error = $error_data['message'] ?? 'Yearly quota exceeded';
                break;
            }

            // License invalid
            if ($response_code === 401) {
                prorank_log('[ProRank Image Optimization] License authentication failed');
                $last_error = 'License authentication failed';

                // Clear cached license status
                delete_transient('prorank_license_status');
                break;
            }

            // Other client errors - don't retry
            if ($response_code >= 400 && $response_code < 500) {
                $error_body = $response_body;
                prorank_log(sprintf(
                    '[ProRank Image Optimization] Client error %d: %s',
                    $response_code,
                    substr($error_body, 0, 200)
                ));
                $last_error = sprintf('HTTP %d: %s', $response_code, substr((string) $error_body, 0, 200));
                break;
            }

            // Server error - retry
            if ($response_code >= 500) {
                prorank_log(sprintf(
                    '[ProRank Image Optimization] Server error %d (attempt %d/%d)',
                    $response_code,
                    $attempt,
                    $max_retries
                ));

                if ($attempt < $max_retries) {
                    usleep($retry_delay * 1000);
                    $retry_delay *= 2;
                }
                $last_error = sprintf('Server error %d', $response_code);
                continue;
            }

            $last_error = sprintf('Unexpected HTTP status %d', $response_code);
        }

        if (!$success || !$optimized_image) {
            // Store failed optimization in queue for later retry
            $this->queue_failed_optimization($source_path, $format, $quality, $last_error);
            return false;
        }
        if (empty($optimized_image)) {
            return false;
        }

        // Save the optimized image
        $result = file_put_contents($target_path, $optimized_image);
        if ($result !== false) {
            // Force quota refresh on next read so dashboard reflects recent cloud usage quickly.
            $this->invalidate_quota_usage_cache();
            delete_transient('prorank_image_quota_exceeded');
            return true;
        }

        return false;
    }

    /**
     * Determine if cloud upload should use streaming transport.
     *
     * @param string $file_path Source file path.
     * @return bool
     */
    private function should_stream_cloud_upload(string $file_path): bool {
        $threshold = (int) apply_filters('prorank_cloud_streaming_threshold_bytes', 5 * 1024 * 1024, $file_path);
        $file_size = @filesize($file_path);
        if ($file_size === false) {
            return false;
        }

        return $file_size >= max(0, $threshold);
    }

    /**
     * Send multipart cloud request with streaming support and fallback.
     *
     * @param string $endpoint API endpoint.
     * @param string $file_path File path.
     * @param array $headers Request headers.
     * @param array $params Multipart params.
     * @param int $timeout Request timeout.
     * @return array{ok:bool,status_code:int,body:string,headers:array,error:string}
     */
    private function send_cloud_multipart_request(string $endpoint, string $file_path, array $headers, array $params, int $timeout): array {
        return $this->send_cloud_multipart_request_via_wp($endpoint, $file_path, $headers, $params, $timeout);
    }

    /**
     * Send multipart cloud request via cURL (streaming upload).
     *
     * @param string $endpoint API endpoint.
     * @param string $file_path File path.
     * @param array $headers Request headers.
     * @param array $params Multipart params.
     * @param int $timeout Request timeout.
     * @return array{ok:bool,status_code:int,body:string,headers:array,error:string}
     */
    private function send_cloud_multipart_request_via_curl(string $endpoint, string $file_path, array $headers, array $params, int $timeout): array {
        return $this->send_cloud_multipart_request_via_wp($endpoint, $file_path, $headers, $params, $timeout);
    }

    /**
     * Send multipart cloud request via WP HTTP API.
     *
     * @param string $endpoint API endpoint.
     * @param string $file_path File path.
     * @param array $headers Request headers.
     * @param array $params Multipart params.
     * @param int $timeout Request timeout.
     * @return array{ok:bool,status_code:int,body:string,headers:array,error:string}
     */
    private function send_cloud_multipart_request_via_wp(string $endpoint, string $file_path, array $headers, array $params, int $timeout): array {
        $file_data = file_get_contents($file_path);
        if ($file_data === false) {
            return [
                'ok' => false,
                'status_code' => 0,
                'body' => '',
                'headers' => [],
                'error' => 'Failed to read source file',
            ];
        }

        $boundary = wp_generate_password(24);
        $body = '';

        $body .= '--' . $boundary . "\r\n";
        $body .= 'Content-Disposition: form-data; name="image"; filename="' . basename($file_path) . '"' . "\r\n";
        $filetype = wp_check_filetype($file_path);
        $body .= 'Content-Type: ' . ($filetype['type'] ?: 'application/octet-stream') . "\r\n\r\n";
        $body .= $file_data . "\r\n";

        foreach ($params as $key => $value) {
            $body .= '--' . $boundary . "\r\n";
            $body .= 'Content-Disposition: form-data; name="' . $key . '"' . "\r\n\r\n";
            $body .= (string) $value . "\r\n";
        }

        $body .= '--' . $boundary . '--';

        $request_headers = $headers;
        $request_headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;

        $response = wp_remote_post($endpoint, [
            'headers' => $request_headers,
            'body' => $body,
            'timeout' => $timeout,
        ]);

        if (is_wp_error($response)) {
            return [
                'ok' => false,
                'status_code' => 0,
                'body' => '',
                'headers' => [],
                'error' => $response->get_error_message(),
            ];
        }

        $normalized_headers = [];
        $raw_headers = wp_remote_retrieve_headers($response);
        if (is_array($raw_headers) || $raw_headers instanceof \Traversable) {
            foreach ($raw_headers as $key => $value) {
                $normalized_headers[strtolower((string) $key)] = is_array($value) ? implode(', ', $value) : (string) $value;
            }
        }

        return [
            'ok' => true,
            'status_code' => (int) wp_remote_retrieve_response_code($response),
            'body' => (string) wp_remote_retrieve_body($response),
            'headers' => $normalized_headers,
            'error' => '',
        ];
    }

    /**
     * Check server WebP support
     *
     * @return array Support information
     */
    public function check_webp_support(): array {
        $support = [
            'gd' => false,
            'imagick' => false,
            'details' => [],
        ];

        // Check GD
        if (function_exists('gd_info')) {
            $gd_info = gd_info();
            $support['gd'] = !empty($gd_info['WebP Support']);
            $support['details']['gd_version'] = $gd_info['GD Version'] ?? 'Unknown';
        }

        // Check Imagick
        if (extension_loaded('imagick') && class_exists('Imagick')) {
            try {
                $imagick = new \Imagick();
                $formats = $imagick->queryFormats('WEBP');
                $support['imagick'] = !empty($formats);
                $support['details']['imagick_version'] = \Imagick::getVersion()['versionString'] ?? 'Unknown';
            } catch (\Exception $e) {
                $support['details']['imagick_error'] = $e->getMessage();
            }
        }

        return $support;
    }

    /**
     * Generate AVIF version of image
     *
     * @param string $source_path   Source image path
     * @param int    $attachment_id Attachment ID
     * @param string $size          Image size name
     * @return bool Success status
     */
    private function generate_avif(string $source_path, int $attachment_id, string $size = ''): bool {
        if (!$this->is_supported_source_path($source_path)) {
            return false;
        }

        // Check if system supports AVIF
        if (!$this->can_generate_avif()) {
            return false;
        }

        $avif_path = $this->get_optimized_path($source_path, 'avif');

        $is_responsive_subsize = strpos($size, 'prorank-responsive-') === 0;

        // Skip if already exists unless the derivative is byte-heavy enough to
        // keep triggering image-delivery diagnostics. This covers both ProRank
        // responsive sizes and standard WordPress sub-sizes generated before a
        // Smart Compression quality-profile update.
        if (file_exists($avif_path)) {
            if (!$this->should_regenerate_existing_optimized_variant($avif_path, $source_path, $size)) {
                return true;
            }

            \ProRank\SEO\Core\FilesystemHelper::delete_file($avif_path);
        }

        // Create directory if needed
        $dir = dirname($avif_path);
        if (!is_dir($dir)) {
            wp_mkdir_p($dir);
        }

        // Get quality setting based on compression type
        $compression_type = $this->get_setting('compression_type', 'lossy');
        $is_subsize = $size !== '';

        // Override with custom quality if SmartCompression is disabled
        if (!$this->get_setting('smart_compression', true)) {
            $quality = (int) $this->get_setting('avif_quality', self::DEFAULT_AVIF_QUALITY);
        } elseif ($is_subsize) {
            // Any generated sub-size can tolerate more aggressive AVIF compression than originals.
            $base = $this->get_subsize_quality($compression_type, 'avif_quality', self::DEFAULT_AVIF_QUALITY);
            $quality = (int) apply_filters('prorank_subsize_avif_quality', $base, $source_path, $size);
            if ($is_responsive_subsize) {
                $quality = (int) apply_filters('prorank_responsive_subsize_avif_quality', $quality, $source_path, $size);
            }
        } else {
            $quality = self::COMPRESSION_TYPES[$compression_type]['avif_quality'] ?? self::DEFAULT_AVIF_QUALITY;
        }

        $quality = $this->adjust_smart_format_quality($source_path, 'avif', (int) $quality);

        // Try to generate AVIF
        $success = false;

        if (extension_loaded('imagick') && class_exists('Imagick')) {
            try {
                $imagick = new \Imagick($source_path);
                $formats = $imagick->queryFormats('AVIF');
                if (!empty($formats)) {
                    $imagick->setImageFormat('avif');
                    $imagick->setImageCompressionQuality($quality);
                    $success = $imagick->writeImage($avif_path);
                }
                $imagick->destroy();
            } catch (\Exception $e) {
                // AVIF not supported
            }
        }

        if (!$success && function_exists('imageavif')) {
            $image = $this->load_image_gd($source_path);
            if ($image) {
                $success = imageavif($image, $avif_path, $quality);
                imagedestroy($image);
            }
        }

        if ($success && file_exists($avif_path) && filesize($avif_path) === 0) {
            \ProRank\SEO\Core\FilesystemHelper::delete_file($avif_path);
            return false;
        }

        // Size validation: AVIF must be smaller than source, otherwise discard
        if ($success && file_exists($avif_path)) {
            $source_size = filesize($source_path);
            $avif_size = filesize($avif_path);
            if ($avif_size >= $source_size) {
                \ProRank\SEO\Core\FilesystemHelper::delete_file($avif_path);
                return false;
            }
        }

        if ($success && $attachment_id > 0) {
            // Store metadata
            $meta_key = '_prorank_avif_path' . ($size ? '_' . $size : '');
            update_post_meta($attachment_id, $meta_key, $avif_path);

            // Update statistics
            $this->update_optimization_stats($source_path, $avif_path, 'avif');
        }

        return $success;
    }

    /**
     * Decide whether an optimized responsive variant is still too heavy.
     *
     * @param string $optimized_path Optimized file path.
     * @param string $source_path    Source sub-size path.
     * @return bool
     */
    private function should_regenerate_responsive_optimized_variant(string $optimized_path, string $source_path): bool {
        if (!file_exists($optimized_path) || !file_exists($source_path)) {
            return false;
        }

        $dimensions = @getimagesize($source_path);
        if (!is_array($dimensions)) {
            return false;
        }

        $width = (int) ($dimensions[0] ?? 0);
        $height = (int) ($dimensions[1] ?? 0);
        if ($width <= 0 || $height <= 0) {
            return false;
        }

        $max_reasonable_bytes = max(8192, (int) ceil($width * $height * 0.11));
        return filesize($optimized_path) > $max_reasonable_bytes;
    }

    /**
     * Decide whether an existing optimized derivative is bad enough to regenerate.
     *
     * @param string $optimized_path Existing optimized output path.
     * @param string $source_path    Source image path.
     * @param string $size           WordPress / ProRank size key. Empty means original source.
     * @return bool
     */
    private function should_regenerate_existing_optimized_variant(string $optimized_path, string $source_path, string $size = ''): bool {
        if (!file_exists($optimized_path)) {
            return false;
        }

        $optimized_size = (int) @filesize($optimized_path);
        if ($optimized_size <= 0) {
            return true;
        }

        if (!file_exists($source_path)) {
            return false;
        }

        $source_size = (int) @filesize($source_path);
        if ($source_size > 0 && $optimized_size >= $source_size) {
            return true;
        }

        if ($size === '') {
            return false;
        }

        return $this->should_regenerate_responsive_optimized_variant($optimized_path, $source_path);
    }

    /**
     * Tune modern-format quality for small flat PNG assets when Smart Compression is enabled.
     *
     * Badges, logos, and other UI-style PNGs often remain larger than necessary
     * after WebP conversion, and PageSpeed flags them even when responsive
     * delivery is correct. AVIF handles these small graphics much better at a
     * lower quality setting while preserving acceptable visual fidelity.
     *
     * @param string $source_path Source image path.
     * @param string $format      Target format.
     * @param int    $quality     Current quality.
     * @return int
     */
    private function adjust_smart_format_quality(string $source_path, string $format, int $quality): int {
        if (!$this->get_setting('smart_compression', true)) {
            return $quality;
        }

        if ($this->get_setting('compression_type', 'lossy') === 'lossless') {
            return $quality;
        }

        $format = strtolower($format);
        if (!in_array($format, ['webp', 'avif'], true)) {
            return $quality;
        }

        $extension = strtolower((string) pathinfo($source_path, PATHINFO_EXTENSION));
        if ($extension !== 'png') {
            return $quality;
        }

        $dimensions = function_exists('wp_getimagesize') ? wp_getimagesize($source_path) : @getimagesize($source_path);
        if (!$dimensions || empty($dimensions[0]) || empty($dimensions[1])) {
            return $quality;
        }

        $width = (int) $dimensions[0];
        $height = (int) $dimensions[1];
        if ($width <= 0 || $height <= 0 || $width > 640 || $height > 320) {
            return $quality;
        }

        $source_size = file_exists($source_path) ? (int) filesize($source_path) : 0;
        if ($source_size <= 0 || $source_size > 128 * 1024) {
            return $quality;
        }

        if ($format === 'avif') {
            return min($quality, (int) apply_filters('prorank_smart_png_badge_avif_quality', 28, $source_path, $width, $height));
        }

        return min($quality, (int) apply_filters('prorank_smart_png_badge_webp_quality', 55, $source_path, $width, $height));
    }

    /**
     * Generate JPEG XL version of an image
     *
     * @param string $source_path   Source image path
     * @param int    $attachment_id Attachment ID
     * @param string $size          Image size name
     * @return bool Success status
     */
    private function generate_jxl(string $source_path, int $attachment_id, string $size = ''): bool {
        $jxl_path = $this->get_optimized_path($source_path, 'jxl');

        if (file_exists($jxl_path)) {
            if (!$this->should_regenerate_existing_optimized_variant($jxl_path, $source_path, $size)) {
                return true;
            }

            \ProRank\SEO\Core\FilesystemHelper::delete_file($jxl_path);
        }

        // Create directory if needed
        $dir = dirname($jxl_path);
        if (!is_dir($dir)) {
            wp_mkdir_p($dir);
        }

        // Get quality setting
        $compression_type = $this->get_setting('compression_type', 'lossy');
        $is_subsize = $size !== '';
        $is_responsive_subsize = strpos($size, 'prorank-responsive-') === 0;

        if (!$this->get_setting('smart_compression', true)) {
            $quality = self::COMPRESSION_TYPES[$compression_type]['jxl_quality'] ?? self::DEFAULT_JXL_QUALITY;
        } elseif ($is_subsize) {
            $base = $this->get_subsize_quality($compression_type, 'jxl_quality', self::DEFAULT_JXL_QUALITY);
            $quality = (int) apply_filters('prorank_subsize_jxl_quality', $base, $source_path, $size);
            if ($is_responsive_subsize) {
                $quality = (int) apply_filters('prorank_responsive_subsize_jxl_quality', $quality, $source_path, $size);
            }
        } else {
            $quality = self::COMPRESSION_TYPES[$compression_type]['jxl_quality'] ?? self::DEFAULT_JXL_QUALITY;
        }

        $success = false;

        if (extension_loaded('imagick') && class_exists('Imagick')) {
            try {
                $imagick = new \Imagick($source_path);
                $formats = $imagick->queryFormats('JXL');
                if (!empty($formats)) {
                    $imagick->setImageFormat('jxl');
                    $imagick->setImageCompressionQuality($quality);
                    $success = $imagick->writeImage($jxl_path);
                }
                $imagick->destroy();
            } catch (\Exception $e) {
                // JXL not supported
            }
        }

        if ($success && $attachment_id > 0) {
            $meta_key = '_prorank_jxl_path' . ($size ? '_' . $size : '');
            update_post_meta($attachment_id, $meta_key, $jxl_path);
            $this->update_optimization_stats($source_path, $jxl_path, 'jxl');
        }

        return $success;
    }

    /**
     * Convert quality percentage to JXL distance value
     * JXL uses distance where 0 = lossless, higher = more compression
     *
     * @param int $quality Quality 0-100
     * @return string Distance value
     */
    private function quality_to_jxl_distance(int $quality): string {
        if ($quality >= 100) {
            return '0'; // Lossless
        }
        // Map 0-99 quality to 0.1-3.0 distance (lower quality = higher distance)
        $distance = 0.1 + ((100 - $quality) / 100) * 2.9;
        return number_format($distance, 2);
    }

    /**
     * Check if cjxl CLI tool is available
     *
     * @return bool
     */
    private function has_jxl_cli(): bool {
        return false;
    }

    /**
     * Load image using GD
     *
     * @param string $path Image path
     * @return resource|false GD image resource
     */
    private function load_image_gd(string $path) {
        $info = wp_getimagesize($path);
        if (!$info) {
            return false;
        }

        switch ($info['mime']) {
            case 'image/jpeg':
                return imagecreatefromjpeg($path);
            case 'image/png':
                $image = imagecreatefrompng($path);
                if (!$image) {
                    return false;
                }

                if (function_exists('imageistruecolor') && function_exists('imagepalettetotruecolor') && !imageistruecolor($image)) {
                    imagepalettetotruecolor($image);
                }

                if (function_exists('imagealphablending')) {
                    imagealphablending($image, false);
                }

                if (function_exists('imagesavealpha')) {
                    imagesavealpha($image, true);
                }

                return $image;
            default:
                return false;
        }
    }

    /**
     * Confirm the source asset is a raster format this module officially supports.
     *
     * @param string $path Image path
     * @return bool
     */
    private function is_supported_source_path(string $path): bool {
        $info = wp_getimagesize($path);
        if (!$info || empty($info['mime']) || !is_string($info['mime'])) {
            return false;
        }

        return in_array(strtolower($info['mime']), self::SUPPORTED_MIME_TYPES, true);
    }

    /**
     * Get optimized image path
     *
     * @param string $original_path Original image path
     * @param string $format        Target format (webp/avif)
     * @return string Optimized image path
     */
    public function get_optimized_path(string $original_path, string $format): string {
        $pathinfo = pathinfo($original_path);
        return $pathinfo['dirname'] . '/' . $pathinfo['filename'] . '.' . $format;
    }

    /**
     * Delete optimized images when original is deleted
     *
     * @param int $attachment_id Attachment ID
     * @return void
     */
    public function delete_optimized_images(int $attachment_id): void {
        foreach ($this->get_optimized_path_meta_keys($attachment_id) as $meta_key) {
            $path = get_post_meta($attachment_id, $meta_key, true);
            if ($path && file_exists($path)) {
                \ProRank\SEO\Core\FilesystemHelper::delete_file($path);
            }

            delete_post_meta($attachment_id, $meta_key);
        }
    }

    /**
     * Add optimized formats to srcset
     *
     * @param array  $sources       Array of image sources
     * @param array  $size_array    Requested size
     * @param string $image_src     Image source URL
     * @param array  $image_meta    Image metadata
     * @param int    $attachment_id Attachment ID
     * @return array Modified sources
     */
    public function add_optimized_to_srcset(array $sources, array $size_array, string $image_src, array $image_meta, int $attachment_id): array {
        // Not used anymore - we're using picture element instead
        return $sources;
    }

    /**
     * Check if WebP generation is supported
     *
     * @return bool
     */
    private function can_generate_webp(): bool {
        if (function_exists('imagewebp')) {
            return true;
        }

        // Check Imagick
        if (extension_loaded('imagick') && class_exists('Imagick')) {
            $imagick = new \Imagick();
            $formats = $imagick->queryFormats('WEBP');
            return !empty($formats);
        }

        return false;
    }

    /**
     * Check if AVIF generation is supported
     *
     * @return bool
     */
    private function can_generate_avif(): bool {
        if (function_exists('imageavif')) {
            return true;
        }

        // Check Imagick
        if (extension_loaded('imagick') && class_exists('Imagick')) {
            $imagick = new \Imagick();
            $formats = $imagick->queryFormats('AVIF');
            return !empty($formats);
        }

        return false;
    }

    /**
     * Check if AVIF is supported by server
     *
     * @return bool
     */
    private function is_avif_supported(): bool {
        return $this->can_generate_avif();
    }

    /**
     * Check if JPEG XL generation is supported
     *
     * @return bool
     */
    private function can_generate_jxl(): bool {
        if (extension_loaded('imagick') && class_exists('Imagick')) {
            try {
                $imagick = new \Imagick();
                $formats = $imagick->queryFormats('JXL');
                if (!empty($formats)) {
                    return true;
                }
            } catch (\Exception $e) {
                return false;
            }
        }

        return false;
    }

    /**
     * Update optimization statistics
     *
     * @param string $original_path Original file path
     * @param string $optimized_path Optimized file path
     * @param string $format Format type
     * @return void
     */
    private function update_optimization_stats(string $original_path, string $optimized_path, string $format): void {
        $original_size = filesize($original_path);
        $optimized_size = filesize($optimized_path);

        if ($original_size && $optimized_size) {
            $savings = max(0, $original_size - $optimized_size);

            // Update global stats
            $stats = get_option('prorank_image_optimization_stats', [
                'total_images' => 0,
                'total_savings' => 0,
                'webp_count' => 0,
                'avif_count' => 0,
            ]);

            if (!is_array($stats)) {
                $stats = [];
            }
            $stats['total_images'] = (int) ($stats['total_images'] ?? 0);
            $stats['total_savings'] = (int) ($stats['total_savings'] ?? 0);
            $count_key = $format . '_count';
            $stats[$count_key] = (int) ($stats[$count_key] ?? 0);

            $stats['total_images']++;
            $stats['total_savings'] += $savings;
            $stats[$count_key]++;

            update_option('prorank_image_optimization_stats', $stats, false);
        }
    }

    /**
     * Handle bulk conversion AJAX request
     *
     * @return void
     */
    public function handle_bulk_conversion(): void {
        // Check nonce
        if (!check_ajax_referer('prorank_image_optimization', 'nonce', false)) {
            wp_send_json_error(__('Invalid nonce', 'prorank-seo'));
        }

        // Check capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(__('Insufficient permissions', 'prorank-seo'));
        }

        // Get batch of images
        $args = [
            'post_type' => 'attachment',
            'post_mime_type' => 'image',
            'post_status' => 'inherit',
            'posts_per_page' => 5,
            'meta_query' => [
                [
                    'key' => '_prorank_optimized',
                    'compare' => 'NOT EXISTS',
                ],
            ],
        ];

        $attachments = get_posts($args);

        if (empty($attachments)) {
            wp_send_json_success([
                'complete' => true,
                'message' => __('All images have been optimized', 'prorank-seo'),
            ]);
        }

        $processed = 0;
        foreach ($attachments as $attachment) {
            $metadata = wp_get_attachment_metadata($attachment->ID);
            if ($metadata) {
                $this->generate_optimized_images($metadata, $attachment->ID);
                $processed++;
            }
        }

        // Get remaining count
        $remaining_args = $args;
        $remaining_args['posts_per_page'] = -1;
        $remaining_args['fields'] = 'ids';
        $remaining = count(get_posts($remaining_args));

        wp_send_json_success([
            'complete' => false,
            'processed' => $processed,
            'remaining' => $remaining,
            'message' => sprintf(
                /* translators: %d: number of images processed */
                __('Processed %1$d images', 'prorank-seo'),
                $processed
            ),
        ]);
    }

    /**
     * Register custom image sizes
     *
     * @return void
     */
    public function register_image_sizes(): void {
        if (
            !$this->get_setting('serve_scaled_images', true)
            && !$this->get_setting('enable_responsive_images', true)
        ) {
            return;
        }

        foreach (self::RESPONSIVE_SUBSIZES as $width) {
            add_image_size('prorank-responsive-' . $width, $width, 0, false);
        }
    }

    /**
     * Get optimization statistics
     *
     * @return array Statistics
     */
    public function get_stats(): array {
        $stats = get_option('prorank_image_optimization_stats', [
            'total_images' => 0,
            'total_savings' => 0,
            'webp_count' => 0,
            'avif_count' => 0,
            'bytes_saved' => 0,
        ]);

        $retry_limit = $this->get_bulk_retry_limit();

        // Add capability info
        $stats['can_generate_webp'] = $this->can_generate_webp();
        $stats['can_generate_avif'] = $this->can_generate_avif();
        $stats['can_generate_jxl'] = $this->can_generate_jxl();

        global $wpdb;

        $mime_placeholders = implode(', ', array_fill(0, count(self::SUPPORTED_MIME_TYPES), '%s'));
        $modern_mime_placeholders = implode(', ', array_fill(0, count(self::MODERN_ORIGINAL_MIME_TYPES), '%s'));
        $excluded_mime_types = array_merge(self::SUPPORTED_MIME_TYPES, self::MODERN_ORIGINAL_MIME_TYPES);
        $excluded_mime_placeholders = implode(', ', array_fill(0, count($excluded_mime_types), '%s'));

        // Count only images that actually produced a modern output or real savings.
        $optimized_count = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(DISTINCT p.ID)
                 FROM {$wpdb->posts} p
                 WHERE p.post_type = 'attachment'
                 AND p.post_status <> 'trash'
                 AND p.post_mime_type IN ({$mime_placeholders})
                 AND EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm
                     WHERE pm.post_id = p.ID
                     AND (
                         (pm.meta_key = '_prorank_webp_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_avif_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_jxl_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_bytes_saved' AND CAST(pm.meta_value AS UNSIGNED) > 0)
                     )
                 )",
                ...self::SUPPORTED_MIME_TYPES
            )
        );

        // Count source images this module can actually convert.
        $source_count = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*)
                 FROM {$wpdb->posts}
                 WHERE post_type = 'attachment'
                 AND post_status <> 'trash'
                 AND post_mime_type IN ({$mime_placeholders})",
                ...self::SUPPORTED_MIME_TYPES
            )
        );

        // Count modern originals separately so the UI can show them as already optimized.
        $modern_original_count = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*)
                 FROM {$wpdb->posts}
                 WHERE post_type = 'attachment'
                 AND post_status <> 'trash'
                 AND post_mime_type IN ({$modern_mime_placeholders})",
                ...self::MODERN_ORIGINAL_MIME_TYPES
            )
        );

        $unsupported_count = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*)
                 FROM {$wpdb->posts}
                 WHERE post_type = 'attachment'
                 AND post_status <> 'trash'
                 AND post_mime_type LIKE %s
                 AND post_mime_type NOT IN ({$excluded_mime_placeholders})",
                $wpdb->esc_like('image/') . '%',
                ...$excluded_mime_types
            )
        );

        $current_saved_bytes = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COALESCE(SUM(CAST(pm.meta_value AS UNSIGNED)), 0)
                 FROM {$wpdb->posts} p
                 INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
                 WHERE p.post_type = 'attachment'
                 AND p.post_status <> 'trash'
                 AND p.post_mime_type IN ({$mime_placeholders})
                 AND pm.meta_key = '_prorank_bytes_saved'",
                ...self::SUPPORTED_MIME_TYPES
            )
        );

        $retryable_count = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(DISTINCT p.ID)
                 FROM {$wpdb->posts} p
                 WHERE p.post_type = 'attachment'
                 AND p.post_status <> 'trash'
                 AND p.post_mime_type IN ({$mime_placeholders})
                 AND NOT EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm
                     WHERE pm.post_id = p.ID
                     AND (
                         (pm.meta_key = '_prorank_webp_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_avif_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_jxl_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_bytes_saved' AND CAST(pm.meta_value AS UNSIGNED) > 0)
                     )
                 )
                 AND NOT EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm_no_gain
                     WHERE pm_no_gain.post_id = p.ID
                     AND (
                         (pm_no_gain.meta_key = '_prorank_optimization_no_gain' AND pm_no_gain.meta_value = '1')
                         OR (pm_no_gain.meta_key = '_prorank_optimization_failure_reason' AND pm_no_gain.meta_value = 'no_effective_output')
                     )
                 )
                 AND NOT EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm_attempts
                     WHERE pm_attempts.post_id = p.ID
                     AND pm_attempts.meta_key = '_prorank_optimization_attempts'
                     AND CAST(pm_attempts.meta_value AS UNSIGNED) >= %d
                 )",
                ...array_merge(self::SUPPORTED_MIME_TYPES, [$retry_limit])
            )
        );

        $no_gain_count = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(DISTINCT p.ID)
                 FROM {$wpdb->posts} p
                 WHERE p.post_type = 'attachment'
                 AND p.post_status <> 'trash'
                 AND p.post_mime_type IN ({$mime_placeholders})
                 AND NOT EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm
                     WHERE pm.post_id = p.ID
                     AND (
                         (pm.meta_key = '_prorank_webp_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_avif_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_jxl_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_bytes_saved' AND CAST(pm.meta_value AS UNSIGNED) > 0)
                     )
                 )
                 AND EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm_no_gain
                     WHERE pm_no_gain.post_id = p.ID
                     AND (
                         (pm_no_gain.meta_key = '_prorank_optimization_no_gain' AND pm_no_gain.meta_value = '1')
                         OR (pm_no_gain.meta_key = '_prorank_optimization_failure_reason' AND pm_no_gain.meta_value = 'no_effective_output')
                     )
                 )",
                ...self::SUPPORTED_MIME_TYPES
            )
        );

        $failed_count = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(DISTINCT p.ID)
                 FROM {$wpdb->posts} p
                 WHERE p.post_type = 'attachment'
                 AND p.post_status <> 'trash'
                 AND p.post_mime_type IN ({$mime_placeholders})
                 AND NOT EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm
                     WHERE pm.post_id = p.ID
                     AND (
                         (pm.meta_key = '_prorank_webp_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_avif_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_jxl_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_bytes_saved' AND CAST(pm.meta_value AS UNSIGNED) > 0)
                     )
                 )
                 AND NOT EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm_no_gain
                     WHERE pm_no_gain.post_id = p.ID
                     AND (
                         (pm_no_gain.meta_key = '_prorank_optimization_no_gain' AND pm_no_gain.meta_value = '1')
                         OR (pm_no_gain.meta_key = '_prorank_optimization_failure_reason' AND pm_no_gain.meta_value = 'no_effective_output')
                     )
                 )
                 AND EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm_failed
                     WHERE pm_failed.post_id = p.ID
                     AND pm_failed.meta_key = '_prorank_optimization_failed'
                     AND pm_failed.meta_value = '1'
                 )",
                ...self::SUPPORTED_MIME_TYPES
            )
        );

        $exhausted_failed_count = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(DISTINCT p.ID)
                 FROM {$wpdb->posts} p
                 WHERE p.post_type = 'attachment'
                 AND p.post_status <> 'trash'
                 AND p.post_mime_type IN ({$mime_placeholders})
                 AND NOT EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm
                     WHERE pm.post_id = p.ID
                     AND (
                         (pm.meta_key = '_prorank_webp_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_avif_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_jxl_generated' AND pm.meta_value = '1')
                         OR (pm.meta_key = '_prorank_bytes_saved' AND CAST(pm.meta_value AS UNSIGNED) > 0)
                     )
                 )
                 AND NOT EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm_no_gain
                     WHERE pm_no_gain.post_id = p.ID
                     AND (
                         (pm_no_gain.meta_key = '_prorank_optimization_no_gain' AND pm_no_gain.meta_value = '1')
                         OR (pm_no_gain.meta_key = '_prorank_optimization_failure_reason' AND pm_no_gain.meta_value = 'no_effective_output')
                     )
                 )
                 AND EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm_failed
                     WHERE pm_failed.post_id = p.ID
                     AND pm_failed.meta_key = '_prorank_optimization_failed'
                     AND pm_failed.meta_value = '1'
                 )
                 AND EXISTS (
                     SELECT 1 FROM {$wpdb->postmeta} pm_attempts
                     WHERE pm_attempts.post_id = p.ID
                     AND pm_attempts.meta_key = '_prorank_optimization_attempts'
                     AND CAST(pm_attempts.meta_value AS UNSIGNED) >= %d
                 )",
                ...array_merge(self::SUPPORTED_MIME_TYPES, [$retry_limit])
            )
        );

        $webp_count = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(DISTINCT p.ID)
                 FROM {$wpdb->posts} p
                 LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
                    AND pm.meta_key = '_prorank_webp_generated'
                    AND pm.meta_value = '1'
                 WHERE p.post_type = 'attachment'
                 AND p.post_status <> 'trash'
                 AND (p.post_mime_type = %s OR pm.post_id IS NOT NULL)",
                'image/webp'
            )
        );

        $avif_count = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(DISTINCT p.ID)
                 FROM {$wpdb->posts} p
                 LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
                    AND pm.meta_key = '_prorank_avif_generated'
                    AND pm.meta_value = '1'
                 WHERE p.post_type = 'attachment'
                 AND p.post_status <> 'trash'
                 AND (p.post_mime_type = %s OR pm.post_id IS NOT NULL)",
                'image/avif'
            )
        );

        $jxl_count = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(DISTINCT p.ID)
                 FROM {$wpdb->posts} p
                 LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
                    AND pm.meta_key = '_prorank_jxl_generated'
                    AND pm.meta_value = '1'
                 WHERE p.post_type = 'attachment'
                 AND p.post_status <> 'trash'
                 AND (p.post_mime_type = %s OR pm.post_id IS NOT NULL)",
                'image/jxl'
            )
        );

        $optimizable_total = $source_count + $modern_original_count;

        // Only count images ProRank actually optimized (have _prorank_optimized meta).
        // Modern originals (WebP/AVIF/JXL uploads) are tracked separately so the
        // dashboard doesn't show inflated numbers on a fresh install.
        $stats['optimized'] = $optimized_count;
        $stats['optimized_images'] = $optimized_count;
        $stats['source_images'] = $source_count;
        $stats['already_modern_images'] = $modern_original_count;
        $stats['total'] = $optimizable_total;
        $stats['total_images'] = $optimizable_total;
        $stats['pending'] = $retryable_count;
        $stats['unresolved_images'] = max(0, $source_count - $optimized_count);
        $stats['failed_images'] = $failed_count;
        $stats['exhausted_failed_images'] = $exhausted_failed_count;
        $stats['retryable_failed_images'] = max(0, $failed_count - $exhausted_failed_count);
        $stats['no_gain_images'] = $no_gain_count;
        $stats['bulk_retry_limit'] = $retry_limit;
        $stats['unsupported_images'] = $unsupported_count;
        $stats['all_image_attachments'] = $optimizable_total + $unsupported_count;
        $stats['bytes_saved'] = $current_saved_bytes;
        $stats['total_savings'] = $current_saved_bytes;
        $stats['webp_count'] = $webp_count;
        $stats['avif_count'] = $avif_count;
        $stats['jxl_count'] = $jxl_count;

        // Update cached stats with current values to prevent stale dashboard totals.
        $cached_stats = get_option('prorank_image_optimization_stats', []);
        if (
            !isset($cached_stats['total_images']) ||
            (int) $cached_stats['total_images'] !== $optimizable_total ||
            (int) ($cached_stats['total_savings'] ?? 0) !== $current_saved_bytes ||
            (int) ($cached_stats['bytes_saved'] ?? 0) !== $current_saved_bytes ||
            (int) ($cached_stats['webp_count'] ?? 0) !== $webp_count ||
            (int) ($cached_stats['avif_count'] ?? 0) !== $avif_count ||
            (int) ($cached_stats['jxl_count'] ?? 0) !== $jxl_count
        ) {
            $cached_stats['total_images'] = (int) $optimizable_total;
            $cached_stats['total_savings'] = $current_saved_bytes;
            $cached_stats['bytes_saved'] = $current_saved_bytes;
            $cached_stats['webp_count'] = $webp_count;
            $cached_stats['avif_count'] = $avif_count;
            $cached_stats['jxl_count'] = $jxl_count;
            update_option('prorank_image_optimization_stats', $cached_stats, false);
        }

        $stats['optimization_percentage'] = $stats['total'] > 0
            ? round(($stats['optimized'] / $stats['total']) * 100, 2)
            : 0;

        // Add backup folder size if backups are enabled.
        if ($this->get_setting('backup_originals', true)) {
            $backup_size = 0;
            foreach ($this->get_all_image_backup_directories() as $backup_dir) {
                if (is_dir($backup_dir)) {
                    $backup_size += $this->get_directory_size($backup_dir);
                }
            }

            if ($backup_size > 0) {
                $stats['backup_size'] = $backup_size;
                $stats['backup_size_formatted'] = size_format($backup_size);
            }
        }

        return $stats;
    }

    /**
     * Convert img tag to picture element with modern image sources
     *
     * @param string $html          Original HTML
     * @param int    $attachment_id Attachment ID
     * @param string $size          Image size
     * @param bool   $icon          Whether it's an icon
     * @param array  $attr          Image attributes
     * @return string Modified HTML
     */
    public function convert_to_picture_element(string $html, int $attachment_id, string $size, bool $icon, array $attr): string {
        if (!$this->is_frontend_optimization_context()) {
            return $html;
        }

        // Skip if delivery is disabled
        if (!$this->get_setting('enable_picture_element', true)) {
            return $html;
        }

        // Skip icons
        if ($icon) {
            return $html;
        }

        // Skip if no attachment ID
        if (!$attachment_id) {
            return $html;
        }

        // Get image metadata
        $metadata = wp_get_attachment_metadata($attachment_id);
        if (!$metadata) {
            return $html;
        }

        // Parse the img tag
        $doc = new \DOMDocument();
        @$doc->loadHTML('<?xml encoding="UTF-8">' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
        $img = $doc->getElementsByTagName('img')->item(0);

        if (!$img) {
            return $html;
        }

        // Get original src
        $original_src = $img->getAttribute('src');
        $srcset = $img->getAttribute('srcset');
        $sizes = $img->getAttribute('sizes');

        // Check if optimized versions exist
        $has_webp = false;
        $has_avif = false;
        $has_jxl = false;
        $webp_srcset = [];
        $avif_srcset = [];
        $jxl_srcset = [];

        // Build srcsets for optimized formats
        if ($srcset) {
            $srcset_parts = explode(',', $srcset);
            foreach ($srcset_parts as $part) {
                $part = trim($part);
                if (preg_match('/(\S+)\s+(\d+w)/', $part, $matches)) {
                    $url = $matches[1];
                    $descriptor = $matches[2];

                    // Generate WebP URL (handles WordPress suffix like -2, -3)
                    $webp_url = $this->get_optimized_url($url, 'webp');
                    $actual_webp_url = $this->get_actual_optimized_url($webp_url);
                    if ($actual_webp_url) {
                        $webp_srcset[] = $actual_webp_url . ' ' . $descriptor;
                        $has_webp = true;
                    }

                    // Generate AVIF URL
                    $avif_url = $this->get_optimized_url($url, 'avif');
                    $actual_avif_url = $this->get_actual_optimized_url($avif_url);
                    if ($actual_avif_url) {
                        $avif_srcset[] = $actual_avif_url . ' ' . $descriptor;
                        $has_avif = true;
                    }

                    // Generate JXL URL
                    $jxl_url = $this->get_optimized_url($url, 'jxl');
                    $actual_jxl_url = $this->get_actual_optimized_url($jxl_url);
                    if ($actual_jxl_url) {
                        $jxl_srcset[] = $actual_jxl_url . ' ' . $descriptor;
                        $has_jxl = true;
                    }
                }
            }
        }

        // Check main image - get actual URLs with WordPress suffixes
        $webp_src = $this->get_optimized_url($original_src, 'webp');
        $actual_webp_src = $this->get_actual_optimized_url($webp_src);
        if ($actual_webp_src) {
            $webp_src = $actual_webp_src;
            $has_webp = true;
        }

        $avif_src = $this->get_optimized_url($original_src, 'avif');
        $actual_avif_src = $this->get_actual_optimized_url($avif_src);
        if ($actual_avif_src) {
            $avif_src = $actual_avif_src;
            $has_avif = true;
        }

        $jxl_src = $this->get_optimized_url($original_src, 'jxl');
        $actual_jxl_src = $this->get_actual_optimized_url($jxl_src);
        if ($actual_jxl_src) {
            $jxl_src = $actual_jxl_src;
            $has_jxl = true;
        }

        // If no optimized versions exist, return original
        if (!$has_webp && !$has_avif && !$has_jxl) {
            return $html;
        }

        // Create picture element
        $picture = $doc->createElement('picture');

        // Add JXL source if available
        if ($has_jxl && $this->get_setting('jxl_enabled', false)) {
            $jxl_source = $doc->createElement('source');
            $jxl_source->setAttribute('type', 'image/jxl');
            $jxl_source->setAttribute('srcset', $jxl_src);

            if (!empty($jxl_srcset)) {
                $jxl_source->setAttribute('srcset', implode(', ', $jxl_srcset));
            }

            if ($sizes) {
                $jxl_source->setAttribute('sizes', $sizes);
            }

            $picture->appendChild($jxl_source);
        }

        // Add AVIF source if available
        if ($has_avif && $this->get_setting('avif_enabled', false)) {
            $avif_source = $doc->createElement('source');
            $avif_source->setAttribute('type', 'image/avif');
            $avif_source->setAttribute('srcset', $avif_src);

            if (!empty($avif_srcset)) {
                $avif_source->setAttribute('srcset', implode(', ', $avif_srcset));
            }

            if ($sizes) {
                $avif_source->setAttribute('sizes', $sizes);
            }

            $picture->appendChild($avif_source);
        }

        // Add WebP source if available
        if ($has_webp && $this->get_setting('webp_enabled', false)) {
            $webp_source = $doc->createElement('source');
            $webp_source->setAttribute('type', 'image/webp');
            $webp_source->setAttribute('srcset', $webp_src);

            if (!empty($webp_srcset)) {
                $webp_source->setAttribute('srcset', implode(', ', $webp_srcset));
            }

            if ($sizes) {
                $webp_source->setAttribute('sizes', $sizes);
            }

            $picture->appendChild($webp_source);
        }

        // Clone original img as fallback
        $fallback_img = $img->cloneNode(true);
        $picture->appendChild($fallback_img);

        // Replace img with picture
        $img->parentNode->replaceChild($picture, $img);

        // Get the modified HTML
        $body = $doc->getElementsByTagName('body')->item(0);
        if ($body) {
            $html = $doc->saveHTML($body);
            // Remove body tags
            $html = preg_replace('/<\/?body>/', '', $html);
        } else {
            $html = $doc->saveHTML();
        }

        return trim($html);
    }

    /**
     * Start output buffering for global picture element conversion
     *
     * @return void
     */
    public function start_output_buffer(): void {
        if (!$this->is_frontend_optimization_context()) {
            return;
        }

        ob_start([$this, 'process_output_buffer']);
    }

    /**
     * Process output buffer to convert all images
     *
     * @param string $buffer Output buffer content
     * @return string Modified content
     */
    public function process_output_buffer(string $buffer): string {
        if (!$this->is_frontend_optimization_context()) {
            return $buffer;
        }

        // Skip if not HTML
        if (!preg_match('/<html/i', $buffer)) {
            return $buffer;
        }

        // Skip admin pages
        if (is_admin()) {
            return $buffer;
        }

        if ($this->is_current_request_excluded()) {
            return $buffer;
        }

        // Reset per-response image counter for lazy/eager heuristics.
        $this->buffer_image_counter = 0;
        $buffer = $this->refresh_buffer_inline_images($buffer);

        // Process img tags, but skip those already inside picture elements
        // Use a more sophisticated approach: split by picture elements, process only outside
        return preg_replace_callback(
            '/(<picture[^>]*>.*?<\/picture>)|(<img[^>]+>)/is',
            function($matches) {
                // If it's a picture element, return unchanged
                if (!empty($matches[1])) {
                    return $matches[1];
                }
                // Otherwise process the img tag
                if (!empty($matches[2])) {
                    // Extract attributes from the img tag
                    if (preg_match('/<img([^>]+)>/i', $matches[2], $img_match)) {
                        return $this->process_img_tag($img_match);
                    }
                }
                return $matches[0];
            },
            $buffer
        );
    }

    /**
     * Start output buffer for Accept header based URL replacement
     * This method replaces image URLs directly based on browser support
     * Works for CSS backgrounds, inline styles, and all image URLs
     */
    public function start_accept_header_buffer(): void {
        if (!$this->is_frontend_optimization_context()) {
            return;
        }

        ob_start([$this, 'process_accept_header_buffer']);
    }

    /**
     * Process buffer using Accept header detection
     * Replaces image URLs with WebP/AVIF versions if browser supports them
     *
     * @param string $buffer Output buffer content
     * @return string Modified content
     */
    public function process_accept_header_buffer(string $buffer): string {
        if (!$this->is_frontend_optimization_context()) {
            return $buffer;
        }

        // Skip if not HTML
        if (!preg_match('/<html/i', $buffer)) {
            return $buffer;
        }

        // Skip admin pages
        if (is_admin()) {
            return $buffer;
        }

        if ($this->is_current_request_excluded()) {
            return $buffer;
        }

        $buffer = $this->refresh_buffer_inline_images($buffer);

        // Check browser support via Accept header
        $accepts_jxl = $this->browser_accepts_jxl();
        $accepts_avif = $this->browser_accepts_avif();
        $accepts_webp = $this->browser_accepts_webp();

        // If browser doesn't support modern formats, return unchanged
        if (!$accepts_webp && !$accepts_avif && !$accepts_jxl) {
            return $buffer;
        }

        // Get upload directory info
        $upload_dir = wp_upload_dir();
        $upload_url = preg_quote($upload_dir['baseurl'], '/');

        // Match image URLs in the content (src, srcset, url(), etc.)
        $pattern = '/(' . $upload_url . '\/[^\s"\'\)]+\.(jpe?g|png))/i';

        return preg_replace_callback($pattern, function($matches) use ($accepts_jxl, $accepts_avif, $accepts_webp) {
            $original_url = $matches[1];

            if ($this->matches_exclude_path($original_url)) {
                return $original_url;
            }

            $this->ensure_runtime_optimized_variants($original_url);

            // Prefer JXL if supported and enabled (best compression)
            if ($accepts_jxl && $this->get_setting('jxl_enabled', false)) {
                $jxl_url = $this->get_optimized_url($original_url, 'jxl');
                $actual_jxl = $this->get_actual_optimized_url($jxl_url);
                if ($actual_jxl) {
                    return $actual_jxl;
                }
            }

            // Prefer AVIF if supported and enabled
            if ($accepts_avif && $this->get_setting('avif_enabled', false)) {
                $avif_url = $this->get_optimized_url($original_url, 'avif');
                $actual_avif = $this->get_actual_optimized_url($avif_url);
                if ($actual_avif) {
                    return $actual_avif;
                }
            }

            // Fall back to WebP
            if ($accepts_webp) {
                $webp_url = $this->get_optimized_url($original_url, 'webp');
                $actual_webp = $this->get_actual_optimized_url($webp_url);
                if ($actual_webp) {
                    return $actual_webp;
                }
            }

            // Return original if no optimized version exists
            return $original_url;
        }, $buffer);
    }

    /**
     * Check if browser accepts WebP via Accept header
     *
     * @return bool
     */
    private function browser_accepts_webp(): bool {
        $accept = $_SERVER['HTTP_ACCEPT'] ?? '';
        return strpos($accept, 'image/webp') !== false;
    }

    /**
     * Check if browser accepts AVIF via Accept header
     *
     * @return bool
     */
    private function browser_accepts_avif(): bool {
        $accept = $_SERVER['HTTP_ACCEPT'] ?? '';
        return strpos($accept, 'image/avif') !== false;
    }

    /**
     * Check if browser accepts JXL
     *
     * @return bool
     */
    private function browser_accepts_jxl(): bool {
        $accept = $_SERVER['HTTP_ACCEPT'] ?? '';
        return strpos($accept, 'image/jxl') !== false;
    }

    /**
     * Process individual img tag for picture element conversion
     *
     * @param array $matches Regex matches
     * @return string Modified HTML
     */
    private function process_img_tag(array $matches): string {
        $img_html = $matches[0];
        $attributes = $matches[1];
        $this->buffer_image_counter++;

        // Backfill missing width/height where we can resolve dimensions from local uploads.
        // This helps reduce layout-shift diagnostics on themes/builders that omit intrinsic sizes.
        $img_html = $this->ensure_intrinsic_dimensions($img_html);
        $attributes = $this->extract_img_attributes($img_html);

        // Never rewrite branding/UI assets (logos/icons/avatars) to avoid theme header regressions.
        if ($this->should_skip_runtime_image_rewrite($img_html, $attributes)) {
            return $img_html;
        }

        // Force eager loading on the first likely above-the-fold images.
        if ($this->should_force_eager_loading($img_html, $this->buffer_image_counter)) {
            $img_html = preg_replace('/loading=["\'][^"\']*["\']/i', 'loading="eager"', $img_html, 1);
            $attributes = preg_replace('/loading=["\'][^"\']*["\']/i', 'loading="eager"', $attributes, 1);
        }

        // Avoid conflicting hints: fetchpriority=high should never keep lazy loading.
        // This improves LCP consistency for hero/above-the-fold images across themes/builders.
        if (preg_match('/fetchpriority=["\']high["\']/i', $attributes) && preg_match('/loading=["\']lazy["\']/i', $img_html)) {
            $img_html = preg_replace('/loading=["\']lazy["\']/i', 'loading="eager"', $img_html, 1);
            $attributes = preg_replace('/loading=["\']lazy["\']/i', 'loading="eager"', $attributes, 1);
        }

        // Stabilize replaced image markup to avoid inline baseline/box shifts.
        $img_html = $this->apply_stable_img_defaults($img_html);
        $attributes = $this->extract_img_attributes($img_html);

        if (preg_match('/\bsrcset\s*=\s*(["\'])(.*?)\1/i', $attributes, $srcset_match)) {
            $pruned_srcset = $this->prune_oversized_srcset_candidates((string) ($srcset_match[2] ?? ''), $img_html);
            if ($pruned_srcset !== (string) ($srcset_match[2] ?? '')) {
                $img_html = $this->replace_or_insert_attribute($img_html, 'srcset', $pruned_srcset);
                $attributes = $this->extract_img_attributes($img_html);
            }
        }

        // Extract src
        if (!preg_match('/src=["\']([^"\']*)["\']/i', $attributes, $src_matches)) {
            return $img_html;
        }

        $src = $src_matches[1];

        // Skip external images
        $upload_dir = wp_upload_dir();
        if (strpos($src, $upload_dir['baseurl']) === false) {
            return $img_html;
        }

        if ($this->matches_exclude_path($src)) {
            return $img_html;
        }

        $this->ensure_runtime_optimized_variants($src);

        // Check if optimized versions exist (with WordPress suffix support)
        $has_webp = false;
        $has_avif = false;
        $has_jxl = false;

        $webp_url = $this->get_optimized_url($src, 'webp');
        $actual_webp_url = $this->get_actual_optimized_url($webp_url);
        if ($actual_webp_url) {
            $webp_url = $actual_webp_url;
            $has_webp = true;
        }

        $avif_url = $this->get_optimized_url($src, 'avif');
        $actual_avif_url = $this->get_actual_optimized_url($avif_url);
        if ($actual_avif_url) {
            $avif_url = $actual_avif_url;
            $has_avif = true;
        }

        $jxl_url = $this->get_optimized_url($src, 'jxl');
        $actual_jxl_url = $this->get_actual_optimized_url($jxl_url);
        if ($actual_jxl_url) {
            $jxl_url = $actual_jxl_url;
            $has_jxl = true;
        }

        // If no optimized versions, return original
        if (!$has_webp && !$has_avif && !$has_jxl) {
            return $img_html;
        }

        // Build picture element — format order: JXL (best compression) > AVIF > WebP > original
        [$width, $height] = $this->extract_dimensions_from_img_tag($img_html);
        // Keep wrapper semantics untouched to avoid alignment/line-box regressions.
        $picture_html = '<picture class="prorank-picture">';

        // Add JXL source (best compression, Chrome 145+ / Firefox support)
        if ($has_jxl && $this->get_setting('jxl_enabled', false)) {
            $jxl_srcset = $jxl_url;
            if (preg_match('/srcset=["\']([^"\']*)["\']/i', $attributes, $srcset_matches)) {
                $responsive_srcset = $this->convert_srcset_to_format($srcset_matches[1], 'jxl');
                if ($responsive_srcset) {
                    $jxl_srcset = $responsive_srcset;
                }
            }

            $picture_html .= '<source type="image/jxl" srcset="' . esc_attr($jxl_srcset) . '"';

            if (preg_match('/sizes=["\']([^"\']*)["\']/i', $attributes, $sizes_matches)) {
                $picture_html .= ' sizes="' . esc_attr($sizes_matches[1]) . '"';
            }

            $picture_html .= '>';
        }

        // Add AVIF source
        if ($has_avif && $this->get_setting('avif_enabled', false)) {
            // Build srcset - prefer responsive srcset if available, otherwise single URL
            $avif_srcset = $avif_url;
            if (preg_match('/srcset=["\']([^"\']*)["\']/i', $attributes, $srcset_matches)) {
                $responsive_srcset = $this->convert_srcset_to_format($srcset_matches[1], 'avif');
                if ($responsive_srcset) {
                    $avif_srcset = $responsive_srcset;
                }
            }

            $sizes_attr = $this->extract_attribute_value($attributes, 'sizes');
            $mobile_avif_srcset = $this->get_mobile_constrained_srcset($avif_srcset, $sizes_attr, $attributes);
            if ($mobile_avif_srcset !== '' && $mobile_avif_srcset !== $avif_srcset) {
                $picture_html .= '<source media="(max-width: 767px)" type="image/avif" srcset="' . esc_attr($mobile_avif_srcset) . '"';
                if ($sizes_attr !== '') {
                    $picture_html .= ' sizes="' . esc_attr($sizes_attr) . '"';
                }
                $picture_html .= '>';
            }

            $picture_html .= '<source type="image/avif" srcset="' . esc_attr($avif_srcset) . '"';

            // Copy sizes if exists
            if ($sizes_attr !== '') {
                $picture_html .= ' sizes="' . esc_attr($sizes_attr) . '"';
            }

            $picture_html .= '>';
        }

        // Add WebP source
        if ($has_webp && $this->get_setting('webp_enabled', false)) {
            // Build srcset - prefer responsive srcset if available, otherwise single URL
            $webp_srcset = $webp_url;
            if (preg_match('/srcset=["\']([^"\']*)["\']/i', $attributes, $srcset_matches)) {
                $responsive_srcset = $this->convert_srcset_to_format($srcset_matches[1], 'webp');
                if ($responsive_srcset) {
                    $webp_srcset = $responsive_srcset;
                }
            }

            $sizes_attr = $this->extract_attribute_value($attributes, 'sizes');
            $mobile_webp_srcset = $this->get_mobile_constrained_srcset($webp_srcset, $sizes_attr, $attributes);
            if ($mobile_webp_srcset !== '' && $mobile_webp_srcset !== $webp_srcset) {
                $picture_html .= '<source media="(max-width: 767px)" type="image/webp" srcset="' . esc_attr($mobile_webp_srcset) . '"';
                if ($sizes_attr !== '') {
                    $picture_html .= ' sizes="' . esc_attr($sizes_attr) . '"';
                }
                $picture_html .= '>';
            }

            $picture_html .= '<source type="image/webp" srcset="' . esc_attr($webp_srcset) . '"';

            // Copy sizes if exists
            if ($sizes_attr !== '') {
                $picture_html .= ' sizes="' . esc_attr($sizes_attr) . '"';
            }

            $picture_html .= '>';
        }

        // Add original img as fallback
        $picture_html .= $img_html;
        $picture_html .= '</picture>';

        return $picture_html;
    }

    /**
     * Decide whether an image should be forced eager for early paint stability.
     *
     * @param string $img_html   Raw image HTML.
     * @param int    $index      1-based image index in buffered HTML.
     * @return bool
     */
    private function should_force_eager_loading(string $img_html, int $index): bool {
        unset($index); // Kept for backward-compatible signature.

        // Only force eager when another optimizer has already identified this
        // image as the LCP candidate via fetchpriority=high.
        if (!preg_match('/fetchpriority=["\']high["\']/i', $img_html)) {
            return false;
        }

        return preg_match('/loading=["\']lazy["\']/i', $img_html) === 1;
    }

    /**
     * Determine if runtime image rewriting should be skipped.
     *
     * @param string $img_html Full img tag HTML.
     * @param string $attributes Inner img attributes.
     * @return bool
     */
    private function should_skip_runtime_image_rewrite(string $img_html, string $attributes): bool {
        $attrs_lower = strtolower($attributes);

        // Theme/builder explicit opt-out hooks.
        foreach ([
            'data-no-optimize',
            'data-no-optimization',
            'data-skip-optimize',
            'data-prorank-skip-image',
            'data-skip-lazy',
            'data-no-lazyload',
        ] as $optout_attr) {
            if (strpos($attrs_lower, $optout_attr) !== false) {
                return true;
            }
        }

        $class_value = strtolower($this->extract_attribute_value($attributes, 'class'));
        foreach ([
            'logo',
            'custom-logo',
            'site-logo',
            'brand-logo',
            'header-logo',
            'site-icon',
            'avatar',
            'emoji',
            'smiley',
            'spinner',
            'loader',
        ] as $needle) {
            if ($class_value !== '' && strpos($class_value, $needle) !== false) {
                return true;
            }
        }

        $variant = strtolower($this->extract_attribute_value($attributes, 'data-variant'));
        if (in_array($variant, ['logo', 'icon', 'avatar'], true)) {
            return true;
        }

        // Heuristic fallback for unclassed branding assets.
        foreach (['alt', 'title', 'src'] as $attr) {
            $value = strtolower($this->extract_attribute_value($attributes, $attr));
            if ($value === '') {
                continue;
            }
            if (preg_match('/(^|[^a-z0-9])(logo|favicon|site-icon|avatar)([^a-z0-9]|$)/', $value)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Ensure converted <img> tags have stable rendering defaults.
     *
     * @param string $img_html Raw image HTML.
     * @return string
     */
    private function apply_stable_img_defaults(string $img_html): string {
        [$width, $height] = $this->extract_dimensions_from_img_tag($img_html);
        if ($width <= 0 || $height <= 0) {
            return $img_html;
        }

        $aspect_ratio = 'aspect-ratio:' . $width . '/' . $height;

        if (preg_match('/style\s*=\s*["\']([^"\']*)["\']/i', $img_html, $style_match)) {
            $current = trim((string) ($style_match[1] ?? ''));
            if (stripos($current, 'aspect-ratio:') !== false) {
                return $img_html;
            }

            $current = rtrim($current, '; ') . ';' . $aspect_ratio . ';';
            return str_replace($style_match[0], 'style="' . $current . '"', $img_html);
        }

        // Keep original display/alignment semantics; only add ratio reservation.
        return str_replace('<img', '<img style="' . $aspect_ratio . ';"', $img_html);
    }

    /**
     * Extract inner attribute segment from a full img tag.
     *
     * @param string $img_html
     * @return string
     */
    private function extract_img_attributes(string $img_html): string {
        if (preg_match('/<img([^>]+)>/i', $img_html, $m)) {
            return (string) ($m[1] ?? '');
        }
        return '';
    }

    /**
     * Extract a single attribute value from an img attribute string.
     *
     * @param string $attributes Img attribute segment.
     * @param string $attribute Attribute name.
     * @return string
     */
    private function extract_attribute_value(string $attributes, string $attribute): string {
        $quoted = '/\b' . preg_quote($attribute, '/') . '\s*=\s*["\']([^"\']*)["\']/i';
        if (preg_match($quoted, $attributes, $match)) {
            return (string) ($match[1] ?? '');
        }

        $unquoted = '/\b' . preg_quote($attribute, '/') . '\s*=\s*([^\s>]+)/i';
        if (preg_match($unquoted, $attributes, $match)) {
            return (string) ($match[1] ?? '');
        }

        return '';
    }

    /**
     * Extract width/height from an img tag.
     *
     * @param string $img_html
     * @return array{0:int,1:int}
     */
    private function extract_dimensions_from_img_tag(string $img_html): array {
        $width = 0;
        $height = 0;
        if (preg_match('/\bwidth\s*=\s*["\']?(\d+)/i', $img_html, $w_match)) {
            $width = (int) ($w_match[1] ?? 0);
        }
        if (preg_match('/\bheight\s*=\s*["\']?(\d+)/i', $img_html, $h_match)) {
            $height = (int) ($h_match[1] ?? 0);
        }

        return [$width, $height];
    }

    /**
     * Ensure an image tag includes intrinsic width/height attributes when resolvable.
     *
     * @param string $img_html
     * @return string
     */
    private function ensure_intrinsic_dimensions(string $img_html): string {
        [$width, $height] = $this->extract_dimensions_from_img_tag($img_html);
        if ($width > 0 && $height > 0) {
            return $this->mark_intrinsic_dimension_html($img_html, $width, $height);
        }

        if ($this->should_skip_intrinsic_dimensions_for_markup($img_html)) {
            return $img_html;
        }

        $src = $this->extract_attribute_value($this->extract_img_attributes($img_html), 'src');
        if ($src === '' || stripos($src, 'data:') === 0) {
            return $img_html;
        }

        [$resolved_width, $resolved_height] = $this->resolve_dimensions_from_src($src);
        if ($resolved_width <= 0 || $resolved_height <= 0) {
            return $img_html;
        }

        // Insert attributes right after <img for consistency.
        $rewritten = preg_replace(
            '/<img\b/i',
            sprintf('<img width="%d" height="%d"', $resolved_width, $resolved_height),
            $img_html,
            1
        ) ?: $img_html;

        return $this->mark_intrinsic_dimension_html($rewritten, $resolved_width, $resolved_height);
    }

    /**
     * Determine whether this image markup should keep builder-managed sizing.
     *
     * @param string $img_html
     * @return bool
     */
    private function should_skip_intrinsic_dimensions_for_markup(string $img_html): bool {
        $haystack = strtolower($img_html);
        foreach (PerformanceCompatibilityRegistry::get_image_dimension_skip_markers() as $marker) {
            if ($marker !== '' && strpos($haystack, $marker) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Resolve image dimensions from URL or filename pattern.
     *
     * @param string $src
     * @return array{0:int,1:int}
     */
    private function resolve_dimensions_from_src(string $src): array {
        $cache_key = trim((string) $src);
        if ($cache_key === '') {
            return [0, 0];
        }
        if (isset($this->dimension_lookup_cache[$cache_key])) {
            return $this->dimension_lookup_cache[$cache_key];
        }

        $clean_src = preg_replace('/[#?].*$/', '', $cache_key) ?: $cache_key;

        // Fast path: WordPress sub-size filenames usually include dimensions, e.g. -768x512.
        if (preg_match('/-(\d{1,5})x(\d{1,5})\.(?:avif|webp|jxl|jpe?g|png|gif)$/i', $clean_src, $m)) {
            $result = [(int) ($m[1] ?? 0), (int) ($m[2] ?? 0)];
            $this->dimension_lookup_cache[$cache_key] = $result;
            return $result;
        }

        $path = $this->map_upload_url_to_path($clean_src);
        if ($path === '' || !file_exists($path)) {
            $this->dimension_lookup_cache[$cache_key] = [0, 0];
            return [0, 0];
        }

        if (preg_match('/\.svg$/i', $path)) {
            $svg_dimensions = $this->resolve_svg_dimensions($path);
            $this->dimension_lookup_cache[$cache_key] = $svg_dimensions;
            return $svg_dimensions;
        }

        if (!function_exists('wp_getimagesize')) {
            $this->dimension_lookup_cache[$cache_key] = [0, 0];
            return [0, 0];
        }

        $size = wp_getimagesize($path);
        if (!is_array($size) || empty($size[0]) || empty($size[1])) {
            $this->dimension_lookup_cache[$cache_key] = [0, 0];
            return [0, 0];
        }

        $result = [(int) $size[0], (int) $size[1]];
        $this->dimension_lookup_cache[$cache_key] = $result;
        return $result;
    }

    /**
     * Resolve width/height from a local SVG file.
     *
     * @param string $path
     * @return array{0:int,1:int}
     */
    private function resolve_svg_dimensions(string $path): array {
        $svg = @file_get_contents($path, false, null, 0, 8192);
        if (!is_string($svg) || $svg === '') {
            return [0, 0];
        }

        $width = $this->extract_svg_length($svg, 'width');
        $height = $this->extract_svg_length($svg, 'height');
        if ($width > 0 && $height > 0) {
            return [$width, $height];
        }

        if (preg_match('/viewBox\s*=\s*["\']\s*[-\d.]+\s+[-\d.]+\s+([-\d.]+)\s+([-\d.]+)\s*["\']/i', $svg, $matches)) {
            $viewbox_width = (int) round((float) ($matches[1] ?? 0));
            $viewbox_height = (int) round((float) ($matches[2] ?? 0));
            if ($viewbox_width > 0 && $viewbox_height > 0) {
                return [$viewbox_width, $viewbox_height];
            }
        }

        return [0, 0];
    }

    /**
     * Extract a numeric SVG length attribute, ignoring common CSS units.
     *
     * @param string $svg
     * @param string $attribute
     * @return int
     */
    private function extract_svg_length(string $svg, string $attribute): int {
        if (!preg_match('/\b' . preg_quote($attribute, '/') . '\s*=\s*["\']\s*([0-9]+(?:\.[0-9]+)?)(?:px)?\s*["\']/i', $svg, $matches)) {
            return 0;
        }

        return (int) round((float) ($matches[1] ?? 0));
    }

    /**
     * Persist intrinsic dimensions as data attributes so client-side widgets
     * can restore them after lazy/hydration mutations.
     *
     * @param string $img_html
     * @param int    $width
     * @param int    $height
     * @return string
     */
    private function mark_intrinsic_dimension_html(string $img_html, int $width, int $height): string {
        if ($width <= 0 || $height <= 0) {
            return $img_html;
        }

        $replacements = [];
        if (!preg_match('/\bdata-prorank-width\s*=/i', $img_html)) {
            $replacements[] = sprintf('data-prorank-width="%d"', $width);
        }
        if (!preg_match('/\bdata-prorank-height\s*=/i', $img_html)) {
            $replacements[] = sprintf('data-prorank-height="%d"', $height);
        }

        if (empty($replacements)) {
            return $img_html;
        }

        return preg_replace(
            '/<img\b/i',
            '<img ' . implode(' ', $replacements),
            $img_html,
            1
        ) ?: $img_html;
    }

    /**
     * Attach intrinsic-dimension data attributes to attachment image attrs.
     *
     * @param array<string,mixed> $attr
     * @return array<string,mixed>
     */
    private function append_intrinsic_dimension_data_attrs(array $attr): array {
        $width = isset($attr['width']) ? (int) $attr['width'] : 0;
        $height = isset($attr['height']) ? (int) $attr['height'] : 0;

        if ($width > 0 && $height > 0) {
            $attr['data-prorank-width'] = (string) $width;
            $attr['data-prorank-height'] = (string) $height;
        }

        return $attr;
    }

    /**
     * Map an uploads URL to a local filesystem path.
     *
     * @param string $url
     * @return string
     */
    private function map_upload_url_to_path(string $url): string {
        $upload_dir = wp_upload_dir();
        $baseurl = isset($upload_dir['baseurl']) ? (string) $upload_dir['baseurl'] : '';
        $basedir = isset($upload_dir['basedir']) ? (string) $upload_dir['basedir'] : '';

        if ($baseurl !== '' && $basedir !== '' && strpos($url, $baseurl) === 0) {
            $suffix = ltrim(substr($url, strlen($baseurl)), '/');
            return trailingslashit($basedir) . $suffix;
        }

        $parsed = wp_parse_url($url);
        $path = is_array($parsed) ? (string) ($parsed['path'] ?? '') : '';
        if ($path === '') {
            return '';
        }

        $uploads_pos = strpos($path, '/wp-content/uploads/');
        if ($uploads_pos === false) {
            return '';
        }

        $relative = ltrim(substr($path, $uploads_pos + strlen('/wp-content/uploads/')), '/');
        if ($relative === '') {
            return '';
        }

        if ($basedir !== '') {
            return trailingslashit($basedir) . $relative;
        }

        return '';
    }

    /**
     * Convert srcset to optimized format
     *
     * @param string $srcset Original srcset
     * @param string $format Target format (webp or avif)
     * @return string|null Converted srcset or null if no optimized versions
     */
    private function convert_srcset_to_format(string $srcset, string $format): ?string {
        $converted_parts = [];
        $parts = explode(',', $srcset);

        foreach ($parts as $part) {
            $part = trim($part);
            if (preg_match('/(\S+)\s+(\d+w|\d+x)/', $part, $matches)) {
                $url = $matches[1];
                $descriptor = $matches[2];

                if ($this->matches_exclude_path($url)) {
                    continue;
                }

                $this->ensure_runtime_optimized_variants($url);

                $optimized_url = $this->get_optimized_url($url, $format);
                $actual_url = $this->get_actual_optimized_url($optimized_url);
                if ($actual_url) {
                    $converted_parts[] = $actual_url . ' ' . $descriptor;
                }
            }
        }

        return !empty($converted_parts) ? implode(', ', $converted_parts) : null;
    }

    /**
     * Setup .htaccess rules for serving optimized images
     *
     * @return void
     */
    public function setup_htaccess_rules(): void {
        // Check if server supports it
        if (!$this->server_supports_htaccess_delivery()) {
            return;
        }

        // Guarantee ProRank owns the active image-delivery layer. This rewrites
        // the whole .htaccess so AVIF-before-WebP wins over stale inactive
        // competitor image-rewrite blocks, such as Seraphinite WebP-only rules.
        $this->write_owned_image_htaccess();
    }

    private function get_image_htaccess_marker(): string {
        return 'ProRank Image Optimization';
    }

    private function get_htaccess_path(): string {
        return get_home_path() . '.htaccess';
    }

    private function htaccess_content_indicates_active_delivery(string $content): bool {
        if ($content === '') {
            return false;
        }

        return (bool) preg_match('/^\s*RewriteEngine\s+On\b/mi', $content)
            || stripos($content, '# BEGIN WordPress') !== false
            || stripos($content, '# BEGIN ProRank SEO Browser Cache') !== false
            || stripos($content, 'ExpiresByType') !== false
            || stripos($content, '<IfModule mod_rewrite.c>') !== false;
    }

    private function site_has_effective_htaccess_file(): bool {
        $path = $this->get_htaccess_path();
        if (!file_exists($path) || !is_readable($path)) {
            return false;
        }

        return $this->htaccess_content_indicates_active_delivery((string) @file_get_contents($path));
    }

    private function should_manage_htaccess_delivery_layer(string $delivery_method): bool {
        if ($delivery_method === 'picture') {
            $delivery_method = 'picture_global';
        }

        if (in_array($delivery_method, ['none', 'cdn'], true)) {
            return false;
        }

        return (bool) (
            $this->get_setting('avif_enabled', false)
            || $this->get_setting('webp_enabled', false)
            || $this->get_setting('jxl_enabled', false)
        );
    }

    /**
     * @return array<int, array<string, string>>
     */
    private function get_known_competitor_image_blocks(): array {
        return [
            [
                'slug'   => 'seraphinite',
                'label'  => 'Seraphinite Accelerator',
                'plugin' => 'seraphinite-accelerator/plugin_root.php',
                'begin'  => '# BEGIN Seraphinite',
                'end'    => '# END Seraphinite',
            ],
        ];
    }

    /**
     * @return array<int, string>
     */
    private function get_seraphinite_signature_markers(): array {
        return ['seraph_accel_ai', 'seraph_accel', '# BEGIN Seraphinite', '# END Seraphinite'];
    }

    private function htaccess_contains_prorank_block(string $content): bool {
        return strpos($content, '# BEGIN ' . $this->get_image_htaccess_marker()) !== false;
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function detect_competitor_image_blocks(string $content): array {
        $found = [];

        foreach ($this->get_known_competitor_image_blocks() as $block) {
            $has_block = false;

            if (strpos($content, $block['begin']) !== false) {
                $has_block = true;
            } elseif ($block['slug'] === 'seraphinite') {
                foreach ($this->get_seraphinite_signature_markers() as $needle) {
                    if (strpos($content, $needle) !== false) {
                        $has_block = true;
                        break;
                    }
                }
            }

            if (!$has_block) {
                continue;
            }

            $found[] = [
                'slug'            => $block['slug'],
                'label'           => $block['label'],
                'plugin'          => $block['plugin'],
                'plugin_active'   => $this->is_competitor_plugin_active($block['plugin']),
                'negotiates_webp' => (bool) preg_match('/HTTP_ACCEPT.*image\/webp/i', $content),
                'negotiates_avif' => (bool) preg_match('/HTTP_ACCEPT.*image\/avif/i', $content),
            ];
        }

        return $found;
    }

    private function is_competitor_plugin_active(string $plugin): bool {
        if (!function_exists('is_plugin_active') && defined('ABSPATH')) {
            $admin_inc = ABSPATH . 'wp-admin/includes/plugin.php';
            if (is_readable($admin_inc)) {
                require_once $admin_inc;
            }
        }

        if (function_exists('is_plugin_active')) {
            return (bool) is_plugin_active($plugin);
        }

        $active = get_option('active_plugins', []);
        return is_array($active) && in_array($plugin, $active, true);
    }

    private function prorank_block_is_after_competitor(string $content): bool {
        $prorank_pos = strpos($content, '# BEGIN ' . $this->get_image_htaccess_marker());
        if ($prorank_pos === false) {
            return false;
        }

        foreach ($this->detect_competitor_image_blocks($content) as $block) {
            $signatures = $block['slug'] === 'seraphinite'
                ? $this->get_seraphinite_signature_markers()
                : [];

            foreach ($signatures as $needle) {
                $competitor_pos = strpos($content, $needle);
                if ($competitor_pos !== false && $competitor_pos < $prorank_pos) {
                    return true;
                }
            }
        }

        return false;
    }

    private function strip_known_competitor_image_blocks(string $content): string {
        foreach ($this->get_known_competitor_image_blocks() as $block) {
            $begin = preg_quote($block['begin'], '/');
            $end   = preg_quote($block['end'], '/');
            $pattern = '/[\r\n]*' . $begin . '.*?' . $end . '[^\r\n]*[\r\n]*/s';
            $content = (string) preg_replace($pattern, "\n", $content);

            if ($block['slug'] === 'seraphinite') {
                $content = $this->strip_unmarked_seraphinite_rewrite_snippets($content);
            }
        }

        return $content;
    }

    private function strip_unmarked_seraphinite_rewrite_snippets(string $content): string {
        $lines = preg_split("/(\r\n|\n|\r)/", $content);
        if (!is_array($lines)) {
            return $content;
        }

        $remove = [];
        $count  = count($lines);

        for ($i = 0; $i < $count; $i++) {
            if (stripos($lines[$i], 'seraph_accel') === false) {
                continue;
            }

            $start = $i;
            while ($start > 0 && preg_match('/^\s*(RewriteCond|RewriteRule)\b/i', $lines[$start - 1])) {
                $start--;
            }

            $end = $i;
            while (($end + 1) < $count && preg_match('/^\s*(RewriteCond|RewriteRule)\b/i', $lines[$end + 1])) {
                $end++;
            }

            for ($j = $start; $j <= $end; $j++) {
                $remove[$j] = true;
            }
        }

        if (empty($remove)) {
            return $content;
        }

        $kept = [];
        foreach ($lines as $index => $line) {
            if (!isset($remove[$index])) {
                $kept[] = $line;
            }
        }

        return implode("\n", $kept);
    }

    private function build_prorank_htaccess_block_string(): string {
        $lines = ['# BEGIN ' . $this->get_image_htaccess_marker()];
        foreach ($this->generate_htaccess_rules() as $rule) {
            $lines[] = $rule;
        }
        $lines[] = '# END ' . $this->get_image_htaccess_marker();

        return implode("\n", $lines);
    }

    private function compose_owned_htaccess(string $content, bool $allow_competitor_removal = true): string {
        $marker = $this->get_image_htaccess_marker();
        $pattern = '/[\r\n]*# BEGIN ' . preg_quote($marker, '/') . '.*?# END ' . preg_quote($marker, '/') . '[^\r\n]*[\r\n]*/s';
        $content = (string) preg_replace($pattern, "\n", $content);

        if ($allow_competitor_removal) {
            foreach ($this->detect_competitor_image_blocks($content) as $block) {
                if (empty($block['plugin_active'])) {
                    $content = $this->strip_known_competitor_image_blocks($content);
                    break;
                }
            }
        }

        return $this->build_prorank_htaccess_block_string() . "\n\n" . ltrim($content, "\r\n");
    }

    /**
     * @return array<string, mixed>
     */
    private function write_owned_image_htaccess(): array {
        $path = $this->get_htaccess_path();

        if (file_exists($path) && !is_readable($path)) {
            return [
                'success'      => false,
                'reason'       => 'unreadable',
                'message'      => __('The .htaccess file exists but is not readable. Apply the rules manually.', 'prorank-seo'),
                'manual_rules' => $this->build_prorank_htaccess_block_string(),
            ];
        }

        if (!wp_is_writable($path)) {
            return [
                'success'      => false,
                'reason'       => 'not_writable',
                'message'      => __('The .htaccess file is not writable. Apply the rules manually.', 'prorank-seo'),
                'manual_rules' => $this->build_prorank_htaccess_block_string(),
            ];
        }

        $content = file_exists($path) ? (string) @file_get_contents($path) : '';
        $new_content = $this->compose_owned_htaccess($content, true);

        if ($new_content === $content) {
            return [
                'success' => true,
                'reason'  => 'already_owned',
                'message' => __('ProRank already owns image delivery in .htaccess.', 'prorank-seo'),
            ];
        }

        $written = @file_put_contents($path, $new_content);
        if ($written === false) {
            return [
                'success'      => false,
                'reason'       => 'write_failed',
                'message'      => __('Could not write to .htaccess. Apply the rules manually.', 'prorank-seo'),
                'manual_rules' => $this->build_prorank_htaccess_block_string(),
            ];
        }

        return [
            'success' => true,
            'reason'  => 'written',
            'message' => __('ProRank now owns image delivery in .htaccess.', 'prorank-seo'),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    public function get_delivery_health(): array {
        $delivery_method = (string) $this->get_setting('delivery_method', 'picture_hooks');
        if ($delivery_method === 'picture') {
            $delivery_method = 'picture_global';
        }

        $avif_enabled = (bool) $this->get_setting('avif_enabled', false);
        $webp_enabled = (bool) $this->get_setting('webp_enabled', false);
        $path         = $this->get_htaccess_path();
        $exists       = file_exists($path);
        $readable     = $exists ? is_readable($path) : true;
        $writable     = wp_is_writable($path);
        $content      = ($exists && $readable) ? (string) @file_get_contents($path) : '';

        $has_prorank_block  = $this->htaccess_contains_prorank_block($content);
        $competitors        = $this->detect_competitor_image_blocks($content);
        $stale_competitors  = array_values(array_filter($competitors, static function ($block) {
            return empty($block['plugin_active']);
        }));
        $wrong_order        = $this->prorank_block_is_after_competitor($content);
        $negotiation_active = $this->should_manage_htaccess_delivery_layer($delivery_method)
            || $delivery_method === 'accept_header';

        $issues = [];
        $status = 'healthy';

        if (!$readable) {
            $status   = 'error';
            $issues[] = [
                'code'    => 'htaccess_unreadable',
                'message' => __('.htaccess exists but cannot be read. Apply the rules manually.', 'prorank-seo'),
            ];
        }

        if ($negotiation_active) {
            if (!$has_prorank_block) {
                $status   = ($status === 'error') ? 'error' : 'warning';
                $issues[] = [
                    'code'    => 'prorank_block_missing',
                    'message' => __('The ProRank image-delivery block is not present in .htaccess.', 'prorank-seo'),
                ];
            }

            if (!empty($stale_competitors)) {
                $status = ($status === 'error') ? 'error' : 'warning';
                $names  = implode(', ', array_map(static function ($b) {
                    return $b['label'];
                }, $stale_competitors));
                $issues[] = [
                    'code'    => 'stale_competitor_block',
                    'message' => sprintf(__('Stale image-rewrite rules from %s are present and may serve WebP instead of AVIF.', 'prorank-seo'), $names),
                ];
            }

            if ($wrong_order) {
                $status   = ($status === 'error') ? 'error' : 'warning';
                $issues[] = [
                    'code'    => 'wrong_order',
                    'message' => __('The ProRank block appears after a competitor image-rewrite block, so AVIF never wins.', 'prorank-seo'),
                ];
            }

            if ($avif_enabled && $status !== 'healthy') {
                $issues[] = [
                    'code'    => 'avif_not_served',
                    'message' => __('AVIF generated but not currently being served.', 'prorank-seo'),
                ];
            }

            if (!$writable && (!$has_prorank_block || !empty($stale_competitors) || $wrong_order)) {
                $issues[] = [
                    'code'    => 'htaccess_not_writable',
                    'message' => __('.htaccess is not writable, so ProRank cannot repair it automatically.', 'prorank-seo'),
                ];
            }
        }

        $can_repair = $writable && $readable && $negotiation_active && (
            !$has_prorank_block || $wrong_order || !empty($stale_competitors)
        );

        return [
            'status'                   => $status,
            'delivery_method'          => $delivery_method,
            'negotiation_active'       => $negotiation_active,
            'avif_enabled'             => $avif_enabled,
            'webp_enabled'             => $webp_enabled,
            'htaccess_exists'          => $exists,
            'htaccess_readable'        => $readable,
            'htaccess_writable'        => $writable,
            'has_prorank_block'        => $has_prorank_block,
            'prorank_after_competitor' => $wrong_order,
            'competitors'              => $competitors,
            'stale_competitors'        => $stale_competitors,
            'issues'                   => $issues,
            'can_repair'               => $can_repair,
            'delivery_explanation'     => $this->get_delivery_method_explanation($delivery_method),
            'manual_instructions'      => $this->build_prorank_htaccess_block_string(),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    public function repair_delivery(): array {
        if (!$this->server_supports_htaccess_delivery()) {
            return [
                'success' => false,
                'reason'  => 'unsupported_server',
                'message' => __('This server does not support .htaccess image delivery.', 'prorank-seo'),
                'health'  => $this->get_delivery_health(),
            ];
        }

        $result = $this->write_owned_image_htaccess();
        $result['health'] = $this->get_delivery_health();

        if (!empty($result['success']) && ($result['health']['status'] ?? 'healthy') !== 'healthy') {
            $result['success'] = false;
            $result['reason']  = 'repair_incomplete';
            $result['message'] = __('Image delivery repair ran, but delivery health still needs attention.', 'prorank-seo');
        }

        return $result;
    }

    /**
     * @return array<string, mixed>
     */
    public function get_delivery_method_explanation(?string $delivery_method = null): array {
        if ($delivery_method === null) {
            $delivery_method = (string) $this->get_setting('delivery_method', 'picture_hooks');
            if ($delivery_method === 'picture') {
                $delivery_method = 'picture_global';
            }
        }

        if (in_array($delivery_method, ['picture_hooks', 'picture_global'], true)) {
            return [
                'method'                 => $delivery_method,
                'url_accept_negotiation' => false,
                'covers_css_backgrounds' => false,
                'message'                => __('Picture delivery rewrites image markup. ProRank also manages .htaccess so direct URLs and CSS backgrounds can receive AVIF when supported.', 'prorank-seo'),
                'recommendation'         => __('Keep delivery health healthy so stale WebP-only rules cannot override AVIF delivery.', 'prorank-seo'),
            ];
        }

        if ($delivery_method === 'htaccess') {
            return [
                'method'                 => $delivery_method,
                'url_accept_negotiation' => true,
                'covers_css_backgrounds' => true,
                'message'                => __('.htaccess delivery negotiates image URLs by browser Accept headers.', 'prorank-seo'),
                'recommendation'         => '',
            ];
        }

        if ($delivery_method === 'accept_header') {
            return [
                'method'                 => $delivery_method,
                'url_accept_negotiation' => true,
                'covers_css_backgrounds' => true,
                'message'                => __('Accept header delivery rewrites frontend HTML and ProRank keeps .htaccess ownership for direct image URLs.', 'prorank-seo'),
                'recommendation'         => '',
            ];
        }

        return [
            'method'                 => $delivery_method,
            'url_accept_negotiation' => false,
            'covers_css_backgrounds' => false,
            'message'                => __('This delivery method does not use ProRank direct URL Accept negotiation.', 'prorank-seo'),
            'recommendation'         => '',
        ];
    }

    /**
     * Generate .htaccess rules for image delivery
     *
     * @return array Array of rules
     */
    private function generate_htaccess_rules(): array {
        $rules = [];

        $rules[] = '<IfModule mod_mime.c>';
        $rules[] = 'AddType image/avif .avif';
        $rules[] = 'AddType image/webp .webp';
        $rules[] = 'AddType image/jxl .jxl';
        $rules[] = '</IfModule>';
        $rules[] = '';

        $rules[] = '<IfModule mod_rewrite.c>';
        $rules[] = 'RewriteEngine On';

        // JXL rules (best compression, Chrome 145+ / Firefox)
        if ($this->get_setting('jxl_enabled', false)) {
            $rules[] = '';
            $rules[] = '# Serve JPEG XL images if available';
            $rules[] = 'RewriteCond %{HTTP_ACCEPT} image/jxl';
            $rules[] = 'RewriteCond %{REQUEST_FILENAME} ^(.+)\.(jpe?g|png)$ [NC]';
            $rules[] = 'RewriteCond %1.jxl -f';
            $rules[] = 'RewriteRule ^(.+)\.(jpe?g|png)$ $1.jxl [T=image/jxl,E=prorank_image_format:1,L]';
        }

        // AVIF rules
        if ($this->get_setting('avif_enabled', false)) {
            $rules[] = '';
            $rules[] = '# Serve AVIF images if available';
            $rules[] = 'RewriteCond %{HTTP_ACCEPT} image/avif';
            $rules[] = 'RewriteCond %{REQUEST_FILENAME} ^(.+)\.(jpe?g|png)$ [NC]';
            $rules[] = 'RewriteCond %1.avif -f';
            $rules[] = 'RewriteRule ^(.+)\.(jpe?g|png)$ $1.avif [T=image/avif,E=prorank_image_format:1,L]';
        }

        // WebP rules
        if ($this->get_setting('webp_enabled', false)) {
            $rules[] = '';
            $rules[] = '# Serve WebP images if available';
            $rules[] = 'RewriteCond %{HTTP_ACCEPT} image/webp';
            $rules[] = 'RewriteCond %{REQUEST_FILENAME} ^(.+)\.(jpe?g|png)$ [NC]';
            $rules[] = 'RewriteCond %1.webp -f';
            $rules[] = 'RewriteRule ^(.+)\.(jpe?g|png)$ $1.webp [T=image/webp,E=prorank_image_format:1,L]';
        }

        $rules[] = '</IfModule>';
        $rules[] = '';
        $rules[] = '<IfModule mod_headers.c>';
        $rules[] = 'Header merge Vary Accept env=prorank_image_format';
        $rules[] = 'Header merge Vary Accept env=REDIRECT_prorank_image_format';
        $rules[] = '</IfModule>';

        return $rules;
    }

    /**
     * Check if server supports .htaccess delivery
     *
     * @return bool
     */
    private function server_supports_htaccess_delivery(): bool {
        // Apache and LiteSpeed both honor .htaccess rewrite rules.
        $server_software = \prorank_get_server_var( 'SERVER_SOFTWARE' );
        $known_htaccess_server = $server_software !== ''
            && (
                stripos($server_software, 'apache') !== false
                || stripos($server_software, 'litespeed') !== false
            );

        if ($known_htaccess_server) {
            if (!function_exists('apache_get_modules')) {
                return true;
            }

            $modules = apache_get_modules();
            if (in_array('mod_rewrite', $modules, true)) {
                return true;
            }
        }

        // Managed hosts often report nginx while Apache/LiteSpeed still serves
        // WordPress behind it and honors .htaccess. If a readable .htaccess
        // already contains active rewrite/cache blocks, treat it as supported.
        if ($this->site_has_effective_htaccess_file()) {
            return true;
        }

        return false;
    }

    /**
     * Add rewrite rules for serving optimized images
     *
     * @return void
     */
    public function add_rewrite_rules(): void {
        // Add endpoint for checking optimized image availability
        add_rewrite_endpoint('prorank-image-check', EP_ROOT);
    }

    /**
     * Process single image optimization via Action Scheduler
     *
     * @param int $attachment_id Attachment ID to optimize
     * @return void
     */
    public function process_single_image_optimization(int $attachment_id): void {
        $file_path = get_attached_file($attachment_id);
        $current_image = $file_path ? basename($file_path) : ('ID ' . $attachment_id);
        $attempts = (int) get_post_meta($attachment_id, '_prorank_optimization_attempts', true);
        update_post_meta($attachment_id, '_prorank_optimization_attempts', (string) ($attempts + 1));
        $metadata = wp_get_attachment_metadata($attachment_id);

        if (!$metadata && $file_path && file_exists($file_path)) {
            if (!function_exists('wp_generate_attachment_metadata')) {
                require_once ABSPATH . 'wp-admin/includes/image.php';
            }

            $generated_metadata = wp_generate_attachment_metadata($attachment_id, $file_path);
            if (is_array($generated_metadata) && !empty($generated_metadata)) {
                wp_update_attachment_metadata($attachment_id, $generated_metadata);
                $metadata = $generated_metadata;
            }
        }

        if (!$metadata) {
            update_post_meta($attachment_id, '_prorank_optimization_failed', '1');
            update_post_meta($attachment_id, '_prorank_optimization_failure_reason', 'missing_attachment_metadata');
            $this->record_bulk_job_failure($current_image, 'missing_attachment_metadata', $attachment_id);
            return;
        }

        delete_post_meta($attachment_id, '_prorank_optimization_failed');
        delete_post_meta($attachment_id, '_prorank_optimization_failure_reason');
        $this->clear_attachment_no_gain_state($attachment_id);

        $progress = get_transient('prorank_image_optimization_progress');

        // Ensure base structure
        if (!is_array($progress)) {
            $progress = [
                'current' => 0,
                'total' => 0,
                'bytes_saved' => 0,
                'log' => [],
            ];
        }

        // Mark current image
        $progress['current_image'] = $current_image;
        $progress['active_index'] = ($progress['current'] ?? 0) + 1;
        $progress['log'][] = [
            'image' => $current_image,
            'status' => 'processing',
            'ts' => time(),
        ];
        if (count($progress['log']) > 50) {
            $progress['log'] = array_slice($progress['log'], -50);
        }
        $progress['updated_at'] = time();
        set_transient('prorank_image_optimization_progress', $progress, HOUR_IN_SECONDS);

        // Update bulk job progress (if a bulk job is running)
        $this->record_bulk_job_progress($current_image, 0, false);

        // Check memory availability before processing
        $file_info = wp_getimagesize($file_path);
        if ($file_info) {
            $required_memory = ($file_info[0] ?? 0) * ($file_info[1] ?? 0) * 4 * 2; // 2 copies in GD
            $available = $this->get_available_memory();
            if ($available > 0 && $required_memory > $available) {
                update_post_meta($attachment_id, '_prorank_optimization_failed', '1');
                update_post_meta($attachment_id, '_prorank_optimization_failure_reason', 'insufficient_memory');
                $this->record_bulk_job_failure($current_image, 'insufficient_memory', $attachment_id);
                return;
            }
        }

        try {
            $this->generate_optimized_images($metadata, $attachment_id);
        } catch (\Throwable $e) {
            update_post_meta($attachment_id, '_prorank_optimization_failed', '1');
            update_post_meta($attachment_id, '_prorank_optimization_failure_reason', $e->getMessage());
            $this->record_bulk_job_failure($current_image, $e->getMessage(), $attachment_id);
            return;
        }

        $failure_reason = (string) get_post_meta($attachment_id, '_prorank_optimization_failure_reason', true);
        if ((string) get_post_meta($attachment_id, '_prorank_optimization_failed', true) === '1' && $failure_reason !== '') {
            $this->record_bulk_job_failure($current_image, $failure_reason, $attachment_id);
            return;
        }

        if ($this->is_attachment_no_gain($attachment_id)) {
            $this->record_bulk_job_skipped(
                $current_image,
                $this->get_attachment_no_gain_reason($attachment_id),
                $attachment_id
            );
            return;
        }

        // Update progress after processing
        $saved_bytes = (int) get_post_meta($attachment_id, '_prorank_bytes_saved', true);
        $progress['current'] = isset($progress['current']) ? ((int) $progress['current'] + 1) : 1;
        $progress['bytes_saved'] = ($progress['bytes_saved'] ?? 0) + $saved_bytes;
        $progress['current_image'] = '';
        $progress['active_index'] = null;
        $progress['log'][] = [
            'image' => $current_image,
            'status' => 'done',
            'bytes_saved' => $saved_bytes,
            'ts' => time(),
        ];
        if (count($progress['log']) > 50) {
            $progress['log'] = array_slice($progress['log'], -50);
        }
        // Summary aggregation
        if (!isset($progress['summary']) || !is_array($progress['summary'])) {
            $progress['summary'] = ['processed' => 0, 'saved_bytes' => 0];
        }
        $progress['summary']['processed'] = ($progress['summary']['processed'] ?? 0) + 1;
        $progress['summary']['saved_bytes'] = ($progress['summary']['saved_bytes'] ?? 0) + $saved_bytes;

        $progress['completed'] = isset($progress['total'], $progress['current']) && $progress['total'] > 0
            ? $progress['current'] >= $progress['total']
            : false;
        if (!empty($progress['total'])) {
            $progress['percent'] = round(($progress['current'] / $progress['total']) * 100, 2);
        }

        $progress['updated_at'] = time();
        set_transient('prorank_image_optimization_progress', $progress, HOUR_IN_SECONDS);

        // Update bulk job progress with completion (if a bulk job is running)
        $this->record_bulk_job_progress($current_image, $saved_bytes, true);

        if (!empty($progress['completed'])) {
            delete_transient('prorank_image_conversion_running');
        }
    }

    /**
     * Record a failed image in bulk progress so a bad attachment cannot block the queue forever.
     *
     * @param string $current_image Current image label.
     * @param string $reason Failure reason slug.
     * @param int    $attachment_id Attachment ID.
     * @return void
     */
    private function record_bulk_job_failure(string $current_image, string $reason, int $attachment_id = 0): void {
        $progress = get_transient('prorank_image_optimization_progress');
        if (!is_array($progress)) {
            $progress = [
                'current' => 0,
                'total' => 0,
                'bytes_saved' => 0,
                'log' => [],
            ];
        }

        $progress['current'] = isset($progress['current']) ? ((int) $progress['current'] + 1) : 1;
        $progress['current_image'] = '';
        $progress['active_index'] = null;
        $progress['log'][] = [
            'image' => $current_image,
            'status' => 'failed',
            'reason' => $reason,
            'ts' => time(),
        ];
        if (count($progress['log']) > 50) {
            $progress['log'] = array_slice($progress['log'], -50);
        }

        if (!isset($progress['summary']) || !is_array($progress['summary'])) {
            $progress['summary'] = ['processed' => 0, 'saved_bytes' => 0, 'failed' => 0];
        }
        $progress['summary']['processed'] = ($progress['summary']['processed'] ?? 0) + 1;
        $progress['summary']['failed'] = ($progress['summary']['failed'] ?? 0) + 1;

        $progress['completed'] = isset($progress['total'], $progress['current']) && $progress['total'] > 0
            ? $progress['current'] >= $progress['total']
            : false;
        if (!empty($progress['total'])) {
            $progress['percent'] = round(($progress['current'] / $progress['total']) * 100, 2);
        }

        $progress['updated_at'] = time();
        set_transient('prorank_image_optimization_progress', $progress, HOUR_IN_SECONDS);

        $job_id = (string) get_option('prorank_bulk_job_active', '');
        if ($job_id !== '') {
            $job_data = get_option('prorank_bulk_job_' . $job_id, []);
            $job_status = is_array($job_data) ? (string) ($job_data['status'] ?? '') : '';
            if (is_array($job_data) && !empty($job_data) && !in_array($job_status, ['completed', 'stopped', 'failed'], true)) {
                $log = $job_data['log'] ?? [];
                $log[] = [
                    'image' => $current_image,
                    'status' => 'failed',
                    'reason' => $reason,
                    'attachment_id' => $attachment_id,
                    'ts' => time(),
                ];
                if (count($log) > 50) {
                    $log = array_slice($log, -50);
                }

                $processed = (int) ($job_data['processed'] ?? 0) + 1;
                $failed = (int) ($job_data['failed'] ?? 0) + 1;
                $failed_images = $job_data['failed_images'] ?? [];
                $failed_images[] = [
                    'image' => $current_image,
                    'reason' => $reason,
                    'attachment_id' => $attachment_id,
                    'ts' => time(),
                ];
                if (count($failed_images) > 50) {
                    $failed_images = array_slice($failed_images, -50);
                }

                $update = [
                    'current_image' => '',
                    'processed' => $processed,
                    'failed' => $failed,
                    'failed_images' => $failed_images,
                    'log' => $log,
                    'status' => 'running',
                ];

                $total = (int) ($job_data['total'] ?? 0);
                if ($total > 0 && $processed >= $total) {
                    $update['status'] = 'completed';
                    $update['completed_at'] = time();
                    update_option('prorank_bulk_job_last', $job_id, false);
                    delete_option('prorank_bulk_job_active');
                }

                $controller = new ImageOptimizationController();
                $controller->record_bulk_job_update($job_id, $update);
            }
        }

        if (!empty($progress['completed'])) {
            delete_transient('prorank_image_conversion_running');
        }
    }

    /**
     * Record a terminal no-gain result so bulk progress can advance without treating the image as failed.
     *
     * @param string $current_image Current image label.
     * @param string $reason Skip reason slug.
     * @param int    $attachment_id Attachment ID.
     * @return void
     */
    private function record_bulk_job_skipped(string $current_image, string $reason, int $attachment_id = 0): void {
        $progress = get_transient('prorank_image_optimization_progress');
        if (!is_array($progress)) {
            $progress = [
                'current' => 0,
                'total' => 0,
                'bytes_saved' => 0,
                'log' => [],
            ];
        }

        $progress['current'] = isset($progress['current']) ? ((int) $progress['current'] + 1) : 1;
        $progress['current_image'] = '';
        $progress['active_index'] = null;
        $progress['log'][] = [
            'image' => $current_image,
            'status' => 'skipped',
            'reason' => $reason !== '' ? $reason : 'no_effective_output',
            'ts' => time(),
        ];
        if (count($progress['log']) > 50) {
            $progress['log'] = array_slice($progress['log'], -50);
        }

        if (!isset($progress['summary']) || !is_array($progress['summary'])) {
            $progress['summary'] = ['processed' => 0, 'saved_bytes' => 0, 'failed' => 0, 'skipped' => 0];
        }
        $progress['summary']['processed'] = ($progress['summary']['processed'] ?? 0) + 1;
        $progress['summary']['skipped'] = ($progress['summary']['skipped'] ?? 0) + 1;

        $progress['completed'] = isset($progress['total'], $progress['current']) && $progress['total'] > 0
            ? $progress['current'] >= $progress['total']
            : false;
        if (!empty($progress['total'])) {
            $progress['percent'] = round(($progress['current'] / $progress['total']) * 100, 2);
        }

        $progress['updated_at'] = time();
        set_transient('prorank_image_optimization_progress', $progress, HOUR_IN_SECONDS);

        $job_id = (string) get_option('prorank_bulk_job_active', '');
        if ($job_id !== '') {
            $job_data = get_option('prorank_bulk_job_' . $job_id, []);
            $job_status = is_array($job_data) ? (string) ($job_data['status'] ?? '') : '';
            if (is_array($job_data) && !empty($job_data) && !in_array($job_status, ['completed', 'stopped', 'failed'], true)) {
                $log = $job_data['log'] ?? [];
                $log[] = [
                    'image' => $current_image,
                    'status' => 'skipped',
                    'reason' => $reason !== '' ? $reason : 'no_effective_output',
                    'attachment_id' => $attachment_id,
                    'ts' => time(),
                ];
                if (count($log) > 50) {
                    $log = array_slice($log, -50);
                }

                $processed = (int) ($job_data['processed'] ?? 0) + 1;
                $skipped = (int) ($job_data['skipped'] ?? 0) + 1;

                $update = [
                    'current_image' => '',
                    'processed' => $processed,
                    'skipped' => $skipped,
                    'log' => $log,
                    'status' => 'running',
                ];

                $total = (int) ($job_data['total'] ?? 0);
                if ($total > 0 && $processed >= $total) {
                    $update['status'] = 'completed';
                    $update['completed_at'] = time();
                    update_option('prorank_bulk_job_last', $job_id, false);
                    delete_option('prorank_bulk_job_active');
                }

                $controller = new ImageOptimizationController();
                $controller->record_bulk_job_update($job_id, $update);
            }
        }

        if (!empty($progress['completed'])) {
            delete_transient('prorank_image_conversion_running');
        }
    }

    /**
     * Handle bulk optimization continuation action.
     *
     * @param mixed $job_id Job ID or array with job_id/next_offset.
     * @param mixed $next_offset Next offset.
     * @return void
     */
    public function handle_bulk_optimization_continue($job_id = '', $next_offset = 0): void {
        if (is_array($job_id)) {
            $next_offset = $job_id['next_offset'] ?? 0;
            $job_id = $job_id['job_id'] ?? '';
        }

        $job_id = is_scalar($job_id) ? (string) $job_id : '';
        $next_offset = is_numeric($next_offset) ? (int) $next_offset : 0;

        if ($job_id === '') {
            return;
        }

        $controller = new ImageOptimizationController();
        $controller->handle_bulk_optimization_continue($job_id, $next_offset);
    }

    /**
     * Record progress for active bulk job and sync UI data.
     *
     * @param string $current_image Current image label.
     * @param int    $saved_bytes Bytes saved for this image.
     * @param bool   $completed Whether this image finished processing.
     * @return void
     */
    private function record_bulk_job_progress(string $current_image, int $saved_bytes, bool $completed): void {
        $job_id = (string) get_option('prorank_bulk_job_active', '');
        if ($job_id === '') {
            return;
        }

        $job_data = get_option('prorank_bulk_job_' . $job_id, []);
        if (!is_array($job_data) || empty($job_data)) {
            return;
        }

        if (!empty($job_data['status']) && in_array($job_data['status'], ['completed', 'stopped', 'failed'], true)) {
            return;
        }

        $log = $job_data['log'] ?? [];
        $log[] = [
            'image' => $current_image,
            'status' => $completed ? 'done' : 'processing',
            'bytes_saved' => $saved_bytes,
            'ts' => time(),
        ];
        if (count($log) > 50) {
            $log = array_slice($log, -50);
        }

        $update = [
            'current_image' => $completed ? '' : $current_image,
            'log' => $log,
            'status' => 'running',
        ];

        if ($completed) {
            $processed = (int) ($job_data['processed'] ?? 0) + 1;
            $bytes_total = (int) ($job_data['bytes_saved'] ?? 0) + $saved_bytes;
            $update['processed'] = $processed;
            $update['bytes_saved'] = $bytes_total;

            $total = (int) ($job_data['total'] ?? 0);
            if ($total > 0 && $processed >= $total) {
                $update['status'] = 'completed';
                $update['completed_at'] = time();
                update_option('prorank_bulk_job_last', $job_id, false);
                delete_option('prorank_bulk_job_active');
            }
        }

        $controller = new ImageOptimizationController();
        $controller->record_bulk_job_update($job_id, $update);
    }

    /**
     * Queue failed optimization for later retry
     *
     * @param string $file_path Source file path
     * @param string $format Target format
     * @param int $quality Quality setting
     * @param string $error_message Error message
     * @return void
     */
    private function queue_failed_optimization(string $file_path, string $format, int $quality, string $error_message): void {
        $queue = get_option('prorank_failed_optimizations', []);

        // Add to queue with metadata
        $queue[] = [
            'file_path' => $file_path,
            'format' => $format,
            'quality' => $quality,
            'error' => $error_message,
            'attempts' => 1,
            'queued_at' => current_time('mysql'),
            'last_attempt' => current_time('mysql'),
        ];

        // Keep only last 100 failed items
        if (count($queue) > 100) {
            $queue = array_slice($queue, -100);
        }

        update_option('prorank_failed_optimizations', $queue, false);

        // Schedule retry job if not already scheduled
        if (!wp_next_scheduled('prorank_retry_failed_optimizations')) {
            wp_schedule_single_event(time() + 3600, 'prorank_retry_failed_optimizations');
        }
    }

    /**
     * Process failed optimization queue
     *
     * @return void
     */
    public function process_failed_optimization_queue(): void {
        $queue = get_option('prorank_failed_optimizations', []);

        if (empty($queue)) {
            return;
        }

        $processed = [];
        $remaining = [];

        foreach ($queue as $item) {
            // Skip if too many attempts
            if ($item['attempts'] >= 5) {
                continue;
            }

            // Skip if tried recently (within 1 hour)
            $last_attempt = strtotime($item['last_attempt']);
            if ((time() - $last_attempt) < 3600) {
                $remaining[] = $item;
                continue;
            }

            // Try to optimize again
            $success = $this->optimize_via_cloud(
                $item['file_path'],
                $this->get_optimized_path($item['file_path'], $item['format']),
                $item['format'],
                $item['quality']
            );

            if (!$success) {
                // Increment attempts and re-queue
                $item['attempts']++;
                $item['last_attempt'] = current_time('mysql');
                $remaining[] = $item;
            } else {
                $processed[] = $item['file_path'];
            }
        }

        // Update queue
        update_option('prorank_failed_optimizations', $remaining, false);

        // Log results
        if (!empty($processed)) {
            prorank_log(sprintf(
                '[ProRank Image Optimization] Processed %d failed optimizations from queue',
                count($processed)
            ));
        }

        // Reschedule if items remain
        if (!empty($remaining)) {
            wp_schedule_single_event(time() + 7200, 'prorank_retry_failed_optimizations');
        }
    }

    /**
     * Backup original image before optimization
     *
     * @param string $file_path     Original file path
     * @param int    $attachment_id Attachment ID
     * @return bool Success status
     */
    private function backup_original_image(string $file_path, int $attachment_id): bool {
        if ($file_path === '' || !file_exists($file_path) || !is_readable($file_path)) {
            update_post_meta($attachment_id, '_prorank_backup_failure_reason', 'source_file_unavailable');
            return false;
        }

        $existing_backup_path = (string) get_post_meta($attachment_id, '_prorank_backup_path', true);
        if ($existing_backup_path !== '' && file_exists($existing_backup_path)) {
            delete_post_meta($attachment_id, '_prorank_backup_failure_reason');
            return true;
        }

        $time = current_time('mysql');
        $y = substr($time, 0, 4);
        $m = substr($time, 5, 2);

        foreach ($this->get_all_image_backup_directories() as $backup_dir) {
            if (!$this->prepare_backup_root($backup_dir)) {
                continue;
            }

            $backup_subdir = $backup_dir . "/$y/$m";
            if (!$this->ensure_backup_directory($backup_subdir)) {
                continue;
            }

            $filename = basename($file_path);
            $backup_filename = $attachment_id . '-' . $filename;
            $backup_path = $backup_subdir . '/' . $backup_filename;

            if (file_exists($backup_path) || @copy($file_path, $backup_path)) {
                update_post_meta($attachment_id, '_prorank_backup_path', $backup_path);
                delete_post_meta($attachment_id, '_prorank_backup_failure_reason');
                return true;
            }
        }

        update_post_meta($attachment_id, '_prorank_backup_failure_reason', 'backup_copy_failed');
        return false;
    }

    /**
     * Ensure a backup directory exists and is writable.
     *
     * @param string $directory Directory path.
     * @return bool
     */
    private function ensure_backup_directory(string $directory): bool {
        if ($directory === '') {
            return false;
        }

        if (!file_exists($directory) && !wp_mkdir_p($directory)) {
            return false;
        }

        return is_dir($directory) && is_writable($directory);
    }

    /**
     * Create and protect the backup root before using it.
     *
     * @param string $backup_dir Backup directory path.
     * @return bool
     */
    private function prepare_backup_root(string $backup_dir): bool {
        if (!$this->ensure_backup_directory($backup_dir)) {
            return false;
        }

        $htaccess = $backup_dir . '/.htaccess';
        if (!file_exists($htaccess)) {
            @file_put_contents($htaccess, "Require all denied\n<IfModule !mod_authz_core.c>\nOrder Deny,Allow\nDeny from all\n</IfModule>\n");
        }

        $index_file = $backup_dir . '/index.php';
        if (!file_exists($index_file)) {
            @file_put_contents($index_file, "<?php\n// Silence is golden.\n");
        }

        return true;
    }

    /**
     * Schedule background optimization cron job
     *
     * @return void
     */
    public function schedule_background_optimization(): void {
        if (!wp_next_scheduled('prorank_background_image_optimization')) {
            wp_schedule_event(time(), 'hourly', 'prorank_background_image_optimization');
        }
    }

    /**
     * Run background optimization
     *
     * @return void
     */
    public function run_background_optimization(): void {
        // Check if already running
        if (get_transient('prorank_background_optimization_running')) {
            return;
        }

        // Set running flag
        set_transient('prorank_background_optimization_running', true, HOUR_IN_SECONDS);

        // Get unoptimized images
        $args = [
            'post_type' => 'attachment',
            'post_mime_type' => 'image',
            'post_status' => 'inherit',
            'posts_per_page' => 10, // Process 10 images per cron run
            'meta_query' => [
                [
                    'key' => '_prorank_optimized',
                    'compare' => 'NOT EXISTS',
                ],
            ],
        ];

        $attachments = get_posts($args);

        foreach ($attachments as $attachment) {
            $metadata = wp_get_attachment_metadata($attachment->ID);
            if ($metadata) {
                $this->generate_optimized_images($metadata, $attachment->ID);
            }
        }

        // Clear running flag
        delete_transient('prorank_background_optimization_running');
    }

    /**
     * Process bulk optimization (triggered by REST API)
     *
     * Handles the prorank_bulk_optimize_images cron action scheduled by
     * ImageOptimizationSettingsController::start_bulk_optimization()
     *
     * @return void
     */
    public function process_bulk_optimization(): void {
        // Get unoptimized images - process in batches of 20
        $batch_size = 20;
        $processed = 0;
        $max_execution = 50; // Max images per cron run

        do {
            $args = [
                'post_type' => 'attachment',
                'post_mime_type' => 'image',
                'post_status' => 'inherit',
                'posts_per_page' => $batch_size,
                'meta_query' => [
                    'relation' => 'OR',
                    [
                        'key' => '_prorank_optimized',
                        'compare' => 'NOT EXISTS',
                    ],
                    [
                        'key' => '_prorank_optimized',
                        'value' => '',
                    ],
                ],
            ];

            $attachments = get_posts($args);

            if (empty($attachments)) {
                break;
            }

            foreach ($attachments as $attachment) {
                // Check memory and time limits
                if (memory_get_usage(true) > (wp_convert_hr_to_bytes(ini_get('memory_limit')) * 0.8)) {
                    break 2;
                }

                $metadata = wp_get_attachment_metadata($attachment->ID);
                if ($metadata) {
                    $this->generate_optimized_images($metadata, $attachment->ID);
                } else {
                    // Mark as processed even without metadata to avoid infinite loop
                    update_post_meta($attachment->ID, '_prorank_optimized', '0');
                }

                $processed++;

                if ($processed >= $max_execution) {
                    break 2;
                }
            }
        } while (!empty($attachments) && $processed < $max_execution);

        // Check if more images remain
        $remaining = get_posts([
            'post_type' => 'attachment',
            'post_mime_type' => 'image',
            'post_status' => 'inherit',
            'posts_per_page' => 1,
            'fields' => 'ids',
            'meta_query' => [
                'relation' => 'OR',
                [
                    'key' => '_prorank_optimized',
                    'compare' => 'NOT EXISTS',
                ],
                [
                    'key' => '_prorank_optimized',
                    'value' => '',
                ],
            ],
        ]);

        if (!empty($remaining)) {
            // Schedule next batch
            if (!wp_next_scheduled('prorank_bulk_optimize_images')) {
                wp_schedule_single_event(time() + 30, 'prorank_bulk_optimize_images');
            }
        } else {
            // All done - clear running flag
            delete_transient('prorank_bulk_optimization_running');
        }
    }

    /**
     * Add custom media menu
     *
     * @return void
     */
    public function add_custom_media_menu(): void {
        add_media_page(
            __('Custom Media', 'prorank-seo'),
            __('Custom Media', 'prorank-seo'),
            'manage_options',
            'prorank-custom-media',
            [$this, 'render_custom_media_page']
        );
    }

    /**
     * Render custom media page
     *
     * @return void
     */
    public function render_custom_media_page(): void {
        ?>
        <div class="wrap">
            <h1><?php echo esc_html__('Custom Media Optimization', 'prorank-seo'); ?></h1>
            <p><?php echo esc_html__('Optimize images from custom folders not managed by WordPress Media Library.', 'prorank-seo'); ?></p>

            <div id="prorank-custom-media-root"></div>
        </div>
        <?php
    }

    /**
     * Restore original image from backup
     *
     * @param int $attachment_id Attachment ID
     * @return bool Success status
     */
    public function restore_from_backup(int $attachment_id): bool {
        $backup_path = get_post_meta($attachment_id, '_prorank_backup_path', true);

        if (!$backup_path || !file_exists($backup_path)) {
            return false;
        }

        $file_path = get_attached_file($attachment_id);
        if (!$file_path) {
            return false;
        }

        // Restore original file
        if (copy($backup_path, $file_path)) {
            // Delete optimized versions
            $this->delete_optimized_images($attachment_id);

            $this->reset_attachment_optimization_state($attachment_id);

            // Regenerate thumbnails
            $metadata = wp_get_attachment_metadata($attachment_id);
            if ($metadata) {
                wp_update_attachment_metadata($attachment_id, $metadata);
            }

            return true;
        }

        return false;
    }

    /**
     * Get directory size recursively
     *
     * @param string $dir Directory path
     * @return int Size in bytes
     */
    private function get_directory_size(string $dir): int {
        $size = 0;

        if (!is_dir($dir)) {
            return $size;
        }

        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::LEAVES_ONLY
        );

        foreach ($iterator as $file) {
            if ($file->isFile()) {
                $size += $file->getSize();
            }
        }

        return $size;
    }

    /**
     * Delete all backup files
     *
     * @return bool Success status
     */
    public function delete_all_backups(): bool {
        // Use WordPress filesystem API
        global $wp_filesystem;
        if (!function_exists('WP_Filesystem')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }

        WP_Filesystem();

        foreach ($this->get_all_image_backup_directories() as $backup_dir) {
            if ($wp_filesystem->exists($backup_dir) && !$wp_filesystem->delete($backup_dir, true)) {
                return false;
            }
        }

        $attachment_ids = get_posts([
            'post_type'      => 'attachment',
            'post_status'    => 'inherit',
            'posts_per_page' => -1,
            'fields'         => 'ids',
            'meta_query'     => [
                [
                    'key'     => '_prorank_backup_path',
                    'compare' => 'EXISTS',
                ],
            ],
        ]);

        foreach ($attachment_ids as $attachment_id) {
            delete_post_meta((int) $attachment_id, '_prorank_backup_path');
        }

        return true;
    }

    /**
     * Restore all images from backups
     *
     * @return array{restored:int,failed:int,missing_files:int}
     */
    public function restore_all_backups() {
        $args = [
            'post_type'      => 'attachment',
            'post_status'    => 'inherit',
            'posts_per_page' => -1,
            'fields'         => 'ids',
            'meta_query'     => [
                [
                    'key'     => '_prorank_backup_path',
                    'compare' => 'EXISTS',
                ],
            ],
        ];

        $ids = get_posts($args);
        $restored = 0;
        $failed = 0;
        $missing_files = 0;

        foreach ($ids as $attachment_id) {
            $backup_path = (string) get_post_meta((int) $attachment_id, '_prorank_backup_path', true);
            if ($backup_path === '' || !file_exists($backup_path)) {
                $failed++;
                $missing_files++;
                continue;
            }

            $ok = $this->restore_from_backup((int) $attachment_id);
            if ($ok) {
                $restored++;
            } else {
                $failed++;
            }
        }

        return [
            'restored' => $restored,
            'failed' => $failed,
            'missing_files' => $missing_files,
        ];
    }

    /**
     * Store image backups outside the public uploads directory.
     */
    private function get_image_backup_directory(): string {
        $site_root        = wp_normalize_path(untrailingslashit(ABSPATH));
        $site_parent_root = wp_normalize_path(dirname($site_root));
        $site_fingerprint = md5((string) home_url('/'));

        return trailingslashit($site_parent_root) . 'prorank-image-backups-' . $site_fingerprint;
    }

    /**
     * Legacy backup location used by older builds.
     */
    private function get_legacy_image_backup_directory(): string {
        $upload_dir = wp_upload_dir();

        return trailingslashit($upload_dir['basedir']) . 'prorank-backups';
    }

    /**
     * Backup directories that may contain image backups.
     *
     * @return array<int, string>
     */
    private function get_all_image_backup_directories(): array {
        return array_values(
            array_unique(
                array_map(
                    'wp_normalize_path',
                    [
                        $this->get_image_backup_directory(),
                        $this->get_legacy_image_backup_directory(),
                    ]
                )
            )
        );
    }

    /**
     * Meta keys that point to generated derivative files, excluding the original backup copy.
     *
     * @param int $attachment_id Attachment ID.
     * @return array<int, string>
     */
    private function get_optimized_path_meta_keys(int $attachment_id): array {
        return array_values(
            array_filter(
                array_map(
                    'strval',
                    (array) $GLOBALS['wpdb']->get_col(
                        $GLOBALS['wpdb']->prepare(
                            "SELECT meta_key FROM {$GLOBALS['wpdb']->postmeta}
                             WHERE post_id = %d
                             AND meta_key LIKE '_prorank_%_path%'",
                            $attachment_id
                        )
                    )
                ),
                static function (string $meta_key): bool {
                    return $meta_key !== '_prorank_backup_path';
                }
            )
        );
    }

    /**
     * Clear optimization state so restored images can be optimized again cleanly.
     *
     * @param int $attachment_id Attachment ID.
     * @return void
     */
    private function reset_attachment_optimization_state(int $attachment_id): void {
        $meta_keys = [
            '_prorank_optimized',
            '_prorank_optimization_date',
            '_prorank_optimization_failed',
            '_prorank_optimization_failure_reason',
            '_prorank_optimization_no_gain',
            '_prorank_optimization_skip_reason',
            '_prorank_optimization_attempts',
            '_prorank_bytes_saved',
            '_prorank_original_source_bytes_saved',
            '_prorank_webp_generated',
            '_prorank_avif_generated',
            '_prorank_jxl_generated',
            '_prorank_original_size',
            '_prorank_optimization_method',
        ];

        foreach ($meta_keys as $meta_key) {
            delete_post_meta($attachment_id, $meta_key);
        }
    }

    /**
     * Remove the terminal no-gain state so the image can be evaluated again.
     *
     * @param int $attachment_id Attachment ID.
     * @return void
     */
    private function clear_attachment_no_gain_state(int $attachment_id): void {
        delete_post_meta($attachment_id, '_prorank_optimization_no_gain');
        delete_post_meta($attachment_id, '_prorank_optimization_skip_reason');
    }

    /**
     * Mark an attachment as terminally unchanged because no worthwhile optimized output was produced.
     *
     * @param int    $attachment_id Attachment ID.
     * @param string $reason Skip reason slug.
     * @return void
     */
    private function mark_attachment_no_gain(int $attachment_id, string $reason = 'no_effective_output'): void {
        delete_post_meta($attachment_id, '_prorank_optimized');
        delete_post_meta($attachment_id, '_prorank_optimization_date');
        delete_post_meta($attachment_id, '_prorank_optimization_failed');
        delete_post_meta($attachment_id, '_prorank_optimization_failure_reason');
        delete_post_meta($attachment_id, '_prorank_optimization_attempts');
        update_post_meta($attachment_id, '_prorank_optimization_no_gain', '1');
        update_post_meta($attachment_id, '_prorank_optimization_skip_reason', $reason);
    }

    /**
     * Determine whether an attachment is in the terminal no-gain state.
     *
     * @param int $attachment_id Attachment ID.
     * @return bool
     */
    private function is_attachment_no_gain(int $attachment_id): bool {
        if ((string) get_post_meta($attachment_id, '_prorank_optimization_no_gain', true) === '1') {
            return true;
        }

        return (string) get_post_meta($attachment_id, '_prorank_optimization_failure_reason', true) === 'no_effective_output';
    }

    /**
     * Get the no-gain reason for an attachment, supporting legacy failure metadata.
     *
     * @param int $attachment_id Attachment ID.
     * @return string
     */
    private function get_attachment_no_gain_reason(int $attachment_id): string {
        $reason = (string) get_post_meta($attachment_id, '_prorank_optimization_skip_reason', true);
        if ($reason !== '') {
            return $reason;
        }

        $legacy_reason = (string) get_post_meta($attachment_id, '_prorank_optimization_failure_reason', true);
        if ($legacy_reason === 'no_effective_output') {
            return $legacy_reason;
        }

        return '';
    }

    /**
     * Resolve the per-attachment retry limit for failed bulk optimizations.
     *
     * @return int
     */
    private function get_bulk_retry_limit(): int {
        return max(1, (int) apply_filters('prorank_image_optimization_retry_limit', self::DEFAULT_BULK_RETRY_LIMIT));
    }

    /**
    /**
     * Get available PHP memory in bytes (0 if unlimited).
     *
     * @return int
     */
    private function get_available_memory(): int {
        $limit = ini_get('memory_limit');
        if ($limit === '-1' || $limit === '' || $limit === false) {
            return 0; // unlimited
        }
        $limit = wp_convert_hr_to_bytes($limit);
        $used  = memory_get_usage(true);
        return max(0, $limit - $used);
    }

    /**
     * Check if file should be excluded from optimization
     *
     * @param string $file_path     File path
     * @param int    $attachment_id Attachment ID
     * @param string $size          Image size (optional)
     * @return bool True if should be excluded
     */
    private function should_exclude_file(string $file_path, int $attachment_id, string $size = ''): bool {
        if ($this->matches_exclude_path($file_path)) {
            return true;
        }

        if ($attachment_id > 0) {
            $attachment_url = wp_get_attachment_url($attachment_id);
            if (is_string($attachment_url) && $attachment_url !== '' && $this->matches_exclude_path($attachment_url)) {
                return true;
            }
        }

        // Check size exclusions
        $exclude_sizes = $this->get_setting('exclude_sizes', []);
        if (!empty($size) && in_array($size, $exclude_sizes, true)) {
            return true;
        }

        // Check pattern exclusions
        $exclude_patterns = $this->get_setting('exclude_patterns', []);
        if (empty($exclude_patterns)) {
            return false;
        }

        // Get file info
        $filename = basename($file_path);
        $filesize = filesize($file_path);

        foreach ($exclude_patterns as $pattern) {
            $matches = false;

            switch ($pattern['type']) {
                case 'filename':
                    $matches = strpos($filename, $pattern['pattern']) !== false;
                    break;

                case 'path':
                    $matches = strpos($file_path, $pattern['pattern']) !== false;
                    break;

                case 'size':
                    $operator = substr($pattern['pattern'], 0, 1);
                    $kb = (int) substr($pattern['pattern'], 1);
                    $bytes = $kb * 1024;

                    if ($operator === '<') {
                        $matches = $filesize < $bytes;
                    } elseif ($operator === '>') {
                        $matches = $filesize > $bytes;
                    }
                    break;
            }

            if ($matches) {
                // Check target applicability
                switch ($pattern['target']) {
                    case 'all':
                        return true;

                    case 'thumbnails':
                        // Exclude only if it's a thumbnail (not full/scaled)
                        return !empty($size) && $size !== 'full' && $size !== 'scaled';

                    case 'custom_media':
                        // This would need additional context to determine
                        return false;

                    case 'specific_sizes':
                        return !empty($size) && in_array($size, $pattern['sizes'], true);
                }
            }
        }

        return false;
    }

    /**
     * Get all registered image sizes
     *
     * @return array Array of image sizes with details
     */
    public function get_image_sizes(): array {
        global $_wp_additional_image_sizes;

        $sizes = [];

        // Default WordPress sizes
        $default_sizes = ['thumbnail', 'medium', 'medium_large', 'large'];

        foreach ($default_sizes as $size) {
            $sizes[] = [
                'name' => $size,
                'label' => ucfirst(str_replace('_', ' ', $size)),
                'width' => get_option($size . '_size_w'),
                'height' => get_option($size . '_size_h'),
                'crop' => get_option($size . '_crop', false),
            ];
        }

        // Additional sizes
        if (isset($_wp_additional_image_sizes) && is_array($_wp_additional_image_sizes)) {
            foreach ($_wp_additional_image_sizes as $name => $data) {
                $sizes[] = [
                    'name' => $name,
                    'label' => ucfirst(str_replace(['_', '-'], ' ', $name)),
                    'width' => $data['width'],
                    'height' => $data['height'],
                    'crop' => $data['crop'],
                ];
            }
        }

        return $sizes;
    }

    /**
     * Convert images in content to picture elements
     *
     * @param string $content Post content
     * @return string Modified content
     */
    public function convert_content_images(string $content): string {
        if (!$this->is_frontend_optimization_context()) {
            return $content;
        }

        // Skip if delivery is disabled
        if (!$this->get_setting('enable_picture_element', true)) {
            return $content;
        }

        // Skip if no images
        if (stripos($content, '<img') === false) {
            return $content;
        }

        // Use DOMDocument to parse HTML
        $doc = new \DOMDocument();
        libxml_use_internal_errors(true);
        @$doc->loadHTML('<?xml encoding="UTF-8"><div>' . $content . '</div>', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
        libxml_clear_errors();

        $xpath = new \DOMXPath($doc);
        $images = $xpath->query('//img[contains(@class, "wp-image-")]');

        if ($images->length === 0) {
            return $content;
        }

        $modified = false;

        foreach ($images as $img) {
            // Extract attachment ID from class
            $classes = $img->getAttribute('class');
            if (preg_match('/wp-image-(\d+)/', $classes, $matches)) {
                $attachment_id = (int) $matches[1];

                // Get original HTML for this image
                $img_html = $doc->saveHTML($img);

                // Convert to picture element
                $picture_html = $this->convert_to_picture_element($img_html, $attachment_id, '', false, []);

                // If conversion happened, replace in DOM
                if ($picture_html !== $img_html) {
                    $fragment = $doc->createDocumentFragment();
                    @$fragment->appendXML($picture_html);
                    if ($fragment->firstChild) {
                        $img->parentNode->replaceChild($fragment->firstChild, $img);
                        $modified = true;
                    }
                }
            }
        }

        if (!$modified) {
            return $content;
        }

        // Get the modified content
        $body = $xpath->query('//div')->item(0);
        if ($body) {
            $content = '';
            foreach ($body->childNodes as $child) {
                $content .= $doc->saveHTML($child);
            }
        }

        return $content;
    }

    /**
     * Get optimized image URL
     *
     * @param string $original_url Original image URL
     * @param string $format       Target format (webp/avif)
     * @return string Optimized URL
     */
    private function get_optimized_url(string $original_url, string $format): string {
        $pathinfo = pathinfo($original_url);
        return $pathinfo['dirname'] . '/' . $pathinfo['filename'] . '.' . $format;
    }

    /**
     * Check if optimized file exists
     * Also checks for WordPress-suffixed versions (e.g., filename-2.webp)
     *
     * @param string $url Optimized file URL
     * @return bool
     */
    private function optimized_file_exists(string $url): bool {
        // Convert URL to path
        $upload_dir = wp_upload_dir();
        $path = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $url);

        // Check exact match first
        if (file_exists($path) && filesize($path) > 0) {
            return true;
        }

        // Check for WordPress-suffixed versions (e.g., filename-2.webp, filename-3.webp)
        // WordPress adds numeric suffixes to prevent filename collisions
        $pathinfo = pathinfo($path);
        for ($i = 2; $i <= 10; $i++) {
            $suffixed_path = $pathinfo['dirname'] . '/' . $pathinfo['filename'] . '-' . $i . '.' . $pathinfo['extension'];
            if (file_exists($suffixed_path) && filesize($suffixed_path) > 0) {
                return true;
            }
        }

        return false;
    }

    /**
     * Get actual optimized file URL (handles WordPress suffixes)
     *
     * @param string $url Expected optimized URL
     * @return string|null Actual URL if exists, null otherwise
     */
    private function get_actual_optimized_url(string $url): ?string {
        $upload_dir = wp_upload_dir();
        $path = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $url);

        // Check exact match first
        if (file_exists($path) && filesize($path) > 0) {
            return $this->version_optimized_asset_url($url, $path);
        }

        // Check for WordPress-suffixed versions
        // WordPress adds -N suffix BEFORE dimensions, e.g., image-2-300x187.webp (not image-300x187-2.webp)
        $pathinfo = pathinfo($path);
        $url_pathinfo = pathinfo($url);
        $filename = $pathinfo['filename'];
        $url_filename = $url_pathinfo['filename'];

        // Check if filename has dimension suffix like -300x187
        $dimension_pattern = '/-(\d+x\d+)$/';
        $has_dimensions = preg_match($dimension_pattern, $filename, $dim_matches);

        for ($i = 2; $i <= 10; $i++) {
            if ($has_dimensions) {
                // Insert -N before the dimensions: image-300x187 -> image-2-300x187
                $base_filename = preg_replace($dimension_pattern, '', $filename);
                $dimensions = $dim_matches[1];
                $suffixed_path = $pathinfo['dirname'] . '/' . $base_filename . '-' . $i . '-' . $dimensions . '.' . $pathinfo['extension'];

                if (file_exists($suffixed_path) && filesize($suffixed_path) > 0) {
                    $base_url_filename = preg_replace($dimension_pattern, '', $url_filename);
                    $suffixed_url = $url_pathinfo['dirname'] . '/' . $base_url_filename . '-' . $i . '-' . $dimensions . '.' . $url_pathinfo['extension'];
                    return $this->version_optimized_asset_url($suffixed_url, $suffixed_path);
                }
            }

            // Also check traditional suffix at end (for non-sized images)
            $suffixed_path = $pathinfo['dirname'] . '/' . $filename . '-' . $i . '.' . $pathinfo['extension'];
            if (file_exists($suffixed_path) && filesize($suffixed_path) > 0) {
                $suffixed_url = $url_pathinfo['dirname'] . '/' . $url_filename . '-' . $i . '.' . $url_pathinfo['extension'];
                return $this->version_optimized_asset_url($suffixed_url, $suffixed_path);
            }
        }

        return null;
    }

    /**
     * Append a stable filemtime version to optimized image URLs.
     *
     * Optimized variants use long-lived browser/CDN cache headers. When ProRank
     * recompresses an AVIF/WebP in place, the filename stays the same and edge
     * caches can keep serving stale heavier bytes. Versioning the emitted URL
     * moves regenerated variants onto a fresh cache key without changing the
     * underlying file layout.
     *
     * @param string $url  Public optimized asset URL.
     * @param string $path Local optimized asset path.
     * @return string Versioned public URL.
     */
    private function version_optimized_asset_url(string $url, string $path): string {
        if (!file_exists($path)) {
            return $url;
        }

        $version = (int) filemtime($path);
        if ($version <= 0) {
            return $url;
        }

        return add_query_arg(['prorankv' => (string) $version], $url);
    }

    /**
     * Convert PNG to JPEG
     *
     * @param array $metadata      Attachment metadata
     * @param int   $attachment_id Attachment ID
     * @return array Modified metadata
     */
    public function convert_png_to_jpeg(array $metadata, int $attachment_id): array {
        $file_path = get_attached_file($attachment_id);
        if (!$file_path || !file_exists($file_path)) {
            return $metadata;
        }

        // Check if it's a PNG
        $mime_type = get_post_mime_type($attachment_id);
        if ($mime_type !== 'image/png') {
            return $metadata;
        }

        // Check if it has transparency
        if ($this->png_has_transparency($file_path)) {
            // Keep PNG if it has transparency
            return $metadata;
        }

        // Convert to JPEG
        $jpeg_path = preg_replace('/\.png$/i', '.jpg', $file_path);

        $success = false;
        if (extension_loaded('imagick') && class_exists('Imagick')) {
            try {
                $imagick = new \Imagick($file_path);
                $imagick->setImageFormat('jpeg');
                $imagick->setImageCompressionQuality($this->get_jpeg_quality());
                $imagick->setImageBackgroundColor('white');
                $imagick = $imagick->flattenImages();
                $success = $imagick->writeImage($jpeg_path);
                $imagick->destroy();
            } catch (\Exception $e) {
                // Fall back to GD
            }
        }

        if (!$success && function_exists('imagecreatefromstring')) {
            $image = imagecreatefromstring(file_get_contents($file_path));
            if ($image) {
                // Create a white background
                $width = imagesx($image);
                $height = imagesy($image);
                $jpeg_image = imagecreatetruecolor($width, $height);
                $white = imagecolorallocate($jpeg_image, 255, 255, 255);
                imagefill($jpeg_image, 0, 0, $white);
                imagecopy($jpeg_image, $image, 0, 0, 0, 0, $width, $height);

                $success = imagejpeg($jpeg_image, $jpeg_path, $this->get_jpeg_quality());
                imagedestroy($image);
                imagedestroy($jpeg_image);
            }
        }

        if ($success) {
            // Update attachment
            update_attached_file($attachment_id, $jpeg_path);
            wp_update_post([
                'ID' => $attachment_id,
                'post_mime_type' => 'image/jpeg',
            ]);

            // Delete original PNG
            \ProRank\SEO\Core\FilesystemHelper::delete_file($file_path);

            // Update metadata
            $metadata = wp_generate_attachment_metadata($attachment_id, $jpeg_path);
        }

        return $metadata;
    }

    /**
     * Convert CMYK to RGB
     *
     * @param array $metadata      Attachment metadata
     * @param int   $attachment_id Attachment ID
     * @return array Modified metadata
     */
    public function convert_cmyk_to_rgb(array $metadata, int $attachment_id): array {
        $file_path = get_attached_file($attachment_id);
        if (!$file_path || !file_exists($file_path)) {
            return $metadata;
        }

        // Check if it's CMYK
        if (!$this->is_cmyk_image($file_path)) {
            return $metadata;
        }

        $success = false;
        if (extension_loaded('imagick') && class_exists('Imagick')) {
            try {
                $imagick = new \Imagick($file_path);
                if ($imagick->getImageColorspace() == \Imagick::COLORSPACE_CMYK) {
                    $imagick->transformImageColorspace(\Imagick::COLORSPACE_RGB);
                    $success = $imagick->writeImage($file_path);
                    $imagick->destroy();

                    if ($success) {
                        // Regenerate metadata
                        $metadata = wp_generate_attachment_metadata($attachment_id, $file_path);
                    }
                }
            } catch (\Exception $e) {
                // Log error
            }
        }

        return $metadata;
    }

    /**
     * Manage EXIF data
     *
     * @param array $metadata      Attachment metadata
     * @param int   $attachment_id Attachment ID
     * @return array Modified metadata
     */
    public function manage_exif_data(array $metadata, int $attachment_id): array {
        $file_path = get_attached_file($attachment_id);
        if (!$file_path || !file_exists($file_path)) {
            return $metadata;
        }

        // Check if it's a JPEG
        $mime_type = get_post_mime_type($attachment_id);
        if ($mime_type !== 'image/jpeg') {
            return $metadata;
        }

        // Remove EXIF data
        if (extension_loaded('imagick') && class_exists('Imagick')) {
            try {
                $imagick = new \Imagick($file_path);

                // Keep orientation data if needed
                $orientation = $imagick->getImageOrientation();

                // Strip all EXIF data
                $imagick->stripImage();

                // Restore orientation
                if ($orientation !== \Imagick::ORIENTATION_UNDEFINED) {
                    $imagick->setImageOrientation($orientation);
                }

                $imagick->writeImage($file_path);
                $imagick->destroy();
            } catch (\Exception $e) {
                // Log error
            }
        }

        return $metadata;
    }

    /**
     * Optimize PDF files
     *
     * @param array $metadata      Attachment metadata
     * @param int   $attachment_id Attachment ID
     * @return array Modified metadata
     */
    public function optimize_pdf(array $metadata, int $attachment_id): array {
        $file_path = get_attached_file($attachment_id);
        if (!$file_path || !file_exists($file_path)) {
            return $metadata;
        }

        // Check if it's a PDF
        $mime_type = get_post_mime_type($attachment_id);
        if ($mime_type !== 'application/pdf') {
            return $metadata;
        }

        // Backup original
        if ($this->get_setting('backup_originals', true) && !$this->backup_original_image($file_path, $attachment_id)) {
            update_post_meta($attachment_id, '_prorank_optimization_failed', '1');
            update_post_meta($attachment_id, '_prorank_optimization_failure_reason', 'backup_original_failed');
            return $metadata;
        }

        // Use Imagick if available
        if (extension_loaded('imagick') && class_exists('Imagick')) {
            try {
                $imagick = new \Imagick();
                $imagick->setResolution(150, 150);
                $imagick->readImage($file_path);
                $imagick->setImageCompression(\Imagick::COMPRESSION_JPEG);
                $imagick->setImageCompressionQuality(85);
                $imagick->writeImage($file_path);
                $imagick->destroy();

                // Update optimization status
                update_post_meta($attachment_id, '_prorank_pdf_optimized', true);
            } catch (\Exception $e) {
                // Log error
            }
        }

        return $metadata;
    }

    /**
     * Generate retina images
     *
     * @param array $metadata      Attachment metadata
     * @param int   $attachment_id Attachment ID
     * @return array Modified metadata
     */
    public function generate_retina_images(array $metadata, int $attachment_id): array {
        $file_path = get_attached_file($attachment_id);
        if (!$file_path || !file_exists($file_path)) {
            return $metadata;
        }

        // Check if it's an image
        if (!wp_attachment_is_image($attachment_id)) {
            return $metadata;
        }

        // Generate 2x version for each size
        if (!empty($metadata['sizes'])) {
            $upload_dir = wp_upload_dir();
            $base_dir = dirname($file_path);

            foreach ($metadata['sizes'] as $size => $size_data) {
                $size_path = $base_dir . '/' . $size_data['file'];
                if (file_exists($size_path)) {
                    $this->generate_retina_version($size_path, $size_data['width'] * 2, $size_data['height'] * 2);
                }
            }
        }

        // Generate 2x for full size
        $size = wp_getimagesize($file_path);
        if ($size && $size[0] > 0 && $size[1] > 0) {
            $this->generate_retina_version($file_path, $size[0] * 2, $size[1] * 2);
        }

        return $metadata;
    }

    /**
     * Add AI training protection
     *
     * @param int $attachment_id Attachment ID
     * @return void
     */
    public function add_ai_training_protection(int $attachment_id): void {
        // Add AI training protection meta
        update_post_meta($attachment_id, '_prorank_no_ai_training', '1');

        // Add to protected content list
        $protected_images = get_option('prorank_ai_protected_images', []);
        if (!in_array($attachment_id, $protected_images)) {
            $protected_images[] = $attachment_id;
            update_option('prorank_ai_protected_images', $protected_images);
        }
    }

    /**
     * Add noai attributes to images
     *
     * @param array $attr       Image attributes
     * @param object $attachment Attachment object
     * @param string|array $size Image size
     * @return array Modified attributes
     */
    public function add_noai_attributes(array $attr, $attachment, $size): array {
        $no_ai = get_post_meta($attachment->ID, '_prorank_no_ai_training', true);

        if ($no_ai === '1') {
            // Add data attribute for AI training prevention
            $attr['data-noai'] = '1';
            $attr['data-no-ai-training'] = 'true';

            // Add to alt text if configured
            if ($this->get_setting('add_noai_to_alt', false)) {
                $attr['alt'] = ($attr['alt'] ?? '') . ' [No AI Training]';
            }
        }

        return $attr;
    }

    /**
     * Get JPEG quality based on compression type
     *
     * @param bool $is_subsize Whether the target is a responsive sub-size candidate.
     * @return int Quality value
     */
    private function get_jpeg_quality(bool $is_subsize = false): int {
        $compression_type = $this->get_setting('compression_type', 'lossy');

        // Override with custom quality if SmartCompression is disabled
        if (!$this->get_setting('smart_compression', true)) {
            return (int) $this->get_setting('jpeg_quality', 85);
        }

        if ($is_subsize) {
            return $this->get_subsize_quality($compression_type, 'jpeg_quality', 85);
        }

        return self::COMPRESSION_TYPES[$compression_type]['jpeg_quality'] ?? 85;
    }

    /**
     * Return the quality for a responsive sub-size candidate for the given
     * compression profile and format key.
     *
     * Lossless profiles are never made lossy: the guard returns the full-quality
     * value whenever the profile is not present in SUBSIZE_COMPRESSION_TYPES.
     *
     * When smart_compression is disabled the caller is responsible for using
     * the manual slider value instead of calling this method.
     *
     * @param string $compression_type Active compression profile.
     * @param string $quality_key      One of jpeg_quality / webp_quality / avif_quality / jxl_quality.
     * @param int    $fallback         Default quality when the profile is unknown.
     * @return int
     */
    private function get_subsize_quality(string $compression_type, string $quality_key, int $fallback): int {
        // Lossless must never become lossy for any size.
        if ($compression_type === 'lossless') {
            return 100;
        }

        return self::SUBSIZE_COMPRESSION_TYPES[$compression_type][$quality_key] ?? $fallback;
    }

    /**
     * Check if PNG has transparency
     *
     * @param string $file_path File path
     * @return bool
     */
    private function png_has_transparency(string $file_path): bool {
        if (extension_loaded('imagick') && class_exists('Imagick')) {
            try {
                $imagick = new \Imagick($file_path);
                $has_alpha = $imagick->getImageAlphaChannel();
                $imagick->destroy();
                return $has_alpha === \Imagick::ALPHACHANNEL_ACTIVATE;
            } catch (\Exception $e) {
                // Fall back to GD
            }
        }

        if (function_exists('imagecreatefrompng')) {
            $image = @imagecreatefrompng($file_path);
            if ($image) {
                $width = imagesx($image);
                $height = imagesy($image);
                $total_pixels = $width * $height;

                // Use sampling for large images to avoid O(n²) performance issues
                // Sample up to 2000 random pixels - statistically sufficient to detect transparency
                $max_samples = min(2000, $total_pixels);

                if ($total_pixels <= 2000) {
                    // Small image - scan all pixels (still fast)
                    for ($x = 0; $x < $width; $x++) {
                        for ($y = 0; $y < $height; $y++) {
                            $rgba = imagecolorat($image, $x, $y);
                            $alpha = ($rgba & 0x7F000000) >> 24;
                            if ($alpha > 0) {
                                imagedestroy($image);
                                return true;
                            }
                        }
                    }
                } else {
                    // Large image - use random sampling + edge scanning
                    // First check corners and edges (common places for transparency)
                    $edge_points = [
                        [0, 0], [$width - 1, 0], [0, $height - 1], [$width - 1, $height - 1], // corners
                        [(int)($width / 2), 0], [(int)($width / 2), $height - 1], // top/bottom center
                        [0, (int)($height / 2)], [$width - 1, (int)($height / 2)], // left/right center
                    ];

                    foreach ($edge_points as $point) {
                        $rgba = imagecolorat($image, $point[0], $point[1]);
                        $alpha = ($rgba & 0x7F000000) >> 24;
                        if ($alpha > 0) {
                            imagedestroy($image);
                            return true;
                        }
                    }

                    // Then sample random pixels throughout the image
                    for ($i = 0; $i < $max_samples; $i++) {
                        $x = wp_rand(0, $width - 1);
                        $y = wp_rand(0, $height - 1);
                        $rgba = imagecolorat($image, $x, $y);
                        $alpha = ($rgba & 0x7F000000) >> 24;
                        if ($alpha > 0) {
                            imagedestroy($image);
                            return true;
                        }
                    }
                }
                imagedestroy($image);
            }
        }

        return false;
    }

    /**
     * Check if image is CMYK
     *
     * @param string $file_path File path
     * @return bool
     */
    private function is_cmyk_image(string $file_path): bool {
        if (extension_loaded('imagick') && class_exists('Imagick')) {
            try {
                $imagick = new \Imagick($file_path);
                $colorspace = $imagick->getImageColorspace();
                $imagick->destroy();
                return $colorspace === \Imagick::COLORSPACE_CMYK;
            } catch (\Exception $e) {
                // Log error
            }
        }

        return false;
    }

    /**
     * Generate retina version of image
     *
     * @param string $file_path Source file path
     * @param int $width Target width
     * @param int $height Target height
     * @return bool Success status
     */
    private function generate_retina_version(string $file_path, int $width, int $height): bool {
        $pathinfo = pathinfo($file_path);
        $retina_path = $pathinfo['dirname'] . '/' . $pathinfo['filename'] . '@2x.' . $pathinfo['extension'];

        // Skip if already exists
        if (file_exists($retina_path)) {
            return true;
        }

        // Skip if source is smaller than target
        $size = wp_getimagesize($file_path);
        if (!$size || $size[0] < $width || $size[1] < $height) {
            return false;
        }

        $editor = wp_get_image_editor($file_path);
        if (!is_wp_error($editor)) {
            $editor->resize($width, $height, true);
            $result = $editor->save($retina_path);
            return !is_wp_error($result);
        }

        return false;
    }

    /**
     * Smart crop and resize image
     *
     * @param string $file_path Source file path
     * @param int $width Target width
     * @param int $height Target height
     * @param array $options Crop options
     * @return bool Success status
     */
    public function smart_crop_resize(string $file_path, int $width, int $height, array $options = []): bool {
        $defaults = [
            'crop_focus' => 'center', // center, faces, entropy, attention
            'upscale' => false,
            'quality' => $this->get_jpeg_quality(),
        ];

        $options = wp_parse_args($options, $defaults);

        // Use cloud service if enabled and supports smart crop
        $method = $this->get_optimization_method();
        if (in_array($method, ['cloud', 'smart', 'prorank'], true)) {
            return $this->cloud_smart_crop($file_path, $width, $height, $options);
        }

        // Fallback to local crop
        $editor = wp_get_image_editor($file_path);
        if (is_wp_error($editor)) {
            return false;
        }

        // Get current dimensions
        $size = $editor->get_size();
        $orig_width = $size['width'];
        $orig_height = $size['height'];

        // Calculate crop position based on focus
        $crop_x = 0;
        $crop_y = 0;

        switch ($options['crop_focus']) {
            case 'top':
                $crop_y = 0;
                break;
            case 'bottom':
                $crop_y = $orig_height - $height;
                break;
            case 'left':
                $crop_x = 0;
                break;
            case 'right':
                $crop_x = $orig_width - $width;
                break;
            case 'entropy':
                // Simple entropy-based crop (find area with most detail)
                list($crop_x, $crop_y) = $this->find_entropy_crop($file_path, $width, $height);
                break;
            default: // center
                $crop_x = ($orig_width - $width) / 2;
                $crop_y = ($orig_height - $height) / 2;
        }

        // Crop and resize
        $result = $editor->crop($crop_x, $crop_y, $width, $height, $width, $height, false);
        if (is_wp_error($result)) {
            return false;
        }

        // Save
        $filetype = wp_check_filetype($file_path);
        $saved = $editor->save($file_path, $filetype['type'] ?: 'image/jpeg');
        return !is_wp_error($saved);
    }

    /**
     * Cloud-based smart crop
     *
     * @param string $file_path Source file path
     * @param int $width Target width
     * @param int $height Target height
     * @param array $options Crop options
     * @return bool Success status
     */
    private function cloud_smart_crop(string $file_path, int $width, int $height, array $options): bool {
        return false;
    }

    /**
     * Find optimal crop position based on entropy
     *
     * @param string $file_path Source file path
     * @param int $width Target width
     * @param int $height Target height
     * @return array [x, y] coordinates
     */
    private function find_entropy_crop(string $file_path, int $width, int $height): array {
        // Simple entropy calculation - find area with most variation
        // This is a simplified version - real implementation would be more sophisticated

        if (!extension_loaded('gd')) {
            return [0, 0];
        }

        $image = imagecreatefromstring(file_get_contents($file_path));
        if (!$image) {
            return [0, 0];
        }

        $orig_width = imagesx($image);
        $orig_height = imagesy($image);

        $best_x = 0;
        $best_y = 0;
        $max_entropy = 0;

        // Sample areas and find highest entropy
        $step = 10; // Sample every 10 pixels for performance

        for ($y = 0; $y <= $orig_height - $height; $y += $step) {
            for ($x = 0; $x <= $orig_width - $width; $x += $step) {
                $entropy = $this->calculate_area_entropy($image, $x, $y, $width, $height);
                if ($entropy > $max_entropy) {
                    $max_entropy = $entropy;
                    $best_x = $x;
                    $best_y = $y;
                }
            }
        }

        imagedestroy($image);

        return [$best_x, $best_y];
    }

    /**
     * Calculate entropy for an area of image
     *
     * @param resource $image GD image resource
     * @param int $x Start X
     * @param int $y Start Y
     * @param int $width Area width
     * @param int $height Area height
     * @return float Entropy value
     */
    private function calculate_area_entropy($image, int $x, int $y, int $width, int $height): float {
        $histogram = array_fill(0, 256, 0);
        $total_pixels = $width * $height;

        // Build histogram of grayscale values
        for ($py = $y; $py < $y + $height; $py++) {
            for ($px = $x; $px < $x + $width; $px++) {
                $rgb = imagecolorat($image, $px, $py);
                $r = ($rgb >> 16) & 0xFF;
                $g = ($rgb >> 8) & 0xFF;
                $b = $rgb & 0xFF;

                // Convert to grayscale
                $gray = (int)(0.299 * $r + 0.587 * $g + 0.114 * $b);
                $histogram[$gray]++;
            }
        }

        // Calculate entropy
        $entropy = 0;
        foreach ($histogram as $count) {
            if ($count > 0) {
                $probability = $count / $total_pixels;
                $entropy -= $probability * log($probability, 2);
            }
        }

        return $entropy;
    }

    /**
     * Rewrite image URLs to use CDN
     *
     * @param string $url Original URL
     * @param int $attachment_id Attachment ID
     * @return string Modified URL
     */
    public function rewrite_cdn_url(string $url, int $attachment_id): string {
        $cdn_url = $this->get_setting('cdn_url', '');

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

        // Only rewrite for frontend
        if (!$this->is_frontend_optimization_context()) {
            return $url;
        }

        if ($this->is_current_request_excluded() || $this->matches_exclude_path($url)) {
            return $url;
        }

        // Parse URLs
        $site_url = get_site_url();
        $cdn_url = rtrim($cdn_url, '/');

        // Replace site URL with CDN URL
        if (strpos($url, $site_url) === 0) {
            $url = str_replace($site_url, $cdn_url, $url);
        }

        return $url;
    }

    /**
     * Rewrite srcset URLs to use CDN
     *
     * @param array $sources Sources array
     * @param array $size_array Size array
     * @param string $image_src Image source
     * @param array $image_meta Image metadata
     * @param int $attachment_id Attachment ID
     * @return array Modified sources
     */
    public function rewrite_srcset_cdn(array $sources, array $size_array, string $image_src, array $image_meta, int $attachment_id): array {
        $cdn_url = $this->get_setting('cdn_url', '');

        if (empty($cdn_url) || !$this->is_frontend_optimization_context()) {
            return $sources;
        }

        if ($this->is_current_request_excluded()) {
            return $sources;
        }

        $site_url = get_site_url();
        $cdn_url = rtrim($cdn_url, '/');

        foreach ($sources as &$source) {
            if (
                isset($source['url'])
                && strpos($source['url'], $site_url) === 0
                && !$this->matches_exclude_path((string) $source['url'])
            ) {
                $source['url'] = str_replace($site_url, $cdn_url, $source['url']);
            }
        }

        return $sources;
    }

    /**
     * Rewrite image URLs in content to use CDN
     *
     * @param string $content Post content
     * @return string Modified content
     */
    public function rewrite_content_cdn_urls(string $content): string {
        $cdn_url = $this->get_setting('cdn_url', '');

        if (empty($cdn_url) || !$this->is_frontend_optimization_context()) {
            return $content;
        }

        if ($this->is_current_request_excluded()) {
            return $content;
        }

        $site_url = get_site_url();
        $cdn_url = rtrim($cdn_url, '/');
        $upload_dir = wp_upload_dir();

        // Replace upload URLs with CDN
        $content = str_replace($upload_dir['baseurl'], str_replace($site_url, $cdn_url, $upload_dir['baseurl']), $content);

        return $content;
    }

    /**
     * Normalize opt-in timestamp value.
     *
     * @param mixed $value Opt-in timestamp value.
     * @return int|null
     */
    private function normalize_opt_in_timestamp($value): ?int {
        if (is_numeric($value)) {
            return (int) $value;
        }

        if (is_string($value)) {
            $timestamp = strtotime($value);
            if ($timestamp !== false) {
                return $timestamp;
            }
        }

        return null;
    }

    /**
     * Resolve the current quota period for cloud credits.
     *
     * @param array $license_status License status data.
     * @param bool  $opt_in_enabled Whether free opt-in credits are enabled.
     * @param int|null $opt_in_at   Opt-in timestamp (unix).
     * @return array
     */
    private function resolve_quota_period(array $license_status, bool $opt_in_enabled, ?int $opt_in_at): array {
        $now = time();
        $start = null;
        $end = null;
        $source = 'calendar';

        if ($opt_in_enabled) {
            $start = $opt_in_at ?: $now;
            $end = $start + YEAR_IN_SECONDS;
            $source = 'opt_in';
        }

        if (!$start || !$end) {
            $expires_raw = $license_status['expires'] ?? null;
            $expires = $expires_raw ? strtotime($expires_raw) : false;
            // Only trust license expiry as quota window if it is near-term.
            // Lifetime or very long expiries should use calendar-year quota windows.
            $max_window = YEAR_IN_SECONDS + (45 * DAY_IN_SECONDS);
            if ($expires && $expires > $now && ($expires - $now) <= $max_window) {
                $end = $expires;
                $start = $expires - YEAR_IN_SECONDS;
                $source = 'license';
            }
        }

        if (!$start || !$end) {
            $year = (int) gmdate('Y');
            $start = strtotime($year . '-01-01 00:00:00');
            $end = strtotime(($year + 1) . '-01-01 00:00:00');
            $source = 'calendar';
        }

        if ($end <= $now) {
            $start = $now;
            $end = $now + YEAR_IN_SECONDS;
            $source = 'rolling';
        }

        if ($start > $now) {
            $start = $now - YEAR_IN_SECONDS;
        }

        return [
            'start' => $start,
            'end' => $end,
            'source' => $source,
        ];
    }

    /**
     * Build a cache key for quota usage.
     *
     * @param int $start Period start timestamp.
     * @param int $end   Period end timestamp.
     * @return string
     */
    private function get_usage_cache_key(int $start, int $end): string {
        return 'prorank_image_usage_' . $start . '_' . $end;
    }

    /**
     * Build a cache key for the last normalized API quota payload.
     *
     * @return string
     */
    private function get_api_quota_status_cache_key(): string {
        return 'prorank_image_api_quota_status';
    }

    /**
     * Get image optimization quota status
     *
     * @return array Quota information
     */
    public function get_quota_status(): array {
        return [
            'status' => 'no_license',
            'message' => __('Cloud image optimization is not available in the free plugin.', 'prorank-seo'),
            'remaining' => 0,
            'used' => 0,
            'quota' => [
                'yearly' => 0,
            ],
            'quota_formatted' => [
                'yearly' => '0',
            ],
            'used_formatted' => '0',
            'remaining_formatted' => '0',
        ];

        // Check cached quota exceeded status
        $quota_exceeded = get_transient('prorank_image_quota_exceeded');
        if ($quota_exceeded) {
            return [
                'status' => 'exhausted',
                'message' => $quota_exceeded['message'],
                'remaining' => 0,
                'next_reset' => $quota_exceeded['next_reset'] ?? '',
            ];
        }

        $plugin = \ProRank\SEO\Plugin::get_instance();
        if (!$plugin || !$plugin->is_initialized()) {
            return [
                'status' => 'no_license',
                'message' => 'Unable to determine license status',
                'remaining' => 0,
            ];
        }

        $license_manager = $plugin->license();
        $license_status = $license_manager->get_license_status_snapshot();
        $tier = $license_status['tier'] ?? $license_manager->get_tier();
        $auth_token = $this->get_cloud_auth_token();
        $has_valid_license = $license_manager->has_valid_license_status();

        // Normalize tier names to support both base and *_plus variants
        $normalized_tier = $tier;
        if (strpos($normalized_tier, '_plus') !== false) {
            $normalized_tier = str_replace('_plus', '', $normalized_tier);
        }

        $cloud_opt_in = (bool) $this->get_setting('cloud_opt_in', false);
        $cloud_opt_in_at = $this->normalize_opt_in_timestamp($this->get_setting('cloud_opt_in_at', null));

        if (!$has_valid_license) {
            $tier = 'free';
            $normalized_tier = 'free';
        }

        $tier = $normalized_tier;

        // Base quotas per tier (images per year)
        $tier_quotas = [
            'free' => 0,
            'basic' => 0,
            'pro' => 5000,
            'business' => 20000,
            'agency' => 100000,
        ];

        $quota_total = $tier_quotas[$normalized_tier] ?? 0;
        $is_free_tier = ($normalized_tier === 'free');

        // Free opt-in credits (requires license key)
        if ($is_free_tier && $cloud_opt_in) {
            $quota_total = 50;
        }

        // Prefer server-provided limits but cap them to the tier matrix
        $license_limit = $license_status['limits']['image_optimization_credits'] ?? null;
        if ($license_limit !== null && $quota_total > 0) {
            $quota_total = min((int) $license_limit, (int) $quota_total);
        }

        if ($quota_total <= 0) {
            $message = $cloud_opt_in
                ? 'Connect your license to use free cloud credits'
                : 'Cloud optimization is available with Pro or free opt-in credits';

            return [
                'status' => 'no_license',
                'message' => $message,
                'remaining' => 0,
                'tier' => $tier,
                'upgrade_url' => 'https://prorank.io/pricing',
            ];
        }

        if (empty($auth_token)) {
            return [
                'status' => 'no_license',
                'message' => 'Cloud optimization requires a valid authentication token',
                'remaining' => 0,
                'tier' => $tier,
                'upgrade_url' => 'https://prorank.io/pricing',
            ];
        }

        $period = $this->resolve_quota_period($license_status, $is_free_tier && $cloud_opt_in, $cloud_opt_in_at);
        $usage_cache_key = $this->get_usage_cache_key($period['start'], $period['end']);
        $cached_usage = get_transient($usage_cache_key);
        $api_data = null;

        if ($cached_usage === false) {
            $api_data = $this->check_api_quota($period);
            if (is_array($api_data)) {
                if (isset($api_data['used'])) {
                    $cached_usage = (int) $api_data['used'];
                } elseif (isset($api_data['remaining'])) {
                    $cached_usage = max(0, $quota_total - max(0, (int) $api_data['remaining']));
                }

                if ($cached_usage !== false) {
                    set_transient($usage_cache_key, (int) $cached_usage, HOUR_IN_SECONDS);
                }
            }
        }

        $used = (int) ($cached_usage ?? 0);
        $remaining = max(0, $quota_total - $used);
        if (is_array($api_data)) {
            if (isset($api_data['remaining'])) {
                $remaining = max(0, (int) $api_data['remaining']);
            } elseif (isset($api_data['credits'])) {
                $remaining = max(0, (int) $api_data['credits']);
            }
        }

        $quota_yearly = $quota_total;
        $quota_monthly = (int) max(1, round($quota_total / 12));
        if (is_array($api_data)) {
            if (!empty($api_data['yearly_quota'])) {
                $quota_yearly = max(0, (int) $api_data['yearly_quota']);
            }

            if (!empty($api_data['monthly_quota'])) {
                $quota_monthly = max(0, (int) $api_data['monthly_quota']);
            }
        }

        $percentage_base = $quota_monthly > 0 ? $quota_monthly : $quota_yearly;
        $percentage = $percentage_base > 0 ? min(100, ($used / $percentage_base) * 100) : 0;

        $quota = [
            'yearly' => $quota_yearly,
            'period_start' => $period['start'],
            'period_end' => $period['end'],
            'period_source' => $period['source'],
        ];

        if ($quota_monthly > 0) {
            $quota['monthly'] = $quota_monthly;
        }

        $quota_formatted = [
            'yearly' => self::format_number($quota_yearly),
            'monthly' => self::format_number($quota_monthly),
        ];

        $next_reset = gmdate('c', $period['end']);
        $next_reset_display = $next_reset;
        $next_reset_timestamp = strtotime($next_reset);
        if ($next_reset_timestamp) {
            $next_reset_display = wp_date(
                get_option('date_format') . ' ' . get_option('time_format'),
                $next_reset_timestamp
            );
        }

        $message = __('Cloud optimization is available.', 'prorank-seo');
        if ($remaining <= 0) {
            if ($quota_monthly > 0) {
                $message = sprintf(
                    __('Cloud quota is exhausted for the current billing period. %1$s of %2$s images have been used. Next reset: %3$s.', 'prorank-seo'),
                    self::format_number($used),
                    self::format_number($quota_monthly),
                    $next_reset_display
                );
            } else {
                $message = sprintf(
                    __('Cloud quota is exhausted for the current billing period. Next reset: %s.', 'prorank-seo'),
                    $next_reset_display
                );
            }
        }

        $status = [
            'status' => $remaining > 0 ? 'active' : 'exhausted',
            'message' => $message,
            'tier' => $tier,
            'quota' => $quota,
            'quota_formatted' => $quota_formatted,
            'used' => $used,
            'used_formatted' => self::format_number($used),
            'remaining' => $remaining,
            'remaining_formatted' => self::format_number($remaining),
            'percentage' => $percentage,
            'next_reset' => $next_reset,
            'opt_in' => $cloud_opt_in,
        ];

        if (is_array($api_data) && isset($api_data['credits'])) {
            $status['credits'] = (int) $api_data['credits'];
            $status['credits_formatted'] = self::format_number((int) $api_data['credits']);
        }

        return $status;
    }

    /**
     * Check API quota status
     *
     * @return array|false API response or false on error
     */
    public function check_api_quota(array $period = null) {
        return false;
    }

    /**
     * Invalidate cached quota usage for the active billing period.
     *
     * @return void
     */
    private function invalidate_quota_usage_cache(): void {
        return;
    }

    /**
     * Format quota display
     *
     * @param int $number Number to format
     * @return string Formatted number
     */
    public static function format_number($number): string {
        if ($number >= 1000000) {
            return number_format($number / 1000000, 1) . 'M';
        } elseif ($number >= 1000) {
            return number_format($number / 1000, 1) . 'k';
        }
        return number_format($number);
    }

    /**
     * Backfill any WordPress-registered subsizes that didn't end up in the
     * attachment's metadata.
     *
     * Useful for restored media libraries, theme switches that register new
     * sizes, or images uploaded before a size was registered. Skips entries
     * whose file already exists on disk.
     *
     * @param array $metadata
     * @param int   $attachment_id
     * @return array
     */
    public function fill_missing_subsizes($metadata, $attachment_id): array {
        if (!is_array($metadata) || empty($metadata['file'])) {
            return is_array($metadata) ? $metadata : [];
        }

        if (!function_exists('wp_get_registered_image_subsizes')
            || !function_exists('image_make_intermediate_size')
            || !function_exists('get_attached_file')) {
            return $metadata;
        }

        $source = get_attached_file((int) $attachment_id);
        if (!$source || !is_string($source) || !file_exists($source)) {
            return $metadata;
        }

        $existing = isset($metadata['sizes']) && is_array($metadata['sizes']) ? $metadata['sizes'] : [];
        $registered = wp_get_registered_image_subsizes();
        if (!is_array($registered)) {
            return $metadata;
        }

        $uploads = function_exists('wp_get_upload_dir') ? wp_get_upload_dir() : ['basedir' => ''];
        $basedir = isset($uploads['basedir']) ? trailingslashit((string) $uploads['basedir']) : '';
        $source_dir = trailingslashit(dirname($source));

        foreach ($registered as $size_name => $size_args) {
            if (!is_string($size_name) || $size_name === '' || isset($existing[$size_name])) {
                continue;
            }

            $width  = isset($size_args['width']) ? (int) $size_args['width'] : 0;
            $height = isset($size_args['height']) ? (int) $size_args['height'] : 0;
            $crop   = isset($size_args['crop']) ? (bool) $size_args['crop'] : false;

            if ($width <= 0 && $height <= 0) {
                continue;
            }

            $generated = image_make_intermediate_size($source, $width, $height, $crop);
            if (!is_array($generated) || empty($generated['file'])) {
                continue;
            }

            // Store relative to attachment dir, matching WP convention.
            $existing[$size_name] = $generated;
        }

        $metadata['sizes'] = $existing;
        return $metadata;
    }

    /**
     * Override WordPress's default big_image_size_threshold (2560px) so users
     * can set a smaller bound on stored originals.
     *
     * @param int    $threshold
     * @param array  $imagesize
     * @param string $file
     * @param int    $attachment_id
     * @return int
     */
    public function filter_big_image_threshold($threshold, $imagesize = [], $file = '', $attachment_id = 0): int {
        $configured = (int) $this->get_setting('oversized_threshold', 0);
        if ($configured > 0) {
            // Reject pathological values that would over-shrink images.
            return max(640, $configured);
        }

        return is_numeric($threshold) ? (int) $threshold : 2560;
    }
}
