<?php
/**
 * Page Cache Service
 *
 * Core full-page caching implementation. Handles cache serving, storage,
 * HTML minification, browser cache headers, and stale-while-revalidate logic.
 *
 * @package Mamba\Modules\Caching\Services
 * @since   1.0.0
 */

namespace Mamba\Modules\Caching\Services;

/**
 * Class PageCache
 *
 * Manages full-page cache serving and storage with support for
 * HTML minification, browser caching, and background regeneration.
 *
 * @since 1.0.0
 */
final class PageCache {
    /**
     * Clear regeneration lock file to prevent orphaned locks
     */
    private static function clearRegenLock(?array $paths): void {
        if (is_array($paths) && !empty($paths['lock']) && file_exists($paths['lock'])) {
            @unlink($paths['lock']);
        }
    }

    /**
     * Add prefetch-compatible headers
     */
    private static function addPrefetchHeaders(): void {
        // Add headers that help with prefetch requests
        if (!headers_sent()) {
            // Allow prefetch requests from same origin
            header('X-Frame-Options: SAMEORIGIN');
            
            // Prevent MIME type sniffing (helps with prefetch security)
            header('X-Content-Type-Options: nosniff');
            
            // Set referrer policy for prefetch compatibility
            header('Referrer-Policy: same-origin');
            
            // Note: Let's not set Cache-Control or Vary here. Authoritative
            // cache headers are computed and emitted later in the HIT path
            // (or by WordPress/plugins on MISS).
        }
    }

