<?php
/**
 * Warmup HTTP Request Processor
 *
 * Handles parallel HTTP requests for cache warming using cURL multi-handle
 * with SSL verification, HTTP/2 support, and connection pooling.
 *
 * @package Mamba\Modules\Caching\Services\Preload\Warmup
 * @since   1.0.0
 */

namespace Mamba\Modules\Caching\Services\Preload\Warmup;

/**
 * Class Warmer
 *
 * Warmup HTTP Request Processor with features:
 * - SSL Verification: Enabled by default, disabled when WP_DEBUG is true
 * - HTTP/2 with TLS: Enables multiplexing for better parallel performance
 * - Automatic decompression: Handles gzip/deflate responses automatically
 * - Connection reuse: HTTP/2 connection pooling for multiple URLs
 *
 * @since 1.0.0
 */
final class Warmer {
    
    /**
     * Process URLs in parallel using cURL multi-handle for true concurrency
     */
    public static function parallel(array $urls, int $batchSize = 10): int {
        $success = 0;
        $urls = array_values($urls);
        $total = count($urls);
        if ($total === 0) return 0;

        for ($offset = 0; $offset < $total; $offset += $batchSize) {
            $batch = array_slice($urls, $offset, $batchSize);
            $results = self::processBatchWithCurl($batch);
            foreach ($results as $result) {
                if ($result === true) {
                    $success++;
                } elseif (is_array($result) && isset($result['error'])) {
                    // Error occurred during warmup
                }
            }
        }
        
        return $success;
    }
    
    /**
     * Process a batch of URLs using cURL multi-handle for true parallel processing
     */
    private static function processBatchWithCurl(array $batch): array {
        if (!function_exists('curl_multi_init')) {
            // Fallback to sequential processing if cURL multi is not available
            return self::processBatchSequential($batch);
        }
        
        // Get concurrency setting with safe defaults
        $concurrency = max(1, min(20, (int)get_option('mamba_preload_concurrency', 5)));
        
        $results = [];
        $batchSize = count($batch);
        
        // Process URLs in chunks based on concurrency setting
        for ($offset = 0; $offset < $batchSize; $offset += $concurrency) {
            $chunk = array_slice($batch, $offset, $concurrency);
            $chunkResults = self::processChunkWithCurl($chunk, $offset);
            $results = array_merge($results, $chunkResults);
        }
        
        return $results;
    }
    
