<?php

/**
 * CSS Optimization Module
 *
 * Implements advanced CSS optimization features using specialized services
 *
 * @package ProRank\SEO\Modules\Performance
 * @since   1.0.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Modules\Performance;

use ProRank\SEO\Modules\BaseModule;
use ProRank\SEO\Core\Buffer;
use ProRank\SEO\Core\Optimization\CSS\CssMinifierService;
use ProRank\SEO\Core\Optimization\CSS\CssPathRewriterService;
use ProRank\SEO\Core\Optimization\CSS\CssImportProcessorService;
use ProRank\SEO\Core\Optimization\CSS\ExternalAssetCacheService;
use ProRank\SEO\Core\Tasks\CssCacheGenerationTask;
use ProRank\SEO\Core\Tasks\ExternalCssDownloadTask;
use ProRank\SEO\Core\Tasks\CssCacheCleanupTask;
use ProRank\SEO\Core\Optimization\CSS\CssCacheServer;
use ProRank\SEO\Core\Compatibility\PerformanceCompatibilityRegistry;
use Exception;

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


/**
 * CssOptimize module class
 */


class CssOptimizeModule extends BaseModule {

    /**
     * Required tier for this module
     */
    protected string $feature_tier = 'free';
    
    /**
     * CSS Minifier Service
     *
     * @var CssMinifierService|null
     */
    private ?CssMinifierService $minifier = null;
    
    /**
     * CSS Path Rewriter Service
     *
     * @var CssPathRewriterService|null
     */
    private ?CssPathRewriterService $path_rewriter = null;
    
    /**
     * CSS Import Processor Service
     *
     * @var CssImportProcessorService|null
     */
    private ?CssImportProcessorService $import_processor = null;
    
    /**
     * External Asset Cache Service
     *
     * @var ExternalAssetCacheService|null
     */
    private ?ExternalAssetCacheService $external_cache = null;
    
    /**
     * Excluded stylesheets cache
     *
     * @var array
     */
    private array $excluded_styles = [];
    
    /**
     * Beta features enabled flag
     *
     * @var bool|null
     */
    private ?bool $beta_enabled = null;
    
    /**
     * Async CSS loader added flag
     *
     * @var bool
     */
    private bool $async_loader_added = false;

    /**
     * Combined CSS delay loader output guard
     *
     * @var bool
     */
    private bool $combined_delay_loader_added = false;

    /**
     * Combined styles output guard
     *
     * @var bool
     */
    private bool $combined_output_done = false;

    /**
     * Number of async styles already promoted to preload for this request.
     *
     * @var int
     */
    private int $async_preload_promoted = 0;

    /**
     * Guard to ensure only one image per response gets LCP priority promotion.
     *
     * @var bool
     */
    private bool $lcp_image_promoted = false;

    /**
     * Guard to avoid nested duplicate output buffers for LCP enforcement.
     *
     * @var bool
     */
    private bool $lcp_output_buffer_started = false;

    /**
     * Cached result of has_critical_css_content()
     *
     * @var bool|null
     */
    private ?bool $critical_css_check = null;

    /**
     * Cached CLS guardrail decision for this request.
     *
     * @var bool|null
     */
    private ?bool $cls_guardrail_triggered = null;
    
    /**
     * Processed styles cache
     *
     * @var array
     */
    private array $processed_styles = [];

    /**
     * Inline CSS captured from handles being combined.
     *
     * When CSS combine is enabled, WordPress still outputs inline styles
     * (wp_add_inline_style) as separate <style> tags even when the parent
     * <link> is suppressed. We capture and clear them so they can be
     * included in the combined CSS file instead.
     *
     * @var array<string, string>
     */
    private array $collected_inline_css = [];

    /**
     * Request-local cache of optimized inline CSS blocks, keyed by content hash.
     *
     * @var array<string, string>
     */
    private array $optimized_inline_css = [];
    
    /**
     * CSS cache generation task
     *
     * @var CssCacheGenerationTask|null
     */
    private ?CssCacheGenerationTask $cache_generation_task = null;
    
    /**
     * External CSS download task
     *
     * @var ExternalCssDownloadTask|null
     */
    private ?ExternalCssDownloadTask $external_download_task = null;
    
    /**
     * CSS cache cleanup task
     *
     * @var CssCacheCleanupTask|null
     */
    private ?CssCacheCleanupTask $cleanup_task = null;
    
    /**
     * CSS cache server
     *
     * @var CssCacheServer|null
     */
    private ?CssCacheServer $cache_server = null;
    
    /**
     * Constructor
     */
    public function __construct() {
        $this->slug = 'css_opt';
        $this->name = 'CSS Optimization';
        $this->description = 'Advanced CSS optimization with minification, combining, and caching';
$this->parent_slug = 'performance';
    }

    /**
     * Get setting from unified cache settings
     * Overrides BaseModule to read from prorank_cache_settings
     *
     * @param string $key Setting key
     * @param mixed $default Default value
     * @return mixed
     */
    protected function get_setting(string $key, $default = null) {
        static $settings = null;
        if ($settings === null) {
            $settings = get_option('prorank_cache_settings', []);
        }
        return $settings[$key] ?? $default;
    }

    /**
     * Get module default settings
     *
     * @return array
     */
    public function get_default_settings(): array {
        return [
            'css_minify' => false,
            'css_combine' => false,
            'css_inline_imports' => true,
            'css_external_cache' => true,
            'css_font_display_swap' => true,
            'css_gzip' => true,
            'css_async' => false,
            'css_block_assets_on_demand' => true,
            'css_exclude' => '',
            'css_cache_lifetime' => 30, // days
        ];
    }
    
    /**
     * Initialize module hooks
     *
     * @return void
     */
    public function init_hooks(): void {
        // Security validation checkpoint - performance optimization
        if (class_exists('\ProRank\SEO\Core\SecurityValidator')) {
            \ProRank\SEO\Core\SecurityValidator::validate('cp_perf', ['feature' => 'css_optimize']);
        }

        // Cache management hooks must be registered even in wp-admin.
        // Otherwise settings changes + cache purges won’t clear CSS artifacts when page cache is enabled.
        add_action('prorank_clear_cache', [$this, 'clear_css_cache']);
        add_action('switch_theme', [$this, 'clear_css_cache']);
        add_action('wp_update_nav_menu', [$this, 'clear_css_cache']);
        add_action('et_save_post', [$this, 'handle_divi_template_change'], 10, 1);
        add_action('update_option_prorank_cache_settings', [$this, 'handle_settings_updated'], 10, 3);
        add_action('elementor/core/files/clear_cache', [$this, 'handle_elementor_css_change']);
        add_action('elementor/maintenance_mode/mode_changed', [$this, 'handle_elementor_css_change']);
        add_action('update_option__elementor_global_css', [$this, 'handle_elementor_css_change'], 10, 3);
        add_action('delete_option__elementor_global_css', [$this, 'handle_elementor_css_change'], 10, 1);
        add_action('update_option_elementor_css_print_method', [$this, 'handle_elementor_css_change'], 10, 3);

        // Register Action Scheduler callbacks in all execution contexts.
        // Async queue runners execute in wp-admin/AJAX context, where should_run()
        // returns false; without this registration queued jobs fail as "no callbacks".
        $this->init_background_tasks();
        
        // Check if module should run
        if (!$this->should_run()) {
            return;
        }

        // Reduce globally enqueued block CSS/JS payload by asking WordPress
        // to load block assets only when blocks are actually rendered.
        $this->register_block_asset_filters();
        
        // Initialize services
        $this->init_services();

        // Large inline <style> blocks are common across builders and plugins.
        // Route them through the same CSS minifier service as linked stylesheets
        // so css_opt can reduce HTML/CSS weight globally, not just for enqueued files.
        if ($this->should_optimize_inline_style_tags()) {
            Buffer::init();
            add_filter('prorank_output_buffer', [$this, 'optimize_inline_style_tags_in_html'], 18);
        }
        
        // Hook into style loading
        add_filter('style_loader_tag', [$this, 'optimize_style_tag'], 10, 4);

        // Strip admin/onboarding frontend assets for anonymous visitors.
        add_action('wp_enqueue_scripts', [$this, 'strip_guest_admin_assets'], 9999);

        // Fallback LCP image prioritization for free mode:
        // ensure one meaningful above-fold image is eager + high priority
        // even when dedicated image/LCP modules are disabled.
        add_filter('the_content', [$this, 'promote_content_lcp_image'], 999);
        add_filter('elementor/frontend/the_content', [$this, 'promote_content_lcp_image'], 999);
        add_filter('elementor/widget/render_content', [$this, 'promote_elementor_widget_lcp_image'], 999, 2);
        add_filter('elementor/image_size/get_attachment_image_html', [$this, 'promote_elementor_image_html_lcp'], 999, 4);
        add_action('template_redirect', [$this, 'start_lcp_priority_output_buffer'], 9998);
        add_filter('template_include', [$this, 'ensure_lcp_priority_output_buffer'], 9999);
        
        // Hook for processing external styles
        add_filter('style_loader_src', [$this, 'process_external_style'], 10, 2);
        
        // Always register the async CSS loader hook — the method itself checks
        // is_async_enabled() at runtime when WP conditionals (is_front_page etc.)
        // are available. Checking here (during init) is too early and poisons the cache.
        add_action('wp_head', [$this, 'add_async_css_loader'], 1);
        
        // Hook for combining styles after individual tags are filtered
        if ($this->get_setting('css_combine', false)) {
            add_action('wp_head', [$this, 'output_combined_styles'], 9999);
            add_action('wp_footer', [$this, 'add_combined_css_delay_loader'], 1);
        }
        
        // Initialize cache server
        $this->init_cache_server();
    }

    /**
     * Handle settings update to clear CSS cache
     * 
     * @param mixed $old_value
     * @param mixed $value
     * @param string $option
     */
    public function handle_settings_updated($old_value, $value, string $option): void {
        $this->clear_css_cache();
    }

    /**
     * Clear CSS cache when Elementor regenerates or changes CSS delivery settings.
     *
     * @param mixed ...$args Ignored action arguments.
     * @return void
     */
    public function handle_elementor_css_change(...$args): void {
        $this->clear_css_cache();
    }

    /**
     * Clear CSS cache when Divi template/layout saves change frontend output.
     *
     * @param mixed ...$args Ignored action arguments.
     * @return void
     */
    public function handle_divi_template_change(...$args): void {
        $this->clear_css_cache();
    }
    
    /**
     * Initialize services
     *
     * @return void
     */
    private function init_services(): void {
        try {
            if (class_exists(CssMinifierService::class)) {
                $this->minifier = new CssMinifierService([
                    'gzip' => $this->get_setting('css_gzip', true),
                    'preserve_comments' => false,
                ]);
            }
            
            if (class_exists(CssPathRewriterService::class)) {
                $this->path_rewriter = new CssPathRewriterService();
            }
            
            if (class_exists(CssImportProcessorService::class) && $this->path_rewriter) {
                $this->import_processor = new CssImportProcessorService($this->path_rewriter);
            }
            
            if (class_exists(ExternalAssetCacheService::class)) {
                $cache_lifetime = $this->get_setting('css_cache_lifetime', 30) * DAY_IN_SECONDS;
                $this->external_cache = new ExternalAssetCacheService($cache_lifetime);
            }
        } catch (Exception $e) {
            prorank_log('ProRank CSS Optimization: Failed to initialize services - ' . $e->getMessage());
        }
    }

    /**
     * Determine whether request-wide inline style optimization should run.
     *
     * @return bool
     */
    private function should_optimize_inline_style_tags(): bool {
        return $this->minifier instanceof CssMinifierService
            && $this->is_truthy_setting($this->get_setting('inline_css_minify_enabled', true));
    }

    /**
     * Minify eligible inline <style> blocks in the final HTML.
     *
     * This targets builder/plugin-generated CSS blobs that never pass through
     * style_loader_tag, which means traditional CSS optimization misses them.
     *
     * @param string $html Full page HTML
     * @return string
     */
    public function optimize_inline_style_tags_in_html(string $html): string {
        if ($html === '' || stripos($html, '<style') === false || !$this->should_optimize_inline_style_tags()) {
            return $html;
        }

        $html = $this->relocate_body_style_tags_to_head($html);

        $min_bytes = (int) apply_filters('prorank_inline_css_minify_min_bytes', 512);
        if ($min_bytes < 128) {
            $min_bytes = 128;
        }

        $result = preg_replace_callback(
            '/<style\b([^>]*)>(.*?)<\/style>/is',
            function (array $matches) use ($min_bytes): string {
                $attrs = (string) ($matches[1] ?? '');
                $css   = (string) ($matches[2] ?? '');

                if (!$this->should_optimize_inline_style_tag($attrs, $css, $min_bytes)) {
                    return $matches[0];
                }

                $hash = md5($css);
                if (!isset($this->optimized_inline_css[$hash])) {
                    try {
                        $minified = $this->minifier ? $this->minifier->minify($css) : $css;
                    } catch (\Throwable $e) {
                        return $matches[0];
                    }

                    if (!is_string($minified) || trim($minified) === '' || strlen($minified) >= strlen($css)) {
                        $this->optimized_inline_css[$hash] = $css;
                    } else {
                        $this->optimized_inline_css[$hash] = str_ireplace('</style', '<\\/style', $minified);
                    }
                }

                $optimized = $this->optimized_inline_css[$hash];
                if ($optimized === $css) {
                    return $matches[0];
                }

                return '<style' . $attrs . '>' . $optimized . '</style>';
            },
            $html
        );

        return $result !== null ? $result : $html;
    }