    public function maybeServe(): void {
        // Remove Content-Length header if compression is active
        self::removeContentLengthIfCompressed();
        
        if (!get_option('mamba_enable_page_cache', 0)) return;
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized via sanitize_text_field
        $method = isset($_SERVER['REQUEST_METHOD']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_METHOD'])) : 'GET';
        if (is_user_logged_in() || is_admin()) return;
        if ($method !== 'GET' && $method !== 'HEAD') return;
        
        // SECURITY: Check for cache bypass (e.g., non-canonical host)
        if (defined('MAMBA_CACHE_BYPASS') && MAMBA_CACHE_BYPASS) return;
        
        // Authorization/preview/customizer/password-protected content should never be cached
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Only checking existence
        if (!empty($_SERVER['HTTP_AUTHORIZATION'])) return;
        if ((function_exists('is_preview') && is_preview()) || isset($_GET['preview']) || isset($_GET['preview_id'])) return;
        if (isset($_GET['customize_changeset_uuid'])) return;
        if (function_exists('post_password_required') && post_password_required()) return;
        if (defined('DOING_AJAX') && DOING_AJAX) return;
        if (defined('REST_REQUEST') && REST_REQUEST) return;
        
        // Bypass search pages to prevent caching personalized search results
        if (function_exists('is_search') && is_search()) return;
        
        // Optional client "no-cache" bypass for debugging/force revalidation
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized via sanitize_text_field
        $cacheControl = isset($_SERVER['HTTP_CACHE_CONTROL']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_CACHE_CONTROL'])) : '';
        $clientNoCache = !empty($cacheControl) && stripos($cacheControl, 'no-cache') !== false;
        $honorClientNoCache = (bool) apply_filters('mamba_cache_honor_client_no_cache', false);
        if ($honorClientNoCache && $clientNoCache) return;
        
        // Cookie-driven variant blocking for personalized content
        $blockCookies = (array) apply_filters('mamba_cache_block_on_cookies', [
            'wp-wpml_current_language'
        ]);
        foreach ($blockCookies as $ck) {
            if (isset($_COOKIE[$ck]) && !empty($_COOKIE[$ck])) return;
        }
        
        // Add prefetch-compatible headers
        self::addPrefetchHeaders();
        // Bypass on mutating GET actions (add to cart, remove, coupons, wc-ajax etc.)
        $mutatingKeys = [
            'add-to-cart','remove_item','undo_item','wc-ajax',
            'apply_coupon','remove_coupon','update_cart',
            'order_again','switch_subscription',
            'add_to_wishlist','remove_from_wishlist'
        ];
        foreach ($mutatingKeys as $k) {
            if (isset($_GET[$k])) return;
        }
        // Woo-only: only cache WooCommerce catalog/product contexts
        if (!$this->isWooCacheableContext()) return;
        if (function_exists('is_cart') && (
            is_cart() || is_checkout() || is_account_page() ||
            (function_exists('is_wc_endpoint_url') && is_wc_endpoint_url())
        )) return;
        if ($this->userHasCartItems()) return;
        
        // Check if this is a warmup request to skip statistics recording
        $isWarmupRequest = self::isWarmupRequest();
        
        // Cache class_exists check for micro-performance optimization
        $statsClassExists = class_exists(__NAMESPACE__.'\\Stats');
        
        $paths = Paths::forCurrentRequest(); if (!$paths) return;
        $cache_file = $paths['file']; $meta_file = $paths['meta'];
        if (file_exists($cache_file) && file_exists($meta_file)) {
            $meta = json_decode(@file_get_contents($meta_file), true) ?: [];
            $baseTTL = (int)get_option('mamba_cache_ttl', 7200); // 2 hours default
            
            // Apply adaptive TTL if enabled (uses context from cached metadata)
            $ttl = apply_filters('mamba_cache_ttl_for_request', $baseTTL, [
                'post_id' => $meta['post_id'] ?? 0,
                'post_type' => $meta['post_type'] ?? '',
                'url' => $meta['url'] ?? '',
                'tags' => $meta['tags'] ?? []
            ]);
            
            // Conservative freshness check - use min of meta time and file mtime
            $meta_time  = (int)($meta['time'] ?? 0);
            $file_mtime = (int) (@filemtime($cache_file) ?: 0);
            $effective_time = $file_mtime > 0 ? min($meta_time, $file_mtime) : $meta_time;
            $fresh = (time() - $effective_time) < $ttl;
            // Make "serve-stale window" filterable for customization
            $staleWindow = (int) apply_filters('mamba_cache_stale_lock_window', 30);
            
            // Auto-clean stale locks to prevent pile-up
            if (file_exists($paths['lock']) && (time() - @filemtime($paths['lock'])) >= $staleWindow) {
                @unlink($paths['lock']);
            }
            
            $stale_ok = file_exists($paths['lock']) && (time() - @filemtime($paths['lock'])) < $staleWindow;
            // if tags exist and any has been bumped after the file mtime, treat as stale regardless of TTL
            $is_tag_stale = false;
            if (!empty($meta['tags']) && is_array($meta['tags'])) {
                // Use consistent time calculation (same as freshness check)
                $file_time = $file_mtime ?: $meta_time;
                $is_tag_stale = class_exists(__NAMESPACE__ . '\\Tags')
                    ? Tags::isStale($meta['tags'], (int) $file_time)
                    : false;
            }
            if ($fresh || $stale_ok) {
                if ($is_tag_stale && !$stale_ok) {
                    // do not serve stale by tags unless regeneration is ongoing; fall through to MISS
                } else {
                    if (!headers_sent()) {
                        header('X-Mamba-Cache: HIT' . (($stale_ok && !$fresh) ? ' (STALE)' : ''));
                        // Explicitly disable range requests for HTML
                        header('Accept-Ranges: none', true);
                    }
                    
                    // Set Last-Modified based on cache file mtime for better browser caching
                    $fileMtime = @filemtime($cache_file);
                    if ($fileMtime && !headers_sent()) {
                        header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $fileMtime) . ' GMT');
                    }
                    
                    // Generate ETag for browser revalidation (if not already present in cached headers)
                    $etag = null;
                    if (!empty($meta['headers'])) {
                        foreach ($meta['headers'] as $h) {
                            if (stripos($h, 'etag:') === 0) {
                                $etag = trim(substr($h, 5)); // Extract existing ETag
                                break;
                            }
                        }
                    }
                    
                    // Generate simple ETag if none exists (based on file mtime + size)
                    if (!$etag && $fileMtime) {
                        $fileSize = @filesize($cache_file) ?: 0;
                        $etag = '"' . md5($fileMtime . '_' . $fileSize) . '"';
                    }
                    
                    // Always send ETag if we have one (from cache or generated)
                    // Send Cache-Control and Vary headers BEFORE 304 checks for proxy compatibility
                    // 1) Re-emit cached Cache-Control if present
                    $hasCC = false;
                    if (!headers_sent() && !empty($meta['headers'])) {
                        foreach ($meta['headers'] as $h) {
                            if (stripos($h, 'cache-control:') === 0) {
                                header($h, true);
                                $hasCC = true;
                                break;
                            }
                        }
                    }
                    
                    // Default Cache-Control when none was captured (align with admin TTLs; still filterable)
                    if (!$hasCC && !headers_sent()) {
                        // Prefer page cache TTL for catalog pages for consistency
                        $browserTtl = 0;
                        if (class_exists(__NAMESPACE__ . '\PageCache')) {
                            $browserTtl = (int) self::getIntendedTtl();
                        }
                        if ($browserTtl <= 0) {
                            $browserTtl = (int) get_option('mamba_browser_ttl', 300); // fallback 5m
                        }
                        $cdnTtl = (int) get_option('mamba_cdn_ttl', 7200); // 2h default
                        $staleIfError = (int) get_option('mamba_stale_if_error', 86400); // 24h default
                        $staleWhileRevalidate = (int) min(max(0, $browserTtl / 2), 120);
                        $recommended = $browserTtl > 0
                            ? sprintf('public, max-age=%d, s-maxage=%d, stale-while-revalidate=%d, stale-if-error=%d', $browserTtl, $cdnTtl, $staleWhileRevalidate, $staleIfError)
                            : 'max-age=0, must-revalidate';
                        $cc = apply_filters('mamba_cache_default_cache_control', $recommended);
                        header('Cache-Control: ' . $cc, true);
                    }

                    // 2) Build and send merged Vary (same logic as below)
                    if (!headers_sent()) {
                        $tokens = [];
                        if (!empty($meta['headers'])) {
                            foreach ($meta['headers'] as $h) {
                                if (stripos($h, 'vary:') === 0) {
                                    $existing = trim(substr($h, 5));
                                    if ($existing !== '') {
                                        $tokens = array_merge($tokens, array_map('trim', explode(',', $existing)));
                                    }
                                    break;
                                }
                            }
                        }
                        $tokens[] = 'Accept-Encoding';
                        $includeAcceptLanguage = get_option('mamba_include_accept_language_vary', true);
                        if ($includeAcceptLanguage && (function_exists('pll_current_language') || defined('ICL_LANGUAGE_CODE'))) {
                            $tokens[] = 'Accept-Language';
                        }
                        $tokens = apply_filters('mamba_cache_vary', $tokens);
                        
                        // CI de-dupe
                        $ci = [];
                        foreach ($tokens as $t) { 
                            $ci[strtolower($t)] = $ci[strtolower($t)] ?? $t; 
                        }
                        $tokens = array_values($ci);
                        
                        if (!empty($tokens)) {
                            header('Vary: ' . implode(', ', $tokens), true);
                        }
                    }
                    
                    if ($etag && !headers_sent()) {
                        header('ETag: ' . $etag);
                    }
                    
                    // Check If-None-Match header for 304 response (robust comparison)
                    if ($etag && !headers_sent()) {
                        $ifNoneMatch = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_IF_NONE_MATCH'])) : '';
                        if ($ifNoneMatch) {
                            $normalize_etag = static function(string $t): string {
                                $t = trim($t);
                                $t = preg_replace('/^W\/\s*/i', '', $t); // drop weak prefix
                                return trim($t, " \t\"");               // strip quotes/space
                            };
                            
                            $needle = $normalize_etag($etag);
                            foreach (array_map('trim', explode(',', $ifNoneMatch)) as $candidate) {
                                if ($normalize_etag($candidate) === $needle) {
                                    // Only record hit if not a warmup request
                                    if (!$isWarmupRequest && $statsClassExists) {
                                        Stats::recordHit($this->currentType());
                                    }
                                    http_response_code(304);
                                    // Set Content-Length for strict proxies
                                    if (!headers_sent()) {
                                        header('Content-Length: 0');
                                    }
                                    exit; // 304 Not Modified - no content sent
                                }
                            }
                        }
                    }
                    
                    // Check If-Modified-Since header for 304 response
                    if ($fileMtime && !headers_sent()) {
                        $ifModifiedSince = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_IF_MODIFIED_SINCE'])) : '';
                        if ($ifModifiedSince) {
                            $ifModifiedSinceTime = strtotime($ifModifiedSince);
                            if ($ifModifiedSinceTime !== false && $fileMtime <= $ifModifiedSinceTime) {
                                // Only record hit if not a warmup request
                                if (!$isWarmupRequest && $statsClassExists) {
                                    Stats::recordHit($this->currentType());
                                }
                                http_response_code(304);
                                // FIX: Set Content-Length for strict proxies
                                if (!headers_sent()) {
                                    header('Content-Length: 0');
                                }
                                exit; // 304 Not Modified - no content sent
                            }
                        }
                    }
                    
                    // Serve cached headers (excluding Set-Cookie, ETag, Last-Modified, Cache-Control, Vary, Surrogate-Control for security and consistency)
                    if (!headers_sent() && !empty($meta['headers'])) {
                        foreach ($meta['headers'] as $h) { 
                            $lh = strtolower($h);
                            if (strpos($lh, 'set-cookie:') === 0) continue; 
                            if (strpos($lh, 'etag:') === 0) continue; // Skip ETag (we set authoritative value)
                            if (strpos($lh, 'last-modified:') === 0) continue; // Skip Last-Modified (we set authoritative value)
                            if (strpos($lh, 'cache-control:') === 0) continue; // already sent pre-304
                            if (strpos($lh, 'vary:') === 0) continue;          // already sent pre-304
                            if (strpos($lh, 'surrogate-control:') === 0) continue; // already sent pre-304

                            // FIX: preserve multiple Link: headers (don't replace)
                            $replace = (strpos($lh, 'link:') === 0) ? false : true;
                            header($h, $replace);
                        }
                    }
                    
                    // Note: Cache-Control and Vary headers are sent BEFORE 304 checks for proxy compatibility
                    // Send Content-Length on HIT 200 for proxy compatibility (gated behind robust compression check and configurable filter)
                    if (!headers_sent()) {
                        $len = @filesize($cache_file);
                        $zlib = (bool) ini_get('zlib.output_compression');
                        $handlers = array_map('strtolower', ob_list_handlers());
                        $hasCompressionHandler = false;
                        foreach ($handlers as $h) {
                            if (strpos($h, 'zlib') !== false || strpos($h, 'gz') !== false) { 
                                $hasCompressionHandler = true; 
                                break; 
                            }
                        }
                        // Make Content-Length configurable to handle upstream proxy compression
                        $sendContentLength = apply_filters('mamba_cache_send_content_length_on_hit', false);
                        if ($sendContentLength && $len && !$zlib && !$hasCompressionHandler) {
                            header('Content-Length: ' . $len);
                        }
                    }
                    
                    // Set Content-Length for HEAD 200 too (reuse compression-aware logic and configurable filter)
                    if ($method === 'HEAD' && !headers_sent()) {
                        $len = @filesize($cache_file);
                        $zlib = (bool) ini_get('zlib.output_compression');
                        $handlers = array_map('strtolower', ob_list_handlers());
                        $hasCompressionHandler = false;
                        foreach ($handlers as $h) {
                            if (strpos($h, 'zlib') !== false || strpos($h, 'gz') !== false) { 
                                $hasCompressionHandler = true; 
                                break; 
                            }
                        }
                        // Make Content-Length configurable for HEAD requests too
                        $sendContentLength = apply_filters('mamba_cache_send_content_length_on_hit', false);
                        if ($sendContentLength && $len && !$zlib && !$hasCompressionHandler) {
                            header('Content-Length: ' . $len);
                        }
                        // Record HEAD hit too
                        if (!$isWarmupRequest && $statsClassExists) {
                            Stats::recordHit($this->currentType());
                        }
                        exit;
                    }
                    
                    // Only record hit if not a warmup request
                    if (!$isWarmupRequest && $statsClassExists) {
                        Stats::recordHit($this->currentType());
                    }
                    readfile($cache_file); exit;
                }
            }
        }
        if (!headers_sent()) header('X-Mamba-Cache: MISS');
        if ($method === 'HEAD') {
            // Only record the miss for HEAD requests if not a warmup request
            if (!$isWarmupRequest && $statsClassExists) {
                Stats::recordMiss($this->currentType(), 0);
            }
            // do not buffer or cache on HEAD miss; let WP handle generating headers
            return;
        }
        // Create regeneration lock so other requests can serve-stale
        if (!file_exists($paths['lock'])) {
            @touch($paths['lock']);
        }
        $start = microtime(true);
        ob_start(function($buffer) use ($start, $isWarmupRequest) { return $this->save($buffer, $start, $isWarmupRequest); });
        
        // Register shutdown handler to ensure buffer is explicitly flushed
        // This guarantees the buffer is closed even if WordPress exits unexpectedly
        register_shutdown_function(function () {
            if (ob_get_level() > 0) {
                ob_end_flush();
            }
        });
    }
    
