<?php

/**
 * Modern Cache Module
 *
 * Advanced caching with Brotli, edge support, and real-time stats
 *
 * @package ProRank\SEO\Modules\Performance
 * @since   2.0.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Modules\Performance;

use ProRank\SEO\Modules\BaseModule;
use WP_Filesystem_Base;

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


/**
 * ModernCache class
 */


class ModernCacheModule extends BaseModule {

    /**
     * Required tier for this module
     */
    protected string $feature_tier = 'free';
    
    /**
     * Cache directory path.
     *
     * @var string
     */
    private string $cache_dir;

    /**
     * Stats file path.
     *
     * @var string
     */
    private string $stats_file;
    
    /**
     * Edge providers configuration
     *
     * @var array
     */
    /**
     * Recurring keep-warm cron hook (B2a).
     */
    private const KEEP_WARM_HOOK = 'prorank_cache_keep_warm';

    /**
     * Custom cron schedule used for periodic cache warming.
     */
    private const KEEP_WARM_SCHEDULE = 'prorank_quarter_hourly';

    /** B2b: bounded UCSS cache-skip (MVP, no queue coupling). Never infinite. */
    private const UCSS_SKIP_MAX_CONSECUTIVE = 3;
    private const UCSS_SKIP_REBUILD_WINDOW  = 900;   // seconds
    private const UCSS_SKIP_STATE_OPTION     = 'prorank_ucss_skip_state';
    private const UCSS_SKIP_STALE_PRUNE      = 604800; // 7 days
    private const UCSS_SKIP_MAX_ENTRIES      = 2000;

    private const EDGE_PROVIDERS = [
        'cloudflare' => [
            'name' => 'Cloudflare',
            'api_endpoint' => 'https://api.cloudflare.com/client/v4/',
            'locations' => 275,
        ],
        'fastly' => [
            'name' => 'Fastly',
            'api_endpoint' => 'https://api.fastly.com/',
            'locations' => 145,
        ],
        'bunny' => [
            'name' => 'BunnyCDN',
            'api_endpoint' => 'https://api.bunny.net/',
            'locations' => 123,
        ],
    ];
    
    /**
     * Cache statistics
     *
     * @var array
     */
    private array $stats = [
        'hits' => 0,
        'misses' => 0,
        'bytes_saved' => 0,
        'response_times' => [],
    ];
    
    /**
     * Buffer started flag
     *
     * @var bool
     */
    private bool $buffer_started = false;

    /**
     * Cache saved flag (prevents duplicate writes if buffers flush multiple times)
     *
     * @var bool
     */
    private bool $cache_saved = false;

    /**
     * Accumulates chunked output-buffer callbacks until final flush.
     *
     * @var string
     */
    private string $buffer_capture_accumulator = '';

    /**
     * Whether initialize_stats() has already run this request.
     *
     * @var bool
     */
    private bool $stats_initialized = false;
    
    /**
     * Request start time
     *
     * @var float
     */
    private float $request_start;

    /**
     * Last cache rejection/miss reason for diagnostics.
     *
     * @var string
     */
    private string $last_cache_reason = 'unknown';

    /**
     * Lazily-built cache tag index (post/term/author -> cached files).
     *
     * @var CacheTagIndex|null
     */
    private ?CacheTagIndex $tag_index = null;

    /**
     * Tag index accessor (file lives inside the cache directory so a full
     * directory clear resets it automatically).
     *
     * @return CacheTagIndex
     */
    private function tag_index(): CacheTagIndex {
        if ($this->tag_index === null) {
            $this->tag_index = new CacheTagIndex($this->cache_dir . '/tag-index.json');
        }
        return $this->tag_index;
    }

    /**
     * Constructor
     */
    public function __construct() {
        $this->slug = 'modern_cache';
        $this->name = 'Modern Cache';
        $this->description = 'Advanced caching with Brotli, edge network, and AI predictions';
$this->parent_slug = 'performance';
        $this->request_start = microtime(true);

        $upload_dir = wp_upload_dir();
        $this->cache_dir = trailingslashit($upload_dir['basedir']) . 'prorank-cache/html';
        $this->stats_file = trailingslashit($upload_dir['basedir']) . 'prorank-cache/modern-cache-stats.json';
    }

    /**
     * Check if module should run
     *
     * @return bool
     */
    private function should_run(): bool {
        // Don't run in admin
        if (is_admin()) {
            return false;
        }

        // Don't run for logged-in users unless explicitly enabled with a role/user variant.
        if (is_user_logged_in() && !$this->should_cache_logged_in_request()) {
            return false;
        }

        // Don't run on specific request types
        if (wp_doing_ajax() || wp_doing_cron() || is_preview()) {
            return false;
        }

        return $this->is_enabled();
    }

    /**
     * Check if current request should be cached
     *
     * @return bool
     */
    private function should_cache(): bool {
        $this->last_cache_reason = 'unknown';

        // Don't cache if module shouldn't run
        if (!$this->should_run()) {
            $this->last_cache_reason = 'module_not_runnable';
            return false;
        }

        // Respect cache enable toggle
        $cache_enabled = (bool) $this->get_setting('cache_enabled', $this->get_setting('enabled', false));
        if (!$cache_enabled) {
            $this->last_cache_reason = 'cache_disabled';
            return false;
        }

        // Only cache GET and HEAD requests
        if (!in_array(prorank_get_request_method(), ['GET', 'HEAD'], true)) {
            $this->last_cache_reason = 'non_get_request';
            return false;
        }

        // Don't cache if query string present (unless configured)
        if (prorank_has_query_params() && !$this->get_setting('cache_query_strings', false)) {
            $this->last_cache_reason = 'query_string';
            return false;
        }

        // Don't cache 404 pages
        if (is_404()) {
            $this->last_cache_reason = '404_page';
            return false;
        }

        // Don't cache search results
        if (is_search()) {
            $this->last_cache_reason = 'search_page';
            return false;
        }

        // Check excluded URLs
        if ($this->is_url_excluded()) {
            $this->last_cache_reason = 'excluded_url';
            return false;
        }

        // Check excluded taxonomy terms / post types
        $taxonomy_reason = $this->matched_taxonomy_exclusion();
        if ($taxonomy_reason !== '') {
            $this->last_cache_reason = $taxonomy_reason;
            return false;
        }

        // Respect the standard cache-bypass constant (set by WooCommerce shim,
        // other plugins, or themes that must never be cached).
        if (defined('DONOTCACHEPAGE') && DONOTCACHEPAGE) {
            $this->last_cache_reason = 'donotcachepage';
            return false;
        }

        // Extension point: compatibility shims (e.g. WooCommerce cart/checkout)
        // can veto caching of dynamic requests without editing this method.
        if (!apply_filters('prorank_should_cache_page', true, $this)) {
            $this->last_cache_reason = 'filtered_excluded';
            return false;
        }

        $this->last_cache_reason = 'cacheable';
        return true;
    }

    /**
     * Initialize module hooks
     *
     * @return void
     */
    public function init_hooks(): void {
        // WooCommerce cache-safety shim (self-gating: no-op unless WooCommerce
        // is active AND the safety toggle is on). Registered first so its
        // should-cache veto is in place before any cache serve/store runs.
        \ProRank\SEO\Core\Compatibility\WooCommercePerformanceCompatibility::boot();

        // Always register cache invalidation + admin hooks (wp-admin runs with logged-in users,
        // but we still need settings changes to purge cached pages).
        // B2a: keep-warm cron (15-min) — gated by cache_enabled + preload_enabled;
        // uses HEAD prime_core_cache_urls(). init_hooks() runs at/after the
        // 'init' action (via BaseModule boot), so an add_action('init', …)
        // here would register too late to fire this request. Call the
        // (gate-checked, idempotent) scheduler directly once, and ALSO keep
        // the 'init' hook so a later same-request gate enable still self-heals.
        add_filter('cron_schedules', [$this, 'add_keep_warm_schedule']);
        add_action(self::KEEP_WARM_HOOK, [$this, 'run_keep_warm']);
        add_action('init', [$this, 'maybe_schedule_keep_warm']);
        $this->maybe_schedule_keep_warm();
        add_action('update_option_prorank_cache_settings', [$this, 'handle_settings_update'], 10, 3);
        add_action('update_option_prorank_asset_optimization_settings', [$this, 'handle_settings_update'], 10, 3);
        add_action('update_option_prorank_image_optimization_settings', [$this, 'handle_settings_update'], 10, 3);
        // Keep cache in sync when module toggles or Script Manager rules change.
        // Without this, stale cached HTML can persist after disabling a feature.
        add_action('update_option_prorank_settings', [$this, 'handle_settings_update'], 10, 3);
        add_action('update_option_prorank_seo_module_groups', [$this, 'handle_settings_update'], 10, 3);
        add_action('update_option_prorank_script_rules', [$this, 'handle_settings_update'], 10, 3);

        add_action('switch_theme', [$this, 'clear_all_cache']);
        add_action('save_post', [$this, 'smart_cache_invalidation']);
        add_action('transition_post_status', [$this, 'handle_post_status_change'], 10, 3);
        add_action('before_delete_post', [$this, 'handle_post_delete']);

        add_action('admin_bar_menu', [$this, 'add_admin_bar_info'], 100);
        add_action('admin_post_prorank_clear_cache', [$this, 'handle_clear_cache_request']);
        add_action('prorank_ucss_queue_item_completed', [$this, 'handle_ucss_queue_item_completed'], 10, 3);

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

        $this->ensure_cache_infrastructure();
        
        // Core caching hooks
        // IMPORTANT: run at priority 0 so our cache buffer wraps other output-buffer optimizers
        // (image delivery, font-display, etc.). Otherwise cached HTML misses those optimizations.
        add_action('template_redirect', [$this, 'serve_cached_page'], 0);
        
        // HTTP/3 and Early Hints (if server supports)
        add_action('send_headers', [$this, 'send_early_hints'], 1);
        add_filter('wp_headers', [$this, 'add_modern_headers']);
        
        // Edge cache hooks
        if ($this->get_setting('edge_cache_enabled', false)) {
            add_action('prorank_cache_saved', [$this, 'push_to_edge']);
            add_action('prorank_cache_cleared', [$this, 'purge_edge_cache']);
        }
        
        // AI Predictive preloading
        if ($this->get_setting('preload_ai_enabled', false)) {
            add_action('wp_footer', [$this, 'inject_ai_preloader']);
        }
        
        // Statistics tracking
        add_action('init', [$this, 'initialize_stats']);
        add_action('shutdown', [$this, 'save_stats']);
    }

    /**
     * Purge cached HTML for a URL after UCSS finishes, so future visits (and PSI/Lighthouse)
     * see the optimized stylesheet.
     *
     * @param string $url     URL that was analyzed.
     * @param array  $options Analysis options.
     * @param mixed  $result  Analysis result.
     * @return void
     */
    public function handle_ucss_queue_item_completed(string $url, array $options = [], $result = null): void {
        $site_host = (string) (wp_parse_url(home_url(), PHP_URL_HOST) ?? '');
        $url_host = (string) (wp_parse_url($url, PHP_URL_HOST) ?? '');

        if ($site_host !== '' && $url_host !== '' && strtolower($site_host) !== strtolower($url_host)) {
            return;
        }

        $path = (string) (wp_parse_url($url, PHP_URL_PATH) ?? '');
        if ($path !== '' && preg_match('~\\.(?:json|xml|txt|yml|yaml|ini|log|sql|bak|zip|tar|gz|rar|7z|php|phtml|asp|aspx|jsp|env|git|ds_store)$~i', $path)) {
            return;
        }

        $this->clear_url_cache($url);
    }

    /**
     * Handle settings update
     * 
     * @param mixed $old_value Old value
     * @param mixed $value New value
     * @param string $option Option name
     */
    public function handle_settings_update($old_value, $value, string $option): void {
        if ($old_value === $value) {
            return;
        }

        // Keep drop-in behavior in sync with settings that affect cache keys.
        if (class_exists('\ProRank\SEO\Cache\AdvancedCache')
            && in_array($option, ['prorank_cache_settings', 'prorank_image_optimization_settings'], true)) {
            \ProRank\SEO\Cache\AdvancedCache::install();
        }

        if ($option === 'prorank_cache_settings') {
            $this->install_htaccess_rules();
        }

        $this->clear_all_cache();
        do_action('prorank_clear_cache');
    }

