<?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 {
    /**
     * Recurring hook used to keep critical frontend URLs warm.
     */
    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';

    /**
     * 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
     */
    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';
    
    /**
     * 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
        if (is_user_logged_in()) {
            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;
        }

        // Raw Cloud UCSS probes must reach the normal frontend pipeline and
        // must not be cached, otherwise SaaS analysis sees optimized HTML.
        if (!empty($_SERVER['HTTP_X_PRORANK_ANALYZE_RAW'])) {
            $this->last_cache_reason = 'raw_ucss_probe';
            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;
        }

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

    /**
     * Initialize module hooks
     *
     * @return void
     */
    public function init_hooks(): void {
        // Always register cache invalidation + admin hooks (wp-admin runs with logged-in users,
        // but we still need settings changes to purge cached pages).
        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']);
        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('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();
        $this->maybe_schedule_keep_warm();
        do_action('prorank_clear_cache');
    }

    /**
     * Register the recurring cadence used for cache keep-warm requests.
     *
     * @param array<string,array<string,mixed>> $schedules Existing schedules.
     * @return array<string,array<string,mixed>>
     */
    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;
    }

    /**
     * Ensure the recurring keep-warm event stays aligned with the cache settings.
     *
     * @return void
     */
    public function maybe_schedule_keep_warm(): void {
        if (!$this->should_keep_warm_core_urls()) {
            wp_clear_scheduled_hook(self::KEEP_WARM_HOOK);
            return;
        }

        if (!wp_next_scheduled(self::KEEP_WARM_HOOK)) {
            wp_schedule_event(time() + MINUTE_IN_SECONDS, self::KEEP_WARM_SCHEDULE, self::KEEP_WARM_HOOK);
        }
    }

    /**
     * Periodically refresh homepage-level cache entries so upstream proxies do not
     * lapse into cold/expired document responses between organic visits.
     *
     * @return void
     */
    public function run_keep_warm(): void {
        if (!$this->should_keep_warm_core_urls()) {
            return;
        }

        $this->prime_core_cache_urls();
    }

    /**
     * Decide whether proactive keep-warm should run for this site.
     *
     * @return bool
     */
    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 (bool) $this->get_setting('preload_enabled', false);
    }

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

        // When Cloud UCSS is enabled but its artifact is missing/stale, the
        // dynamic response falls back to heavier blocking CSS. Do not freeze
        // that temporary fallback into page cache; the Cloud UCSS provider will
        // queue a rebuild and the next clean render can be cached.
        if ($this->should_skip_cache_due_to_ucss_miss($candidate_payload)) {
            $this->last_cache_reason = 'ucss_artifact_miss';
            return $content;
        }

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

        return $content;
    }

    /**
     * Avoid caching fallback HTML while Cloud UCSS artifacts are rebuilding.
     *
     * @param string $content Full HTML response.
     * @return bool
     */
    private function should_skip_cache_due_to_ucss_miss(string $content): bool {
        if (!class_exists('\\ProRank\\SEO\\Modules\\Performance\\CloudUCSSProvider')) {
            return false;
        }

        $settings = get_option('prorank_asset_optimization_settings', []);
        if (!is_array($settings) || empty($settings['unused_css_enabled'])) {
            return false;
        }

        if (array_key_exists('unused_css_use_cloud', $settings) && empty($settings['unused_css_use_cloud'])) {
            return false;
        }

        return stripos($content, 'id="prorank-used-css"') === false
            && stripos($content, "id='prorank-used-css'") === false;
    }

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

            // 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');

        $shared_ttl = max(60, ($cdn_provider === 'none'
            ? $lifetime
            : (int) $this->get_setting('cdn_ttl', 86400)));

        if ($cdn_provider === 'cloudflare') {
            header('Cache-Control: public, max-age=0, must-revalidate, s-maxage=' . $shared_ttl . ', stale-while-revalidate=60, stale-if-error=86400');
            header('Cloudflare-CDN-Cache-Control: max-age=' . $shared_ttl . ', stale-while-revalidate=60, stale-if-error=86400');

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

        } elseif ($cdn_provider === 'bunnycdn') {
            header('Cache-Control: public, max-age=0, must-revalidate, s-maxage=' . $shared_ttl . ', stale-while-revalidate=60, stale-if-error=86400');
            header('CDN-Cache-Control: max-age=' . $shared_ttl . ', stale-while-revalidate=60, stale-if-error=86400');

        } else {
            header('Cache-Control: public, max-age=0, must-revalidate, s-maxage=' . $shared_ttl . ', stale-while-revalidate=60, stale-if-error=86400');
        }
        
        // 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);
            }

            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);
        
        // 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);
        }
    }
    
    /**
     * 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.
        }
    }
    
    /**
     * 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 = get_transient('prorank_cache_stats') ?: [
            'hits' => 0,
            'misses' => 0,
            'bytes_served' => 0,
            'response_times' => [],
            'last_updated' => time(),
        ];
        
        $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 = get_transient('prorank_cache_stats') ?: [
            'hits' => 0,
            'misses' => 0,
            'bytes_served' => 0,
            'response_times' => [],
            'last_updated' => time(),
        ];
        
        $current_stats['misses']++;
        set_transient('prorank_cache_stats', $current_stats, HOUR_IN_SECONDS);
        
        // Track miss reasons for optimization
        $this->track_miss_reason($reason);
    }
    
    /**
     * Get cache statistics via API
     *
     * @return array
     */
    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 versions
        $files = [
            $cache_file,
            $cache_file . '.gz',
            $cache_file . '.br',
        ];
        
        foreach ($files as $file) {
            if (file_exists($file)) {
                \ProRank\SEO\Core\FilesystemHelper::delete_file($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 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);

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

    /**
     * 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 {
        // 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' => $this->get_current_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();

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

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

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

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

    /**
     * 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 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)) {
            return $this->remove_htaccess_rules();
        }

        $htaccess_path = ABSPATH . '.htaccess';

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

        try {
            $rules = $this->generate_htaccess_cache_rules();
            $current = file_exists($htaccess_path) ? file_get_contents($htaccess_path) : '';

            // 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 file_put_contents($htaccess_path, $new_content) !== false;

        } 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 file_put_contents($htaccess_path, $new_content) !== false;
            }

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

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

        // Only GET requests
        $rules .= "# Only serve cache for GET requests\n";
        $rules .= "RewriteCond %{REQUEST_METHOD} !GET\n";
        $rules .= "RewriteRule .* - [S=20]\n\n";

        // Skip if query string (except UTM params)
        $rules .= "# Skip cache if query string (except tracking params)\n";
        $rules .= "RewriteCond %{QUERY_STRING} !^(utm_|fbclid|gclid|ref=).*$\\n";
        $rules .= "RewriteCond %{QUERY_STRING} !^$\\n";
        $rules .= "RewriteRule .* - [S=18]\n\n";

        // Skip for logged-in users
        $rules .= "# Skip cache for logged-in users\n";
        $rules .= "RewriteCond %{HTTP_COOKIE} wordpress_logged_in_ [OR]\n";
        $rules .= "RewriteCond %{HTTP_COOKIE} wp-postpass_ [OR]\n";
        $rules .= "RewriteCond %{HTTP_COOKIE} comment_author_\n";
        $rules .= "RewriteRule .* - [S=15]\n\n";

        // Skip for WooCommerce cart
        $rules .= "# Skip cache if WooCommerce cart has items\n";
        $rules .= "RewriteCond %{HTTP_COOKIE} woocommerce_items_in_cart=1\n";
        $rules .= "RewriteRule .* - [S=14]\n\n";

        // Skip admin and system URLs
        $rules .= "# Skip admin and system URLs\n";
        $rules .= "RewriteCond %{REQUEST_URI} ^/(wp-admin|wp-login|wp-cron|wp-json|xmlrpc|feed|cart|checkout|my-account|add-to-ca
rt) [NC]\n";
        $rules .= "RewriteRule .* - [S=12]\n\n";

        // Serve Brotli compressed version first
        $rules .= "# Serve Brotli compressed cache if available\n";
        $rules .= "RewriteCond %{HTTP:Accept-Encoding} br\n";
        $rules .= "RewriteCond %{DOCUMENT_ROOT}/" . $cache_dir . "/%{HTTP_HOST}/%{REQUEST_URI}/index.html.br -f\n";
        $rules .= "RewriteRule ^(.*)$ /" . $cache_dir . "/%{HTTP_HOST}/$1/index.html.br [L]\n\n";

        // Serve Gzip compressed version
        $rules .= "# Serve Gzip compressed cache if available\n";
        $rules .= "RewriteCond %{HTTP:Accept-Encoding} gzip\n";
        $rules .= "RewriteCond %{DOCUMENT_ROOT}/" . $cache_dir . "/%{HTTP_HOST}/%{REQUEST_URI}/index.html.gz -f\n";
        $rules .= "RewriteRule ^(.*)$ /" . $cache_dir . "/%{HTTP_HOST}/$1/index.html.gz [L]\n\n";

        // Serve uncompressed cache
        $rules .= "# Serve uncompressed cache\n";
        $rules .= "RewriteCond %{DOCUMENT_ROOT}/" . $cache_dir . "/%{HTTP_HOST}/%{REQUEST_URI}/index.html -f\n";
        $rules .= "RewriteRule ^(.*)$ /" . $cache_dir . "/%{HTTP_HOST}/$1/index.html [L]\n";

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