    public function save(string $buffer, float $start=0.0, bool $isWarmupRequest=false): string {
        $paths = Paths::forCurrentRequest(); // Get paths early for lock cleanup
        
        if (strlen($buffer) < 255 || strlen($buffer) > 10000000) { 
            self::clearRegenLock($paths); 
            return $buffer; 
        }
        if (function_exists('wc_notice_count') && wc_notice_count() > 0) { 
            self::clearRegenLock($paths); 
            return $buffer; 
        }
        foreach (headers_list() as $h) if (stripos($h, 'set-cookie:')===0) { 
            self::clearRegenLock($paths); 
            return $buffer; 
        }
        
        // Respect "do not cache" directives from upstream
        foreach (headers_list() as $h) {
            if (stripos($h, 'cache-control:') === 0) {
                $v = strtolower($h);
                if (strpos($v, 'no-store') !== false || strpos($v, 'private') !== false) {
                    self::clearRegenLock($paths);
                    return $buffer; // do not cache
                }
            }
            if (stripos($h, 'pragma:') === 0 && stripos(strtolower($h), 'no-cache') !== false) {
                self::clearRegenLock($paths);
                return $buffer; // legacy signal – safest to skip
            }
            // Don't cache compressed output (zlib)
            if (stripos($h, 'content-encoding:') === 0) {
                self::clearRegenLock($paths);
                return $buffer; // avoid caching compressed payloads
            }
        }
        
        // Also skip if PHP-level compression is active (even if header wasn't set)
        $zlib = (bool) ini_get('zlib.output_compression');
        $handlers = array_map('strtolower', ob_list_handlers());
        $hasCompressionHandler = false;
        foreach ($handlers as $h) {
            if (strpos($h, 'zlib') !== false || strpos($h, 'gz') !== false) { 
                $hasCompressionHandler = true; 
                break; 
            }
        }
        if ($zlib || $hasCompressionHandler) {
            self::clearRegenLock($paths);
            return $buffer; // avoid caching compressed payloads produced by PHP
        }
        
        // Cache class_exists check here too (maybeServe()'s var isn't in scope)
        $statsClassExists = class_exists(__NAMESPACE__.'\\Stats');
        
        $status = function_exists('http_response_code') ? (int) http_response_code() : 200;
        if ($status !== 200) { 
            self::clearRegenLock($paths); 
            return $buffer; 
        }
        if (!$paths) { 
            self::clearRegenLock($paths); 
            return $buffer; 
        }
        if (!is_dir($paths['dir'])) { 
            if (!wp_mkdir_p($paths['dir'])) { 
                self::clearRegenLock($paths); 
                return $buffer; 
            }
            // Create protection files when directory is created
            self::createProtectionFiles($paths['dir']);
        }

        $headers = [];
        $varyHeaders = [];
        
        // Make header whitelist extensible for plugin compatibility
        $allow = apply_filters('mamba_cache_header_whitelist', [
            'Content-Type','Link','Last-Modified','ETag','Cache-Control','Surrogate-Control',
            'X-Frame-Options','X-Content-Type-Options','Referrer-Policy', // Security headers for prefetch compatibility
            'Cache-Tag','CDN-Tag' // Persist CDN tagging headers so tags are re-emitted on HITs
        ]);
        
        foreach (headers_list() as $h) {
            if (preg_match('/^('.implode('|', array_map('preg_quote',$allow)).'):/i', $h)) {
                $headers[] = $h;
            } elseif (stripos($h, 'vary:') === 0) {
                // Collect Vary headers for deduplication
                $varyHeaders[] = trim(substr($h, 5));
            }
        }
        
        // Merge existing and computed Vary headers into single value
        $existing = [];
        foreach ($varyHeaders as $v) { 
            $existing = array_merge($existing, array_map('trim', explode(',', $v))); 
        }
        
        $computed = [];
        $computed[] = 'Accept-Encoding';
        $includeAcceptLanguage = get_option('mamba_include_accept_language_vary', true);
        if ($includeAcceptLanguage && (function_exists('pll_current_language') || defined('ICL_LANGUAGE_CODE'))) {
            $computed[] = 'Accept-Language';
        }
        $computed = apply_filters('mamba_cache_vary', $computed);
        
        // Union (case-insensitive) and emit once
        $all = array_merge($existing, $computed);
        // de-dupe case-insensitively while preserving first-seen casing
        $ci = [];
        foreach ($all as $t) { 
            $ci[strtolower($t)] = $ci[strtolower($t)] ?? $t; 
        }
        $allVary = array_values($ci);
        
        if (!empty($allVary)) {
            $varyHeader = 'Vary: ' . implode(', ', $allVary);
            if (!headers_sent()) {
                header($varyHeader, true);
            }
            // Ensure we do not also push an earlier 'Vary:' line
            $headers = array_filter($headers, fn($h) => stripos($h, 'vary:') !== 0);
            $headers[] = $varyHeader;
        }
        
        $tags = class_exists(__NAMESPACE__.'\\Tags') ? Tags::detectForCurrentRequest() : [];
    
        // Optionally apply HTML/CSS/JS minification when enabled and response is HTML
        $shouldMinify = (bool) get_option('mamba_enable_html_minify', 0);
        if ($shouldMinify) {
            // Check if Content-Type indicates HTML
            $isHtml = false;
            foreach ($headers as $h) {
                if (stripos($h, 'content-type:') === 0 && stripos($h, 'text/html') !== false) { $isHtml = true; break; }
            }
            if (!$isHtml) {
                // Fallback heuristic if header not captured
                $isHtml = (stripos($buffer, '<html') !== false);
            }
            if ($isHtml) {
                $mode = get_option('mamba_html_minify_mode', 'conservative');
                // Aggressive mode is premium-only - force conservative for free users
                if ($mode === 'aggressive' && (!function_exists('mamba_fs') || !mamba_fs()->can_use_premium_code__premium_only())) {
                    $mode = 'conservative';
                }
                // Use fully qualified name to avoid namespace import issues
                if (class_exists('Mamba\Modules\Bloat\Services\HtmlMinifier')) {
                    $originalSize = strlen($buffer);
                    $buffer = \Mamba\Modules\Bloat\Services\HtmlMinifier::minify($buffer, is_string($mode) ? $mode : 'conservative');
                    $minifiedSize = strlen($buffer);
                    
                    // Track minification savings
                    if ($minifiedSize < $originalSize && class_exists('Mamba\Support\SavingsTracker')) {
                        \Mamba\Support\SavingsTracker::trackMinifySavings($originalSize, $minifiedSize);
                    }
                }
            }
        }

        $meta = ['time'=>time(),'tags'=>$tags,'headers'=>$headers];

        // Get old sizes BEFORE writing to prevent overcounting
        $oldSize = (file_exists($paths['file']) ? (int)@filesize($paths['file']) : 0);
        $oldMetaSize = (file_exists($paths['meta']) ? (int)@filesize($paths['meta']) : 0);
        $oldTotal = $oldSize + $oldMetaSize;

        $tmp = $paths['dir'].'index.tmp.'.uniqid('', true);
        $metaTmp = $paths['meta'].'.tmp.'.uniqid('', true);
        $ok = @file_put_contents($tmp, $buffer) !== false;
        $metaOk = @file_put_contents($metaTmp, wp_json_encode($meta)) !== false;
        if ($ok && $metaOk && @rename($tmp, $paths['file']) && @rename($metaTmp, $paths['meta'])) {
            // Set secure file permissions (0640) for cache files
            @chmod($paths['file'], 0640);
            @chmod($paths['meta'], 0640);
            
            @unlink($paths['lock']);
            $genMs = $start > 0 ? (int)round((microtime(true) - $start) * 1000) : 0;
            // Only record miss if not a warmup request
            if (!$isWarmupRequest && $statsClassExists) {
                Stats::recordMiss($this->currentType(), $genMs);
            }
            
            // Update cache size with delta (newTotal - oldTotal)
            $newSize = (int)@filesize($paths['file']);
            $newMetaSize = (int)@filesize($paths['meta']);
            $newTotal = $newSize + $newMetaSize;
            
            $stats = get_option('mamba_cache_stats', ['cache_size' => 0]);
            $stats['cache_size'] = max(0, (int)($stats['cache_size'] ?? 0) + ($newTotal - $oldTotal));
            update_option('mamba_cache_stats', $stats, false);
        }
        else { 
            @unlink($tmp); 
            @unlink($metaTmp);
            self::clearRegenLock($paths); // Clear lock on write failure
        }
        return $buffer;
    }