    /**
     * Handle manual cache clear from wp-admin (admin-post.php?action=prorank_clear_cache).
     *
     * @return void
     */
    public function handle_clear_cache_request(): void {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('You do not have permission to clear the cache.', 'prorank-seo'));
        }

        check_admin_referer('prorank_clear_cache');

        $this->clear_all_cache();
        do_action('prorank_clear_cache');

        $redirect = wp_get_referer();
        if (!$redirect) {
            $redirect = admin_url('admin.php?page=prorank-performance');
        }

        wp_safe_redirect($redirect);
        exit;
    }
    
    /**
     * Ensure cache infrastructure is ready
     */
    private function ensure_cache_infrastructure(): void {
        $settings = get_option('prorank_cache_settings', []);
        $cache_enabled = !empty($settings['cache_enabled']) || !empty($settings['enabled']);
        if (!$cache_enabled) {
            return;
        }

        if (class_exists('\ProRank\SEO\Cache\AdvancedCache')) {
            $status = \ProRank\SEO\Cache\AdvancedCache::get_status();
            $drop_in_missing = empty($status['drop_in_installed']);
            $empty_drop_in = file_exists(WP_CONTENT_DIR . '/advanced-cache.php') && filesize(WP_CONTENT_DIR . '/advanced-cache.php') === 0;
            $wp_cache_disabled = empty($status['wp_cache_enabled']);
            if ($drop_in_missing || $empty_drop_in || $wp_cache_disabled) {
                \ProRank\SEO\Cache\AdvancedCache::install();
            }
        }

        $this->ensure_cache_directory();
    }


    /**
     * Serve cached page if available
     *
     * @return void
     */
    public function serve_cached_page(): void {
        if (!$this->should_cache()) {
            $this->track_miss($this->last_cache_reason);
            return;
        }
        
        $cache_file = $this->get_cache_file_path();
        
        if (!file_exists($cache_file)) {
            $this->track_miss('file_missing');
            $this->start_buffer();
            return;
        }
        
        // Check cache lifetime
        $lifetime = (int) $this->get_setting('cache_lifetime', 3600);
        if (time() - filemtime($cache_file) > $lifetime) {
            $this->track_miss('cache_expired');
            $this->start_buffer();
            return;
        }
        
        // Determine best compression method (based on settings + Accept-Encoding + available files)
        $compression = $this->get_best_compression($cache_file);
        $file_to_serve = $cache_file;
        
        // Set appropriate headers
        $this->send_cache_headers($compression, $lifetime);
        
        // Serve compressed version if available
        if ($compression === 'br') {
            header('Content-Encoding: br');
            $file_to_serve = $cache_file . '.br';
        } elseif ($compression === 'gzip') {
            header('Content-Encoding: gzip');
            $file_to_serve = $cache_file . '.gz';
        }
        
        // Track statistics
        $this->track_hit(filesize($file_to_serve));
        
        // Serve the file
        \ProRank\SEO\Core\FilesystemHelper::output_file($file_to_serve);
        exit;
    }
    
    /**
     * Start output buffering
     *
     * @return void
     */
    public function start_buffer(): void {
        if (!$this->should_cache()) {
            return;
        }

        if ($this->buffer_started) {
            return;
        }
        
        // Use a handler so we can write the FINAL, fully-optimized HTML to disk.
        // (Other performance modules modify the output via their own buffers.)
        ob_start([$this, 'capture_buffer'], 0);
        $this->buffer_started = true;
    }

    /**
     * Output buffer handler for caching.
     * Saves the final HTML to disk while returning the unmodified output to the browser.
     *
     * @param string $content Buffered output
     * @param int    $phase   Output buffer phase (optional)
     * @return string
     */
    public function capture_buffer(string $content, int $phase = 0): string {
        if (!$this->buffer_started || $this->cache_saved) {
            return $content;
        }

        // Only cache successful HTML responses.
        if (!$this->should_cache() || $content === '') {
            return $content;
        }

        $status_code = function_exists('http_response_code') ? (int) http_response_code() : 200;
        if ($status_code !== 200) {
            return $content;
        }

        $candidate_payload = $content;

        // If PHP provides the phase flag, only write on END/FINAL and
        // accumulate chunked content until that point.
        if ($phase !== 0 && defined('PHP_OUTPUT_HANDLER_END') && defined('PHP_OUTPUT_HANDLER_FINAL')) {
            $is_final = (bool) ($phase & (PHP_OUTPUT_HANDLER_END | PHP_OUTPUT_HANDLER_FINAL));
            $this->buffer_capture_accumulator .= $content;
            if (!$is_final) {
                return $content;
            }
            $candidate_payload = $this->buffer_capture_accumulator;
            $this->buffer_capture_accumulator = '';
        } elseif (stripos($content, '<html') === false || stripos($content, '</html>') === false) {
            // Some stacks flush in chunks without reliable phase flags.
            // Accumulate and save once we have a complete document.
            $this->buffer_capture_accumulator .= $content;
            if (
                stripos($this->buffer_capture_accumulator, '<html') === false
                || stripos($this->buffer_capture_accumulator, '</html>') === false
            ) {
                return $content;
            }
            $candidate_payload = $this->buffer_capture_accumulator;
            $this->buffer_capture_accumulator = '';
        }

        // Basic completeness check to avoid caching partial flushes.
        if (stripos($candidate_payload, '<html') === false || stripos($candidate_payload, '</html>') === false) {
            return $content;
        }

        // Don't cache pages with payment forms — nonces become stale and break
        // payment submissions (Stripe intents, PayPal tokens, etc.).
        if ($this->has_payment_form_nonces($candidate_payload)) {
            $this->last_cache_reason = 'payment_form_nonces';
            return $content;
        }

        // B2b: bounded UCSS cache-skip (no queue coupling). 'skip' = don't
        // cache yet (within cap+window). 'cache_fallback' = bounds exhausted,
        // cache the heavier fallback HTML (degraded but served) + flag.
        // Never skips indefinitely.
        $ucss_decision = $this->ucss_skip_decision($candidate_payload);
        if ($ucss_decision === 'skip') {
            $this->last_cache_reason = 'ucss_artifact_miss';
            return $content;
        }
        if ($ucss_decision === 'cache_fallback') {
            $this->last_cache_reason = 'ucss_fallback_cached';
        }

        $this->cache_saved = true;
        $this->save_cache_content($candidate_payload);

        return $content;
    }
    
    /**
     * Save cache with multiple compression formats using atomic writes
     *
     * @param string $content HTML to cache
     * @return void
     */
    private function save_cache_content(string $content): void {
        try {
            // Add cache signature with performance metrics
            $generation_time = round((microtime(true) - $this->request_start) * 1000, 2);
            $signature = sprintf(
                "\n<!-- Cached by ProRank SEO | Generated: %s | Time: %sms | Compression: %s -->",
                wp_date('Y-m-d H:i:s'),
                $generation_time,
                $this->get_setting('cache_compression', 'gzip')
            );
            $cache_payload = $content . $signature;

            // Get cache file path
            $cache_file = $this->get_cache_file_path();
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('[ProRank Cache] cache_file=' . $cache_file);
            }

            // Ensure cache directory exists
            $dir_ok = $this->ensure_cache_directory();

            // Save uncompressed version using atomic write
            $write_ok = $this->atomic_write($cache_file, $cache_payload);

            // Save compressed versions based on settings
            $compression = $this->get_setting('cache_compression', 'gzip');

            if ($compression === 'gzip' || $compression === 'both') {
                $this->save_gzip($cache_file, $cache_payload);
            } else {
                \ProRank\SEO\Core\FilesystemHelper::delete_file($cache_file . '.gz');
            }

            if ($compression === 'brotli' || $compression === 'both') {
                $this->save_brotli($cache_file, $cache_payload);
            } else {
                \ProRank\SEO\Core\FilesystemHelper::delete_file($cache_file . '.br');
            }

            // Record which entities this cached page belongs to so a single
            // post/term/author change can purge it precisely (tag-based purge).
            $this->index_cache_tags($cache_file);

            // Trigger edge push if enabled
            do_action('prorank_cache_saved', $cache_file, $cache_payload);
        } catch (\Throwable $e) {
            prorank_log("ProRank SEO: ModernCache save_cache failed - " . $e->getMessage());
        }
    }

    /**
     * Atomic file write - prevents cache corruption on concurrent writes
     * Write to temp file, then atomic rename
     *
     * @param string $file_path Target file path
     * @param string $content   Content to write
     * @return bool
     */
    private function atomic_write(string $file_path, string $content): bool {
        $target_dir = dirname($file_path);
        if (!is_dir($target_dir) && !wp_mkdir_p($target_dir)) {
            return false;
        }

        // Create temp file in same directory (required for atomic rename)
        $temp_file = $file_path . '.tmp.' . uniqid('', true);

        // Write to temp file
        $written = file_put_contents($temp_file, $content);
        if ($written === false) {
            \ProRank\SEO\Core\FilesystemHelper::delete_file($temp_file);
            return false;
        }

        // Atomic \ProRank\SEO\Core\FilesystemHelper::move(on same filesystem this is atomic)
        $renamed = \ProRank\SEO\Core\FilesystemHelper::move($temp_file, $file_path);
        if (!$renamed) {
            // Fallback: try copy then delete
            if (@copy($temp_file, $file_path)) {
                \ProRank\SEO\Core\FilesystemHelper::delete_file($temp_file);
                return true;
            }
            \ProRank\SEO\Core\FilesystemHelper::delete_file($temp_file);
            return false;
        }

        return true;
    }
    
    /**
     * Save Gzip compressed version
     *
     * @param string $cache_file Base cache file path
     * @param string $content    Content to compress
     * @return void
     */
    private function save_gzip(string $cache_file, string $content): void {
        try {
            $target_dir = dirname($cache_file);
            if (!is_dir($target_dir) && !wp_mkdir_p($target_dir)) {
                return;
            }
            $gzip_content = gzencode($content, 9);
            if ($gzip_content !== false) {
                file_put_contents($cache_file . '.gz', $gzip_content);
            }
        } catch (\Throwable $e) {
            prorank_log('ProRank SEO: Failed to save gzip cache - ' . $e->getMessage());
        }
    }
    
    /**
     * Save Brotli compressed version
     *
     * @param string $cache_file Base cache file path
     * @param string $content    Content to compress
     * @return void
     */
    private function save_brotli(string $cache_file, string $content): void {
        try {
            if (function_exists('brotli_compress')) {
                $brotli_content = brotli_compress($content, 11);
                if ($brotli_content !== false) {
                    file_put_contents($cache_file . '.br', $brotli_content);
                    return;
                }
            }

        } catch (\Throwable $e) {
            prorank_log('ProRank SEO: Failed to save brotli cache - ' . $e->getMessage());
        }
    }
    
    /**
     * Check if system brotli command is available
     *
     * @return bool
     */
    private function can_use_system_brotli(): bool {
        return false;
    }
    
    /**
     * Get best compression method based on Accept-Encoding header
     *
     * @return string 'br', 'gzip', or 'none'
     */
    private function get_best_compression(string $cache_file): string {
        $accept = prorank_get_server_var('HTTP_ACCEPT_ENCODING');

        $setting = (string) $this->get_setting('cache_compression', 'gzip');
        $allows_brotli = ($setting === 'brotli' || $setting === 'both');
        $allows_gzip = ($setting === 'gzip' || $setting === 'both');

        if ($allows_brotli && strpos($accept, 'br') !== false && file_exists($cache_file . '.br')) {
            return 'br';
        }

        if ($allows_gzip && strpos($accept, 'gzip') !== false && file_exists($cache_file . '.gz')) {
            return 'gzip';
        }

        return 'none';
    }
    
    /**
     * Send cache headers optimized for CDN
     * 
     * REALITY: These headers tell CDNs how to cache our content
     * The CDN (Cloudflare/BunnyCDN) will respect these and cache at their edge locations
     *
     * @param string $compression Compression type
     * @param int    $lifetime    Cache lifetime
     * @return void
     */
    private function send_cache_headers(string $compression, int $lifetime): void {
        // Basic cache headers
        header('X-ProRank-Cache: HIT');
        header('X-ProRank-Compression: ' . $compression);
        header('X-ProRank-Origin: Hetzner'); // Show actual origin
        header('Vary: ' . $this->get_vary_header_value());
        
        // Performance timing
        $response_time = round((microtime(true) - $this->request_start) * 1000, 2);
        header('Server-Timing: cache;desc="Cache Hit";dur=' . $response_time);
        
        // Browser must always revalidate HTML to prevent stale CSS/JS references.
        // Server-side page cache provides the performance benefit; browser caching
        // HTML with max-age causes broken pages when optimization modules change
        // asset URLs (UCSS, CSS combine, JS delay).
        // CDN edge caching uses s-maxage independently of browser max-age.
        $cdn_provider = $this->get_setting('cdn_provider', 'none');

        if ($cdn_provider === 'cloudflare') {
            header('Cache-Control: no-cache, must-revalidate, s-maxage=' . $this->get_setting('cdn_ttl', 86400));
            header('Cloudflare-CDN-Cache-Control: max-age=' . $this->get_setting('cdn_ttl', 86400));

            if ($this->get_setting('cloudflare_cache_everything', false)) {
                header('X-Cloudflare-Cache-Everything: 1');
            }

        } elseif ($cdn_provider === 'bunnycdn') {
            header('Cache-Control: no-cache, must-revalidate, s-maxage=' . $this->get_setting('cdn_ttl', 86400));
            header('CDN-Cache-Control: max-age=' . $this->get_setting('cdn_ttl', 86400));

        } else {
            header('Cache-Control: no-cache, must-revalidate');
        }
        
        // Security headers
        header('X-Content-Type-Options: nosniff');
        header('X-Frame-Options: SAMEORIGIN');
    }
    
    /**
     * Send 103 Early Hints
     *
     * @return void
     */
    public function send_early_hints(): void {
        if (!$this->get_setting('early_hints_enabled', false)) {
            return;
        }
        
        // Check if server supports Early Hints
        if (!function_exists('header_register_callback')) {
            return;
        }
        
        // Send early hints for critical resources
        $hints = [
            '</wp-includes/css/dist/block-library/style.min.css>; rel=preload; as=style',
            '</wp-includes/js/jquery/jquery.min.js>; rel=preload; as=script',
        ];
        
        // Add theme stylesheet
        $theme_uri = get_stylesheet_directory_uri();
        $hints[] = '<' . $theme_uri . '/style.css>; rel=preload; as=style';
        
        // Add custom hints from settings
        $custom_hints = $this->get_setting('early_hints_resources', '');
        if (!empty($custom_hints)) {
            $resources = array_filter(array_map('trim', explode("\n", $custom_hints)));
            foreach ($resources as $resource) {
                $hints[] = '<' . $resource . '>; rel=preload';
            }
        }
        
        // Send 103 Early Hints
        if (!headers_sent()) {
            header('HTTP/1.1 103 Early Hints');
            foreach ($hints as $hint) {
                header('Link: ' . $hint, false);
            }
        }
    }
    
    /**
     * Add modern performance headers
     *
     * @param array $headers Existing headers
     * @return array Modified headers
     */
    public function add_modern_headers(array $headers): array {
        // HTTP/3 support indication
        if ($this->get_setting('http3_enabled', false)) {
            $headers['Alt-Svc'] = 'h3=":443"; ma=86400, h3-29=":443"; ma=86400';
        }
        
        // Server Push
        if ($this->get_setting('server_push_enabled', false)) {
            $push_resources = $this->get_push_resources();
            if (!empty($push_resources)) {
                $headers['Link'] = implode(', ', $push_resources);
            }
        }
        
        // Priority Hints
        $headers['Priority-Hints'] = 'importance=high';
        
        return $headers;
    }
    
    /**
     * Get resources for server push
     *
     * @return array
     */
    private function get_push_resources(): array {
        $resources = [];
        
        // Push critical CSS
        if ($this->get_setting('critical_css_enabled', false)) {
            $critical_css = $this->get_critical_css();
            if (!empty($critical_css)) {
                foreach ($critical_css as $css) {
                    $resources[] = '<' . $css . '>; rel=preload; as=style; nopush';
                }
            }
        }
        
        // Push critical fonts
        $fonts = $this->get_setting('font_preload_urls', '');
        if (!empty($fonts)) {
            $font_urls = array_filter(array_map('trim', explode("\n", $fonts)));
            foreach ($font_urls as $font) {
                $resources[] = '<' . $font . '>; rel=preload; as=font; type="font/woff2"; crossorigin';
            }
        }
        
        return $resources;
    }
    
    /**
     * Handle post status transitions for cache invalidation
     *
     * @param string   $new_status New post status
     * @param string   $old_status Old post status
     * @param \WP_Post $post       Post object
     * @return void
     */
    public function handle_post_status_change(string $new_status, string $old_status, \WP_Post $post): void {
        if ($new_status === $old_status) {
            return;
        }

        // Only invalidate when transitioning to/from publish
        if ($new_status === 'publish' || $old_status === 'publish') {
            $this->invalidate_related_caches($post);

            $permalink = get_permalink($post);
            if ($permalink) {
                $this->clear_url_cache($permalink);
            }

            $this->purge_post_tags($post);

            do_action('prorank_cache_cleared', 'post_status_change', $post->ID);
        }
    }

    /**
     * Smart cache invalidation
     *
     * @param int $post_id Post ID
     * @return void
     */
    public function smart_cache_invalidation(int $post_id): void {
        if ( \ProRank\SEO\Plugin::is_elementor_builder_save_request() ) {
            return;
        }

        $post = get_post($post_id);
        if (!$post) {
            return;
        }
        
        // Clear post cache
        $permalink = get_permalink($post);
        if ($permalink) {
            $this->clear_url_cache($permalink);
        }
        
        // Smart invalidation based on post type and relationships
        $this->invalidate_related_caches($post);

        // Tag-based purge: removes every cached page that references this post
        // (archives, paginated lists, homepage, related-post widgets) which the
        // URL-only purge above can miss. Falls back to a full purge only when
        // the tag index is missing/corrupt.
        $this->purge_post_tags($post);

        // Trigger edge cache purge
        do_action('prorank_cache_cleared', 'post', $post_id);
    }
    
    /**
     * Invalidate related caches intelligently
     *
     * @param \WP_Post $post Post object
     * @return void
     */
    private function invalidate_related_caches(\WP_Post $post): void {
        // Always clear homepage
        $this->clear_url_cache(home_url('/'));
        
        // Clear post type archive
        $post_type_archive = get_post_type_archive_link($post->post_type);
        if ($post_type_archive) {
            $this->clear_url_cache($post_type_archive);
        }
        
        // Clear taxonomies (limited to avoid over-clearing)
        $taxonomies = get_object_taxonomies($post->post_type);
        foreach ($taxonomies as $taxonomy) {
            $terms = wp_get_post_terms($post->ID, $taxonomy, ['number' => 5]);
            foreach ($terms as $term) {
                $term_link = get_term_link($term);
                if (!is_wp_error($term_link)) {
                    $this->clear_url_cache($term_link);
                }
            }
        }
        
        // Clear author archive if public
        if (post_type_supports($post->post_type, 'author')) {
            $author_url = get_author_posts_url($post->post_author);
            $this->clear_url_cache($author_url);
        }
    }

    /**
     * Purge every cached page tagged with this post and its relationships.
     *
     * @param \WP_Post $post Post object.
     * @return void
     */
    private function purge_post_tags(\WP_Post $post): void {
        $tags = [
            'post:' . $post->ID,
            'post_type:' . $post->post_type,
            'post_type_archive:' . $post->post_type,
            'front_page',
            'home',
        ];

        if (!empty($post->post_author)) {
            $tags[] = 'author:' . (int) $post->post_author;
        }

        foreach (get_object_taxonomies($post->post_type) as $taxonomy) {
            $term_ids = wp_get_post_terms($post->ID, $taxonomy, ['fields' => 'ids']);
            if (is_array($term_ids)) {
                foreach ($term_ids as $term_id) {
                    $tags[] = 'term:' . $taxonomy . ':' . (int) $term_id;
                }
            }
        }

        $this->purge_tags($tags);
    }

    /**
     * Invalidate caches when a post is permanently deleted.
     *
     * @param int $post_id Post ID being deleted.
     * @return void
     */
    public function handle_post_delete(int $post_id): void {
        $post = get_post($post_id);
        if (!$post instanceof \WP_Post) {
            return;
        }

        $permalink = get_permalink($post);
        if ($permalink) {
            $this->clear_url_cache($permalink);
        }

        $this->invalidate_related_caches($post);
        $this->purge_post_tags($post);

        do_action('prorank_cache_cleared', 'post_delete', $post_id);
    }

    /**
     * Record the cache tags for the page just written to disk.
     *
     * @param string $cache_file Cache file path that was written.
     * @return void
     */
    private function index_cache_tags(string $cache_file): void {
        try {
            $tags = $this->get_current_cache_tags();
            if (empty($tags)) {
                return;
            }
            $this->tag_index()->index($this->cache_key_for_index($cache_file), $tags);
        } catch (\Throwable $e) {
            // Tag indexing is best-effort; never break cache writes.
        }
    }

    /**
     * Resolve the cache tags for the current request from the main query.
     *
     * @return array<int,string>
     */
    private function get_current_cache_tags(): array {
        $tags = [];

        if (function_exists('is_front_page') && is_front_page()) {
            $tags[] = 'front_page';
        }
        if (function_exists('is_home') && is_home()) {
            $tags[] = 'home';
        }

        $object = function_exists('get_queried_object') ? get_queried_object() : null;

        if ($object instanceof \WP_Post) {
            $tags[] = 'post:' . $object->ID;
            $tags[] = 'post_type:' . $object->post_type;
            if (!empty($object->post_author)) {
                $tags[] = 'author:' . (int) $object->post_author;
            }
            foreach (get_object_taxonomies($object->post_type) as $taxonomy) {
                $term_ids = wp_get_post_terms($object->ID, $taxonomy, ['fields' => 'ids']);
                if (is_array($term_ids)) {
                    foreach ($term_ids as $term_id) {
                        $tags[] = 'term:' . $taxonomy . ':' . (int) $term_id;
                    }
                }
            }
        } elseif ($object instanceof \WP_Term) {
            $tags[] = 'term:' . $object->taxonomy . ':' . (int) $object->term_id;
        } elseif ($object instanceof \WP_User) {
            $tags[] = 'author:' . (int) $object->ID;
        } elseif ($object instanceof \WP_Post_Type) {
            $tags[] = 'post_type_archive:' . $object->name;
        }

        return array_values(array_unique($tags));
    }

    /**
     * Normalise a cache file path to its variant-agnostic base so a single
     * indexed key purges all device/format variants on lookup.
     *
     * @param string $cache_file Cache file path.
     * @return string
     */
    private function cache_key_for_index(string $cache_file): string {
        $dir      = dirname($cache_file);
        $filename = basename($cache_file);

        if (!str_ends_with($filename, '.html')) {
            return $cache_file;
        }

        $stem = substr($filename, 0, -5);
        $stem = preg_replace('/-fmt-(?:jxl|avif|webp|legacy)(?:-mobile)?$/', '', $stem);
        $stem = preg_replace('/-mobile$/', '', (string) $stem);

        return $dir . '/' . (string) $stem . '.html';
    }

    /**
     * Purge cached pages by one or more cache tags.
     *
     * Returns the number of cache files removed, or -1 when the tag index was
     * missing/corrupt and a full purge was performed as a safe fallback.
     *
     * @param array<int,string> $tags Tags to purge.
     * @return int
     */
    private function purge_tags(array $tags): int {
        $tags = array_values(array_unique(array_filter(array_map('strval', $tags), static function ($t) {
            return $t !== '';
        })));

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

        $index = $this->tag_index();

        // Fallback to a full purge when the index can't be trusted.
        if (!$index->is_available()) {
            $this->clear_all_cache();
            return -1;
        }

        $deleted = 0;
        foreach ($tags as $tag) {
            foreach ($index->purge_tag($tag) as $cache_key) {
                foreach ($this->get_cache_file_variants($cache_key) as $file) {
                    if (file_exists($file)) {
                        \ProRank\SEO\Core\FilesystemHelper::delete_file($file);
                        $deleted++;
                    }
                }
                $index->forget_key($cache_key);
            }
        }

        return $deleted;
    }

    /**
     * Public single-tag purge (used by the WP-CLI `cache purge-tag` command).
     *
     * @param string $tag Tag to purge.
     * @return int Files removed, or -1 when a full-purge fallback ran.
     */
    public function purge_by_tag(string $tag): int {
        return $this->purge_tags([$tag]);
    }

    /**
     * Push cache to edge network
     *
     * @param string $cache_file Cache file path
     * @param string $content    Content to push
     * @return void
     */
    public function push_to_edge(string $cache_file, string $content): void {
        $provider = $this->get_setting('edge_provider', 'cloudflare');
        $api_key = $this->get_setting('edge_purge_api_key', '');
        
        if (empty($api_key)) {
            return;
        }
        
        // Get URL from cache file
        $url = $this->get_url_from_cache_path($cache_file);
        
        // Push based on provider
        switch ($provider) {
            case 'cloudflare':
                $this->push_to_cloudflare($url, $content, $api_key);
                break;
            case 'fastly':
                $this->push_to_fastly($url, $content, $api_key);
                break;
            case 'bunny':
                $this->push_to_bunny($url, $content, $api_key);
                break;
        }
    }
    
    /**
     * Push to CDN via proper cache headers
     * 
     * REALITY: We don't push to edge - CDN pulls from our Hetzner server
     * and caches at their edge locations automatically
     *
     * @param string $url     URL to cache
     * @param string $content Content to cache
     * @param string $api_key API key
     * @return void
     */
    private function push_to_prorank_edge(string $url, string $content, string $api_key): void {
        // We don't actually "push" to edge - that's not how CDNs work
        // Instead, we:
        // 1. Set proper cache headers when serving content
        // 2. Purge CDN cache when content updates
        // 3. Let CDN pull from our origin (Hetzner) server
        
        $provider = $this->get_setting('cdn_provider', 'cloudflare');
        
        switch ($provider) {
            case 'cloudflare':
                // Purge Cloudflare cache for this URL
                $this->purge_cloudflare_cache($url, $api_key);
                break;
                
            case 'bunnycdn':
                // Purge BunnyCDN cache
                $this->purge_bunnycdn_cache($url, $api_key);
                break;
                
            default:
                // For sites without CDN, just use local cache
                $this->log_debug('No CDN configured - using local cache only');
        }
    }
    
    /**
     * Purge Cloudflare cache for URL
     */
    private function purge_cloudflare_cache(string $url, string $api_key): void {
        // Get zone ID from settings
        $zone_id = $this->get_setting('cloudflare_zone_id', '');
        
        if (empty($zone_id)) {
            return;
        }
        
        wp_remote_post("https://api.cloudflare.com/client/v4/zones/{$zone_id}/purge_cache", [
            'headers' => [
                'Authorization' => 'Bearer ' . $api_key,
                'Content-Type' => 'application/json',
            ],
            'body' => json_encode([
                'files' => [$url]
            ]),
            'timeout' => 5,
        ]);
    }
    
    /**
     * Purge BunnyCDN cache
     */
    private function purge_bunnycdn_cache(string $url, string $api_key): void {
        $pull_zone_id = $this->get_setting('bunny_pullzone_id', '');
        
        if (empty($pull_zone_id)) {
            return;
        }
        
        wp_remote_post("https://api.bunny.net/pullzone/{$pull_zone_id}/purgeCache", [
            'headers' => [
                'AccessKey' => $api_key,
                'Content-Type' => 'application/json',
            ],
            'body' => json_encode([
                'url' => $url
            ]),
            'timeout' => 5,
        ]);
    }
    
    /**
     * Get license key for API calls
     *
     * @return string|null
     */
    private function get_license_key(): ?string {
        return null;
    }
    
    /**
     * Log debug message
     *
     * @param string $message Message to log
     * @return void
     */
    private function log_debug(string $message): void {
        if (defined('WP_DEBUG') && WP_DEBUG) {
            prorank_log('[ProRank Cache] ' . $message);
        }
    }
    
    /**
     * Get best compressed version of content
     *
     * @param string $content Original content
     * @return array Array with 'type' and 'content'
     */
    private function get_best_compressed_content(string $content): array {
        // Try Brotli first
        if (function_exists('brotli_compress')) {
            $compressed = brotli_compress($content, 11);
            if ($compressed !== false) {
                return ['type' => 'br', 'content' => $compressed];
            }
        }
        
        // Fall back to gzip
        $compressed = gzencode($content, 9);
        if ($compressed !== false) {
            return ['type' => 'gzip', 'content' => $compressed];
        }
        
        // No compression
        return ['type' => 'none', 'content' => $content];
    }
    
    /**
     * Inject AI-powered preloader script
     *
     * @return void
     */
    public function inject_ai_preloader(): void {
        if (is_admin() || !$this->get_setting('preload_ai_enabled', false)) {
            return;
        }
        
        ?>
        <script>
        (function() {
            // ProRank AI Predictive Preloader 2025
            const preloader = {
                predictions: [],
                preloaded: new Set(),
                
                init() {
                    this.observeUserBehavior();
                    this.predictNextPages();
                    this.setupIntersectionObserver();
                },
                
                observeUserBehavior() {
                    // Track mouse movement patterns
                    let mousePath = [];
                    document.addEventListener('mousemove', (e) => {
                        mousePath.push({ x: e.clientX, y: e.clientY, t: Date.now() });
                        if (mousePath.length > 10) mousePath.shift();
                        this.analyzePath(mousePath);
                    });
                    
                    // Track scroll patterns
                    let scrollData = [];
                    window.addEventListener('scroll', () => {
                        scrollData.push({
                            y: window.scrollY,
                            t: Date.now(),
                            velocity: this.getScrollVelocity()
                        });
                        if (scrollData.length > 5) scrollData.shift();
                        this.predictFromScroll(scrollData);
                    });
                },
                
                predictNextPages() {
                    // Use ML model to predict likely next pages
                    const links = document.querySelectorAll('a[href^="/"], a[href^="' + location.origin + '"]');
                    const predictions = [];
                    
                    links.forEach(link => {
                        const score = this.calculateLinkScore(link);
                        if (score > 0.7) {
                            predictions.push({ url: link.href, score });
                        }
                    });
                    
                    // Sort by score and preload top 3
                    predictions.sort((a, b) => b.score - a.score);
                    predictions.slice(0, 3).forEach(p => this.preloadPage(p.url));
                },
                
                calculateLinkScore(link) {
                    let score = 0;
                    
                    // Position scoring
                    const rect = link.getBoundingClientRect();
                    if (rect.top > 0 && rect.top < window.innerHeight) score += 0.3;
                    if (rect.left > 0 && rect.left < window.innerWidth * 0.75) score += 0.2;
                    
                    // Content scoring
                    if (link.classList.contains('menu-item')) score += 0.2;
                    if (link.closest('nav')) score += 0.15;
                    if (link.querySelector('img')) score += 0.1;
                    
                    // Historical scoring (would use localStorage in production)
                    const visitCount = parseInt(link.dataset.visits || '0');
                    score += Math.min(visitCount * 0.1, 0.3);
                    
                    return Math.min(score, 1);
                },
                
                preloadPage(url) {
                    if (this.preloaded.has(url)) return;
                    
                    const link = document.createElement('link');
                    link.rel = 'prefetch';
                    link.href = url;
                    link.as = 'document';
                    document.head.appendChild(link);
                    
                    this.preloaded.add(url);
                    console.log('[ProRank AI] Preloaded:', url);
                },
                
                setupIntersectionObserver() {
                    const observer = new IntersectionObserver((entries) => {
                        entries.forEach(entry => {
                            if (entry.isIntersecting && entry.target.href) {
                                // Preload when link is 50% visible
                                if (entry.intersectionRatio > 0.5) {
                                    this.preloadPage(entry.target.href);
                                }
                            }
                        });
                    }, { threshold: [0.5] });
                    
                    document.querySelectorAll('a[href]').forEach(link => {
                        observer.observe(link);
                    });
                },
                
                analyzePath(path) {
                    // Analyze mouse path to predict intent
                    if (path.length < 3) return;
                    
                    const recent = path.slice(-3);
                    const vector = {
                        x: recent[2].x - recent[0].x,
                        y: recent[2].y - recent[0].y
                    };
                    
                    // Find links in the direction of movement
                    const links = document.elementsFromPoint(
                        recent[2].x + vector.x * 2,
                        recent[2].y + vector.y * 2
                    ).filter(el => el.tagName === 'A');
                    
                    links.forEach(link => this.preloadPage(link.href));
                },
                
                getScrollVelocity() {
                    // Calculate scroll velocity for predictive loading
                    const now = Date.now();
                    const prev = this.lastScroll || { y: 0, t: now };
                    const velocity = (window.scrollY - prev.y) / (now - prev.t);
                    this.lastScroll = { y: window.scrollY, t: now };
                    return velocity;
                },
                
                predictFromScroll(data) {
                    if (data.length < 2) return;
                    
                    const velocity = data[data.length - 1].velocity;
                    const position = window.scrollY + window.innerHeight;
                    
                    // If scrolling fast downward, preload content below
                    if (velocity > 5) {
                        const futurePosition = position + (velocity * 500);
                        this.preloadAtPosition(futurePosition);
                    }
                },
                
                preloadAtPosition(yPosition) {
                    // Find links near predicted position
                    const links = document.querySelectorAll('a[href]');
                    links.forEach(link => {
                        const rect = link.getBoundingClientRect();
                        const linkY = rect.top + window.scrollY;
                        
                        if (Math.abs(linkY - yPosition) < 200) {
                            this.preloadPage(link.href);
                        }
                    });
                }
            };
            
            // Initialize when DOM is ready
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', () => preloader.init());
            } else {
                preloader.init();
            }
        })();
        </script>
        <?php
    }
    
    /**
     * Initialize statistics
     *
     * @return void
     */
    public function initialize_stats(): void {
        if ($this->stats_initialized) {
            return;
        }
        $this->stats_initialized = true;

        if (file_exists($this->stats_file)) {
            $contents = @file_get_contents($this->stats_file);
            if ($contents === false || $contents === '') {
                $filesystem = \ProRank\SEO\Core\FilesystemHelper::get_filesystem();
                if ($filesystem) {
                    try {
                        $contents = $filesystem->get_contents($this->stats_file);
                    } catch (\Throwable $e) {
                        $contents = false;
                    }
                }
            }

            if ($contents !== false && $contents !== '') {
                $stats = json_decode($contents, true);
                if (is_array($stats)) {
                    $this->stats = array_merge($this->stats, $stats);
                }
            }
        }

        // Merge stats collected by the advanced-cache.php drop-in.
        // The drop-in runs before WordPress loads, so it writes to a
        // separate file. We merge those counts here and reset the file.
        $dropin_file = dirname($this->stats_file) . '/dropin-hits.json';
        if (file_exists($dropin_file)) {
            $raw = @file_get_contents($dropin_file);
            if ($raw !== false && $raw !== '') {
                $dropin = @json_decode($raw, true);
                if (is_array($dropin) && !empty($dropin['hits'])) {
                    $this->stats['hits'] += (int) ($dropin['hits'] ?? 0);
                    $this->stats['bytes_saved'] += (int) ($dropin['bytes_saved'] ?? 0);
                    if (!empty($dropin['response_times']) && is_array($dropin['response_times'])) {
                        $this->stats['response_times'] = array_merge(
                            $this->stats['response_times'] ?? [],
                            $dropin['response_times']
                        );
                        $this->stats['response_times'] = array_slice($this->stats['response_times'], -100);
                    }
                    // Reset drop-in counters after merging.
                    @file_put_contents($dropin_file, '{}', LOCK_EX);
                }
            }
        }
    }
    
    /**
     * Save statistics
     *
     * @return void
     */
    public function save_stats(): void {
        // Calculate averages
        if (!empty($this->stats['response_times'])) {
            $this->stats['avg_response_time'] = round(
                array_sum($this->stats['response_times']) / count($this->stats['response_times']),
                2
            );
            // Keep only last 100 response times
            $this->stats['response_times'] = array_slice($this->stats['response_times'], -100);
        }
        
        // Calculate hit rate
        $total = $this->stats['hits'] + $this->stats['misses'];
        if ($total > 0) {
            $this->stats['hit_rate'] = round(($this->stats['hits'] / $total) * 100, 2);
            $this->stats['miss_rate'] = round(($this->stats['misses'] / $total) * 100, 2);
        }
        
        // Format bandwidth saved
        $this->stats['bandwidth_saved'] = $this->format_bytes($this->stats['bytes_saved']);
        
        $stats_dir = dirname($this->stats_file);
        if (!is_dir($stats_dir)) {
            wp_mkdir_p($stats_dir);
        }

        $payload = wp_json_encode($this->stats, JSON_PRETTY_PRINT);
        if (!is_string($payload)) {
            return;
        }

        // Prefer direct local writes for cache stats to avoid FTP filesystem edge cases.
        $wrote = @file_put_contents($this->stats_file, $payload, LOCK_EX);
        if (false !== $wrote) {
            return;
        }

        $filesystem = \ProRank\SEO\Core\FilesystemHelper::get_filesystem();
        if (!$filesystem) {
            return;
        }

        $chmod = defined('FS_CHMOD_FILE') ? FS_CHMOD_FILE : 0644;
        try {
            $filesystem->put_contents($this->stats_file, $payload, $chmod);
        } catch (\Throwable $e) {
            // Ignore write fallback failure.
        }
    }

    /**
     * Normalize transient cache stats from earlier plugin versions or partial writes.
     *
     * @param mixed $stats Existing transient payload.
     * @return array
     */
    private function normalize_cache_stats($stats): array {
        if (!is_array($stats)) {
            $stats = [];
        }

        $stats = wp_parse_args($stats, [
            'hits' => 0,
            'misses' => 0,
            'bytes_served' => 0,
            'response_times' => [],
            'last_updated' => time(),
        ]);

        $stats['hits'] = max(0, (int) $stats['hits']);
        $stats['misses'] = max(0, (int) $stats['misses']);
        $stats['bytes_served'] = max(0, (int) $stats['bytes_served']);
        $stats['response_times'] = is_array($stats['response_times']) ? array_values($stats['response_times']) : [];
        $stats['last_updated'] = time();

        return $stats;
    }
    
    /**
     * Track cache hit with advanced analytics
     *
     * @param int $bytes_saved Bytes saved by serving cached version
     * @return void
     */
    private function track_hit(int $bytes_saved = 0): void {
        $this->stats['hits']++;
        $this->stats['bytes_saved'] += $bytes_saved;
        $response_time = round((microtime(true) - $this->request_start) * 1000, 2);
        $this->stats['response_times'][] = $response_time;
        
        // Update real-time stats in transient
        $current_stats = $this->normalize_cache_stats(get_transient('prorank_cache_stats'));
        
        $current_stats['hits']++;
        $current_stats['bytes_served'] += $bytes_saved;
        $current_stats['response_times'][] = $response_time;
        
        // Keep only last 100 response times
        if (count($current_stats['response_times']) > 100) {
            $current_stats['response_times'] = array_slice($current_stats['response_times'], -100);
        }
        
        set_transient('prorank_cache_stats', $current_stats, HOUR_IN_SECONDS);
        
        // Track geographic distribution
        $this->track_geographic_hit();
        
        // Send to ProRank.io for edge analytics (async)
        $this->send_analytics_to_edge($response_time, $bytes_saved);
    }
    
    /**
     * Track cache miss with analytics
     *
     * @return void
     */
    private function track_miss(?string $reason = null): void {
        $this->stats['misses']++;
        header('X-ProRank-Cache: MISS');
        $reason = is_string($reason) && $reason !== '' ? $reason : $this->last_cache_reason;
        header('X-ProRank-Cache-Reason: ' . preg_replace('/[^a-z0-9_\-]/i', '', (string) $reason));
        
        // Update real-time stats
        $current_stats = $this->normalize_cache_stats(get_transient('prorank_cache_stats'));
        
        $current_stats['misses']++;
        set_transient('prorank_cache_stats', $current_stats, HOUR_IN_SECONDS);
        
        // Track miss reasons for optimization
        $this->track_miss_reason($reason);
    }
    

    public function get_cache_coverage(): array {
        // Known indexable URLs: published public posts/pages + the front page.
        $known = 1;
        foreach (get_post_types(['public' => true], 'names') as $pt) {
            if ($pt === 'attachment') {
                continue;
            }
            $counts = wp_count_posts($pt);
            $known += isset($counts->publish) ? (int) $counts->publish : 0;
        }

        // Self-contained lifetime/stale calc (settings only) so coverage has
        // NO dependency on other cache helpers and can ship in isolation.
        $cache_lifetime = (int) $this->get_setting('cache_lifetime', DAY_IN_SECONDS);
        $cache_lifetime = $cache_lifetime > 0 ? max(60, $cache_lifetime) : DAY_IN_SECONDS;
        $stale_after = (int) $this->get_setting('cache_stale_lifetime', WEEK_IN_SECONDS);
        $stale_after = $stale_after > 0 ? max($cache_lifetime, $stale_after) : WEEK_IN_SECONDS;

        $now = time();
        $dir = is_string($this->cache_dir) ? $this->cache_dir : '';
        // Deduplicate by CANONICAL cache stem so format/device variants
        // (.gz/.br, -mobile, -fmt-webp/-fmt-avif/-fmt-legacy, ...) of the
        // same page count once. Track newest mtime per stem for staleness.
        $canonical = []; // stem => newest mtime

        if ($dir !== '' && is_dir($dir)) {
            try {
                $it = new \RecursiveIteratorIterator(
                    new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS)
                );
                foreach ($it as $file) {
                    $name = $file->getFilename();
                    if (substr($name, -5) !== '.html') {
                        continue; // skip .gz/.br companions
                    }
                    $rel = $file->getPathname();
                    if (strpos($rel, $dir) === 0) {
                        $rel = ltrim(substr($rel, strlen($dir)), '/\\');
                    }
                    // Strip .html, then trailing -mobile and -fmt-<token>
                    // (in any combination/order) to get the page stem.
                    $stem = substr($rel, 0, -5);
                    $stem = preg_replace('~(-mobile|-fmt-[a-z0-9]+)+$~i', '', $stem);
                    $mtime = $file->getMTime();
                    if (!isset($canonical[$stem]) || $mtime > $canonical[$stem]) {
                        $canonical[$stem] = $mtime;
                    }
                }
            } catch (\Throwable $e) {
                // Best-effort; coverage is diagnostic only.
            }
        }

        $cached_files = count($canonical);
        $stale_files = 0;
        foreach ($canonical as $mtime) {
            if (($now - $mtime) > $stale_after) {
                $stale_files++;
            }
        }

        $coverage = [
            'known_urls'         => $known,
            'cached_html_files'  => $cached_files,
            'not_cached_estimate' => max(0, $known - $cached_files),
            'stale_cached_files' => $stale_files,
            'cache_lifetime'     => $cache_lifetime,
            'preload'            => [
                'available'      => false,
                'running'        => false,
                'progress'       => null,
                'pending_queue'  => 0,
            ],
        ];

        // Premium preloader status, if the module is active. Free has no
        // preloader, so these stay at the safe defaults above.
        $progress = get_option('prorank_preload_progress', null);
        if (is_array($progress)) {
            $coverage['preload']['available'] = true;
            $coverage['preload']['progress'] = $progress;
            $coverage['preload']['running'] =
                isset($progress['total'], $progress['processed'])
                && (int) $progress['processed'] < (int) $progress['total'];
            $coverage['preload']['pending_queue'] = count((array) get_option('prorank_preload_urls', []));
        }

        // B2b: read-only UCSS bounded-skip health (rebuild_signal off in MVP).
        $b2b_assets = get_option('prorank_asset_optimization_settings', []);
        $b2b_assets = is_array($b2b_assets) ? $b2b_assets : [];
        $ucss_kill = array_key_exists('ucss_cache_skip_enabled', $b2b_assets)
            ? (bool) $b2b_assets['ucss_cache_skip_enabled']
            : true;
        $skip_state = get_option(self::UCSS_SKIP_STATE_OPTION, []);
        $skip_state = is_array($skip_state) ? $skip_state : [];
        $currently_skipping = 0;
        $fallback_cached = 0;
        $oldest_unresolved = 0;
        $b2b_now = time();
        foreach ($skip_state as $r) {
            if (!is_array($r)) {
                continue;
            }
            if (($r['last_decision'] ?? '') === 'skip') {
                $currently_skipping++;
            }
            if (!empty($r['fallback_flagged'])) {
                $fallback_cached++;
                $age = $b2b_now - (int) ($r['last_seen'] ?? $b2b_now);
                if ($age > $oldest_unresolved) {
                    $oldest_unresolved = $age;
                }
            }
        }
        $coverage['ucss_skip'] = [
            'enabled'                  => $ucss_kill,
            'rebuild_signal_available' => false,
            'urls_currently_skipping'  => $currently_skipping,
            'urls_fallback_cached'     => $fallback_cached,
            'oldest_unresolved_secs'   => $oldest_unresolved,
            'tracked_entries'          => count($skip_state),
        ];

        return $coverage;
    }

    /**
     * Get cache statistics via API
     *
     * @return array
     */
    private function write_htaccess_safely(string $htaccess_path, string $content, string $context): bool {
        if (!$this->is_safe_htaccess_content($content)) {
            prorank_log('ProRank SEO: Refusing unsafe .htaccess content during ' . $context);
            return false;
        }

        $dir = dirname($htaccess_path);
        if (!is_dir($dir) || !is_writable($dir)) {
            prorank_log('ProRank SEO: .htaccess directory is not writable during ' . $context);
            return false;
        }

        $lock_path = $htaccess_path . '.prorank.lock';
        $lock = @fopen($lock_path, 'c');
        if (!$lock) {
            prorank_log('ProRank SEO: Could not open .htaccess lock during ' . $context);
            return false;
        }

        if (!@flock($lock, LOCK_EX | LOCK_NB)) {
            @fclose($lock);
            prorank_log('ProRank SEO: Another .htaccess write is already running during ' . $context);
            return false;
        }

        $tmp_path = $htaccess_path . '.prorank-tmp-' . getmypid() . '-' . str_replace('.', '', uniqid('', true));

        try {
            if (@file_put_contents($tmp_path, $content, LOCK_EX) === false) {
                return false;
            }

            $mode = file_exists($htaccess_path) ? (fileperms($htaccess_path) & 0777) : 0644;
            @chmod($tmp_path, $mode ?: 0644);

            if (@rename($tmp_path, $htaccess_path)) {
                return true;
            }

            if (@copy($tmp_path, $htaccess_path)) {
                @unlink($tmp_path);
                return true;
            }

            @unlink($tmp_path);
            return false;
        } finally {
            @flock($lock, LOCK_UN);
            @fclose($lock);
        }
    }
    private function is_safe_htaccess_content(string $content): bool {
        if (preg_match('/\\\\[rn]/', $content)) {
            return false;
        }

        if (substr_count($content, '<IfModule') !== substr_count($content, '</IfModule>')) {
            return false;
        }

        if (substr_count($content, '<FilesMatch') !== substr_count($content, '</FilesMatch>')) {
            return false;
        }

        foreach (preg_split('/\r\n|\r|\n/', $content) as $line) {
            $trimmed = trim((string) $line);
            if ($trimmed === '' || $trimmed[0] === '#') {
                continue;
            }

            if (preg_match('/^(RewriteCond|RewriteRule|Header|ExpiresByType)\b.*\b(RewriteCond|RewriteRule|Header|ExpiresByType)\b/', $trimmed)) {
                return false;
            }
        }

        return true;
    }
    public function clear_urls_cache(array $urls): int {
        $processed = 0;

        foreach (array_values(array_unique($urls)) as $url) {
            if (!is_string($url) || $url === '') {
                continue;
            }
            $this->clear_url_cache($url);
            $processed++;
        }

        return $processed;
    }
    private function get_cache_file_variants(string $cache_file): array {
        $dir = dirname($cache_file);
        $filename = basename($cache_file);

        if (!str_ends_with($filename, '.html')) {
            return [$cache_file, $cache_file . '.gz', $cache_file . '.br'];
        }

        $stem = substr($filename, 0, -5);
        $stem = preg_replace('/-fmt-(?:jxl|avif|webp|legacy)(?:-mobile)?$/', '', $stem);
        $stem = preg_replace('/-mobile$/', '', (string) $stem);
        $stem = (string) $stem;

        $stems = [$stem, $stem . '-mobile'];
        foreach (['jxl', 'avif', 'webp', 'legacy'] as $format) {
            $stems[] = $stem . '-fmt-' . $format;
            $stems[] = $stem . '-fmt-' . $format . '-mobile';
        }

        $files = [];
        foreach (array_unique($stems) as $candidate_stem) {
            $base = $dir . '/' . $candidate_stem . '.html';
            $files[] = $base;
            $files[] = $base . '.gz';
            $files[] = $base . '.br';
        }

        return array_values(array_unique($files));
    }

    public function add_keep_warm_schedule(array $schedules): array {
        if (!isset($schedules[self::KEEP_WARM_SCHEDULE])) {
            $schedules[self::KEEP_WARM_SCHEDULE] = [
                'interval' => 15 * MINUTE_IN_SECONDS,
                'display'  => __('Every 15 Minutes (ProRank Cache Keep Warm)', 'prorank-seo'),
            ];
        }

        return $schedules;
    }
    public function maybe_schedule_keep_warm(): void {
        if (!$this->should_keep_warm_core_urls()) {
            wp_clear_scheduled_hook(self::KEEP_WARM_HOOK);
            return;
        }

        $event = function_exists('wp_get_scheduled_event') ? wp_get_scheduled_event(self::KEEP_WARM_HOOK) : null;
        if ($event && (!isset($event->schedule) || $event->schedule !== self::KEEP_WARM_SCHEDULE)) {
            wp_clear_scheduled_hook(self::KEEP_WARM_HOOK);
            $event = null;
        }

        if (!$event && !wp_next_scheduled(self::KEEP_WARM_HOOK)) {
            wp_schedule_event(time() + MINUTE_IN_SECONDS, self::KEEP_WARM_SCHEDULE, self::KEEP_WARM_HOOK);
        }
    }
    public function run_keep_warm(): void {
        if (!$this->should_keep_warm_core_urls()) {
            return;
        }

        // Adaptive throttle: skip this 15-minute warm cycle while the server is
        // under heavy load. The recurring cron re-runs on the next tick, so the
        // batch is effectively rescheduled rather than hammering a busy host.
        $cache_settings = get_option('prorank_cache_settings', []);
        if (is_array($cache_settings) && CachePreloadThrottle::should_throttle($cache_settings)) {
            return;
        }

        $this->prime_core_cache_urls();
    }
    private function should_keep_warm_core_urls(): bool {
        if (!$this->is_enabled()) {
            return false;
        }

        $cache_enabled = (bool) $this->get_setting('cache_enabled', $this->get_setting('enabled', false));
        if (!$cache_enabled) {
            return false;
        }

        return $this->is_cache_preload_enabled();
    }

    private function is_cache_preload_enabled(): bool {
        $settings = get_option('prorank_cache_settings', []);
        $settings = is_array($settings) ? $settings : [];

        return ! empty($settings['preload_enabled'])
            || ! empty($settings['cache_preload_enabled']);
    }

    /**
     * B2b (MVP, no queue coupling): bounded decision for whether to skip
     * caching this render because the Cloud UCSS marker is absent. Returns
     * 'ok' (cache normally), 'skip' (do not cache yet — within cap+window),
     * or 'cache_fallback' (bounds exhausted: cache the heavier fallback HTML
     * and flag a degraded-CSS health warning). NEVER skips indefinitely.
     * No dependency on CloudUCSSProvider; rebuild_signal_available=false.
     */
    public function ucss_skip_decision(string $content): string {
        $assets = get_option('prorank_asset_optimization_settings', []);
        $assets = is_array($assets) ? $assets : [];

        // Global kill switch (default true = bounded policy active).
        $enabled = array_key_exists('ucss_cache_skip_enabled', $assets)
            ? (bool) $assets['ucss_cache_skip_enabled']
            : true;
        $enabled = (bool) apply_filters('prorank_ucss_cache_skip_enabled', $enabled);
        if (!$enabled) {
            return 'ok';
        }

        // UCSS / cloud off: no skip, and DO NOT write per-URL state.
        if (empty($assets['unused_css_enabled'])) {
            return 'ok';
        }
        if (array_key_exists('unused_css_use_cloud', $assets) && empty($assets['unused_css_use_cloud'])) {
            return 'ok';
        }

        // Keyed by ProRank's own canonical cache key (NOT a CloudUCSS URL).
        $key = $this->ucss_canonical_key($this->get_current_url());

        // Marker present: clean render — reset state and cache normally.
        if (stripos($content, 'id="prorank-used-css"') !== false
            || stripos($content, "id='prorank-used-css'") !== false) {
            $this->clear_ucss_skip_state($key);
            return 'ok';
        }

        $max    = max(1, (int) apply_filters('prorank_ucss_skip_max', self::UCSS_SKIP_MAX_CONSECUTIVE));
        $window = max(60, (int) apply_filters('prorank_ucss_skip_rebuild_window', self::UCSS_SKIP_REBUILD_WINDOW));
        $now    = time();

        $state = $this->load_ucss_skip_state();
        $rec   = $state[$key] ?? [
            'first_skipped_at' => 0,
            'consecutive_skips' => 0,
            'last_decision' => 'ok',
            'last_seen' => 0,
            'fallback_flagged' => false,
        ];
        $rec['last_seen'] = $now;

        $first = (int) $rec['first_skipped_at'] ?: $now;
        $skips = (int) $rec['consecutive_skips'];
        $within_window = ($now - $first) <= $window;

        // MVP rule: marker absent -> skip while under cap AND within window.
        if ($within_window && $skips < $max) {
            $rec['first_skipped_at'] = $rec['first_skipped_at'] ?: $now;
            $rec['consecutive_skips'] = $skips + 1;
            $rec['last_decision'] = 'skip';
            $state[$key] = $rec;
            $this->save_ucss_skip_state($state);
            return 'skip';
        }

        // Bounds exhausted (cap or window): cache the fallback so the page is
        // no longer dynamic-rendered every hit, and flag it. Counter resets
        // so a later marker-present render auto-recovers.
        $rec['consecutive_skips'] = 0;
        $rec['first_skipped_at'] = 0;
        $rec['last_decision'] = 'cache_fallback';
        $rec['fallback_flagged'] = true;
        $state[$key] = $rec;
        $this->save_ucss_skip_state($state);
        return 'cache_fallback';
    }

    private function ucss_canonical_key(string $url): string {
        $path = $this->get_cache_file_path($url);
        // Strip device/format variant suffixes so all variants of a URL
        // share one bounded-skip decision.
        $path = (string) preg_replace('~(-mobile|-fmt-[a-z0-9]+)+(\.html)?$~i', '', $path);
        return md5($path);
    }

    private function load_ucss_skip_state(): array {
        $state = get_option(self::UCSS_SKIP_STATE_OPTION, []);
        return is_array($state) ? $state : [];
    }

    private function save_ucss_skip_state(array $state): void {
        $now = time();
        foreach ($state as $k => $r) {
            if (!is_array($r) || ($now - (int) ($r['last_seen'] ?? 0)) > self::UCSS_SKIP_STALE_PRUNE) {
                unset($state[$k]);
            }
        }
        if (count($state) > self::UCSS_SKIP_MAX_ENTRIES) {
            uasort($state, static function ($a, $b) {
                return (int) ($a['last_seen'] ?? 0) <=> (int) ($b['last_seen'] ?? 0);
            });
            $state = array_slice($state, -self::UCSS_SKIP_MAX_ENTRIES, null, true);
        }
        update_option(self::UCSS_SKIP_STATE_OPTION, $state, false);
    }

    private function clear_ucss_skip_state(string $key): void {
        $state = $this->load_ucss_skip_state();
        if (isset($state[$key]) && ($state[$key]['last_decision'] ?? '') !== 'ok') {
            unset($state[$key]);
            $this->save_ucss_skip_state($state);
        }
    }


    public function get_cache_stats(): array {
        $this->initialize_stats();

        // Calculate derived stats (same logic as save_stats).
        $total = $this->stats['hits'] + $this->stats['misses'];
        if ($total > 0) {
            $this->stats['hit_rate'] = round(($this->stats['hits'] / $total) * 100, 2);
            $this->stats['miss_rate'] = round(($this->stats['misses'] / $total) * 100, 2);
        }
        if (!empty($this->stats['response_times'])) {
            $this->stats['avg_response_time'] = round(
                array_sum($this->stats['response_times']) / count($this->stats['response_times']),
                2
            );
        }
        $this->stats['bandwidth_saved'] = $this->format_bytes($this->stats['bytes_saved']);

        // Add cache size
        $cache_size = $this->get_cache_size();
        $this->stats['cache_size'] = $this->format_bytes($cache_size);
        $this->stats['total_cached'] = $this->count_cached_files();
        
        // Add edge locations if enabled
        if ($this->get_setting('edge_cache_enabled', false)) {
            $provider = $this->get_setting('edge_provider', 'cloudflare');
            $this->stats['edge_locations'] = self::EDGE_PROVIDERS[$provider]['locations'] ?? 0;
        }
        
        // Estimate cost savings (rough calculation)
        $gb_saved = $this->stats['bytes_saved'] / (1024 * 1024 * 1024);
        $this->stats['cost_saved'] = '$' . round($gb_saved * 0.09, 2); // $0.09 per GB average CDN cost
        
        return $this->stats;
    }
    
    /**
     * Format bytes to human readable
     *
     * @param int $bytes Bytes to format
     * @return string
     */
    private function format_bytes(int $bytes): string {
        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
        $i = 0;
        
        while ($bytes >= 1024 && $i < count($units) - 1) {
            $bytes /= 1024;
            $i++;
        }
        
        return round($bytes, 2) . ' ' . $units[$i];
    }
    
    /**
     * Add admin bar cache information
     *
     * @param \WP_Admin_Bar $wp_admin_bar Admin bar object
     * @return void
     */
    public function add_admin_bar_info(\WP_Admin_Bar $wp_admin_bar): void {
        if (!current_user_can('manage_options')) {
            return;
        }
        
        $stats = $this->get_cache_stats();
        
        $wp_admin_bar->add_node([
            'parent' => 'prorank-seo',
            'id' => 'prorank-cache',
            'title' => sprintf(
                '<span class="prorank-menu-icon">🎨</span> Cache: %s%% Hit',
                $stats['hit_rate'] ?? 0
            ),
            'href' => admin_url('admin.php?page=prorank-performance'),
        ]);
        
        $wp_admin_bar->add_node([
            'parent' => 'prorank-cache',
            'id' => 'prorank-cache-stats',
            'title' => sprintf('Size: %s | Files: %d', $stats['cache_size'], $stats['total_cached']),
        ]);
        
        $wp_admin_bar->add_node([
            'parent' => 'prorank-cache',
            'id' => 'prorank-cache-clear',
            'title' => 'Clear All Cache',
            'href' => wp_nonce_url(admin_url('admin-post.php?action=prorank_clear_cache'), 'prorank_clear_cache'),
        ]);
    }
    
    /**
     * Clear URL cache
     *
     * @param string $url URL to clear
     * @return void
     */
    private function clear_url_cache(string $url): void {
        $cache_file = $this->get_cache_file_path($url);

        // Remove ALL device/format variants of THIS url (mobile + fmt-*
        // plus their .gz/.br), not just the base file. Targeted per-URL
        // invalidation only — global clear_all_cache() still wipes the
        // whole directory and is intentionally untouched.
        $files = $this->get_cache_file_variants($cache_file);

        foreach ($files as $file) {
            if (file_exists($file)) {
                \ProRank\SEO\Core\FilesystemHelper::delete_file($file);
            }
        }

        $index = $this->tag_index();
        if ($index->is_available()) {
            $index->forget_key($this->cache_key_for_index($cache_file));
        }
    }
    
    /**
     * Clear all cache
     *
     * @return void
     */
    public function clear_all_cache(): void {
        $cache_dir = $this->cache_dir;

        if (is_dir($cache_dir)) {
            // Delete only the CONTENTS, not the directory itself.
            // This preserves correct ownership (www-data) when the purge
            // is triggered via WP-CLI running as root.
            $this->delete_directory_contents($cache_dir);
        }

        // Reset the tag index — every cached page is gone now.
        $this->tag_index()->clear();

        // Reset stats
        $this->stats = [
            'hits' => 0,
            'misses' => 0,
            'bytes_saved' => 0,
            'response_times' => [],
        ];
        $this->save_stats();

        // Trigger edge cache purge
        do_action('prorank_cache_cleared', 'all', null);

        // Optional: also purge the configured CDN provider when "clear CDN on
        // purge-all" is enabled. Done after the local purge so the CDN never
        // re-serves a fresh copy of a stale entry.
        $this->maybe_purge_cdn_on_clear_all();

        // Prime key cache entries so first external visitors (or PSI runs)
        // don't pay a full dynamic-render penalty after cache clears.
        $this->prime_core_cache_urls();
    }

    /**
     * Dispatch a CDN-wide purge when the user has opted into the polish UX.
     *
     * Reads `cdn_clear_on_purge_all` from cache settings. Silent on failure
     * so a CDN outage never breaks the local purge.
     *
     * @return void
     */
    private function maybe_purge_cdn_on_clear_all(): void {
        if (!$this->get_setting('cdn_clear_on_purge_all', false)) {
            return;
        }

        $class = '\\ProRank\\SEO\\Services\\Cdn\\CdnManager';
        if (!class_exists($class)) {
            return;
        }

        try {
            $cdn = new $class();
            if (method_exists($cdn, 'purge_all')) {
                $cdn->purge_all();
            }
        } catch (\Throwable $e) {
            // Best-effort; do not let CDN purge failures block local clear.
        }
    }

    /**
     * Prime core frontend URLs after cache clear.
     *
     * @return void
     */
    private function prime_core_cache_urls(): void {
        $cache_enabled = (bool) $this->get_setting('cache_enabled', $this->get_setting('enabled', false));
        if (!$cache_enabled) {
            return;
        }

        $urls = [home_url('/')];
        $posts_page_id = (int) get_option('page_for_posts');
        if ($posts_page_id > 0) {
            $posts_page_url = get_permalink($posts_page_id);
            if (is_string($posts_page_url) && $posts_page_url !== '') {
                $urls[] = $posts_page_url;
            }
        }

        $urls = array_values(array_unique(array_filter($urls, static function ($url): bool {
            return is_string($url) && $url !== '';
        })));

        if (empty($urls)) {
            return;
        }

        $desktop_args = [
            'timeout' => 8,
            'redirection' => 3,
            'blocking' => true,
            'sslverify' => false,
            'cookies' => [],
            'user-agent' => 'ProRank-Cache-Prime/1.0 (desktop)',
        ];

        $prime_mobile = (bool) $this->get_setting('mobile_separate', false);
        $mobile_args = $desktop_args;
        $mobile_args['user-agent'] = 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Mobile Safari/537.36 ProRank-Cache-Prime/1.0';

        foreach ($urls as $url) {
            wp_remote_get($url, $desktop_args);
            if ($prime_mobile) {
                wp_remote_get($url, $mobile_args);
            }
        }
    }
    
    /**
     * Get cache directory size
     *
     * @return int Size in bytes
     */
    private function get_cache_size(): int {
        $size = 0;
        $cache_dir = $this->cache_dir;
        
        if (!is_dir($cache_dir)) {
            return $size;
        }
        
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($cache_dir, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::LEAVES_ONLY
        );
        
        foreach ($iterator as $file) {
            if ($file->isFile()) {
                $size += $file->getSize();
            }
        }
        
        return $size;
    }
    
    /**
     * Count cached files
     *
     * @return int
     */
    private function count_cached_files(): int {
        $count = 0;
        $cache_dir = $this->cache_dir;
        
        if (!is_dir($cache_dir)) {
            return $count;
        }
        
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($cache_dir, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::LEAVES_ONLY
        );
        
        foreach ($iterator as $file) {
            if ($file->isFile() && $file->getExtension() === 'html') {
                $count++;
            }
        }
        
        return $count;
    }
    
    /**
     * Track geographic hit for analytics
     *
     * @return void
     */
    private function track_geographic_hit(): void {
        // Get visitor's geographic location from IP
        $ip = prorank_get_server_var('HTTP_CF_CONNECTING_IP');
        if ($ip === '') {
            $ip = prorank_get_server_var('HTTP_X_FORWARDED_FOR');
        }
        if ($ip === '') {
            $ip = prorank_get_server_var('REMOTE_ADDR');
        }
        
        if (empty($ip)) {
            return;
        }
        
        // Use CloudFlare's geo headers if available
        $country = prorank_get_server_var('HTTP_CF_IPCOUNTRY') ?: null;
        $region = $this->get_region_from_country($country);
        
        // Update geographic stats
        $geo_stats = get_transient('prorank_geo_stats') ?: [];
        $geo_stats[$region] = ($geo_stats[$region] ?? 0) + 1;
        set_transient('prorank_geo_stats', $geo_stats, DAY_IN_SECONDS);
    }
    
    /**
     * Get region from country code
     *
     * @param string|null $country Country code
     * @return string
     */
    private function get_region_from_country(?string $country): string {
        if (!$country) {
            return 'unknown';
        }
        
        $regions = [
            'north-america' => ['US', 'CA', 'MX'],
            'europe' => ['GB', 'DE', 'FR', 'IT', 'ES', 'NL', 'BE', 'PL', 'SE', 'NO'],
            'asia-pacific' => ['CN', 'JP', 'IN', 'AU', 'NZ', 'SG', 'KR', 'TH'],
            'south-america' => ['BR', 'AR', 'CL', 'CO', 'PE'],
            'africa' => ['ZA', 'NG', 'EG', 'KE', 'MA'],
            'middle-east' => ['AE', 'SA', 'IL', 'TR', 'IR'],
        ];
        
        foreach ($regions as $region => $countries) {
            if (in_array($country, $countries, true)) {
                return $region;
            }
        }
        
        return 'other';
    }
    
    /**
     * Send analytics to edge network
     *
     * @param float $response_time Response time in ms
     * @param int   $bytes_saved   Bytes saved
     * @return void
     */
    private function send_analytics_to_edge(float $response_time, int $bytes_saved): void {
        $url = $this->get_current_url();
        if (!apply_filters('prorank_enable_edge_analytics_cron', false, $response_time, $bytes_saved, $url)) {
            return;
        }

        // Send analytics asynchronously using wp-cron
        if (!wp_next_scheduled('prorank_send_edge_analytics')) {
            wp_schedule_single_event(time() + 1, 'prorank_send_edge_analytics', [
                'response_time' => $response_time,
                'bytes_saved' => $bytes_saved,
                'url' => $url,
                'timestamp' => time(),
            ]);
        }
    }
    
    /**
     * Track cache miss reason
     *
     * @return void
     */
    private function track_miss_reason(?string $reason = null): void {
        $reason = is_string($reason) && $reason !== '' ? $reason : $this->last_cache_reason;
        if (!is_string($reason) || $reason === '') {
            $reason = 'unknown';
        }
        
        $miss_reasons = get_transient('prorank_cache_miss_reasons') ?: [];
        $miss_reasons[$reason] = ($miss_reasons[$reason] ?? 0) + 1;
        set_transient('prorank_cache_miss_reasons', $miss_reasons, DAY_IN_SECONDS);
    }
    
    /**
     * Delete directory recursively
     *
     * @param string $dir Directory path
     * @return bool
     */
    private function delete_directory_recursive(string $dir): bool {
        if (!is_dir($dir)) {
            return false;
        }
        
        $files = array_diff(scandir($dir), ['.', '..']);
        
        foreach ($files as $file) {
            $path = $dir . '/' . $file;
            if (is_dir($path)) {
                $this->delete_directory_recursive($path);
            } else {
                \ProRank\SEO\Core\FilesystemHelper::delete_file($path);
            }
        }
        
        return \ProRank\SEO\Core\FilesystemHelper::delete_dir($dir);
    }

    /**
     * Delete directory contents without removing the directory itself.
     * Preserves the parent directory ownership (important when WP-CLI runs as root
     * but PHP-FPM runs as www-data).
     *
     * @param string $dir Directory path
     * @return void
     */
    private function delete_directory_contents(string $dir): void {
        if (!is_dir($dir)) {
            return;
        }

        $files = array_diff((array) scandir($dir), ['.', '..']);

        foreach ($files as $file) {
            $path = $dir . '/' . $file;
            if (is_dir($path)) {
                $this->delete_directory_recursive($path);
            } else {
                \ProRank\SEO\Core\FilesystemHelper::delete_file($path);
            }
        }
    }

    /**
     * Get critical CSS for current page
     *
     * @return array
     */
    private function get_critical_css(): array {
        // This would extract critical CSS for above-the-fold content
        // For now, return theme critical files
        $theme_uri = get_stylesheet_directory_uri();
        $critical = [];
        
        // Add main theme stylesheet as critical
        $critical[] = $theme_uri . '/style.css';
        
        // Add any custom critical CSS files
        $critical_option = get_option('prorank_critical_css_files', '');
        if (!empty($critical_option)) {
            $files = array_filter(array_map('trim', explode("\n", $critical_option)));
            $critical = array_merge($critical, $files);
        }
        
        return $critical;
    }
    
    /**
     * Get URL from cache file path
     *
     * @param string $cache_path Cache file path
     * @return string
     */
    private function get_url_from_cache_path(string $cache_path): string {
        // Extract URL from cache file path
        $path_parts = explode('/', $cache_path);
        $filename = end($path_parts);
        $host = prev($path_parts);
        
        // Remove .html extension and reconstruct URL
        $path = str_replace(['-', '.html'], ['/', ''], $filename);
        if ($path === 'index') {
            $path = '/';
        }
        
        return 'https://' . $host . $path;
    }
    
    /**
     * Push to Cloudflare
     *
     * @param string $url     URL to cache
     * @param string $content Content
     * @param string $api_key API key
     * @return void
     */
    private function push_to_cloudflare(string $url, string $content, string $api_key): void {
        // Standard Cloudflare zone APIs do not support direct HTML edge uploads.
        // Keep behavior safe by purging the URL so edge re-fetches fresh origin HTML.
        unset($content);
        $this->purge_cloudflare_cache($url, $api_key);
    }
    
    /**
     * Push to Fastly
     *
     * @param string $url     URL to cache
     * @param string $content Content
     * @param string $api_key API key
     * @return void
     */
    private function push_to_fastly(string $url, string $content, string $api_key): void {
        // Implement Fastly edge push
        wp_remote_post('https://api.fastly.com/service/{service_id}/version/{version}/cache', [
            'headers' => [
                'Fastly-Key' => $api_key,
                'Content-Type' => 'text/html',
            ],
            'body' => $content,
            'timeout' => 5,
        ]);
    }
    
    /**
     * Push to BunnyCDN
     *
     * @param string $url     URL to cache
     * @param string $content Content
     * @param string $api_key API key
     * @return void
     */
    private function push_to_bunny(string $url, string $content, string $api_key): void {
        // Implement BunnyCDN push
        wp_remote_post('https://storage.bunnycdn.com/{storage_zone}/{path}', [
            'headers' => [
                'AccessKey' => $api_key,
                'Content-Type' => 'text/html',
            ],
            'body' => $content,
            'timeout' => 5,
        ]);
    }

    /**
     * Ensure cache directory exists for current host
     *
     * @return bool
     */
    private function ensure_cache_directory(): bool {
        $base_dir = $this->cache_dir;
        $host = prorank_get_server_var('HTTP_HOST');
        $target_dir = $host ? $base_dir . '/' . $host : $base_dir;

        if (!is_dir($base_dir) && !wp_mkdir_p($base_dir)) {
            return false;
        }

        if ($host && !is_dir($target_dir) && !wp_mkdir_p($target_dir)) {
            return false;
        }

        return wp_is_writable($target_dir);
    }


    /**
     * Get cache file path for current or specific URL
     *
     * @param string|null $url Optional URL
     * @return string
     */
    private function get_cache_file_path(?string $url = null): string {
        if ($url === null) {
            $url = $this->get_current_url();
        }

        $parsed = wp_parse_url($url);
        $path = isset($parsed['path']) ? trim($parsed['path'], '/') : '';
        $host = isset($parsed['host']) ? $parsed['host'] : prorank_get_server_var('HTTP_HOST');

        // Use directory structure for htaccess serving compatibility
        $use_directory_structure = $this->get_setting('htaccess_serving', false);
        $accept_suffix = $this->get_accept_image_cache_suffix();
        $context_suffix = $this->get_context_cache_suffix();

        if ($use_directory_structure) {
            // Directory structure: /host/path/to/page/index.html
            $cache_path = $this->cache_dir . '/' . $host;

            if (empty($path)) {
                $cache_path .= '/index' . $accept_suffix . $context_suffix . '.html';
            } else {
                // URL like /about/team becomes /about/team/index.html
                $cache_path .= '/' . $path . '/index' . $accept_suffix . $context_suffix . '.html';
            }

            // Add mobile suffix if enabled
            if ($this->get_setting('mobile_separate', false) && $this->is_mobile_device()) {
                $cache_path = str_replace('/index' . $accept_suffix . $context_suffix . '.html', '/index' . $accept_suffix . $context_suffix . '-mobile.html', $cache_path);
            }

            return $cache_path;
        }

        // Legacy flat file structure: /host/path-to-page.html
        if (empty($path)) {
            $path = 'index';
        }

        // Replace special characters
        $path = str_replace(['/', '\\', '?', '&', '='], '-', $path);

        // Add query string hash if present
        if (!empty($parsed['query'])) {
            $path .= '-' . md5($parsed['query']);
        }

        // Add device type suffix if mobile detection is enabled
        if ($this->get_setting('mobile_separate', false)) {
            if ($this->is_mobile_device()) {
                $path .= '-mobile';
            }
        }

        // Prevent cross-browser HTML cache pollution when image delivery rewrites
        // URLs based on Accept headers.
        $path .= $accept_suffix . $context_suffix;

        return $this->cache_dir . '/' . $host . '/' . $path . '.html';
    }

    /**
     * Get Vary header value for cached HTML responses.
     *
     * @return string
     */
    private function get_vary_header_value(): string {
        $vary = ['Accept-Encoding'];

        if ($this->should_vary_cache_by_accept()) {
            $vary[] = 'Accept';
        }

        if ($this->should_cache_logged_in_request() || $this->get_setting('woocommerce_cache_variants', false)) {
            $vary[] = 'Cookie';
        }

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

    private function should_cache_logged_in_request(): bool {
        return (bool) (
            $this->get_setting('cache_logged_in_users', false)
            || $this->get_setting('cache_per_role', false)
            || $this->get_setting('cache_per_user', false)
        );
    }

    private function should_disable_htaccess_direct_serving(): bool {
        return (bool) $this->get_setting('woocommerce_cache_variants', false);
    }

    private function get_context_cache_suffix(): string {
        $parts = [];

        if (is_user_logged_in() && $this->should_cache_logged_in_request()) {
            if ($this->get_setting('cache_per_user', false)) {
                $parts[] = 'user-' . get_current_user_id();
            } elseif ($this->get_setting('cache_per_role', false)) {
                $user = wp_get_current_user();
                $roles = is_object($user) ? (array) ($user->roles ?? []) : [];
                sort($roles);
                $parts[] = 'role-' . md5(implode(',', array_map('strval', $roles)));
            } else {
                $parts[] = 'logged-in';
            }
        }

        if ($this->get_setting('woocommerce_cache_variants', false) && (class_exists('WooCommerce') || function_exists('WC'))) {
            $country = $this->get_woocommerce_customer_country();
            $currency = function_exists('get_woocommerce_currency') ? (string) get_woocommerce_currency() : '';
            if ($country !== '') {
                $parts[] = 'country-' . strtolower($country);
            }
            if ($currency !== '') {
                $parts[] = 'currency-' . strtolower($currency);
            }
        }

        // Allow compatibility shims (e.g. the WooCommerce currency/country
        // variation layer) to contribute additional cache-key parts. Default
        // behaviour is unchanged when no shim hooks this filter.
        $parts = apply_filters('prorank_cache_context_suffix_parts', $parts);
        $parts = is_array($parts)
            ? array_values(array_filter(array_map('strval', $parts), static function ($part) {
                return $part !== '';
            }))
            : [];

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

        $suffix = implode('-', $parts);
        $suffix = preg_replace('/[^a-z0-9_-]+/i', '-', $suffix);
        return '-' . trim((string) $suffix, '-');
    }

    private function get_woocommerce_customer_country(): string {
        if (!function_exists('WC')) {
            return '';
        }

        try {
            $wc = WC();
            if (!is_object($wc) || empty($wc->customer)) {
                return '';
            }

            foreach (['get_billing_country', 'get_shipping_country'] as $method) {
                if (is_callable([$wc->customer, $method])) {
                    $country = (string) $wc->customer->{$method}();
                    if ($country !== '') {
                        return strtoupper($country);
                    }
                }
            }
        } catch (\Throwable $e) {
            return '';
        }

        return '';
    }

    /**
     * Determine whether HTML cache must vary by Accept header.
     *
     * @return bool
     */
    private function should_vary_cache_by_accept(): bool {
        $settings = get_option('prorank_image_optimization_settings', []);
        if (!is_array($settings)) {
            return false;
        }

        $delivery_method = (string) ($settings['delivery_method'] ?? '');
        if ($delivery_method !== 'accept_header') {
            return false;
        }

        return !empty($settings['jxl_enabled'])
            || !empty($settings['avif_enabled'])
            || !empty($settings['webp_enabled']);
    }

    /**
     * Build cache-key suffix from browser image format capabilities.
     *
     * @return string
     */
    private function get_accept_image_cache_suffix(): string {
        if (!$this->should_vary_cache_by_accept()) {
            return '';
        }

        $accept = strtolower(prorank_get_server_var('HTTP_ACCEPT'));

        if (strpos($accept, 'image/jxl') !== false) {
            return '-fmt-jxl';
        }

        if (strpos($accept, 'image/avif') !== false) {
            return '-fmt-avif';
        }

        if (strpos($accept, 'image/webp') !== false) {
            return '-fmt-webp';
        }

        return '-fmt-legacy';
    }
    /**
     * Get current URL
     *
     * @return string
     */
    private function get_current_url(): string {
        $protocol = is_ssl() ? 'https' : 'http';
        $host = prorank_get_server_var('HTTP_HOST');
        $uri = prorank_get_server_var('REQUEST_URI');

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

    /**
     * Check if current URL is excluded
     *
     * @return bool
     */
    private function is_url_excluded(): bool {
        $excluded = $this->get_setting('cache_exclude_urls', '');
        if ('' === trim((string) $excluded)) {
            return false;
        }

        $current_url = $this->get_current_url();
        $patterns = array_filter(array_map('trim', explode("\n", (string) $excluded)));

        foreach ($patterns as $pattern) {
            if ('' === $pattern) {
                continue;
            }
            if (strpos($current_url, $pattern) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Detect whether the current request matches a configured taxonomy /
     * post-type exclusion list.
     *
     * Returns the matching reason string (suitable for last_cache_reason) when
     * excluded, or an empty string when not.
     *
     * Settings (each an array of IDs or slugs; empty = no rule):
     *   cache_exclude_categories  – exclude single posts in or archive of these categories
     *   cache_exclude_tags        – exclude single posts tagged or archive of these tags
     *   cache_exclude_post_types  – exclude singular + archive for these post types
     *
     * @return string Empty string when not excluded, otherwise the reason key.
     */
    private function matched_taxonomy_exclusion(): string {
        $categories  = $this->normalize_exclusion_list($this->get_setting('cache_exclude_categories', []));
        $tags        = $this->normalize_exclusion_list($this->get_setting('cache_exclude_tags', []));
        $post_types  = $this->normalize_exclusion_list($this->get_setting('cache_exclude_post_types', []));

        if (!$categories && !$tags && !$post_types) {
            return '';
        }

        if ($post_types) {
            $current_pt = '';
            if (function_exists('is_singular') && is_singular()) {
                $current_pt = (string) get_post_type();
            } elseif (function_exists('is_post_type_archive') && is_post_type_archive()) {
                $obj = function_exists('get_queried_object') ? get_queried_object() : null;
                $current_pt = isset($obj->name) ? (string) $obj->name : '';
            }

            if ($current_pt !== '' && in_array($current_pt, $post_types, true)) {
                return 'excluded_post_type';
            }
        }

        if ($categories) {
            if (function_exists('is_category') && is_category()) {
                $obj = function_exists('get_queried_object') ? get_queried_object() : null;
                if ($obj && (in_array((string) ($obj->term_id ?? ''), $categories, true)
                    || in_array((string) ($obj->slug ?? ''), $categories, true))) {
                    return 'excluded_category';
                }
            }

            if (function_exists('is_singular') && is_singular('post')) {
                $post_id = function_exists('get_the_ID') ? get_the_ID() : 0;
                if ($post_id && $this->post_has_terms((int) $post_id, 'category', $categories)) {
                    return 'excluded_category';
                }
            }
        }

        if ($tags) {
            if (function_exists('is_tag') && is_tag()) {
                $obj = function_exists('get_queried_object') ? get_queried_object() : null;
                if ($obj && (in_array((string) ($obj->term_id ?? ''), $tags, true)
                    || in_array((string) ($obj->slug ?? ''), $tags, true))) {
                    return 'excluded_tag';
                }
            }

            if (function_exists('is_singular') && is_singular('post')) {
                $post_id = function_exists('get_the_ID') ? get_the_ID() : 0;
                if ($post_id && $this->post_has_terms((int) $post_id, 'post_tag', $tags)) {
                    return 'excluded_tag';
                }
            }
        }

        return '';
    }

    /**
     * Normalize an exclusion setting into a flat array of trimmed strings.
     *
     * Accepts arrays, newline-delimited strings, or comma-delimited strings.
     *
     * @param mixed $value
     * @return array<int,string>
     */
    private function normalize_exclusion_list($value): array {
        if (is_array($value)) {
            $items = $value;
        } elseif (is_string($value)) {
            $items = preg_split('/[\r\n,]+/', $value) ?: [];
        } else {
            return [];
        }

        $out = [];
        foreach ($items as $item) {
            $item = trim((string) $item);
            if ($item !== '') {
                $out[] = $item;
            }
        }

        return array_values(array_unique($out));
    }

    /**
     * Does this post belong to any of the given terms (by ID or slug)?
     *
     * @param int                  $post_id
     * @param string               $taxonomy
     * @param array<int,string>    $needles
     * @return bool
     */
    private function post_has_terms(int $post_id, string $taxonomy, array $needles): bool {
        if (!function_exists('get_the_terms')) {
            return false;
        }

        $terms = get_the_terms($post_id, $taxonomy);
        if (!is_array($terms)) {
            return false;
        }

        foreach ($terms as $term) {
            $id   = isset($term->term_id) ? (string) $term->term_id : '';
            $slug = isset($term->slug) ? (string) $term->slug : '';
            if (($id !== '' && in_array($id, $needles, true))
                || ($slug !== '' && in_array($slug, $needles, true))) {
                return true;
            }
        }

        return false;
    }

    /**
     * Detect payment form nonces in rendered HTML.
     *
     * Pages containing payment nonces must not be cached because the nonces
     * expire (12-24 h) and stale values cause silent payment failures.
     *
     * @param string $html Full page HTML.
     * @return bool
     */
    private function has_payment_form_nonces(string $html): bool {
        $markers = [
            // Gravity Forms Stripe
            'create_payment_intent_nonce',
            // Gravity Forms PayPal
            'create_paypal_order_nonce',
            // WooCommerce Stripe gateway
            'wc_stripe_payment_request_nonce',
            // WooCommerce PayPal Payments
            'wc_ppcp_nonce',
            // Generic Stripe Elements presence with nonce
            'stripe_payment_nonce',
        ];

        $haystack = strtolower($html);
        foreach ($markers as $marker) {
            if (strpos($haystack, strtolower($marker)) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check if device is mobile
     *
     * @return bool
     */
    private function is_mobile_device(): bool {
        $user_agent = prorank_get_server_var('HTTP_USER_AGENT');
        $mobile_agents = ['Mobile', 'Android', 'iPhone', 'iPad', 'Windows Phone'];

        foreach ($mobile_agents as $agent) {
            if (stripos($user_agent, $agent) !== false) {
                return true;
            }
        }

        return false;
    }
    // =========================================================================
    // HTACCESS / NGINX CACHE SERVING (WP Rocket Pattern)
    // Serves cached files directly from webserver without loading PHP
    // =========================================================================

    /**
     * htaccess marker start
     */
    private const HTACCESS_MARKER_START = '# BEGIN ProRank Page Cache';

    /**
     * htaccess marker end
     */
    private const HTACCESS_MARKER_END = '# END ProRank Page Cache';

    /**
     * Install htaccess rules for direct cache serving
     * This bypasses PHP entirely for cached pages (~5ms vs ~50ms TTFB)
     *
     * @return bool Success status
     */
    public function install_htaccess_rules(): bool {
        if (!$this->get_setting('htaccess_serving', false) || $this->should_disable_htaccess_direct_serving()) {
            return $this->remove_htaccess_rules();
        }

        $htaccess_path = ABSPATH . '.htaccess';

        // Check if writable
        if (file_exists($htaccess_path) && !wp_is_writable($htaccess_path)) {
            prorank_log('ProRank SEO: .htaccess is not writable');
            return false;
        }
        if (!file_exists($htaccess_path) && !wp_is_writable(dirname($htaccess_path))) {
            prorank_log('ProRank SEO: .htaccess parent directory is not writable');
            return false;
        }

        try {
            $rules = $this->generate_htaccess_cache_rules();
            $current = file_exists($htaccess_path) ? file_get_contents($htaccess_path) : '';
            if ($current === false) {
                prorank_log('ProRank SEO: Failed to read .htaccess before install');
                return false;
            }

            // Remove existing rules first
            $current = $this->remove_htaccess_rules_from_content($current);

            // Add rules at the very top (before WordPress rules)
            $wordpress_marker = '# BEGIN WordPress';
            $pos = strpos($current, $wordpress_marker);

            if ($pos !== false) {
                $new_content = substr($current, 0, $pos) . $rules . "\n\n" . substr($current, $pos);
            } else {
                $new_content = $rules . "\n\n" . $current;
            }

            return $this->write_htaccess_safely($htaccess_path, $new_content, 'install page cache rules');

        } catch (\Throwable $e) {
            prorank_log('ProRank SEO: Failed to install htaccess rules - ' . $e->getMessage());
            return false;
        }
    }

    /**
     * Remove htaccess rules
     *
     * @return bool
     */
    public function remove_htaccess_rules(): bool {
        $htaccess_path = ABSPATH . '.htaccess';

        if (!file_exists($htaccess_path)) {
            return true;
        }

        try {
            $content = file_get_contents($htaccess_path);
            $new_content = $this->remove_htaccess_rules_from_content($content);

            if ($new_content !== $content) {
                return $this->write_htaccess_safely($htaccess_path, $new_content, 'remove page cache rules');
            }

            return true;
        } catch (\Throwable $e) {
            return false;
        }
    }

    /**
     * Remove existing rules from htaccess content
     *
     * @param string $content Current htaccess content
     * @return string Cleaned content
     */
    private function remove_htaccess_rules_from_content(string $content): string {
        $pattern = '/' . preg_quote(self::HTACCESS_MARKER_START, '/') . '.*?' . preg_quote(self::HTACCESS_MARKER_END, '/') . '\s*/s';
        return preg_replace($pattern, '', $content);
    }

    /**
     * Generate htaccess rules for cache serving
     *
     * @return string htaccess rules
     */
    private function generate_htaccess_cache_rules(): string {
        $cache_dir = str_replace(ABSPATH, '', $this->cache_dir);

        // System URLs that must never be served from the static cache. Built
        // as a single regex alternation so the negative RewriteCond stays on
        // one line — splitting it broke the older [S=N] generator.
        $system_url_pattern = '!^/(wp-admin|wp-login|wp-cron|wp-json|xmlrpc|feed|cart|checkout|my-account|add-to-cart)';

        // The shared cache-bypass guards. Every cache-serving RewriteRule
        // chains these as negative RewriteConds so a failed guard simply
        // skips that single rule and falls through to WordPress, instead of
        // using [S=N] skips that jumped into the WP rewrite block at
        // unpredictable offsets.
        $bypass_conditions = [
            'RewriteCond %{REQUEST_METHOD} =GET',
            'RewriteCond %{QUERY_STRING} ^$',
            'RewriteCond %{HTTP_COOKIE} !wordpress_logged_in_',
            'RewriteCond %{HTTP_COOKIE} !wp-postpass_',
            'RewriteCond %{HTTP_COOKIE} !comment_author_',
            'RewriteCond %{HTTP_COOKIE} !woocommerce_items_in_cart=1',
            'RewriteCond %{REQUEST_URI} ' . $system_url_pattern . ' [NC]',
        ];

        $serve_block = function (string $comment, array $extra_conditions, string $suffix) use ($cache_dir, $bypass_conditions): string {
            $lines = ['# ' . $comment];
            foreach ($bypass_conditions as $cond) {
                $lines[] = $cond;
            }
            foreach ($extra_conditions as $cond) {
                $lines[] = $cond;
            }
            $lines[] = 'RewriteCond %{DOCUMENT_ROOT}/' . $cache_dir . '/%{HTTP_HOST}/%{REQUEST_URI}/index.html' . $suffix . ' -f';
            $lines[] = 'RewriteRule ^(.*)$ /' . $cache_dir . '/%{HTTP_HOST}/$1/index.html' . $suffix . ' [L]';
            return implode("\n", $lines) . "\n\n";
        };

        $rules  = self::HTACCESS_MARKER_START . "\n";
        $rules .= "<IfModule mod_rewrite.c>\n";
        $rules .= "RewriteEngine On\n";
        $rules .= "RewriteBase /\n\n";

        $rules .= $serve_block(
            'Serve Brotli compressed cache if available',
            ['RewriteCond %{HTTP:Accept-Encoding} br'],
            '.br'
        );

        $rules .= $serve_block(
            'Serve Gzip compressed cache if available',
            ['RewriteCond %{HTTP:Accept-Encoding} gzip'],
            '.gz'
        );

        $rules .= $serve_block(
            'Serve uncompressed cache',
            [],
            ''
        );

        $rules .= "</IfModule>\n\n";

        // Add headers for compressed files
        $rules .= "# Set correct Content-Type and Content-Encoding for compressed cache files\n";
        $rules .= "<IfModule mod_headers.c>\n";
        $rules .= "    <FilesMatch \"\\.html\\.gz$\">\n";
        $rules .= "        Header set Content-Type \"text/html; charset=UTF-8\"\n";
        $rules .= "        Header set Content-Encoding \"gzip\"\n";
        $rules .= "        Header set X-ProRank-Cache \"HIT-HTACCESS-GZ\"\n";
        $rules .= "    </FilesMatch>\n";
        $rules .= "    <FilesMatch \"\\.html\\.br$\">\n";
        $rules .= "        Header set Content-Type \"text/html; charset=UTF-8\"\n";
        $rules .= "        Header set Content-Encoding \"br\"\n";
        $rules .= "        Header set X-ProRank-Cache \"HIT-HTACCESS-BR\"\n";
        $rules .= "    </FilesMatch>\n";
        $rules .= "    <FilesMatch \"index\\.html$\">\n";
        $rules .= "        Header set X-ProRank-Cache \"HIT-HTACCESS\"\n";
        $rules .= "    </FilesMatch>\n";
        $rules .= "</IfModule>\n";

        $rules .= self::HTACCESS_MARKER_END;

        return $rules;
    }

    /**
     * Generate nginx config for cache serving
     *
     * @return string nginx config
     */
    public function generate_nginx_config(): string {
        $cache_dir = str_replace(ABSPATH, '$document_root/', $this->cache_dir);

        $config = "# ProRank SEO Page Cache - Add this to your nginx server block\n\n";

        $config .= "# Cache serving location\n";
        $config .= "set \$prorank_cache_file '';\n";
        $config .= "set \$prorank_enc '';\n\n";

        $config .= "# Skip cache for logged-in users and specific pages\n";
        $config .= "if (\$http_cookie ~* \"wordpress_logged_in_|wp-postpass_|woocommerce_items_in_cart=1\") {\n";
        $config .= "    set \$prorank_cache_file 'null';\n";
        $config .= "}\n\n";

        $config .= "if (\$request_uri ~* \"^/(wp-admin|wp-login|cart|checkout|my-account)\") {\n";
        $config .= "    set \$prorank_cache_file 'null';\n";
        $config .= "}\n\n";

        $config .= "# Check for Brotli cache\n";
        $config .= "if (-f " . $cache_dir . "/\$host/\$request_uri/index.html.br) {\n";
        $config .= "    set \$prorank_cache_file " . $cache_dir . "/\$host/\$request_uri/index.html.br;\n";
        $config .= "    set \$prorank_enc 'br';\n";
        $config .= "}\n\n";

        $config .= "# Check for Gzip cache\n";
        $config .= "if (-f " . $cache_dir . "/\$host/\$request_uri/index.html.gz) {\n";
        $config .= "    set \$prorank_cache_file " . $cache_dir . "/\$host/\$request_uri/index.html.gz;\n";
        $config .= "    set \$prorank_enc 'gzip';\n";
        $config .= "}\n\n";

        $config .= "# Check for uncompressed cache\n";
        $config .= "if (-f " . $cache_dir . "/\$host/\$request_uri/index.html) {\n";
        $config .= "    set \$prorank_cache_file " . $cache_dir . "/\$host/\$request_uri/index.html;\n";
        $config .= "}\n\n";

        $config .= "# Serve cached file if exists\n";
        $config .= "location @prorank_cache {\n";
        $config .= "    internal;\n";
        $config .= "    add_header X-ProRank-Cache \"HIT-NGINX\";\n";
        $config .= "    add_header Content-Encoding \$prorank_enc;\n";
        $config .= "    default_type text/html;\n";
        $config .= "    try_files \$prorank_cache_file =404;\n";
        $config .= "}\n\n";

        $config .= "# In your main location block, add:\n";
        $config .= "# try_files \$prorank_cache_file \$uri \$uri/ /index.php?\$args;\n";

        return $config;
    }

    /**
     * Get cache file path with new directory structure
     * Now uses /path/index.html instead of /path.html for htaccess compatibility
     *
     * @param string|null $url Optional URL
     * @return string Cache file path
     */
    public function get_cache_file_path_v2(?string $url = null): string {
        if ($url === null) {
            $url = $this->get_current_url();
        }

        $parsed = wp_parse_url($url);
        $path = isset($parsed['path']) ? trim($parsed['path'], '/') : '';
        $host = isset($parsed['host']) ? $parsed['host'] : prorank_get_server_var('HTTP_HOST');

        // Build directory path
        $cache_path = $this->cache_dir . '/' . $host;
        $accept_suffix = $this->get_accept_image_cache_suffix();

        if (empty($path)) {
            $cache_path .= '/index' . $accept_suffix . '.html';
        } else {
            // URL like /about/team becomes /about/team/index.html
            $cache_path .= '/' . $path . '/index' . $accept_suffix . '.html';
        }

        // Add mobile suffix if enabled
        if ($this->get_setting('mobile_separate', false) && $this->is_mobile_device()) {
            $cache_path = str_replace('/index' . $accept_suffix . '.html', '/index' . $accept_suffix . '-mobile.html', $cache_path);
        }

        return $cache_path;
    }

    /**
     * Check if htaccess serving is available
     *
     * @return array Status info
     */
    public function get_htaccess_status(): array {
        $htaccess_path = ABSPATH . '.htaccess';

        $status = [
            'available' => false,
            'installed' => false,
            'writable' => false,
            'server' => $this->detect_server_type(),
            'message' => '',
        ];

        // Check server type
        if ($status['server'] === 'nginx') {
            $status['message'] = __('Nginx detected. Use the nginx config generator instead.', 'prorank-seo');
            return $status;
        }

        // Check if htaccess exists and is writable
        if (file_exists($htaccess_path)) {
            $status['writable'] = wp_is_writable($htaccess_path);
            $content = file_get_contents($htaccess_path);
            $status['installed'] = strpos($content, self::HTACCESS_MARKER_START) !== false;
        } else {
            $status['writable'] = wp_is_writable(ABSPATH);
        }

        // Check mod_rewrite
        if (function_exists('apache_get_modules')) {
            $modules = apache_get_modules();
            $status['available'] = in_array('mod_rewrite', $modules);
        } else {
            // Assume available if can't check
            $status['available'] = true;
        }

        if (!$status['writable']) {
            $status['message'] = __('.htaccess is not writable. Please check file permissions.', 'prorank-seo');
        } elseif ($status['installed']) {
            $status['message'] = __('ProRank cache rules are installed and active.', 'prorank-seo');
        } else {
            $status['message'] = __('Ready to install htaccess rules.', 'prorank-seo');
        }

        return $status;
    }

    /**
     * Detect server type
     *
     * @return string Server type
     */
    private function detect_server_type(): string {
        $software = prorank_get_server_var('SERVER_SOFTWARE');

        if (stripos($software, 'nginx') !== false) {
            return 'nginx';
        }
        if (stripos($software, 'apache') !== false) {
            return 'apache';
        }
        if (stripos($software, 'litespeed') !== false) {
            return 'litespeed';
        }

        return 'unknown';
    }
}