    /**
     * Move eligible body-injected <style> tags into <head>.
     *
     * Many builders and plugins print large CSS blocks in the body, which delays
     * style application and hurts first paint. Relocating them to <head> is a
     * generic normalization that helps across themes/plugins.
     *
     * @param string $html
     * @return string
     */
    private function relocate_body_style_tags_to_head(string $html): string {
        if (!$this->is_truthy_setting($this->get_setting('inline_css_relocate_enabled', true))) {
            return $html;
        }

        $head_close = stripos($html, '</head>');
        if ($head_close === false) {
            return $html;
        }

        $body_search = substr($html, $head_close);
        if (!is_string($body_search) || preg_match('/<body\b[^>]*>/i', $body_search, $body_match, PREG_OFFSET_CAPTURE) !== 1) {
            return $html;
        }

        $body_offset = $head_close + $body_match[0][1];
        if (!is_int($body_offset) || $body_offset <= 0) {
            return $html;
        }

        $before_body = substr($html, 0, $body_offset);
        $body_and_after = substr($html, $body_offset);
        $relocated = [];

        $updated_body = preg_replace_callback(
            '/<style\b([^>]*)>(.*?)<\/style>/is',
            function (array $matches) use (&$relocated): string {
                $attrs = (string) ($matches[1] ?? '');
                $css   = (string) ($matches[2] ?? '');

                if (!$this->should_relocate_body_style_tag($attrs, $css)) {
                    return $matches[0];
                }

                $relocated[] = $matches[0];
                return '';
            },
            $body_and_after
        );

        if ($updated_body === null || empty($relocated)) {
            return $html;
        }

        $insertion = "\n" . implode("\n", $relocated) . "\n";

        return preg_replace('/<\/head>/i', $insertion . '</head>', $before_body, 1) . $updated_body;
    }

    /**
     * Check whether an inline style block is safe and worthwhile to optimize.
     *
     * @param string $attrs Style tag attributes
     * @param string $css   Style content
     * @param int    $min_bytes Minimum size threshold
     * @return bool
     */
    private function should_optimize_inline_style_tag(string $attrs, string $css, int $min_bytes): bool {
        if (trim($css) === '' || strlen($css) < $min_bytes) {
            return false;
        }

        if (stripos($attrs, 'data-no-optimize') !== false || stripos($attrs, 'data-no-minify') !== false) {
            return false;
        }

        if (
            preg_match('/\btype\s*=\s*([\'"])(.*?)\1/i', $attrs, $type_match) === 1
            && !in_array(strtolower(trim((string) $type_match[2])), ['', 'text/css'], true)
        ) {
            return false;
        }

        if (stripos($css, '<![CDATA[') !== false || stripos($css, '/*<![CDATA[*/') !== false) {
            return false;
        }

        return true;
    }

    /**
     * Decide whether a body-injected style tag should be relocated into <head>.
     *
     * @param string $attrs
     * @param string $css
     * @return bool
     */
    private function should_relocate_body_style_tag(string $attrs, string $css): bool {
        if (trim($css) === '') {
            return false;
        }

        if (stripos($attrs, 'data-no-optimize') !== false || stripos($attrs, 'data-prorank-no-relocate') !== false) {
            return false;
        }

        if (
            preg_match('/\btype\s*=\s*([\'"])(.*?)\1/i', $attrs, $type_match) === 1
            && !in_array(strtolower(trim((string) $type_match[2])), ['', 'text/css'], true)
        ) {
            return false;
        }

        return true;
    }

    /**
     * Normalize mixed setting values into booleans.
     *
     * @param mixed $value
     * @return bool
     */
    private function is_truthy_setting($value): bool {
        if (is_string($value)) {
            return in_array(strtolower($value), ['1', 'true', 'yes', 'on'], true);
        }

        return (bool) $value;
    }
    
    /**
     * Check if module should run
     *
     * @return bool
     */
    private function should_run(): bool {
        // Cache management hooks should run in admin too
        if (current_action() === 'prorank_clear_cache' ||
            current_action() === 'switch_theme' ||
            current_action() === 'update_option_prorank_cache_settings') {
            return true;
        }

        if (!$this->is_enabled()) {
            return false;
        }
        
        // Don't run in admin
        if (is_admin()) {
            return false;
        }
        
        // Don't run for logged-in users if disabled
        if (is_user_logged_in() && !$this->get_setting('process_logged_in', false)) {
            return false;
        }
        
        // Check if any optimization is enabled
        return (bool) $this->get_setting('css_minify', false) ||
               $this->get_setting('css_combine', false) ||
               $this->get_setting('css_async', false) ||
               $this->get_setting('css_external_cache', false);
    }

    /**
     * Register block asset loading filters.
     *
     * @return void
     */
    private function register_block_asset_filters(): void {
        if (!$this->is_block_assets_on_demand_enabled()) {
            return;
        }

        add_filter('should_load_block_assets_on_demand', [$this, 'force_block_assets_on_demand'], 999);
        add_filter('should_load_separate_core_block_assets', [$this, 'force_block_assets_on_demand'], 999);
    }

    /**
     * Force block assets to load on demand.
     *
     * @param mixed $enabled Existing state.
     * @return bool
     */
    public function force_block_assets_on_demand($enabled): bool {
        return true;
    }

    /**
     * Check if on-demand block assets should be enabled.
     *
     * @return bool
     */
    private function is_block_assets_on_demand_enabled(): bool {
        $enabled = (bool) $this->get_setting('css_block_assets_on_demand', true);
        return (bool) apply_filters('prorank_css_block_assets_on_demand_enabled', $enabled);
    }
    
    /**
     * Process external style URL
     *
     * @param string $src Style source URL
     * @param string $handle Style handle
     * @return string Possibly modified URL
     */
    public function process_external_style(string $src, string $handle): string {
        // Skip if not external or caching disabled
        if (!$this->get_setting('css_external_cache', true) || !$this->external_cache) {
            return $src;
        }
        
        // Check if external
        $host = wp_parse_url($src, PHP_URL_HOST);
        $site_host = wp_parse_url(site_url(), PHP_URL_HOST);
        
        if ($host === $site_host || empty($host)) {
            return $src;
        }

        // Never cache Google Fonts CSS when our font optimization/subsetting features are enabled.
        // Caching fonts.googleapis.com locally still references fonts.gstatic.com, which conflicts with local font hosting.
        if (is_string($host) && preg_match('/(^|\\.)fonts\\.googleapis\\.com$/i', $host)) {
            static $asset_settings = null;
            if ($asset_settings === null) {
                $asset_settings = get_option('prorank_asset_optimization_settings', []);
                if (!is_array($asset_settings)) {
                    $asset_settings = [];
                }
            }

            $font_subsetting_enabled = !empty($asset_settings['font_subsetting_enabled']);
            $host_google_fonts_locally = !empty($asset_settings['host_google_fonts_locally']);

            if ($font_subsetting_enabled || $host_google_fonts_locally) {
                if (!empty($asset_settings['font_display_swap']) && function_exists('remove_query_arg') && function_exists('add_query_arg')) {
                    $src = remove_query_arg('display', $src);
                    $src = add_query_arg('display', 'swap', $src);
                }
                return $src;
            }
        }
        
        // Try to cache external asset
        try {
            $cached = $this->external_cache->get($src);
            
            if ($cached && !empty($cached['url'])) {
                return $cached['url'];
            }
        } catch (Exception $e) {
            prorank_log('ProRank CSS: Failed to cache external asset - ' . $e->getMessage());
        }
        
        return $src;
    }
    
    /**
     * Optimize style tag
     *
     * @param string $tag    Style tag HTML
     * @param string $handle Style handle
     * @param string $href   Style source URL
     * @param string $media  Media attribute
     * @return string Modified style tag
     */
    public function optimize_style_tag(string $tag, string $handle, string $href, string $media): string {
        // Skip if stylesheet is excluded
        if ($this->is_style_excluded($handle, $href)) {
            return $tag;
        }
        
        // Skip inline styles
        if (empty($href)) {
            return $tag;
        }
        
        // Skip print stylesheets
        if ($media === 'print') {
            return $tag;
        }
        
        // Process minification if enabled
        if ($this->get_setting('css_minify', false) && $this->minifier) {
            $tag = $this->process_minification($tag, $handle, $href, $media);
        }
        
        // Resolve the actual href from the generated tag — it may differ from
        // $href after minification/rewrites.
        $actual_href = $href;
        if (preg_match("/(?:^|\\s)href=['\"]([^'\"]+)['\"]/i", $tag, $m)) {
            $actual_href = (string) $m[1];
        }

        // Inline tiny font CSS (e.g. @font-face declarations) to remove the
        // network request entirely while keeping render-stable font discovery.
        if ($this->is_font_stylesheet($handle, $href)) {
            $inlined = $this->try_inline_small_css($handle, $actual_href, $media);
            if ($inlined !== null) {
                return $inlined;
            }
        }

        // Global optimization: when async mode is on, keep layout-critical CSS
        // blocking for stability, but inline tiny blocking files to remove RTT.
        if (
            $this->is_async_enabled() &&
            !$this->is_font_stylesheet($handle, $href) &&
            $this->should_keep_style_blocking($handle, $href, $media)
        ) {
            $default_inline_max = 12288;
            $inline_max = (int) apply_filters('prorank_css_inline_blocking_max_bytes', $default_inline_max, $handle, $actual_href, $media);
            $inlined = $this->try_inline_small_css($handle, $actual_href, $media, $inline_max);
            if ($inlined !== null) {
                return $inlined;
            }
        }

        // Apply async loading — skip font and layout-critical stylesheets to
        // avoid persistent layout breakage when critical CSS is incomplete.
        if (
            $this->is_async_enabled() &&
            !$this->is_font_stylesheet($handle, $href) &&
            !$this->should_keep_style_blocking($handle, $href, $media)
        ) {
            $tag = $this->convert_to_async_style($tag, $handle, $href, $media);
        }

        // Store final tag for fallback if we plan to combine later
        if (isset($this->processed_styles[$handle])) {
            $this->processed_styles[$handle]['tag'] = $tag;
        }

        if ($this->should_skip_style_combining($handle, $actual_href, $media)) {
            unset($this->processed_styles[$handle]);
            return $tag;
        }

        // If combining is enabled, suppress individual tags we will combine
        if ($this->get_setting('css_combine', false) && isset($this->processed_styles[$handle])) {
            // Capture inline CSS (wp_add_inline_style) before WordPress outputs it.
            // Must happen here — after filter returns, WP calls print_inline_style().
            $this->capture_inline_css($handle);
            return '<!-- ProRank CSS combined: ' . esc_html($handle) . ' -->';
        }
        
        return $tag;
    }

    /**
     * Keep fragile builder/framework base styles out of combined CSS groups.
     *
     * These stylesheets carry foundational SVG/icon/layout rules that other
     * widget CSS assumes are already present. Minification remains safe, but
     * suppressing the original tag and moving them into late combined chunks can
     * break rendering on Elementor-heavy sites.
     *
     * @param string $handle
     * @param string $href
     * @param string $media
     * @return bool
     */
    private function should_skip_style_combining(string $handle, string $href, string $media): bool {
        if (!$this->get_setting('css_combine', false)) {
            return false;
        }

        $media_normalized = strtolower(trim($media));
        if ($media_normalized !== '' && $media_normalized !== 'all' && $media_normalized !== 'screen') {
            return true;
        }

        $handle_lc = strtolower($handle);
        $href_lc = strtolower($href);

        $protected_handles = PerformanceCompatibilityRegistry::get_builder_combine_protected_handles();

        if (in_array($handle_lc, $protected_handles, true)) {
            return true;
        }

        $protected_href_patterns = PerformanceCompatibilityRegistry::get_builder_combine_protected_href_patterns();

        foreach ($protected_href_patterns as $pattern) {
            if ($pattern !== '' && strpos($href_lc, $pattern) !== false) {
                return true;
            }
        }

        return false;
    }
    
    /**
     * Process CSS minification
     *
     * @param string $tag    Style tag
     * @param string $handle Style handle
     * @param string $href   Style URL
     * @param string $media  Media attribute
     * @return string Modified style tag
     */
    private function process_minification(string $tag, string $handle, string $href, string $media): string {
        try {
            $tag_href = $href;
            if (preg_match("/\\bhref=['\"]([^'\"]+)['\"]/i", $tag, $href_matches)) {
                $tag_href = (string) $href_matches[1];
            }
            $original_href = $tag_href;

            // Convert URL to file path
            $file_path = $this->url_to_path($tag_href);
            
            if (!$file_path || !file_exists($file_path)) {
                return $tag;
            }
            
            // Minify the file
            $result = $this->minifier->minifyFile($file_path);
            
            if (!empty($result['url'])) {
                // Replace href in tag using parsed attribute, not raw string replace.
                // This avoids scheme-mismatch misses (http arg vs https tag).
                $tag = $this->replace_link_href($tag, (string) $result['url']);

                // Keep original URL for runtime fallback if optimized asset is missing.
                if ($original_href !== $result['url'] && strpos($tag, 'data-prorank-original-href=') === false) {
                    $tag = str_replace(
                        '<link',
                        '<link data-prorank-original-href="' . esc_url($original_href) . '"',
                        $tag
                    );
                }

                // If optimized file fails to load, fallback to original stylesheet URL.
                if ($original_href !== $result['url'] && strpos($tag, ' onerror=') === false) {
                    $fallback_js = "this.onerror=null;var o=this.getAttribute('data-prorank-original-href');if(o&&this.href!==o){this.href=o;}";
                    $tag = str_replace(
                        '<link',
                        '<link onerror="' . esc_attr($fallback_js) . '"',
                        $tag
                    );
                }
                
                // Add data attributes for debugging
                if (defined('WP_DEBUG') && WP_DEBUG) {
                    $compression = $result['compression_ratio'] ?? 0;
                    $tag = str_replace('<link', '<link data-prorank-minified="true" data-compression="' . $compression . '%"', $tag);
                }
                
                // Store for potential combining
                $this->processed_styles[$handle] = [
                    'path' => $result['path'],
                    'url' => $result['url'],
                    'media' => $media,
                    'handle' => $handle,
                ];
            }
        } catch (Exception $e) {
            prorank_log('ProRank CSS Minification Error: ' . $e->getMessage());
        }
        
        return $tag;
    }