    private function userHasCartItems(): bool {
        // Skip cart detection for warmup bot requests
        if (self::isWarmupRequest()) {
            return false; // Warmup bot should never have cart items
        }
        
        // Check cart-related cookies by specific key (avoid iterating full $_COOKIE stack)
        $cartCookies = ['woocommerce_items_in_cart', 'woocommerce_cart_hash', 'store_api_cart_hash'];
        foreach ($cartCookies as $cookie) {
            if (isset($_COOKIE[$cookie]) && $_COOKIE[$cookie] !== '' && $_COOKIE[$cookie] !== 'empty') return true;
        }
        
        // Only check Redis/DB if cookies don't indicate cart presence (fallback)
        if (function_exists('WC') && WC()->session) {
            try {
                if (WC()->cart && !WC()->cart->is_empty()) return true;
                $session_cart = WC()->session->get('cart'); if (!empty($session_cart)) return true;
            } catch (\Exception $e) { 
                // Log exception for debugging but don't assume cart presence
                if (defined('WP_DEBUG') && WP_DEBUG) {
                    error_log('Mamba: Cart detection exception: ' . $e->getMessage());
                }
                return false; // Don't assume cart presence on error
            }
        }
        return false;
    }

    private function isWooCacheableContext(): bool {
        if (!function_exists('is_woocommerce')) return false;
        
        // Include homepage for WooCommerce stores
        if (function_exists('is_front_page') && (is_front_page() || is_home())) return true;
        
        // is_woocommerce() includes product, archive, cart, checkout, account; we skip dynamic ones above
        if (is_woocommerce()) return true;
        if (function_exists('is_shop') && is_shop()) return true;
        if (function_exists('is_product') && is_product()) return true;
        if (function_exists('is_product_category') && is_product_category()) return true;
        if (function_exists('is_product_tag') && is_product_tag()) return true;
        return false;
    }
    private function currentType(): string {
        if (function_exists('is_front_page') && (is_front_page() || is_home())) return 'homepage';
        if (function_exists('is_product') && is_product()) return 'product';
        if (function_exists('is_product_category') && is_product_category()) return 'category';
        if (function_exists('is_shop') && is_shop()) return 'shop';
        return 'other';
    }
    