    /**
     * Process a chunk of URLs with cURL multi-handle (respects concurrency limit)
     * 
     * SSL Verification:
     * - Production: SSL verification enabled by default for security
     * - Development: SSL verification disabled when WP_DEBUG is true
     * - Override: Use 'mamba_warmup_ssl_verify' and 'mamba_warmup_ssl_verify_host' filters
     * 
     * Performance Features:
     * - HTTP/2 multiplexing for efficient parallel requests
     * - Automatic gzip/deflate decompression
     * - Connection pooling for multiple URLs on same host
     */
    private static function processChunkWithCurl(array $chunk, int $offset = 0): array {
        $multiHandle = curl_multi_init();
        $handles = [];
        $results = [];
        
        // Prepare cURL handles for this chunk
        foreach ($chunk as $index => $target) {
            if (is_array($target) && isset($target['url'])) {
                $url = $target['url'];
                $userAgent = $target['ua'] ?? 'Mamba Cache Warmup Bot';
                $cookies = $target['cookies'] ?? [];
                $headers = $target['headers'] ?? [];
            } else {
                $url = (string)$target;
                $userAgent = 'Mamba Cache Warmup Bot';
                $cookies = [];
                $headers = [];
            }
            
            // Add warmup cookie for reliable detection
            $cookies['mamba_warmup'] = '1';
            
            // Ensure warmup requests always use the warmup bot user agent for proper identification
            if (strpos($userAgent, 'Mamba Cache Warmup Bot') === false) {
                $userAgent = 'Mamba Cache Warmup Bot';
            }
            
            // Validate URL
            if (empty($url) || !filter_var($url, FILTER_VALIDATE_URL)) {
                $results[$index] = false;
                continue;
            }
            
            $handle = curl_init();
            
            // Set cURL options (headers will be set separately)
            // SSL verification: secure by default, allow opt-out in debug builds
            $sslVerify = apply_filters('mamba_warmup_ssl_verify', !defined('WP_DEBUG') || !WP_DEBUG);
            $sslVerifyHost = apply_filters('mamba_warmup_ssl_verify_host', (!defined('WP_DEBUG') || !WP_DEBUG) ? 2 : 0);
            
            // Set cURL options with performance optimizations
            // - CURLOPT_ENCODING: Automatic gzip/deflate decompression for accurate metrics
            // - CURLOPT_HTTP_VERSION: HTTP/2 multiplexing for efficient parallel requests
            // - CURLOPT_HEADERFUNCTION: Capture headers explicitly
            // - CURLOPT_WRITEFUNCTION: Discard response body to reduce memory usage
            $headerBuf = '';
            $curlOptions = [
                CURLOPT_URL => $url,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_TIMEOUT => 30,
                CURLOPT_USERAGENT => $userAgent,
                CURLOPT_SSL_VERIFYPEER => $sslVerify,
                CURLOPT_SSL_VERIFYHOST => $sslVerifyHost,
                CURLOPT_NOBODY => false,
                CURLOPT_ENCODING => '', // Enable automatic decompression
                CURLOPT_HEADERFUNCTION => function($ch, $str) use (&$headerBuf) {
                    $headerBuf .= $str;
                    return strlen($str);
                },
                CURLOPT_WRITEFUNCTION => function($ch, $str) {
                    return strlen($str); // discard body
                },
            ];
            
            // Enable HTTP/2 if supported (graceful fallback to HTTP/1.1)
            if (defined('CURL_HTTP_VERSION_2TLS')) {
                $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2TLS;
            }
            
            curl_setopt_array($handle, $curlOptions);
            
            // Build complete header array with defaults and custom headers
            $headerArray = [
                'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                'Accept-Language: en-US,en;q=0.5',
                'Accept-Encoding: gzip, deflate',
                'Connection: keep-alive',
                'Upgrade-Insecure-Requests: 1',
                'X-Mamba-Warmup: 1', // FIX: Add warmup header for future-proofing
            ];
            
            // Detect Store API URLs and use appropriate headers
            if (self::isStoreApiUrl($url)) {
                $headerArray = [
                    'Accept: application/json',
                    'X-Mamba-Warmup: 1',
                ];
            }
            
            // Add custom headers if provided
            if (!empty($headers)) {
                foreach ($headers as $name => $value) {
                    $headerArray[] = "$name: $value";
                }
            }
            
            // Set all headers at once
            curl_setopt($handle, CURLOPT_HTTPHEADER, $headerArray);
            
            // Add cookies if provided
            if (!empty($cookies)) {
                $cookieString = '';
                foreach ($cookies as $name => $value) {
                    $cookieString .= "$name=$value; ";
                }
                curl_setopt($handle, CURLOPT_COOKIE, trim($cookieString));
            }
            
            // Store metadata with the handle (use original index for proper result mapping)
            $originalIndex = $offset + $index;
            $handles[$originalIndex] = [
                'handle' => $handle,
                'url' => $url,
                'target' => $target,
                'headers' => &$headerBuf
            ];
            
            curl_multi_add_handle($multiHandle, $handle);
        }
        
        // Execute all requests in parallel
        $active = null;
        do {
            $mrc = curl_multi_exec($multiHandle, $active);
        } while ($mrc == CURLM_CALL_MULTI_PERFORM);
        
        while ($active && $mrc == CURLM_OK) {
            if (curl_multi_select($multiHandle) != -1) {
                do {
                    $mrc = curl_multi_exec($multiHandle, $active);
                } while ($mrc == CURLM_CALL_MULTI_PERFORM);
            }
            
            // Add timeout protection
            usleep(10000); // 10ms sleep to prevent busy waiting
        }
        
        // Process results
        foreach ($handles as $index => $handleData) {
            $handle = $handleData['handle'];
            $url = $handleData['url'];
            $target = $handleData['target'];
            
            $httpCode = curl_getinfo($handle, CURLINFO_HTTP_CODE);
            $responseTime = curl_getinfo($handle, CURLINFO_TOTAL_TIME) * 1000; // Convert to ms
            $headers = $handleData['headers'] ?? '';
            
            // Ensure we have headers (indicates request completed)
            if (empty($headers)) {
                $errorDetails = [
                    'message' => 'No response received',
                    'curl_error' => curl_error($handle),
                    'response_time' => (int)$responseTime,
                    'device' => is_array($target) ? ($target['ua'] ?? 'unknown') : 'unknown'
                ];
                
                // Track detailed error
                ErrorTracker::trackError($url, 'no_response', $errorDetails);
                
                $results[$index] = [
                    'error' => 'no_response',
                    'url' => $url,
                    'curl_error' => curl_error($handle)
                ];
                curl_multi_remove_handle($multiHandle, $handle);
                curl_close($handle);
                continue;
            }
            
            if (curl_errno($handle)) {
                $curlError = curl_error($handle);
                $curlErrno = curl_errno($handle);
                
                // Check for SSL-related errors
                $errorType = 'curl_error';
                if (in_array($curlErrno, [CURLE_SSL_CACERT, CURLE_SSL_CACERT_BADFILE, CURLE_SSL_CERTPROBLEM, CURLE_SSL_CIPHER, CURLE_SSL_CONNECT_ERROR, CURLE_SSL_ENGINE_NOTFOUND, CURLE_SSL_ENGINE_SETFAILED, CURLE_SSL_PEER_CERTIFICATE, CURLE_SSL_PINNEDPUBKEYNOTMATCH])) {
                    $errorType = 'ssl_error';
                }
                
                $errorDetails = [
                    'message' => $curlError,
                    'curl_errno' => $curlErrno,
                    'ssl_error_message' => $errorType === 'ssl_error' ? self::getSslErrorMessage($curlErrno) : '',
                    'response_time' => (int)$responseTime,
                    'device' => is_array($target) ? ($target['ua'] ?? 'unknown') : 'unknown'
                ];
                
                // Track detailed error
                ErrorTracker::trackError($url, $errorType, $errorDetails);
                
                $results[$index] = [
                    'error' => $errorType,
                    'message' => $curlError,
                    'curl_errno' => $curlErrno,
                    'ssl_error_message' => $errorType === 'ssl_error' ? self::getSslErrorMessage($curlErrno) : '',
                    'url' => $url
                ];
            } elseif ($httpCode >= 200 && $httpCode < 400) {
                // Store API URLs (wp-json/wc/store) don't get page cache headers - treat HTTP 200 as success
                $isStoreApiUrl = strpos($url, '/wp-json/wc/store') !== false || strpos($url, '/wp-json/wc/v') !== false;
                
                if ($isStoreApiUrl) {
                    // Store API endpoints are successfully warmed if they return 200
                    // They populate WooCommerce's internal caches, not page cache
                    $results[$index] = true;
                } elseif (strpos($headers, 'X-Mamba-Cache: HIT') !== false || 
                    strpos($headers, 'X-Mamba-Cache: MISS') !== false) {
                    $results[$index] = true;
                } else {
                    // For debugging, also check if the header might be in a different format
                    $cacheHeaderFound = false;
                    $headerLines = explode("\n", $headers);
                    foreach ($headerLines as $line) {
                        if (stripos($line, 'X-Mamba-Cache:') !== false) {
                            $cacheHeaderFound = true;
                            break;
                        }
                    }
                    
                    if ($cacheHeaderFound) {
                        $results[$index] = true;
                    } else {
                        $errorDetails = [
                            'message' => 'No cache header found in response',
                            'status_code' => $httpCode,
                            'response_time' => (int)$responseTime,
                            'device' => is_array($target) ? ($target['ua'] ?? 'unknown') : 'unknown',
                            'headers' => $headers
                        ];
                        
                        // Track detailed error
                        ErrorTracker::trackError($url, 'no_cache_header', $errorDetails);
                        
                        $results[$index] = [
                            'error' => 'no_cache_header',
                            'status_code' => $httpCode,
                            'url' => $url,
                            'headers' => $headers,
                            'header_size' => strlen($headers)
                        ];
                    }
                }
            } else {
                $errorDetails = [
                    'message' => "HTTP Error: {$httpCode}",
                    'status_code' => $httpCode,
                    'response_time' => (int)$responseTime,
                    'device' => is_array($target) ? ($target['ua'] ?? 'unknown') : 'unknown'
                ];
                
                // Track detailed error
                ErrorTracker::trackError($url, 'http_error', $errorDetails);
                
                $results[$index] = [
                    'error' => 'http_error',
                    'status_code' => $httpCode,
                    'url' => $url
                ];
            }
            
            curl_multi_remove_handle($multiHandle, $handle);
            curl_close($handle);
        }
        
        curl_multi_close($multiHandle);
        
        return $results;
    }
    