    /**
     * Replace the href attribute on a <link> tag.
     *
     * @param string $tag
     * @param string $new_href
     * @return string
     */
    private function replace_link_href(string $tag, string $new_href): string {
        $updated = preg_replace_callback(
            '/\\bhref=(["\'])([^"\']+)\\1/i',
            static function (array $matches) use ($new_href): string {
                $quote = isset($matches[1]) ? $matches[1] : '"';
                return 'href=' . $quote . esc_url($new_href) . $quote;
            },
            $tag,
            1
        );

        return is_string($updated) ? $updated : $tag;
    }
    
    /**
     * Process combined styles
     *
     * @return void
     */
    public function output_combined_styles(): void {
        if ($this->combined_output_done || empty($this->processed_styles) || !$this->minifier) {
            return;
        }
        $this->combined_output_done = true;

        $allowed_tags = [
            'link' => [
                'rel' => true,
                'id' => true,
                'href' => true,
                'media' => true,
                'as' => true,
                'onload' => true,
                'onerror' => true,
                'data-prorank-async' => true,
                'data-prorank-delay' => true,
                'data-prorank-href' => true,
                'data-prorank-original-href' => true,
                'crossorigin' => true,
                'type' => true,
            ],
            'noscript' => [],
        ];

        // Group styles by media query
        $grouped = [];
        foreach ($this->processed_styles as $style) {
            $media = $style['media'] ?: 'all';
            $grouped[$media][] = $style;
        }

        foreach ($grouped as $media => $styles) {
            $base_delivery_mode = $this->get_combined_css_delivery_mode();
            $ordered_styles = $this->prioritize_styles_for_delivery($styles, $base_delivery_mode);
            $style_chunks = $this->split_styles_for_combining($ordered_styles);
            if ($this->should_force_blocking_combined_css_group(count($style_chunks), (string) $media)) {
                $base_delivery_mode = 'blocking';
            }

            foreach ($style_chunks as $chunk_index => $chunk_styles) {
                if (count($chunk_styles) < 2) {
                    // Not enough to combine; output originals if they were suppressed.
                    $this->output_fallback_styles($chunk_styles, $allowed_tags);
                    continue;
                }

                try {
                    $files = array_column($chunk_styles, 'path');

                    // Include captured inline CSS (wp_add_inline_style) in the combined file.
                    // This moves inline CSS from the HTML into the cacheable combined CSS file.
                    $inline_file = $this->write_inline_css_cache_file($chunk_styles);
                    if ($inline_file !== null) {
                        $files[] = $inline_file;
                    }

                    $result = $this->minifier->combineFiles($files);

                    if (!empty($result['url'])) {
                        $handle_tokens = [];
                        foreach ($chunk_styles as $chunk_style) {
                            $handle_tokens[] = (string) ($chunk_style['handle'] ?? '');
                        }
                        $handle_seed = $media . '|' . (string) $chunk_index . '|' . implode('|', $handle_tokens);
                        $handle = 'prorank-combined-' . md5($handle_seed);

                        // Allow other modules (e.g. UnusedCssModule) to replace the
                        // combined CSS URL with an optimized version.
                        $combined_url = apply_filters('prorank_combined_css_url', $result['url'], $handle, $media);

                        // Combined CSS delivery mode:
                        // - blocking: standard stylesheet (safest fallback)
                        // - onload: preload + onload rel swap (default async mode)
                        // - delay: interaction/timeout activation (opt-in via filter)
                        //
                        // Safety upgrade: in async mode, keep the earliest chunk(s)
                        // render-blocking so above-the-fold theme/layout CSS is applied
                        // immediately, while later chunks load async.
                        $combined_delivery_mode = $this->resolve_combined_css_chunk_delivery_mode(
                            $base_delivery_mode,
                            (int) $chunk_index,
                            count($style_chunks)
                        );
                        // phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
                        if ($combined_delivery_mode === 'delay') {
                            // Delay mode: keep full combined CSS non-blocking during initial paint.
                            $delayed_tag = sprintf(
                                '<link rel="preload" as="style" id="%s-css" href="%s" data-prorank-delay="1" data-prorank-href="%s" media="%s" />',
                                esc_attr($handle),
                                esc_url($combined_url),
                                esc_url($combined_url),
                                esc_attr($media ?: 'all')
                            );
                            $noscript_tag = sprintf(
                                '<noscript><link rel="stylesheet" id="%s-css" href="%s" media="%s" /></noscript>',
                                esc_attr($handle),
                                esc_url($combined_url),
                                esc_attr($media ?: 'all')
                            );
                            echo wp_kses($delayed_tag, $allowed_tags) . "\n";
                            echo $noscript_tag . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped via esc_attr/esc_url
                        } elseif ($combined_delivery_mode === 'onload') {
                            // Async onload mode: faster first paint than blocking while
                            // avoiding the long "unstyled" window of interaction-delay mode.
                            $async_tag = $this->convert_to_async_style('', $handle, $combined_url, $media ?: 'all');
                            echo wp_kses($async_tag, $allowed_tags) . "\n";
                        } else {
                            // Blocking mode: standard stylesheet link.
                            $combined_tag = sprintf(
                                '<link rel="stylesheet" id="%s-css" href="%s" media="%s" />',
                                esc_attr($handle),
                                esc_url($combined_url),
                                esc_attr($media)
                            );

                            // Let other modules filter the combined tag.
                            if (has_filter('style_loader_tag')) {
                                $removed = false;
                                if (has_filter('style_loader_tag', [$this, 'optimize_style_tag'])) {
                                    remove_filter('style_loader_tag', [$this, 'optimize_style_tag'], 10);
                                    $removed = true;
                                }
                                $combined_tag = apply_filters('style_loader_tag', $combined_tag, $handle, $combined_url, $media);
                                if ($removed) {
                                    add_filter('style_loader_tag', [$this, 'optimize_style_tag'], 10, 4);
                                }
                            }
                            echo wp_kses($combined_tag, $allowed_tags) . "\n";
                        }
                        // phpcs:enable WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
                    } else {
                        // Fallback to originals if combine produced no URL.
                        $this->output_fallback_styles($chunk_styles, $allowed_tags);
                    }
                } catch (Exception $e) {
                    prorank_log('ProRank CSS Combination Error: ' . $e->getMessage());
                    $this->output_fallback_styles($chunk_styles, $allowed_tags);
                }
            }
        }
    }

    /**
     * Capture inline CSS for a handle being combined.
     *
     * WordPress outputs inline styles (wp_add_inline_style) via
     * WP_Styles::print_inline_style() immediately after the <link> tag.
     * When we suppress the <link> for combining, we must also capture
     * the inline CSS so it can be included in the combined file instead
     * of bloating the HTML as separate <style> tags.
     *
     * @param string $handle Style handle
     * @return void
     */
    private function capture_inline_css(string $handle): void {
        global $wp_styles;

        if (!isset($wp_styles->registered[$handle])) {
            return;
        }

        $style = $wp_styles->registered[$handle];

        if (empty($style->extra['after'])) {
            return;
        }

        $parts = [];
        foreach ((array) $style->extra['after'] as $part) {
            if (is_string($part) && trim($part) !== '') {
                $parts[] = $part;
            }
        }

        if (!empty($parts)) {
            $this->collected_inline_css[$handle] = implode("\n", $parts);
        }

        // Clear so WordPress doesn't output it as a separate <style> tag.
        // This is safe because print_inline_style() runs after our filter returns.
        $style->extra['after'] = [];
    }

    /**
     * Write collected inline CSS to a cache file for inclusion in combined CSS.
     *
     * @param array $styles Styles in the current media group
     * @return string|null Cache file path, or null if no inline CSS to include
     */
    private function write_inline_css_cache_file(array $styles): ?string {
        $parts = [];
        foreach ($styles as $style) {
            $handle = $style['handle'] ?? '';
            if ($handle !== '' && isset($this->collected_inline_css[$handle])) {
                $parts[] = $this->collected_inline_css[$handle];
            }
        }

        if (empty($parts)) {
            return null;
        }

        $content = implode("\n", $parts);
        if (trim($content) === '') {
            return null;
        }

        $upload_dir = wp_upload_dir();
        $cache_dir = $upload_dir['basedir'] . '/prorank-cache/css/';
        if (!is_dir($cache_dir)) {
            wp_mkdir_p($cache_dir);
        }

        $hash = md5($content);
        $file_path = $cache_dir . 'inline-' . $hash . '.css';

        if (!file_exists($file_path)) {
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
            $written = @file_put_contents($file_path, $content);
            if ($written === false) {
                prorank_log('ProRank CSS: Failed to write inline CSS cache file');
                return null;
            }
        }

        return $file_path;
    }

    /**
     * Output original style tags with any captured inline CSS restored.
     *
     * Used as fallback when combining fails or when a media group has
     * fewer than 2 styles (not worth combining).
     *
     * @param array $styles    Styles to output
     * @param array $allowed_tags Allowed HTML tags for wp_kses
     * @return void
     */
    private function output_fallback_styles(array $styles, array $allowed_tags): void {
        foreach ($styles as $style) {
            if (!empty($style['tag'])) {
                // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
                echo wp_kses($style['tag'], $allowed_tags) . "\n";
            }
            // Restore captured inline CSS that was cleared from WordPress
            $handle = $style['handle'] ?? '';
            if ($handle !== '' && isset($this->collected_inline_css[$handle])) {
                printf(
                    "<style id='%s-inline-css'>\n%s\n</style>\n",
                    esc_attr($handle),
                    // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- captured from WP registered styles
                    $this->collected_inline_css[$handle]
                );
            }
        }
    }

    /**
     * Split a media stylesheet group into deterministic size-limited chunks.
     *
     * This avoids creating one giant combined CSS file, which is often slower
     * to download and parse on mobile than a few balanced chunks.
     *
     * @param array $styles
     * @return array<int,array>
     */
    private function split_styles_for_combining(array $styles): array {
        if (count($styles) < 4) {
            return [$styles];
        }

        $max_files_per_chunk = (int) apply_filters('prorank_css_combine_chunk_max_files', 24);
        if ($max_files_per_chunk < 4) {
            $max_files_per_chunk = 24;
        }

        $max_bytes_per_chunk = (int) apply_filters('prorank_css_combine_chunk_max_bytes', 393216); // 384KB
        if ($max_bytes_per_chunk < 65536) {
            $max_bytes_per_chunk = 393216;
        }

        $chunks = [];
        $current_chunk = [];
        $current_bytes = 0;

        foreach ($styles as $style) {
            $path = (string) ($style['path'] ?? '');
            $size = $this->estimate_combined_style_size($path);

            $would_exceed = !empty($current_chunk) &&
                ((count($current_chunk) >= $max_files_per_chunk) || (($current_bytes + $size) > $max_bytes_per_chunk));

            if ($would_exceed) {
                $chunks[] = $current_chunk;
                $current_chunk = [];
                $current_bytes = 0;
            }

            $current_chunk[] = $style;
            $current_bytes += $size;
        }

        if (!empty($current_chunk)) {
            $chunks[] = $current_chunk;
        }

        return !empty($chunks) ? $chunks : [$styles];
    }

    /**
     * Reorder styles so blocking chunks keep layout-critical CSS first.
     *
     * In async delivery modes, this prevents low-value plugin CSS from consuming
     * the first blocking combined chunk and delaying LCP.
     *
     * @param array  $styles
     * @param string $base_mode
     * @return array
     */
    private function prioritize_styles_for_delivery(array $styles, string $base_mode): array {
        if ($base_mode !== 'onload' && $base_mode !== 'delay') {
            return $styles;
        }

        if (count($styles) < 3) {
            return $styles;
        }

        $indexed = [];
        foreach ($styles as $index => $style) {
            $indexed[] = [
                'index' => (int) $index,
                'style' => $style,
                'priority' => $this->get_style_delivery_priority($style),
            ];
        }

        usort(
            $indexed,
            static function (array $a, array $b): int {
                if ($a['priority'] === $b['priority']) {
                    return $a['index'] <=> $b['index'];
                }
                return $a['priority'] <=> $b['priority'];
            }
        );

        $ordered = [];
        foreach ($indexed as $item) {
            $ordered[] = $item['style'];
        }

        return $ordered;
    }