    /**
     * Update cache size tracking when files are saved
     * @deprecated Use delta-based size tracking in save() method instead
     */
    private static function updateCacheSize(string $cacheFile, string $metaFile): void {
        try {
            $cacheSize = @filesize($cacheFile) ?: 0;
            $metaSize = @filesize($metaFile) ?: 0;
            $totalSize = $cacheSize + $metaSize;
            
            if ($totalSize > 0) {
                $stats = get_option('mamba_cache_stats', ['cache_size' => 0]);
                $stats['cache_size'] = (int)($stats['cache_size'] ?? 0) + $totalSize;
                update_option('mamba_cache_stats', $stats, false);
            }
        } catch (\Exception $e) {
            // Silently fail - cache size tracking is not critical
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('Mamba: Cache size update failed: ' . $e->getMessage());
            }
        }
    }
    
    /**
     * Remove cache size when files are deleted
     */
    public static function removeCacheSize(string $cacheFile, string $metaFile): void {
        try {
            $cacheSize = @filesize($cacheFile) ?: 0;
            $metaSize = @filesize($metaFile) ?: 0;
            $totalSize = $cacheSize + $metaSize;
            
            if ($totalSize > 0) {
                $stats = get_option('mamba_cache_stats', ['cache_size' => 0]);
                $stats['cache_size'] = max(0, (int)($stats['cache_size'] ?? 0) - $totalSize);
                update_option('mamba_cache_stats', $stats, false);
            }
        } catch (\Exception $e) {
            // Silently fail - cache size tracking is not critical
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('Mamba: Cache size removal failed: ' . $e->getMessage());
            }
        }
    }
    
    /**
     * Recalculate cache size from actual files (cleanup method)
     */
    public static function recalculateCacheSize(): int {
        $totalSize = 0;
        
        if (!defined('WP_CONTENT_DIR')) {
            return $totalSize;
        }
        
        $cacheDir = WP_CONTENT_DIR . '/cache/mamba';
        if (!is_dir($cacheDir)) {
            return $totalSize;
        }
        
        try {
            $iterator = new \RecursiveIteratorIterator(
                new \RecursiveDirectoryIterator($cacheDir, \RecursiveDirectoryIterator::SKIP_DOTS),
                \RecursiveIteratorIterator::LEAVES_ONLY
            );
            
            foreach ($iterator as $file) {
                if ($file->isFile()) {
                    $totalSize += $file->getSize();
                }
            }
            
            // Update stats with accurate size
            $stats = get_option('mamba_cache_stats', ['cache_size' => 0]);
            $stats['cache_size'] = $totalSize;
            update_option('mamba_cache_stats', $stats, false);
            
        } catch (\Exception $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('Mamba: Cache size recalculation failed: ' . $e->getMessage());
            }
        }
        
        return $totalSize;
    }
    
    /**
     * Check if the current request is a warmup request
     */
    public static function isWarmupRequest(): bool {
        // Add X-Mamba-Warmup header detection for future-proofing alongside User-Agent
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized via sanitize_text_field
        $userAgent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized via sanitize_text_field
        $warmupHeader = isset($_SERVER['HTTP_X_MAMBA_WARMUP']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_X_MAMBA_WARMUP'])) : '';
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized via sanitize_text_field
        $warmupCookie = isset($_COOKIE['mamba_warmup']) ? sanitize_text_field(wp_unslash($_COOKIE['mamba_warmup'])) : '';
        
        return strpos($userAgent, 'Mamba Cache Warmup Bot') !== false || 
               $warmupHeader === '1' || 
               $warmupCookie === '1';
    }
    
    /**
     * Create protection files in cache directory
     */
    private static function createProtectionFiles(string $cacheDir): void {
        // Create index.html to prevent directory listing
        $indexFile = $cacheDir . '/index.html';
        if (!file_exists($indexFile)) {
            @file_put_contents($indexFile, '<!-- Directory access denied -->');
        }
        
        // Create .htaccess for Apache protection - DENY ALL ACCESS
        $htaccessFile = $cacheDir . '/.htaccess';
        if (!file_exists($htaccessFile)) {
            $htaccessContent = "<IfModule mod_authz_core.c>\n  Require all denied\n</IfModule>\n<IfModule !mod_authz_core.c>\n  Deny from all\n</IfModule>\n";
            @file_put_contents($htaccessFile, $htaccessContent);
        }
    }
    
    /**
     * Get intended TTL for current request (integration point for BrowserCachePolicy)
     */
    public static function getIntendedTtl(): int {
        // Check if this is a cacheable page
        if (!self::isCacheablePage()) {
            return 0; // Not cacheable
        }
        
        // Return page cache TTL for cacheable pages
        return (int)get_option('mamba_cache_ttl', 7200); // 2 hours default
    }
    
    /**
     * Check if current page is cacheable
     */
    public static function isCacheablePage(): bool {
        // Dynamic WooCommerce pages - never cache
        if (function_exists('is_cart') && (is_cart() || is_checkout() || is_account_page())) {
            return false;
        }
        
        // Logged-in users - never cache
        if (is_user_logged_in()) {
            return false;
        }
        
        // Check if this is a catalog page
        if (!function_exists('is_woocommerce')) {
            return false;
        }
        
        return (function_exists('is_front_page') && (is_front_page() || is_home())) ||
               (function_exists('is_product') && is_product()) ||
               (function_exists('is_product_category') && is_product_category()) ||
               (function_exists('is_product_tag') && is_product_tag()) ||
               (function_exists('is_shop') && is_shop()) ||
               (function_exists('is_woocommerce') && is_woocommerce() && !is_cart() && !is_checkout() && !is_account_page());
    }

    /**
     * Remove Content-Length header when compression is active
     */
    private static function removeContentLengthIfCompressed(): void {
        if (function_exists('ini_get') && ini_get('zlib.output_compression')) {
            if (!headers_sent()) {
                header_remove('Content-Length');
            }
        }
        
        // Also check for output buffer compression
        $handlers = array_map('strtolower', ob_list_handlers());
        foreach ($handlers as $h) {
            if (strpos($h, 'zlib') !== false || strpos($h, 'gz') !== false) {
                if (!headers_sent()) {
                    header_remove('Content-Length');
                }
                break;
            }
        }
    }
}