    /**
     * Get human-readable SSL error message
     */
    private static function getSslErrorMessage(int $curlErrno): string {
        $messages = [
            CURLE_SSL_CACERT => 'SSL certificate authority not found',
            CURLE_SSL_CACERT_BADFILE => 'SSL certificate authority file is invalid',
            CURLE_SSL_CERTPROBLEM => 'SSL certificate problem',
            CURLE_SSL_CIPHER => 'SSL cipher error',
            CURLE_SSL_CONNECT_ERROR => 'SSL connection error',
            CURLE_SSL_ENGINE_NOTFOUND => 'SSL engine not found',
            CURLE_SSL_ENGINE_SETFAILED => 'SSL engine setup failed',
            CURLE_SSL_PEER_CERTIFICATE => 'SSL peer certificate error',
            CURLE_SSL_PINNEDPUBKEYNOTMATCH => 'SSL pinned public key mismatch'
        ];
        
        return $messages[$curlErrno] ?? 'SSL verification failed';
    }
    
    /**
     * Fallback sequential processing when cURL multi is not available
     */
    private static function processBatchSequential(array $batch): array {
        $results = [];
        

        
        foreach ($batch as $index => $target) {
            $result = self::warmSingle($target);
            $results[$index] = $result;
        }
        
        return $results;
    }
    