    /**
     * Score stylesheet priority for combined async delivery ordering.
     *
     * Lower score means earlier in the first blocking chunk.
     *
     * @param array $style
     * @return int
     */
    private function get_style_delivery_priority(array $style): int {
        $handle = strtolower((string) ($style['handle'] ?? ''));
        $url = strtolower((string) ($style['url'] ?? ''));

        $critical_patterns = [
            'astra',
            '/themes/',
            'main.min.css',
            'style.css',
            'elementor-frontend',
            '/uploads/elementor/css/post-',
            'header-footer-elementor',
            'hfe-',
            'block-library',
            'global-styles',
        ];

        foreach ($critical_patterns as $pattern) {
            if (($handle !== '' && strpos($handle, $pattern) !== false) ||
                ($url !== '' && strpos($url, $pattern) !== false)) {
                return 10;
            }
        }

        $deferred_patterns = [
            'surecart',
            'wpforms',
            'contact-form-7',
            'cookie-notice',
            'bbpress',
            'buddypress',
            'rank-math',
            'wordpress-seo',
            'admin-bar',
            'dashicons',
            'link-whisper',
        ];

        foreach ($deferred_patterns as $pattern) {
            if (($handle !== '' && strpos($handle, $pattern) !== false) ||
                ($url !== '' && strpos($url, $pattern) !== false)) {
                return 90;
            }
        }

        return 50;
    }

    /**
     * Estimate local stylesheet size used for chunk balancing.
     *
     * @param string $path
     * @return int
     */
    private function estimate_combined_style_size(string $path): int {
        if ($path === '' || !is_readable($path)) {
            return 16384;
        }

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_filesize
        $size = @filesize($path);
        if (!is_int($size) || $size <= 0) {
            return 16384;
        }

        if ($size > 524288) {
            return 524288;
        }

        return max($size, 1024);
    }

    /**
     * Convert style to async loading
     *
     * @param string $tag    Style tag
     * @param string $handle Style handle
     * @param string $href   Style URL
     * @param string $media  Media attribute
     * @return string Modified style tag
     */
    private function convert_to_async_style(string $tag, string $handle, string $href, string $media): string {
        // Extract the actual href from the tag (it may have been modified by minification)
        if (preg_match("/(?:^|\\s)href=['\"]([^'\"]+)['\"]/i", $tag, $matches)) {
            $href = $matches[1];
        }

        // Preserve original non-minified URL for runtime fallback.
        $original_href = $href;
        if (preg_match("/data-prorank-original-href=['\"]([^'\"]+)['\"]/", $tag, $orig_matches)) {
            $original_href = $orig_matches[1];
        }

        $target_media = $media ?: 'all';
        $promote_preload = $this->should_promote_async_style_to_preload($handle, $href);

        if ($promote_preload) {
            // Promote only a small critical subset to preload.
            // Too many preload styles can starve LCP images on mobile.
            // phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
            $async_tag = sprintf(
                '<link rel="preload" as="style" id="%s" href="%s" media="print" onload="this.onload=null;this.rel=\'stylesheet\';this.media=\'%s\'" onerror="this.onerror=null;var o=this.getAttribute(\'data-prorank-original-href\')||this.href;this.rel=\'stylesheet\';this.href=o;this.media=\'%s\'" data-prorank-async="true" data-prorank-css-owner="css_opt_async" data-prorank-original-href="%s" />',
                esc_attr($handle),
                esc_url($href),
                esc_attr($target_media),
                esc_attr($target_media),
                esc_url($original_href)
            );
            $this->async_preload_promoted++;
        } else {
            // Lower-priority async delivery for non-critical styles.
            // media=print avoids render blocking without forcing high-priority fetch.
            $async_tag = sprintf(
                '<link rel="stylesheet" id="%s" href="%s" media="print" onload="this.onload=null;this.media=\'%s\'" onerror="this.onerror=null;var o=this.getAttribute(\'data-prorank-original-href\')||this.href;this.href=o;this.media=\'%s\'" data-prorank-async="true" data-prorank-css-owner="css_opt_async" data-prorank-original-href="%s" />',
                esc_attr($handle),
                esc_url($href),
                esc_attr($target_media),
                esc_attr($target_media),
                esc_url($original_href)
            );
        }

        // Keep noscript on the original URL to maximize reliability when JS is disabled.
        $noscript = sprintf(
            '<noscript><link rel="stylesheet" id="%s-css" href="%s" media="%s" /></noscript>',
            esc_attr($handle),
            esc_url($original_href),
            esc_attr($target_media)
        );
        // phpcs:enable WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet

        return $async_tag . $noscript;
    }

    /**
     * Decide whether an async stylesheet should be promoted to preload.
     *
     * @param string $handle
     * @param string $href
     * @return bool
     */
    private function should_promote_async_style_to_preload(string $handle, string $href): bool {
        $handle = strtolower(trim($handle));
        $href = strtolower(trim($href));
        if ($handle === '' && $href === '') {
            return false;
        }

        // Keep a small preload budget to avoid request-priority saturation.
        $default_budget = 6;
        $max_preload = (int) apply_filters('prorank_css_async_preload_budget', $default_budget);
        if ($max_preload < 0) {
            $max_preload = 0;
        }
        if ($this->async_preload_promoted >= $max_preload) {
            return false;
        }

        // Always prioritize layout-critical/theme/page CSS when seen.
        $critical_patterns = [
            'astra',
            'twentytwenty',
            'generatepress',
            'kadence',
            'block-library',
            'elementor-frontend',
            'elementor-post-',
            'hfe-style',
            'hfe-widgets',
            'widget-heading',
            'widget-divider',
            'widget-image',
            'widget-testimonial',
            '/themes/',
            '/wp-content/uploads/elementor/css/',
            '/wp-content/plugins/elementor/assets/css/',
        ];

        foreach ($critical_patterns as $pattern) {
            if (($handle !== '' && strpos($handle, $pattern) !== false) ||
                ($href !== '' && strpos($href, $pattern) !== false)) {
                return true;
            }
        }

        // Combined CSS chunks should load with high priority; low-priority
        // media=print loading can cause late layout styling and CLS spikes.
        if ($handle !== '' && strpos($handle, 'prorank-combined-') === 0) {
            return true;
        }

        // Everything else stays async but lower-priority (media=print).
        return false;
    }
    
    /**
     * Add async CSS loader script
     *
     * @return void
     */
    public function add_async_css_loader(): void {
        if ($this->async_loader_added) {
            return;
        }

        // Runtime check — WP conditionals are now available
        if (!$this->is_async_enabled()) {
            return;
        }

        $this->async_loader_added = true;
        
        ?>
        <script id="prorank-async-css-loader">
        /* ProRank CSS Async Loader */
        (function() {
            'use strict';
            
            // Fallback for browsers that don't support onload on link tags
            var links = document.querySelectorAll('link[media="print"][onload]');
            
            if (links.length === 0) return;
            
            // Check if browser supports onload for link elements
            var supportsOnload = 'onload' in document.createElement('link');
            
            if (!supportsOnload) {
                // Polyfill for older browsers
                links.forEach(function(link) {
                    // Create a dummy image to detect when stylesheet loads
                    var img = new Image();
                    img.onerror = function() {
                        link.media = 'all';
                    };
                    img.src = link.href;
                });
            }
            
            // Also ensure styles are loaded before window.onload
            window.addEventListener('load', function() {
                links.forEach(function(link) {
                    if (link.media === 'print') {
                        link.media = 'all';
                    }
                });
            });
        })();
        </script>
        <?php
    }

    /**
     * Resolve combined CSS delivery mode.
     *
     * Uses existing "Optimize CSS Delivery" toggle (`css_async`) but only allows
     * async/delay delivery when usable critical CSS exists for the current request.
     * Default mode is async-onload (universal + safer), while interaction delay
     * is still available via filter for advanced users.
     *
     * @return string One of: blocking|onload|delay
     */
    private function get_combined_css_delivery_mode(): string {
        if (!(bool) $this->get_setting('css_combine', false)) {
            return 'blocking';
        }

        if (!(bool) $this->get_setting('css_async', false)) {
            return 'blocking';
        }

        if ($this->should_disable_async_due_to_cls_guardrail()) {
            return 'blocking';
        }

        $mode = apply_filters('prorank_combined_css_async_mode', 'onload');
        if (!is_string($mode)) {
            return 'onload';
        }

        $mode = strtolower(trim($mode));
        if ($mode === 'delay') {
            return 'delay';
        }

        return 'onload';
    }

    /**
     * Guardrail: force blocking combined CSS when async would be unstable.
     *
     * When valid layout-critical CSS is inlined, the per-chunk delivery mode
     * (resolve_combined_css_chunk_delivery_mode) already provides safety by
     * keeping the first chunk(s) blocking and only async-loading the tail.
     * The global guardrail only kicks in when no critical CSS exists.
     *
     * @param int    $chunk_total
     * @param string $media
     * @return bool
     */
    private function should_force_blocking_combined_css_group(int $chunk_total, string $media): bool {
        if ($chunk_total <= 1) {
            return false;
        }

        // When we have valid critical CSS, trust the per-chunk delivery mode
        // to keep first chunk(s) blocking and async-load the rest.
        if ($this->has_critical_css_content()) {
            return false;
        }

        // Allow hybrid async delivery by default on most pages. The first one/two
        // chunks stay blocking via resolve_combined_css_chunk_delivery_mode().
        // Only force full blocking when chunk count is very high.
        $max_async_chunks = (int) apply_filters('prorank_css_async_max_chunks', 8, $media);
        if ($max_async_chunks < 1) {
            $max_async_chunks = 1;
        }

        return $chunk_total > $max_async_chunks;
    }

    /**
     * Resolve per-chunk delivery mode for combined CSS.
     *
     * In async mode we keep one or more earliest chunks blocking by default.
     * This significantly lowers FOUC/CLS risk while preserving async gains for
     * the remaining chunk(s).
     *
     * @param string $base_mode
     * @param int    $chunk_index Zero-based chunk index.
     * @param int    $chunk_total Total chunk count.
     * @return string One of: blocking|onload|delay
     */
    private function resolve_combined_css_chunk_delivery_mode(string $base_mode, int $chunk_index, int $chunk_total): string {
        if ($base_mode !== 'onload' || $chunk_total <= 1) {
            return $base_mode;
        }

        // When valid critical CSS is inlined, it handles above-fold rendering.
        // Keep first chunk blocking as a safety net; async the rest.
        // Without critical CSS, keep first two chunks blocking (more conservative).
        $has_ccss = $this->has_critical_css_content();
        $default_blocking_chunks = $has_ccss ? 1 : max(1, min(2, $chunk_total - 1));

        // Optional: allow one extra blocking chunk on homepage/front-page
        // if a site needs stricter CLS protection.
        $is_home_like = is_front_page() || is_home() || $this->is_likely_home_request();
        if ($has_ccss && $is_home_like) {
            $extra_home_chunks = (int) apply_filters('prorank_css_async_home_extra_blocking_chunks', 0, $chunk_total);
            if ($extra_home_chunks > 0) {
                $default_blocking_chunks = min($chunk_total, $default_blocking_chunks + $extra_home_chunks);
            }
        }

        $blocking_chunks = (int) apply_filters('prorank_css_async_blocking_chunks', $default_blocking_chunks, $chunk_total);
        if ($blocking_chunks < 0) {
            $blocking_chunks = 0;
        } elseif ($blocking_chunks > $chunk_total) {
            $blocking_chunks = $chunk_total;
        }

        if ($chunk_index < $blocking_chunks) {
            return 'blocking';
        }

        return 'onload';
    }

    /**
     * Check if interaction-delay mode is enabled for combined CSS.
     *
     * @return bool
     */
    private function is_combined_css_delay_enabled(): bool {
        return $this->get_combined_css_delivery_mode() === 'delay';
    }

    /**
     * Read generated critical CSS from supported storage formats.
     *
     * Supports both:
     * - legacy map format: ['critical_css' => ['homepage' => '...']]
     * - structured pages format: ['pages' => ['homepage' => ['critical_css' => '...']]]
     *
     * @return array<string,string>
     */
    private function get_generated_critical_css_map(): array {
        $generator_data = get_option('prorank_critical_css_data', []);
        $css_map = [];

        if (is_array($generator_data) && !empty($generator_data)) {
            $legacy_map = $generator_data['critical_css'] ?? null;
            if (is_array($legacy_map)) {
                foreach ($legacy_map as $key => $css) {
                    if (is_string($key) && $key !== '' && is_string($css) && $css !== '') {
                        $css_map[$key] = $css;
                    }
                }
            }

            $pages = $generator_data['pages'] ?? null;
            if (is_array($pages)) {
                foreach ($pages as $key => $page_data) {
                    if (!is_string($key) || $key === '' || !is_array($page_data)) {
                        continue;
                    }
                    $css = $page_data['critical_css'] ?? '';
                    if (is_string($css) && $css !== '') {
                        $css_map[$key] = $css;
                    }
                }
            }
        }

        // Backward compatibility: some installs store generated CSS only in
        // context options (prorank_critical_css_front_page, etc.).
        $css_map = array_merge($css_map, $this->get_legacy_critical_css_options_map());

        return $css_map;
    }