    /**
     * Warm a single URL (backward compatibility method)
     * 
     * SSL Verification:
     * - Production: SSL verification enabled by default for security
     * - Development: SSL verification disabled when WP_DEBUG is true
     * - Override: Use 'mamba_warmup_ssl_verify' filter
     */
    public static function warmSingle($target): bool|array {
        if (is_array($target) && isset($target['url'])) {
            $url = $target['url'];
            $userAgent = $target['ua'] ?? 'Mamba Cache Warmup Bot';
            $cookies = $target['cookies'] ?? [];
            $headers = $target['headers'] ?? [];
        } else {
            $url = (string)$target;
            $userAgent = 'Mamba Cache Warmup Bot';
            $cookies = [];
            $headers = [];
        }
        
        // Ensure warmup requests always use the warmup bot user agent for proper identification
        if (strpos($userAgent, 'Mamba Cache Warmup Bot') === false) {
            $userAgent = 'Mamba Cache Warmup Bot';
        }
        
        // Validate URL
        if (empty($url) || !filter_var($url, FILTER_VALIDATE_URL)) {
            return false;
        }
        
        // Use WordPress HTTP API for single requests
        // SSL verification: secure by default, allow opt-out in debug builds
        $sslVerify = apply_filters('mamba_warmup_ssl_verify', !defined('WP_DEBUG') || !WP_DEBUG);
        
        $args = [
            'timeout' => 10,
            'redirection' => 3,
            'user-agent' => $userAgent,
            'blocking' => true,
            'sslverify' => $sslVerify,
        ];
        
        // Initialize headers array
        $args['headers'] = $args['headers'] ?? [];
        
        // Set appropriate headers for Store API requests
        if (self::isStoreApiUrl($url)) {
            $args['headers'] = array_merge([
                'Accept' => 'application/json'
                // Note: Don't set Content-Type for GET requests with query parameters
            ], $args['headers']);
        }
        
        // FIX: Add warmup header for future-proofing
        $args['headers']['X-Mamba-Warmup'] = '1';
        
        // Add cookies if provided
        if (!empty($cookies)) {
            $args['cookies'] = [];
            foreach ($cookies as $name => $value) {
                $args['cookies'][] = new \WP_Http_Cookie([
                    'name' => $name,
                    'value' => $value,
                    'domain' => parse_url($url, PHP_URL_HOST)
                ]);
            }
        }
        
        // Add warmup cookie for reliable detection
        $args['cookies'][] = new \WP_Http_Cookie([
            'name' => 'mamba_warmup',
            'value' => '1',
            'domain' => parse_url($url, PHP_URL_HOST)
        ]);
        
        // Add headers if provided
        if (!empty($headers)) {
            $args['headers'] = array_merge($args['headers'], $headers);
        }
        
        $response = wp_remote_get($url, $args);
        

        
        if (is_wp_error($response)) {
            return [
                'error' => 'wp_error',
                'message' => $response->get_error_message(),
                'url' => $url
            ];
        }
        
        $statusCode = wp_remote_retrieve_response_code($response);
        if ($statusCode < 200 || $statusCode >= 400) {
            return [
                'error' => 'http_error',
                'status_code' => $statusCode,
                'url' => $url
            ];
        }
        
        // Check for cache header
        $headers = wp_remote_retrieve_headers($response);
        $cacheHeader = '';
        
        if (is_array($headers)) {
            $cacheHeader = $headers['x-mamba-cache'] ?? '';
        } elseif (is_object($headers) && method_exists($headers, 'get')) {
            $cacheHeader = $headers->get('X-Mamba-Cache');
        } elseif (is_object($headers) && method_exists($headers, 'offsetGet')) {
            $cacheHeader = $headers->offsetGet('X-Mamba-Cache');
        }
        
        // For debugging, also check all headers
        $allHeaders = [];
        if (is_array($headers)) {
            $allHeaders = $headers;
        } elseif (is_object($headers) && method_exists($headers, 'getAll')) {
            $allHeaders = $headers->getAll();
        }
        
        if (in_array($cacheHeader, ['HIT', 'MISS', 'WARMUP'])) {
            return true;
        }
        
        return [
            'error' => 'no_cache_header',
            'status_code' => $statusCode,
            'cache_header' => $cacheHeader,
            'all_headers' => $allHeaders,
            'url' => $url
        ];
    }
    
    /**
     * Check if a URL is a Store API endpoint
     */
    private static function isStoreApiUrl(string $url): bool {
        return strpos($url, '/wp-json/wc/store/') !== false || strpos($url, '/wc/store/') !== false;
    }
    
    /**
     * Process a batch of URLs in parallel (public method for job system)
     */
    public static function processBatchParallel(array $batch): array {
        return self::processBatchWithCurl($batch);
    }

    /**
     * Process a batch with retry logic for failed requests
     */
    public static function processBatchWithRetry(array $batch, int $maxRetries = 2): array {
        $results = self::processBatchWithCurl($batch);
        
        // Collect URLs that should be retried
        $retryUrls = [];
        foreach ($results as $index => $result) {
            if (is_array($result) && isset($result['error'])) {
                $error = $result['error'];
                $statusCode = $result['status_code'] ?? 0;
                
                // Retry on 5xx errors, network errors, and some 4xx like 429
                if (self::shouldRetryError($error, $statusCode)) {
                    $url = $result['url'];
                    if (isset($batch[$index])) {
                        $retryUrls[$index] = $batch[$index];
                    }
                }
            }
        }
        
        // Retry failed URLs up to maxRetries times
        for ($attempt = 1; $attempt <= $maxRetries && !empty($retryUrls); $attempt++) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log("Mamba Warmup: Retrying " . count($retryUrls) . " failed URLs (attempt $attempt)");
            }
            
            // Exponential backoff: 1s, 2s, 4s...
            $delay = pow(2, $attempt - 1) * 1000000; // microseconds
            usleep($delay);
            
            $retryResults = self::processBatchWithCurl(array_values($retryUrls));
            
            // Update results with retry outcomes
            $retryIndex = 0;
            foreach ($retryUrls as $originalIndex => $url) {
                if (isset($retryResults[$retryIndex])) {
                    $results[$originalIndex] = $retryResults[$retryIndex];
                }
                $retryIndex++;
            }
            