    /**
     * Build critical CSS map from legacy option keys.
     *
     * @return array<string,string>
     */
    private function get_legacy_critical_css_options_map(): array {
        $context_aliases = [
            'front_page' => ['front_page', 'homepage', 'home'],
            'home' => ['home', 'blog', 'homepage', 'front_page'],
            'single_post' => ['single_post'],
            'single_page' => ['single_page'],
            'archive' => ['archive'],
            'archive_category' => ['archive_category', 'archive'],
            'archive_tag' => ['archive_tag', 'archive'],
            'search' => ['search'],
            '404' => ['404'],
            'default' => ['default'],
        ];

        $css_map = [];
        foreach ($context_aliases as $context => $aliases) {
            $css = get_option('prorank_critical_css_' . $context, '');
            if (!is_string($css) || $css === '') {
                continue;
            }

            foreach ($aliases as $alias) {
                if (!isset($css_map[$alias])) {
                    $css_map[$alias] = $css;
                }
            }

            // Preserve page_{id} candidate matching for static front/posts pages.
            if ($context === 'front_page') {
                $front_page_id = (int) get_option('page_on_front');
                if ($front_page_id > 0 && !isset($css_map['page_' . $front_page_id])) {
                    $css_map['page_' . $front_page_id] = $css;
                }
            } elseif ($context === 'home') {
                $posts_page_id = (int) get_option('page_for_posts');
                if ($posts_page_id > 0 && !isset($css_map['page_' . $posts_page_id])) {
                    $css_map['page_' . $posts_page_id] = $css;
                }
            }
        }

        return $css_map;
    }

    /**
     * Add combined CSS delayed activation loader.
     *
     * Activates preloaded combined stylesheets on first user interaction,
     * with a timeout fallback for non-interactive visits. This runs only when
     * the optional "delay" combined CSS mode is active.
     *
     * @return void
     */
    public function add_combined_css_delay_loader(): void {
        if ($this->combined_delay_loader_added) {
            return;
        }

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

        $this->combined_delay_loader_added = true;
        $fallback_timeout_ms = (int) apply_filters('prorank_css_delay_timeout_ms', 2500);
        if ($fallback_timeout_ms < 1000) {
            $fallback_timeout_ms = 1000;
        }
        ?>
        <script id="prorank-combined-css-delay-loader">
        (function() {
            'use strict';

            var loaded = false;
            var passiveOptions = { passive: true };
            var events = ['keydown', 'click', 'touchstart', 'pointerdown', 'wheel', 'scroll', 'mousemove'];

            function activateCombinedCss() {
                if (loaded) return;
                loaded = true;

                var links = document.querySelectorAll('link[data-prorank-delay="1"][data-prorank-href]');
                if (links.length) {
                    links.forEach(function(link) {
                        var href = link.getAttribute('data-prorank-href');
                        if (!href) return;

                        link.setAttribute('rel', 'stylesheet');
                        link.setAttribute('href', href);
                        link.removeAttribute('as');
                        link.removeAttribute('data-prorank-delay');
                        link.removeAttribute('data-prorank-href');
                    });
                }

                events.forEach(function(evt) {
                    window.removeEventListener(evt, onInteraction, passiveOptions);
                });
            }

            function onInteraction() {
                activateCombinedCss();
            }

            events.forEach(function(evt) {
                window.addEventListener(evt, onInteraction, passiveOptions);
            });

            // Safety fallback for non-interactive sessions.
            window.setTimeout(activateCombinedCss, <?php echo (int) $fallback_timeout_ms; ?>);
        })();
        </script>
        <?php
    }

    /**
     * Remove admin/onboarding assets from guest frontend requests.
     *
     * @return void
     */
    public function strip_guest_admin_assets(): void {
        if (is_admin() || is_user_logged_in() || is_admin_bar_showing()) {
            return;
        }

        $enabled = (bool) apply_filters('prorank_strip_frontend_admin_assets', true);
        if (!$enabled) {
            return;
        }

        global $wp_styles, $wp_scripts;

        $style_patterns = apply_filters(
            'prorank_frontend_admin_style_patterns',
            [
                'admin-bar',
                'dashicons',
                '/insert-headers-and-footers/build/admin-bar.css',
                '/wordpress-seo/css/dist/adminbar-',
                '/wpforms-lite/assets/css/admin-bar.min.css',
                '/buddypress/bp-core/css/admin-bar.min.css',
                '/astra-sites/inc/lib/zip-ai/sidebar/build/sidebar-app.css',
            ]
        );
        $style_patterns = is_array($style_patterns) ? array_map('strval', $style_patterns) : [];

        $script_patterns = apply_filters(
            'prorank_frontend_admin_script_patterns',
            [
                'admin-bar',
                '/link-whisper-premium/js/frontend.min.js',
                '/elementor/assets/js/elementor-admin-bar.min.js',
                '/elementor/assets/js/dev-tools.min.js',
                '/elementor/assets/js/web-cli.min.js',
                '/astra-sites/inc/lib/zip-ai/sidebar/build/sidebar-app.js',
                '/astra-sites/inc/lib/onboarding/assets/dist/template-preview/main.js',
            ]
        );
        $script_patterns = is_array($script_patterns) ? array_map('strval', $script_patterns) : [];

        if ($wp_styles instanceof \WP_Styles) {
            foreach ((array) $wp_styles->queue as $handle) {
                $handle = (string) $handle;
                if ($handle === '') {
                    continue;
                }
                $src = (string) (($wp_styles->registered[$handle]->src ?? ''));
                if ($this->asset_matches_strip_patterns($handle, $src, $style_patterns)) {
                    wp_dequeue_style($handle);
                    wp_deregister_style($handle);
                }
            }
        }

        if ($wp_scripts instanceof \WP_Scripts) {
            foreach ((array) $wp_scripts->queue as $handle) {
                $handle = (string) $handle;
                if ($handle === '') {
                    continue;
                }
                $src = (string) (($wp_scripts->registered[$handle]->src ?? ''));
                if ($this->asset_matches_strip_patterns($handle, $src, $script_patterns)) {
                    wp_dequeue_script($handle);
                    wp_deregister_script($handle);
                }
            }
        }
    }