            // Collect remaining failures for next retry
            $stillFailed = [];
            foreach ($results as $index => $result) {
                if (is_array($result) && isset($result['error']) && isset($retryUrls[$index])) {
                    $error = $result['error'];
                    $statusCode = $result['status_code'] ?? 0;
                    if (self::shouldRetryError($error, $statusCode)) {
                        $stillFailed[$index] = $batch[$index];
                    }
                }
            }
            $retryUrls = $stillFailed;
        }
        
        return $results;
    }

    /**
     * Determine if an error should trigger a retry
     */
    private static function shouldRetryError(string $error, int $statusCode): bool {
        // Retry on server errors (5xx), network errors, and rate limiting
        if ($error === 'http_error' && $statusCode >= 500) {
            return true;
        }
        if ($error === 'http_error' && $statusCode === 429) { // Too Many Requests
            return true;
        }
        if (in_array($error, ['curl_error', 'ssl_error', 'no_response', 'wp_error'])) {
            return true;
        }
        return false;
    }

    /**
     * Filter out non-cacheable URLs before warming
     * Returns array of cacheable URLs (preserves original format: string or array)
     */
    public static function filterCacheableUrls(array $urls): array {
        $cacheableUrls = [];
        
        foreach ($urls as $item) {
            // Handle both string URLs and expanded arrays with 'url' key
            $url = is_array($item) && isset($item['url']) ? $item['url'] : (is_string($item) ? $item : null);
            
            if (!$url) {
                continue;
            }
            
            if (self::isUrlCacheable($url)) {
                // Preserve original format (string or array)
                $cacheableUrls[] = $item;
            } elseif (defined('WP_DEBUG') && WP_DEBUG) {
                error_log("Mamba Warmup: Skipping non-cacheable URL: $url");
            }
        }
        
        return $cacheableUrls;
    }

    /**
     * Check if a URL is likely cacheable based on known patterns
     */
    private static function isUrlCacheable(string $url): bool {
        // Parse URL to get path and query
        $parsed = parse_url($url);
        if (!$parsed) {
            return false;
        }
        
        $path = $parsed['path'] ?? '/';
        $query = $parsed['query'] ?? '';
        parse_str($query, $queryParams);
        
        // Check for mutating GET parameters
        $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 $key) {
            if (isset($queryParams[$key])) {
                return false;
            }
        }
        
        // Check for search pages
        if (isset($queryParams['s']) && !empty($queryParams['s'])) {
            return false;
        }
        
        // IMPORTANT: Check Store API URLs FIRST before the general path blocking
        // Store API catalog endpoints are cacheable (products, categories, tags, attributes)
        // but cart/checkout/account endpoints are NOT
        if (self::isStoreApiUrl($url)) {
            return self::isStoreApiCacheable($url);
        }
        
        // Check for WooCommerce contexts in path (only for non-Store-API URLs)
        $nonCacheablePaths = [
            '/cart/',
            '/checkout/',
            '/my-account/',
            '/wp-admin/',
            '/wp-login.php',
            '/wp-cron.php'
        ];
        foreach ($nonCacheablePaths as $badPath) {
            if (strpos($path, $badPath) !== false) {
                return false;
            }
        }
        
        // Check for REST API endpoints that aren't Store API (already handled above)
        if (strpos($path, '/wp-json/') !== false) {
            return false;
        }
        
        // Check for AJAX endpoints
        if (strpos($path, '/wp-admin/admin-ajax.php') !== false) {
            return false;
        }
        
        // Check for feed URLs
        if (strpos($path, '/feed/') !== false || strpos($path, '.xml') !== false) {
            return false;
        }
        
        // If it passes all checks, consider it cacheable
        return true;
    }
    
    /**
     * Check if a Store API URL is cacheable (catalog endpoints only)
     */
    private static function isStoreApiCacheable(string $url): bool {
        $parsed = parse_url($url);
        $path = $parsed['path'] ?? '';
        
        // Normalize to relative route: strip `/wp-json` & `/wc/store/v{n}`
        $relative = preg_replace('#^(/wp-json)?/wc/store/v\d+#', '', $path);
        
        // Cacheable catalog endpoints (regex patterns)
        $cacheablePatterns = [
            '#^/products$#',
            '#^/products/\d+$#',                      // individual product
            '#^/products/categories$#',
            '#^/products/tags$#',
            '#^/products/attributes$#',
            '#^/products/collection-data$#',
            '#^/products/reviews$#',                  // global reviews list
            '#^/products/\d+/reviews$#',              // product reviews
        ];
        
        // Allow filtering of patterns
        $cacheablePatterns = (array) apply_filters('mamba_warmup_store_api_cacheable_patterns', $cacheablePatterns);
        
        // Non-cacheable Store API paths (cart, checkout, user-specific data)
        $nonCacheablePatterns = [
            '/cart', '/checkout', '/addresses', '/user', '/account',
            '/orders', '/payments', '/shipping', '/billing', '/session',
            '/batch', '/customers'
        ];
        
        // Block non-cacheable paths first
        foreach ($nonCacheablePatterns as $blocked) {
            if (strpos($relative, $blocked) === 0) {
                return false;
            }
        }
        
        // Check for cacheable patterns
        foreach ($cacheablePatterns as $pattern) {
            if (@preg_match($pattern, $relative)) {
                return true;
            }
        }
        
        // Default: deny unknown Store API endpoints
        return false;
    }

    /**
     * Pre-validate URLs before warming to skip 404s and redirects
     * Returns array of valid URLs (preserves original format: string or array)
     */
    public static function preValidateUrls(array $urls, int $concurrency = 5): array {
        if (empty($urls)) {
            return [];
        }

        // Build a mapping from URL string to original item (to preserve format)
        $urlToOriginal = [];
        $urlStrings = [];
        foreach ($urls as $item) {
            $url = is_array($item) && isset($item['url']) ? $item['url'] : (is_string($item) ? $item : null);
            if ($url) {
                $urlStrings[] = $url;
                // If multiple items have same URL, keep first one
                if (!isset($urlToOriginal[$url])) {
                    $urlToOriginal[$url] = $item;
                }
            }
        }

        $validUrls = [];
        $batches = array_chunk($urlStrings, $concurrency);

        foreach ($batches as $batch) {
            $results = self::validateUrlBatch($batch);
            foreach ($results as $url => $isValid) {
                if ($isValid && isset($urlToOriginal[$url])) {
                    // Return original format (string or array)
                    $validUrls[] = $urlToOriginal[$url];
                }
            }
            // Small delay between batches to be respectful
            usleep(50000); // 50ms
        }

        return $validUrls;
    }

    /**
     * Validate a batch of URLs using HEAD requests
     */
    private static function validateUrlBatch(array $batch): array {
        $results = [];
        $multiHandle = curl_multi_init();
        $handles = [];

        foreach ($batch as $url) {
            $handle = curl_init($url);
            curl_setopt_array($handle, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_NOBODY => true, // HEAD request
                CURLOPT_TIMEOUT => 5,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_MAXREDIRS => 3,
                CURLOPT_SSL_VERIFYPEER => apply_filters('mamba_warmup_ssl_verify', !defined('WP_DEBUG') || !WP_DEBUG),
                CURLOPT_USERAGENT => 'Mamba Cache URL Validator',
                CURLOPT_HEADER => true,
            ]);

            curl_multi_add_handle($multiHandle, $handle);
            $handles[] = ['handle' => $handle, 'url' => $url];
        }

        // Execute requests
        $active = null;
        do {
            $mrc = curl_multi_exec($multiHandle, $active);
        } while ($mrc == CURLM_CALL_MULTI_PERFORM);

        while ($active && $mrc == CURLM_OK) {
            if (curl_multi_select($multiHandle) != -1) {
                do {
                    $mrc = curl_multi_exec($multiHandle, $active);
                } while ($mrc == CURLM_CALL_MULTI_PERFORM);
            }
        }

        // Process results
        foreach ($handles as $handleData) {
            $handle = $handleData['handle'];
            $url = $handleData['url'];
            $httpCode = curl_getinfo($handle, CURLINFO_HTTP_CODE);
            $isValid = ($httpCode >= 200 && $httpCode < 400);
            $results[$url] = $isValid;
            curl_multi_remove_handle($multiHandle, $handle);
            curl_close($handle);
        }

        curl_multi_close($multiHandle);
        return $results;
    }
}