    /**
     * Check whether asset handle or source matches stripping patterns.
     *
     * @param string $handle
     * @param string $src
     * @param array  $patterns
     * @return bool
     */
    private function asset_matches_strip_patterns(string $handle, string $src, array $patterns): bool {
        $haystacks = [strtolower($handle), strtolower($src)];
        foreach ($patterns as $pattern) {
            if (!is_string($pattern) || $pattern === '') {
                continue;
            }
            $needle = strtolower($pattern);
            foreach ($haystacks as $haystack) {
                if ($haystack !== '' && strpos($haystack, $needle) !== false) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Promote a single attachment image to LCP priority.
     *
     * @param array $attributes
     * @param mixed $attachment
     * @param mixed $size
     * @return array
     */
    public function promote_attachment_lcp_image(array $attributes, $attachment, $size): array {
        if ($this->lcp_image_promoted || is_admin()) {
            return $attributes;
        }

        $class = strtolower((string) ($attributes['class'] ?? ''));
        $alt = strtolower((string) ($attributes['alt'] ?? ''));
        $src = strtolower((string) ($attributes['src'] ?? ''));

        $skip_candidate = $this->should_skip_lcp_image_promotion($class, $alt, $src);
        $too_small_candidate = $this->is_image_too_small_for_lcp_promotion($attributes, $attachment, $size);

        if (isset($attributes['fetchpriority']) && strtolower((string) $attributes['fetchpriority']) === 'high') {
            // Demote decorative/tiny images even if another layer already promoted.
            if ($skip_candidate || $too_small_candidate) {
                unset($attributes['fetchpriority']);
                if (($attributes['loading'] ?? '') === 'eager') {
                    unset($attributes['loading']);
                }
                return $attributes;
            }

            $this->lcp_image_promoted = true;
            if (($attributes['loading'] ?? '') === 'lazy') {
                $attributes['loading'] = 'eager';
            }
            return $attributes;
        }

        if ($skip_candidate) {
            return $attributes;
        }

        if ($too_small_candidate) {
            return $attributes;
        }

        $attributes['fetchpriority'] = 'high';
        $attributes['loading'] = 'eager';
        if (!isset($attributes['decoding']) || strtolower((string) $attributes['decoding']) === 'auto') {
            $attributes['decoding'] = 'async';
        }

        $this->lcp_image_promoted = true;

        return $attributes;
    }

    /**
     * Promote one content image in raw HTML to LCP priority.
     *
     * @param string $content
     * @return string
     */
    public function promote_content_lcp_image(string $content): string {
        if ($this->lcp_image_promoted || $content === '' || is_admin()) {
            return $content;
        }
        $promoted = false;
        $updated = $this->apply_lcp_priority_pass($content, false, $promoted);

        if ($promoted) {
            $this->lcp_image_promoted = true;
        }

        return is_string($updated) ? $updated : $content;
    }

    /**
     * Elementor widget-level content fallback for LCP image promotion.
     *
     * @param string $content
     * @param mixed  $widget
     * @return string
     */
    public function promote_elementor_widget_lcp_image(string $content, $widget = null): string {
        unset($widget);
        return $this->promote_content_lcp_image($content);
    }

    /**
     * Elementor image-control HTML fallback for LCP image promotion.
     *
     * @param string $html
     * @param mixed  $settings
     * @param mixed  $image_size_key
     * @param mixed  $image_key
     * @return string
     */
    public function promote_elementor_image_html_lcp(string $html, $settings = null, $image_size_key = null, $image_key = null): string {
        unset($settings, $image_size_key, $image_key);
        return $this->promote_content_lcp_image($html);
    }

    /**
     * Extract HTML attribute from a tag string.
     *
     * @param string $html
     * @param string $attribute
     * @return string
     */
    private function extract_html_attribute(string $html, string $attribute): string {
        if ($html === '' || $attribute === '') {
            return '';
        }

        if (preg_match('/\b' . preg_quote($attribute, '/') . '\s*=\s*["\']([^"\']*)["\']/i', $html, $m) === 1) {
            return strtolower(trim((string) ($m[1] ?? '')));
        }

        return '';
    }

    /**
     * Extract numeric HTML attribute from a tag string.
     *
     * @param string $html
     * @param string $attribute
     * @return int
     */
    private function extract_html_numeric_attribute(string $html, string $attribute): int {
        if ($html === '' || $attribute === '') {
            return 0;
        }

        if (preg_match('/\b' . preg_quote($attribute, '/') . '\s*=\s*["\']?(\d+)/i', $html, $m) === 1) {
            return (int) ($m[1] ?? 0);
        }

        return 0;
    }

    /**
     * Check whether candidate src matches known LCP image URL when available.
     *
     * @param string $src
     * @return bool
     */
    private function matches_known_lcp_image(string $src): bool {
        $known_lcp = $this->get_known_lcp_image_url();
        if ($known_lcp === '') {
            return true;
        }

        $candidate = $this->normalize_lcp_image_compare_url($src);
        $known = $this->normalize_lcp_image_compare_url($known_lcp);
        if ($candidate === '' || $known === '') {
            return false;
        }

        return $candidate === $known;
    }

    /**
     * Get known LCP image URL detected by dedicated optimizer module.
     *
     * @return string
     */
    private function get_known_lcp_image_url(): string {
        $preloaded = $GLOBALS['prorank_preloaded_urls'] ?? [];
        if (is_array($preloaded)) {
            foreach ($preloaded as $entry) {
                if (!is_string($entry) || trim($entry) === '') {
                    continue;
                }

                // Some modules store dedupe key as "url|media".
                $candidate = trim((string) explode('|', $entry, 2)[0]);
                if (preg_match('/\.(avif|webp|jpe?g|png|gif|svg)(\?|$)/i', $candidate) === 1) {
                    if ($this->should_skip_lcp_image_promotion('', '', strtolower($candidate))) {
                        continue;
                    }
                    [$w, $h] = $this->resolve_lcp_dimensions_from_src($candidate);
                    if ($w > 0 && $h > 0 && $this->is_dimensions_too_small_for_lcp_promotion($w, $h)) {
                        continue;
                    }
                    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;
            $candidate = is_array($entry) && !empty($entry['url']) && is_string($entry['url']) ? esc_url_raw($entry['url']) : '';
            if ($candidate !== '') {
                return $candidate;
            }
        }

        $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)) {
                $candidate = isset($entry['background_image']) && is_string($entry['background_image']) ? esc_url_raw($entry['background_image']) : '';
                if ($candidate === '') {
                    $candidate = isset($entry['src']) && is_string($entry['src']) ? esc_url_raw($entry['src']) : '';
                }
                if ($candidate !== '') {
                    return $candidate;
                }
            }
        }

        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;
    }

    /**
     * Normalize an image URL/path for stable cross-module comparison.
     *
     * @param string $url
     * @return string
     */
    private function normalize_lcp_image_compare_url(string $url): string {
        $url = html_entity_decode(trim($url), ENT_QUOTES);
        if ($url === '') {
            return '';
        }

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

        $clean = preg_replace('/[#?].*$/', '', $url);
        return is_string($clean) ? strtolower($clean) : '';
    }

    /**
     * Whether we have a known LCP image URL for strict matching.
     *
     * @return bool
     */
    private function has_known_lcp_image_url(): bool {
        return $this->get_known_lcp_image_url() !== '';
    }

    /**
     * Apply one pass of LCP priority promotion to image tags in HTML.
     *
     * @param string $html
     * @param bool   $strict_known_match Require known-LCP URL match before promoting.
     * @param bool   $promoted           Promotion state carried across passes.
     * @return string
     */
    private function apply_lcp_priority_pass(string $html, bool $strict_known_match, bool &$promoted): string {
        if ($html === '' || stripos($html, '<img') === false) {
            return $html;
        }

        $updated = preg_replace_callback(
            '/<img\b[^>]*>/i',
            function (array $matches) use ($strict_known_match, &$promoted): string {
                $img = $matches[0] ?? '';
                if (!is_string($img) || $img === '') {
                    return is_string($img) ? $img : '';
                }

                $class = $this->extract_html_attribute($img, 'class');
                $alt = $this->extract_html_attribute($img, 'alt');
                $src = $this->extract_html_attribute($img, 'src');
                $width = $this->extract_html_numeric_attribute($img, 'width');
                $height = $this->extract_html_numeric_attribute($img, 'height');

                $matches_known = $this->matches_known_lcp_image($src);
                $has_known_lcp = $this->has_known_lcp_image_url();

                // When a known LCP image exists, ensure non-matching images never keep high priority.
                if ($has_known_lcp && !$matches_known) {
                    if (preg_match('/\bfetchpriority\s*=\s*["\']high["\']/i', $img) === 1) {
                        $img = preg_replace('/\s*fetchpriority\s*=\s*["\']high["\']/i', '', $img, 1) ?: $img;
                    }
                    return $img;
                }

                if ($strict_known_match && !$matches_known) {
                    if (preg_match('/\bfetchpriority\s*=\s*["\']high["\']/i', $img) === 1) {
                        $img = preg_replace('/\s*fetchpriority\s*=\s*["\']high["\']/i', '', $img, 1) ?: $img;
                    }
                    return $img;
                }

                if (
                    $this->should_skip_lcp_image_promotion($class, $alt, $src)
                    || $this->is_dimensions_too_small_for_lcp_promotion($width, $height)
                ) {
                    if (preg_match('/\bfetchpriority\s*=\s*["\']high["\']/i', $img) === 1) {
                        $img = preg_replace('/\s*fetchpriority\s*=\s*["\']high["\']/i', '', $img, 1) ?: $img;
                    }
                    return $img;
                }

                if ($promoted) {
                    return $img;
                }

                // Ensure the promoted image is not lazy and has high priority.
                if (preg_match('/\bfetchpriority\s*=\s*["\']high["\']/i', $img) !== 1) {
                    $img = preg_replace('/<img\b/i', '<img fetchpriority="high"', $img, 1) ?: $img;
                } else {
                    $img = preg_replace('/\bfetchpriority\s*=\s*["\'][^"\']*["\']/i', 'fetchpriority="high"', $img, 1) ?: $img;
                }
                if (preg_match('/\bloading\s*=\s*["\']lazy["\']/i', $img) === 1) {
                    $img = preg_replace('/\bloading\s*=\s*["\']lazy["\']/i', 'loading="eager"', $img, 1) ?: $img;
                } elseif (preg_match('/\bloading\s*=/', $img) !== 1) {
                    $img = preg_replace('/<img\b/i', '<img loading="eager"', $img, 1) ?: $img;
                } else {
                    $img = preg_replace('/\bloading\s*=\s*["\'][^"\']*["\']/i', 'loading="eager"', $img, 1) ?: $img;
                }
                if (preg_match('/\bdecoding\s*=/', $img) !== 1) {
                    $img = preg_replace('/<img\b/i', '<img decoding="async"', $img, 1) ?: $img;
                }

                $promoted = true;
                return $img;
            },
            $html
        );

        return is_string($updated) ? $updated : $html;
    }

    /**
     * Determine if an image should be excluded from LCP priority promotion.
     *
     * @param string $class
     * @param string $alt
     * @param string $src
     * @return bool
     */
    private function should_skip_lcp_image_promotion(string $class, string $alt, string $src): bool {
        $haystack = strtolower(trim($class . ' ' . $alt . ' ' . $src));
        if ($haystack === '') {
            return false;
        }

        $skip_tokens = [
            'logo',
            'site-icon',
            'favicon',
            'avatar',
            'gravatar',
            'emoji',
            'smiley',
            'icon-',
            '/icons/',
        ];

        foreach ($skip_tokens as $token) {
            if (strpos($haystack, $token) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Determine if image dimensions are too small to be a likely LCP candidate.
     *
     * @param array $attributes
     * @param mixed $attachment
     * @return bool
     */
    private function is_image_too_small_for_lcp_promotion(array $attributes, $attachment = null, $size = null): bool {
        [$width, $height] = $this->resolve_lcp_candidate_dimensions($attributes, $attachment, $size);

        return $this->is_dimensions_too_small_for_lcp_promotion($width, $height);
    }

    /**
     * Resolve candidate image dimensions from attributes/attachment/src fallback.
     *
     * @param array $attributes
     * @param mixed $attachment
     * @return array{0:int,1:int}
     */
    private function resolve_lcp_candidate_dimensions(array $attributes, $attachment = null, $size = null): array {
        $width = isset($attributes['width']) ? (int) $attributes['width'] : 0;
        $height = isset($attributes['height']) ? (int) $attributes['height'] : 0;

        if ($width > 0 && $height > 0) {
            return [$width, $height];
        }

        if (is_array($size)) {
            $size_width = isset($size[0]) ? (int) $size[0] : 0;
            $size_height = isset($size[1]) ? (int) $size[1] : 0;
            if ($size_width > 0 && $size_height > 0) {
                return [$size_width, $size_height];
            }
        }

        $attachment_id = $this->resolve_attachment_id_from_candidate($attachment);
        if ($attachment_id <= 0 && !empty($attributes['src'])) {
            $attachment_id = (int) attachment_url_to_postid((string) $attributes['src']);
        }

        if ($attachment_id > 0) {
            $meta = wp_get_attachment_metadata($attachment_id);
            if (is_array($meta)) {
                $meta_width = isset($meta['width']) ? (int) $meta['width'] : 0;
                $meta_height = isset($meta['height']) ? (int) $meta['height'] : 0;
                if ($meta_width > 0 && $meta_height > 0) {
                    return [$meta_width, $meta_height];
                }
            }
        }

        if (!empty($attributes['src'])) {
            [$src_width, $src_height] = $this->resolve_lcp_dimensions_from_src((string) $attributes['src']);
            if ($src_width > 0 && $src_height > 0) {
                return [$src_width, $src_height];
            }
        }

        return [$width, $height];
    }

    /**
     * Resolve attachment ID from wp_get_attachment_image callback payload.
     *
     * @param mixed $attachment
     * @return int
     */
    private function resolve_attachment_id_from_candidate($attachment): int {
        if (is_numeric($attachment)) {
            return (int) $attachment;
        }

        if (is_object($attachment)) {
            if (isset($attachment->ID)) {
                return (int) $attachment->ID;
            }
            if (method_exists($attachment, 'get_id')) {
                return (int) $attachment->get_id();
            }
        }

        return 0;
    }

    /**
     * Resolve image dimensions from local src path when attachment metadata is absent.
     *
     * @param string $src
     * @return array{0:int,1:int}
     */
    private function resolve_lcp_dimensions_from_src(string $src): array {
        $path = $this->resolve_local_image_path_from_url($src);
        if ($path === '' || !file_exists($path)) {
            return [0, 0];
        }

        $upload_dir = wp_upload_dir();
        $base_dir = wp_normalize_path((string) ($upload_dir['basedir'] ?? ''));
        $normalized_path = wp_normalize_path($path);
        if ($base_dir !== '' && strpos($normalized_path, $base_dir) !== 0) {
            return [0, 0];
        }

        $size = wp_getimagesize($path);
        if (!is_array($size)) {
            return [0, 0];
        }

        $width = isset($size[0]) ? (int) $size[0] : 0;
        $height = isset($size[1]) ? (int) $size[1] : 0;

        return [$width, $height];
    }

    /**
     * Convert an image URL into a local path when possible.
     *
     * @param string $url
     * @return string
     */
    private function resolve_local_image_path_from_url(string $url): string {
        $url = html_entity_decode(trim($url), ENT_QUOTES);
        if ($url === '') {
            return '';
        }

        $upload_dir = wp_upload_dir();
        $base_url = (string) ($upload_dir['baseurl'] ?? '');
        $base_dir = (string) ($upload_dir['basedir'] ?? '');
        if ($base_url !== '' && strpos($url, $base_url) === 0) {
            return (string) str_replace($base_url, $base_dir, $url);
        }

        $site_url = (string) site_url();
        if ($site_url !== '' && strpos($url, $site_url) === 0) {
            return (string) str_replace($site_url, rtrim(ABSPATH, '/'), $url);
        }

        if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) {
            return rtrim(ABSPATH, '/') . $url;
        }

        return '';
    }

    /**
     * Evaluate dimensions against conservative LCP-size thresholds.
     *
     * @param int $width
     * @param int $height
     * @return bool
     */
    private function is_dimensions_too_small_for_lcp_promotion(int $width, int $height): bool {
        if ($width <= 0 || $height <= 0) {
            // Unknown size should not be force-promoted to high priority.
            return true;
        }

        if ($width < 320 || $height < 180) {
            return true;
        }

        return ($width * $height) < 100000;
    }

    /**
     * Start output buffering for final HTML-level LCP image priority enforcement.
     *
     * @return void
     */
    public function start_lcp_priority_output_buffer(): void {
        if ($this->lcp_output_buffer_started) {
            return;
        }

        if (is_admin()) {
            return;
        }

        if (function_exists('wp_doing_ajax') && wp_doing_ajax()) {
            return;
        }

        if (defined('REST_REQUEST') && REST_REQUEST) {
            return;
        }

        // Keep overhead low: enforce only on homepage/blog where LCP variance
        // is most visible in PSI.
        if (!(is_front_page() || is_home() || $this->is_likely_home_request())) {
            return;
        }

        ob_start([$this, 'enforce_lcp_priority_in_final_html']);
        $this->lcp_output_buffer_started = true;
    }

    /**
     * Ensure LCP output buffering also starts on template include fallback.
     *
     * @param string $template
     * @return string
     */
    public function ensure_lcp_priority_output_buffer(string $template): string {
        $this->start_lcp_priority_output_buffer();
        return $template;
    }

    /**
     * Enforce one meaningful LCP image priority in final HTML output.
     *
     * @param string $html
     * @return string
     */
    public function enforce_lcp_priority_in_final_html(string $html): string {
        if ($html === '' || stripos($html, '<img') === false) {
            return $html;
        }
        $promoted = false;
        $updated = $this->apply_lcp_priority_pass($html, false, $promoted);

        if ($promoted) {
            $this->lcp_image_promoted = true;
        }

        return is_string($updated) ? $updated : $html;
    }
    
    /**
     * Clear CSS cache
     *
     * @return void
     */
    public function clear_css_cache(): void {
        // Ensure services exist even when called from wp-admin (where should_run() is false).
        if (!$this->minifier || !$this->external_cache) {
            $this->init_services();
        }

        if ($this->minifier) {
            $this->minifier->clearCache(true);
        }
        
        if ($this->external_cache) {
            $this->external_cache->clearCache(false);
        }

        // Keep HTML cache in sync with CSS cache.
        // If old cached HTML points to deleted combined CSS files, visitors can
        // receive an unstyled page. Purging HTML cache here forces regeneration
        // with current CSS URLs.
        $this->clear_related_html_cache();
    }

    /**
     * Purge HTML cache directories that can contain stale CSS references.
     *
     * @return void
     */
    private function clear_related_html_cache(): void {
        $upload_dir = wp_upload_dir();
        $upload_basedir = is_array($upload_dir) ? (string) ($upload_dir['basedir'] ?? '') : '';

        $dirs = [];
        if ($upload_basedir !== '') {
            $dirs[] = trailingslashit($upload_basedir) . 'prorank-cache/html';
        }

        // Legacy file cache location.
        $dirs[] = WP_CONTENT_DIR . '/cache/prorank-seo';

        foreach ($dirs as $dir) {
            $this->delete_directory_contents($dir);
        }
    }

    /**
     * Delete all files/directories inside a directory, keeping root intact.
     *
     * @param string $directory
     * @return void
     */
    private function delete_directory_contents(string $directory): void {
        if ($directory === '' || !is_dir($directory)) {
            return;
        }

        try {
            $iterator = new \RecursiveIteratorIterator(
                new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS),
                \RecursiveIteratorIterator::CHILD_FIRST
            );
        } catch (\Throwable $e) {
            return;
        }

        foreach ($iterator as $item) {
            $path = $item->getPathname();
            if (!is_string($path) || $path === '') {
                continue;
            }

            if ($item->isDir()) {
                \ProRank\SEO\Core\FilesystemHelper::delete_dir($path);
            } else {
                \ProRank\SEO\Core\FilesystemHelper::delete_file($path);
            }
        }
    }
    
    /**
     * Get cache statistics
     *
     * @return array
     */
    public function get_cache_stats(): array {
        $stats = [
            'minified' => ['files' => 0, 'size' => 0],
            'external' => ['files' => 0, 'size' => 0],
        ];
        
        if ($this->minifier) {
            $stats['minified'] = $this->minifier->getCacheStats();
        }
        
        if ($this->external_cache) {
            $stats['external'] = $this->external_cache->getStats();
        }
        
        return $stats;
    }
    
    /**
     * Check if stylesheet is excluded
     *
     * @param string $handle Style handle
     * @param string $href   Style source URL
     * @return bool
     */
    private function is_style_excluded(string $handle, string $href): bool {
        // Get excluded styles (cached)
        if (empty($this->excluded_styles)) {
            $excluded = $this->get_setting('css_exclude', '');
            if (!empty($excluded)) {
                $this->excluded_styles = array_filter(
                    array_map('trim', explode("\n", $excluded))
                );
            }
        }
        
        // Always exclude critical styles
        $always_exclude = [
            'admin-bar',
            'dashicons',
            'wp-block-library',
            'prorank-async-css-loader', // Our own loader
            'prorank-combined', // Our combined styles
        ];
        
        if (in_array($handle, $always_exclude, true)) {
            return true;
        }
        
        // Check if starts with excluded prefix
        foreach ($always_exclude as $prefix) {
            if (strpos($handle, $prefix) === 0) {
                return true;
            }
        }
        
        // Check user-defined exclusions
        foreach ($this->excluded_styles as $pattern) {
            // Check handle match
            if ($handle === $pattern) {
                return true;
            }
            
            // Check URL match
            if (!empty($href) && strpos($href, $pattern) !== false) {
                return true;
            }
        }
        
        return false;
    }

    /**
     * Determine whether a stylesheet should remain blocking even when async mode is enabled.
     *
     * @param string $handle
     * @param string $href
     * @param string $media
     * @return bool
     */
    private function should_keep_style_blocking(string $handle, string $href, string $media): bool {
        if ($media !== '' && $media !== 'all') {
            return false;
        }

        $handle = strtolower(trim($handle));
        $href = strtolower(trim($href));

        if ($this->is_layout_sensitive_elementor_stylesheet($handle, $href)) {
            return true;
        }

        $has_stable_critical_css = $this->has_critical_css_content() && !$this->should_disable_async_due_to_cls_guardrail();

        // Elementor can enqueue multiple post-{id}.css files for the current
        // page, header/footer template parts, loops, and theme-builder
        // surfaces. In practice these files frequently carry layout-critical
        // rules, so keep them blocking by default instead of trying to async
        // all non-current template CSS. A site can opt out via filter if it
        // has proven safe coverage.
        $elementor_post_id = $this->extract_elementor_post_id($handle, $href);
        if ($elementor_post_id !== null) {
            $keep_all_elementor_post_css_blocking = (bool) apply_filters(
                'prorank_css_async_keep_all_elementor_post_css_blocking',
                true,
                $handle,
                $href,
                $elementor_post_id
            );

            if ($keep_all_elementor_post_css_blocking) {
                return true;
            }

            $current_id = (int) get_queried_object_id();
            if ($current_id > 0 && $elementor_post_id === $current_id) {
                return true;
            }
        }

        // Keep only core layout CSS blocking to prevent CLS.
        // Do not blanket-block all theme files; on HTTP/2 many secondary theme
        // stylesheets are faster as split + async.
        $default_patterns = [
            'style-main',
            'main.min.css',
            'elementor-frontend',
            'header-footer-elementor',
            'global-styles',
            'wp-block-library',
        ];

        $patterns = apply_filters('prorank_css_async_force_blocking_patterns', $default_patterns);
        if (!is_array($patterns)) {
            $patterns = $default_patterns;
        }

        foreach ($patterns as $pattern) {
            if (!is_string($pattern) || $pattern === '') {
                continue;
            }
            $pattern = strtolower($pattern);
            if (($handle !== '' && strpos($handle, $pattern) !== false) ||
                ($href !== '' && strpos($href, $pattern) !== false)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Keep layout-critical Elementor/Neve header styles render-blocking.
     *
     * These handles control responsive visibility and header/search/nav layout.
     * Async loading them causes large CLS on Elementor header stacks.
     *
     * @param string $handle
     * @param string $href
     * @return bool
     */
    private function is_layout_sensitive_elementor_stylesheet(string $handle, string $href): bool {
        $is_widget_image_variant = strpos($handle, 'widget-image-') !== false || strpos($href, '/widget-image-') !== false;
        $is_base_widget_image = !$is_widget_image_variant && (
            strpos($handle, 'widget-image') !== false ||
            strpos($href, '/elementor/assets/css/widget-image.') !== false
        );
        if ($is_base_widget_image) {
            return true;
        }

        $patterns = [
            'neve-style',
            'elementor-frontend',
            'widget-nav-menu',
            'widget-search-form',
            'widget-heading',
            'neve-elementor-widgets-styles',
            'neve-blog-pro',
            'prorank-elementor-widgets',
            '/themes/neve/',
            '/elementor/assets/css/frontend',
            '/elementor/assets/css/widget-heading',
            '/elementor-pro/assets/css/widget-nav-menu',
            '/elementor-pro/assets/css/widget-search-form',
            '/neve-pro-addon/includes/modules/blog_pro/assets/style',
            '/neve-pro-addon/includes/modules/elementor_booster/assets/css/style',
            '/prorank-seo-premium/includes/integrations/elementor/assets/css/elementor-widgets.css',
            '/prorank-seo-free/includes/integrations/elementor/assets/css/elementor-widgets.css',
        ];

        foreach ($patterns as $pattern) {
            if (($handle !== '' && strpos($handle, $pattern) !== false) ||
                ($href !== '' && strpos($href, $pattern) !== false)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Extract Elementor post CSS ID from handle/href.
     *
     * Supports:
     * - handle: elementor-post-123
     * - href:   .../uploads/elementor/css/post-123.css
     *
     * @param string $handle
     * @param string $href
     * @return int|null
     */
    private function extract_elementor_post_id(string $handle, string $href): ?int {
        if (preg_match('/elementor-post-(\d+)/i', $handle, $m)) {
            return isset($m[1]) ? (int) $m[1] : null;
        }

        if (preg_match('#/uploads/elementor/css/post-(\d+)\.css#i', $href, $m)) {
            return isset($m[1]) ? (int) $m[1] : null;
        }

        return null;
    }

    /**
     * Check if a stylesheet is font-related.
     *
     * Font stylesheets must remain render-blocking so the browser discovers
     * @font-face declarations early. Async font CSS causes font-swap CLS.
     *
     * @param string $handle Style handle
     * @param string $href   Style source URL
     * @return bool
     */
    private function is_font_stylesheet(string $handle, string $href): bool {
        // Icon packs are not text-layout critical. Keeping them render-blocking
        // adds significant CSS delay on many Elementor sites. Let them load async.
        if ($this->is_icon_font_stylesheet($handle, $href)) {
            return false;
        }

        $indicators = ['font', 'gf-', 'typekit', 'adobe-fonts'];
        foreach ($indicators as $indicator) {
            if (stripos($handle, $indicator) !== false) {
                return true;
            }
        }
        if (stripos($href, '/fonts/') !== false || stripos($href, 'fonts.googleapis.com') !== false) {
            return true;
        }
        return false;
    }

    /**
     * Detect icon-font stylesheets that are safe to load asynchronously.
     *
     * @param string $handle
     * @param string $href
     * @return bool
     */
    private function is_icon_font_stylesheet(string $handle, string $href): bool {
        $haystack = strtolower($handle . ' ' . $href);
        if ($haystack === '') {
            return false;
        }

        $indicators = [
            'font-awesome',
            'fontawesome',
            'fa-solid',
            'fa-brands',
            'eicons',
            'elementor-icons',
            'dashicons',
        ];

        foreach ($indicators as $indicator) {
            if (strpos($haystack, $indicator) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Inline a small local CSS file to eliminate a render-blocking request.
     *
     * @param string $handle    Style handle
     * @param string $href      Style URL
     * @param string $media     Media attribute
     * @param int    $max_bytes Maximum file size to inline
     * @return string|null      Inline style tag, or null when not eligible
     */
    private function try_inline_small_css(string $handle, string $href, string $media, int $max_bytes = 5000): ?string {
        $path = $this->url_to_path($href);
        if ($path === null || !is_readable($path)) {
            return null;
        }

        $size = filesize($path);
        if ($size === false || $size <= 0) {
            return null;
        }

        if ($max_bytes < 256) {
            $max_bytes = 256;
        }

        if ($size > $max_bytes) {
            return null; // Too large to inline
        }

        $css = trim((string) file_get_contents($path));

        // Suppress empty CSS files entirely — no reason to load an empty file
        if ($css === '') {
            return '<!-- ProRank: skipped empty ' . esc_html($handle) . ' -->';
        }

        // Prevent premature </style> termination.
        $css = str_ireplace('</style', '<\\/style', $css);

        $media_attr = ($media !== '' && strtolower($media) !== 'all')
            ? ' media="' . esc_attr($media) . '"'
            : '';

        return sprintf(
            '<style id="%s-inline-css" data-prorank-inline-css="true"%s>%s</style>',
            esc_attr($handle),
            $media_attr,
            $css // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted local CSS file content
        );
    }

    /**
     * Convert URL to file path
     *
     * @param string $url URL to convert
     * @return string|null File path or null
     */
    private function url_to_path(string $url): ?string {
        $url = trim($url);
        if ($url === '') {
            return null;
        }

        // Strip query/fragment and normalize protocol-relative URLs.
        if (strpos($url, '//') === 0) {
            $url = (is_ssl() ? 'https:' : 'http:') . $url;
        }
        $url = strtok($url, '?#');
        if (!is_string($url) || $url === '') {
            return null;
        }

        $parsed = wp_parse_url($url);
        if (!is_array($parsed) || empty($parsed['path']) || !is_string($parsed['path'])) {
            return null;
        }

        // Only resolve local-host assets; ignore third-party URLs.
        $asset_host = isset($parsed['host']) ? strtolower((string) $parsed['host']) : '';
        if ($asset_host !== '') {
            $site_hosts = [];
            $home_host = wp_parse_url(home_url('/'), PHP_URL_HOST);
            $site_host = wp_parse_url(site_url('/'), PHP_URL_HOST);
            if (is_string($home_host) && $home_host !== '') {
                $site_hosts[] = strtolower($home_host);
            }
            if (is_string($site_host) && $site_host !== '') {
                $site_hosts[] = strtolower($site_host);
            }

            $normalized_asset_host = preg_replace('/^www\./', '', $asset_host);
            $matched = false;
            foreach (array_unique($site_hosts) as $site_host_value) {
                $normalized_site_host = preg_replace('/^www\./', '', $site_host_value);
                if ($asset_host === $site_host_value || $normalized_asset_host === $normalized_site_host) {
                    $matched = true;
                    break;
                }
            }
            if (!$matched) {
                return null;
            }
        }

        $path = rawurldecode((string) $parsed['path']);
        if ($path === '') {
            return null;
        }

        // Handle installs in subdirectories by removing site path prefix first.
        $site_path = wp_parse_url(home_url('/'), PHP_URL_PATH);
        if (!is_string($site_path) || $site_path === '') {
            $site_path = '/';
        }
        if (substr($site_path, -1) !== '/') {
            $site_path .= '/';
        }
        if ($site_path !== '/' && strpos($path, $site_path) === 0) {
            $path = '/' . ltrim(substr($path, strlen($site_path)), '/');
        }

        $candidate = wp_normalize_path(trailingslashit(ABSPATH) . ltrim($path, '/'));
        $abspath = wp_normalize_path(trailingslashit(ABSPATH));
        if (strpos($candidate, $abspath) !== 0) {
            return null;
        }

        if (file_exists($candidate)) {
            return $candidate;
        }

        return null;
    }
    
    /**
     * Check if async is enabled
     *
     * @return bool
     */
    private function is_async_enabled(): bool {
        if (!(bool) $this->get_setting('css_async', false)) {
            return false;
        }

        // Runtime guardrail: if recent field data shows high CLS, automatically
        // fall back to blocking CSS for the current path.
        if ($this->should_disable_async_due_to_cls_guardrail()) {
            return false;
        }

        // Safe partial-async mode:
        // even when page-specific critical CSS cannot be validated, we can still
        // async non-layout/plugin styles while keeping theme/builder/layout CSS
        // blocking via should_keep_style_blocking().
        $require_critical = (bool) apply_filters('prorank_css_async_require_critical', false);
        if ($require_critical && !$this->has_critical_css_content()) {
            return false;
        }

        return true;
    }

    /**
     * Disable async CSS when recent RUM data shows unstable layout (high CLS).
     *
     * This protects real users from prolonged layout shift after an aggressive
     * optimization change or incomplete critical CSS generation.
     *
     * @return bool
     */
    private function should_disable_async_due_to_cls_guardrail(): bool {
        if ($this->cls_guardrail_triggered !== null) {
            return $this->cls_guardrail_triggered;
        }

        $enabled = (bool) apply_filters('prorank_css_async_cls_guardrail_enabled', true);
        if (!$enabled) {
            $this->cls_guardrail_triggered = false;
            return false;
        }

        $buffer = get_option('prorank_rum_buffer', []);
        if (!is_array($buffer) || count($buffer) < 5) {
            $this->cls_guardrail_triggered = false;
            return false;
        }

        $path = $this->normalize_cls_guardrail_path((string) (wp_parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?? '/'));
        $min_samples = max(3, (int) apply_filters('prorank_css_async_cls_guardrail_min_samples', 8, $path));
        $max_samples = max($min_samples, (int) apply_filters('prorank_css_async_cls_guardrail_max_samples', 24, $path));

        $samples = [];
        foreach ($buffer as $sample) {
            if (!is_array($sample) || !isset($sample['cls'])) {
                continue;
            }

            $sample_path = $this->normalize_cls_guardrail_path((string) ($sample['url'] ?? '/'));
            if (!$this->cls_guardrail_paths_match($path, $sample_path)) {
                continue;
            }

            $cls = max(0.0, min(10.0, (float) $sample['cls']));
            $samples[] = $cls;
            if (count($samples) >= $max_samples) {
                break;
            }
        }

        if (count($samples) < $min_samples) {
            $this->cls_guardrail_triggered = false;
            return false;
        }

        sort($samples);
        $count = count($samples);
        $p75_index = (int) floor(($count - 1) * 0.75);
        $p75 = (float) ($samples[$p75_index] ?? 0.0);
        $avg = array_sum($samples) / $count;

        $p75_threshold = (float) apply_filters('prorank_css_async_cls_guardrail_p75_threshold', 0.20, $path);
        $avg_threshold = (float) apply_filters('prorank_css_async_cls_guardrail_avg_threshold', 0.12, $path);

        $this->cls_guardrail_triggered = ($p75 > $p75_threshold) || ($avg > $avg_threshold);

        return $this->cls_guardrail_triggered;
    }

    /**
     * Normalize RUM URL path for guardrail comparison.
     *
     * @param string $path
     * @return string
     */
    private function normalize_cls_guardrail_path(string $path): string {
        $path = trim($path);
        if ($path === '') {
            return '/';
        }
        $path = '/' . ltrim($path, '/');
        return rtrim($path, '/') ?: '/';
    }

    /**
     * Compare current request path with sampled RUM path.
     *
     * @param string $current_path
     * @param string $sample_path
     * @return bool
     */
    private function cls_guardrail_paths_match(string $current_path, string $sample_path): bool {
        if ($current_path === $sample_path) {
            return true;
        }

        if ($current_path === '/') {
            return $sample_path === '/' || $sample_path === '';
        }

        return false;
    }

    /**
     * Check if *usable* critical CSS content exists for the current request.
     *
     * For critical CSS to actually prevent FOUC (and thus CLS), it must contain
     * selectors that match the above-the-fold page structure: body, header, nav,
     * hero sections, etc. Plugin-specific styles (SureCart, WooCommerce widgets)
     * do NOT prevent layout shift on the visible page.
     *
     * @return bool
     */
    private function has_critical_css_content(): bool {
        // Use instance property instead of static to avoid cache poisoning when
        // called early (before WP main query) during init_hooks().
        if ($this->critical_css_check !== null) {
            return $this->critical_css_check;
        }

        // Build legacy generator candidate keys (mirrors CriticalCssModule logic)
        $candidates = [];
        if (is_front_page()) {
            $candidates[] = 'homepage';
            $candidates[] = 'front_page';
            $front_page_id = (int) get_option('page_on_front');
            if ($front_page_id > 0) {
                $candidates[] = 'page_' . $front_page_id;
            }
        } elseif (is_home()) {
            $candidates[] = 'home';
            $candidates[] = 'blog';
            $candidates[] = 'homepage';
            $posts_page_id = (int) get_option('page_for_posts');
            if ($posts_page_id > 0) {
                $candidates[] = 'page_' . $posts_page_id;
            }
        } elseif (is_singular()) {
            $post_id = (int) get_queried_object_id();
            if ($post_id > 0) {
                $post_type = get_post_type($post_id);
                if (is_string($post_type) && $post_type !== '') {
                    $candidates[] = 'single_' . $post_type;
                    $candidates[] = $post_type . '_' . $post_id;
                }

                // Include special front/posts aliases for static homepage setups.
                if ($post_id === (int) get_option('page_on_front')) {
                    $candidates[] = 'homepage';
                    $candidates[] = 'front_page';
                }
                if ($post_id === (int) get_option('page_for_posts')) {
                    $candidates[] = 'blog';
                    $candidates[] = 'home';
                }
            }
        } elseif (is_archive()) {
            $candidates[] = 'archive';
        } elseif (is_404()) {
            $candidates[] = '404';
        }

        // Fallback for edge cases where theme/plugin timing causes WP conditionals
        // to be unresolved when style filters run on the homepage.
        if (empty($candidates) && $this->is_likely_home_request()) {
            $candidates[] = 'homepage';
            $candidates[] = 'front_page';
            $front_page_id = (int) get_option('page_on_front');
            if ($front_page_id > 0) {
                $candidates[] = 'page_' . $front_page_id;
            }
        }

        $candidates = $this->expand_critical_css_context_candidates($candidates);

        // Don't cache when called before WP main query is ready (candidates will be empty)
        if (empty($candidates) && !did_action('wp')) {
            return false;
        }

        // Check stored critical CSS — it must contain actual above-the-fold layout rules
        $css_map = $this->get_generated_critical_css_map();
        if (!empty($css_map)) {
            foreach ($candidates as $key) {
                $css = $css_map[$key] ?? '';
                if (is_string($css) && $css !== '' && $this->is_layout_critical_css($css)) {
                    $this->critical_css_check = true;
                    return true;
                }
            }

            // Fallback for real-world homepage/front-page requests where WP conditional
            // timing can produce non-matching context candidates during style rendering.
            // If homepage/front-page/default critical CSS is structurally valid, allow async.
            if ($this->is_likely_home_request()) {
                foreach (['homepage', 'front_page', 'home', 'default'] as $key) {
                    $css = $css_map[$key] ?? '';
                    if (is_string($css) && $css !== '' && $this->is_layout_critical_css($css)) {
                        $this->critical_css_check = true;
                        return true;
                    }
                }
            }
        }

        // No usable layout-critical CSS → keep CSS render-blocking to prevent CLS
        $this->critical_css_check = false;
        return false;
    }

    /**
     * Expand context keys to cover legacy/new generator aliases.
     *
     * @param array<int,string> $candidates
     * @return array<int,string>
     */
    private function expand_critical_css_context_candidates(array $candidates): array {
        if (empty($candidates)) {
            return [];
        }

        $alias_map = [
            'homepage'   => ['front_page', 'home'],
            'front_page' => ['homepage', 'home'],
            'home'       => ['blog', 'homepage', 'front_page'],
            'blog'       => ['home'],
        ];

        $expanded = [];
        foreach ($candidates as $candidate) {
            if (!is_string($candidate) || $candidate === '') {
                continue;
            }

            $expanded[] = $candidate;

            if (isset($alias_map[$candidate])) {
                foreach ($alias_map[$candidate] as $alias) {
                    $expanded[] = $alias;
                }
            }
        }

        return array_values(array_unique($expanded));
    }

    /**
     * Detect whether the current request URL is the site home URL.
     *
     * Helps when WP conditionals are unavailable/late during style filtering.
     *
     * @return bool
     */
    private function is_likely_home_request(): bool {
        $request_uri = $_SERVER['REQUEST_URI'] ?? '';
        if (!is_string($request_uri) || $request_uri === '') {
            return false;
        }

        $request_path = (string) (wp_parse_url($request_uri, PHP_URL_PATH) ?: '/');
        $home_path = (string) (wp_parse_url(home_url('/'), PHP_URL_PATH) ?: '/');

        $normalize = static function (string $path): string {
            $normalized = '/' . trim($path, '/');
            return $normalized === '//' ? '/' : $normalized;
        };

        return $normalize($request_path) === $normalize($home_path);
    }

    /**
     * Check if critical CSS covers the above-the-fold page layout.
     *
     * Just having CSS rules is not enough — they must target elements that form
     * the visible page structure (body, header, nav, container, hero, etc.).
     * Plugin-only styles (SureCart, WooCommerce blocks) won't prevent FOUC.
     *
     * @param string $css
     * @return bool
     */
    private function is_layout_critical_css(string $css): bool {
        $normalized = preg_replace('/\s+/', '', strtolower($css));

        // Async CSS causes CLS if the critical CSS doesn't cover the FULL
        // above-fold layout. We group structural selectors by role — the
        // critical CSS must cover ALL groups, not just one.
        $groups = [
            'body'       => ['body{', 'body,', 'body.'],
            'wrapper'    => ['#page{', '#page,', '.hfeed', '.site{', '.site,', '.wrapper', '#wrapper', '.page-container', '.e-con'],
            'header'     => ['.site-header', 'header{', 'header,', '.ast-header-', '.nv-header', '.elementor-location-header'],
            'navigation' => ['nav{', 'nav,', 'nav.', '.navigation', '.menu{', '.nav-menu', '.primary-menu', '.menu-item', '.nv-nav'],
            'content'    => ['.entry-content', '.site-content', '#primary', '.content-area', '.site-main', '.nv-content', '.neve-main', 'main{', '.elementor-section', '.elementor-widget'],
        ];

        $covered = 0;
        foreach ($groups as $patterns) {
            foreach ($patterns as $pattern) {
                if (strpos($normalized, strtolower(str_replace(' ', '', $pattern))) !== false) {
                    $covered++;
                    break;
                }
            }
        }

        // Guardrail: require substantial structural coverage, but keep this
        // threshold configurable for theme ecosystems with different markup.
        $required_groups = (int) apply_filters(
            'prorank_css_async_required_critical_groups',
            3,
            $covered,
            $groups,
            $css
        );
        $required_groups = max(1, min(count($groups), $required_groups));

        if ($covered < $required_groups) {
            return false;
        }

        // Must be substantial — tiny critical CSS means most layout rules
        // are in the deferred stylesheet and CLS risk increases.
        $min_critical_bytes = (int) apply_filters(
            'prorank_css_async_min_critical_bytes',
            4000,
            $covered,
            $css
        );
        $min_critical_bytes = max(0, $min_critical_bytes);

        if (strlen($css) < $min_critical_bytes) {
            return false;
        }

        // Prefer responsive media queries when present, but don't hard-fail
        // themes/builders that emit mostly mobile-safe base rules without
        // explicit @media(max-width) blocks in extracted critical CSS.
        $has_responsive_media = preg_match('/@media[^{]*\((max|min)-width/', $normalized) === 1;
        if (!$has_responsive_media) {
            $responsive_signals = [
                'max-width:',
                'min-width:',
                'width:100%',
                'display:flex',
                'display:grid',
                'grid-template',
                'flex-wrap',
                'object-fit:',
                'aspect-ratio:',
                'clamp(',
                'vw',
            ];

            $signal_hits = 0;
            foreach ($responsive_signals as $signal) {
                if (strpos($normalized, $signal) !== false) {
                    $signal_hits++;
                    if ($signal_hits >= 3) {
                        break;
                    }
                }
            }

            if ($signal_hits < 3) {
                return false;
            }
        }

        return true;
    }
    
    /**
     * Check if beta features are enabled
     *
     * @return bool
     */
    private function is_beta_enabled(): bool {
        if ($this->beta_enabled === null) {
            $plugin = $this->plugin();
            $settings = $plugin->settings();
            $this->beta_enabled = (bool) $settings->get('performance_beta', false);
        }
        
        return $this->beta_enabled;
    }
    
    /**
     * Initialize background tasks
     *
     * @return void
     */
    private function init_background_tasks(): void {
        // Skip if Action Scheduler is not available
        if (!function_exists('as_enqueue_async_action')) {
            return;
        }

        // Background handlers should run only when module is enabled.
        if (!$this->is_enabled()) {
            return;
        }
        
        try {
            $has_scheduled = function (string $hook): bool {
                if (function_exists('as_has_scheduled_action')) {
                    return (bool) as_has_scheduled_action($hook);
                }
                if (function_exists('as_next_scheduled_action')) {
                    return (bool) as_next_scheduled_action($hook);
                }
                return false;
            };

            // Initialize cache generation task
            $needs_cache_generation = (bool) ($this->get_setting('css_minify', false) || $this->get_setting('css_combine', false));
            if (!$needs_cache_generation) {
                $needs_cache_generation = $has_scheduled(CssCacheGenerationTask::HOOK_NAME);
            }

            if ($needs_cache_generation) {
                if ($this->cache_generation_task === null) {
                    $this->cache_generation_task = new CssCacheGenerationTask();
                }
                $this->cache_generation_task->init();
            }
            
            // Initialize external CSS download task
            $needs_external_download = (bool) $this->get_setting('css_external_cache', true);
            if (!$needs_external_download) {
                $needs_external_download = $has_scheduled(ExternalCssDownloadTask::HOOK_NAME)
                    || $has_scheduled(ExternalCssDownloadTask::HOOK_NAME . '_retry');
            }

            if ($needs_external_download) {
                if ($this->external_download_task === null) {
                    $this->external_download_task = new ExternalCssDownloadTask();
                }
                $this->external_download_task->init();
            }
            
            // Initialize cleanup task
            if ($this->cleanup_task === null) {
                $this->cleanup_task = new CssCacheCleanupTask();
            }
            $this->cleanup_task->init();
            
        } catch (Exception $e) {
            prorank_log('ProRank CSS: Failed to initialize background tasks - ' . $e->getMessage());
        }
    }
    
    /**
     * Queue CSS files for background processing
     *
     * @param array $files Array of file paths
     * @return void
     */
    public function queue_css_files(array $files): void {
        if ($this->cache_generation_task) {
            $this->cache_generation_task->queue_files($files);
        }
    }
    
    /**
     * Queue external CSS URLs for download
     *
     * @param array $urls Array of URL data
     * @return void
     */
    public function queue_external_urls(array $urls): void {
        if ($this->external_download_task) {
            $this->external_download_task->queue_urls($urls);
        }
    }
    
    /**
     * Initialize CSS cache server
     *
     * @return void
     */
    private function init_cache_server(): void {
        try {
            // Initialize cache server for serving optimized CSS
            if ($this->get_setting('css_minify', false) || 
                $this->get_setting('css_combine', false) ||
                $this->get_setting('css_external_cache', true)) {
                
                $this->cache_server = new CssCacheServer();
                $this->cache_server->init();
            }
        } catch (Exception $e) {
            prorank_log('ProRank CSS: Failed to initialize cache server - ' . $e->getMessage());
        }
    }
}
