<?php
/**
 * Cache Preloader Service
 *
 * Manages cache warmup jobs including URL generation, variant expansion,
 * and background job scheduling for proactive cache population.
 *
 * @package Mamba\Modules\Caching\Services\Preload
 * @since   1.0.0
 */

namespace Mamba\Modules\Caching\Services\Preload;

use Mamba\Support\Logger;

/**
 * Class Preloader
 *
 * Warmup Job Management and URL Generation with features:
 * - File-based storage: Large job payloads stored in wp-content/cache/mamba/jobs/
 * - Non-autoloaded options: Small metadata only in wp_options table
 * - Automatic cleanup: Old job files cleaned up after 24 hours
 * - Performance: Prevents options table bloat and frontend performance regression
 *
 * @since 1.0.0
 */
final class Preloader {
    public static function schedule($objectId=null): void {
        if (!get_option('mamba_enable_page_cache', 0)) return;
        if (!wp_next_scheduled('mamba_cache_preload')) wp_schedule_single_event(time()+30, 'mamba_cache_preload');
    }
    
    /**
     * Initialize scheduled warmup (Premium feature)
     * Called from Controller to set up daily scheduled warmup
     */
    public static function initScheduledWarmup(): void {
        // Only for premium users
        if (!function_exists('mamba_fs') || !mamba_fs()->can_use_premium_code__premium_only()) {
            return;
        }
        
        $scheduleEnabled = get_option('mamba_warmup_schedule_enabled', 0);
        $scheduleTime = get_option('mamba_warmup_schedule_time', '03:00');
        
        // Clear existing scheduled warmup
        $existingTimestamp = wp_next_scheduled('mamba_scheduled_warmup');
        if ($existingTimestamp) {
            wp_unschedule_event($existingTimestamp, 'mamba_scheduled_warmup');
        }
        
        if (!$scheduleEnabled || !get_option('mamba_enable_page_cache', 0)) {
            return;
        }
        
        // Parse the schedule time (HH:MM format)
        $timeParts = explode(':', $scheduleTime);
        $hour = isset($timeParts[0]) ? (int)$timeParts[0] : 3;
        $minute = isset($timeParts[1]) ? (int)$timeParts[1] : 0;
        
        // Calculate next run time
        $timezone = wp_timezone();
        $now = new \DateTime('now', $timezone);
        $scheduledTime = new \DateTime('today', $timezone);
        $scheduledTime->setTime($hour, $minute, 0);
        
        // If the time has already passed today, schedule for tomorrow
        if ($scheduledTime <= $now) {
            $scheduledTime->modify('+1 day');
        }
        
        // Schedule the daily warmup
        wp_schedule_event($scheduledTime->getTimestamp(), 'daily', 'mamba_scheduled_warmup');
        
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('Mamba Warmup: Scheduled daily warmup at ' . $scheduledTime->format('Y-m-d H:i:s T'));
        }
    }
    
    /**
     * Run scheduled warmup (triggered by cron)
     */
    public static function runScheduledWarmup(): void {
        if (!get_option('mamba_enable_page_cache', 0)) {
            return;
        }
        
        if (!function_exists('mamba_fs') || !mamba_fs()->can_use_premium_code__premium_only()) {
            return;
        }
        
        if (!get_option('mamba_warmup_schedule_enabled', 0)) {
            return;
        }
        
        Logger::warmup('scheduled_started', ['trigger' => 'cron']);
        
        // Use the background warmup system for scheduled warmups
        BackgroundWarmup::scheduleJob();
    }
    
    public static function run(): int {
        if (!get_option('mamba_enable_page_cache', 0)) return 0;
        $urls = self::buildUrls();
        // Expand variants per URL (device + currency/language/country with primary combo limiting)
        $expanded = self::expandVariants($urls);
        $urls = $expanded;
        $batchSize = (int)get_option('mamba_preload_concurrency', (int)apply_filters('mamba_preload_concurrency', 5));
        
        Logger::warmup('started', ['total_urls' => count($urls), 'batch_size' => $batchSize]);
        
        $success = Warmup\Warmer::parallel($urls, max(1, $batchSize));
        // record simple warmup stats
        update_option('mamba_warmup_stats', ['last_run'=>time(),'total_urls'=>count($urls),'success'=>$success]);
        
        Logger::warmup('completed', ['total_urls' => count($urls), 'success' => $success, 'failed' => count($urls) - $success]);
        
        return $success;
    }

    public static function warmOne(string $url): int {
        if (!get_option('mamba_enable_page_cache', 0)) return 0;
        if (empty($url)) return 0;
        
        // Expand variants for the single URL to populate all configured variants
        $expanded = self::expandVariants([$url]);
        return Warmup\Warmer::parallel($expanded, max(1, count($expanded)));
    }
    
    /**
     * Build URLs for warmup - accessible to BackgroundWarmup
     * 
     * Premium features:
     * - Priority ordering: Bestsellers and featured products first
     * - Selective warmup: Only warm specific categories/products
     * - Exclusions: Skip URLs matching patterns
     */
    public static function buildUrls(): array {
        $urls = [];
        $priorityUrls = []; // High-priority URLs (bestsellers, featured) - warmed first
        
        // Check premium features
        $isPremium = function_exists('mamba_fs') && mamba_fs()->can_use_premium_code__premium_only();
        $priorityMode = $isPremium && get_option('mamba_warmup_priority_mode', 0);
        $selectiveMode = $isPremium && get_option('mamba_warmup_selective_mode', 0);
        $selectedCategories = $isPremium ? array_filter(array_map('intval', (array)get_option('mamba_warmup_selected_categories', []))) : [];
        $selectedProducts = $isPremium ? array_filter(array_map('intval', (array)get_option('mamba_warmup_selected_products', []))) : [];
        $exclusionPatterns = $isPremium ? array_filter((array)get_option('mamba_warmup_exclusions', [])) : [];
        
        // Shop page
        if (function_exists('wc_get_page_permalink')) {
            $shop = wc_get_page_permalink('shop'); 
            if ($shop && filter_var($shop, FILTER_VALIDATE_URL)) {
                $urls[] = self::cleanUrl($shop);

                // Also warm a bounded set of shop pagination + orderby variants
                $shopTrim = rtrim($shop, '/');
                // Pagination: warm pages 2..N (base is considered page 1)
                $maxShopPages = (int) apply_filters('mamba_warmup_shop_pages', 2);
                for ($i = 2; $i <= max(2, $maxShopPages); $i++) {
                    // Pretty pagination
                    $urls[] = self::cleanUrl($shopTrim . '/page/' . $i . '/');
                    // Query pagination
                    $urls[] = self::cleanUrl(add_query_arg('paged', (string)$i, $shop));
                }
                // Common orderby variants on base page only (avoid explosion)
                $orderbyVariants = (array) apply_filters('mamba_warmup_shop_orderby', ['price','price-desc','date','popularity','rating','name']);
                foreach ($orderbyVariants as $ob) {
                    $urls[] = self::cleanUrl(add_query_arg('orderby', $ob, $shop));
                }
            }
        }
        
        // Products
        if (function_exists('wc_get_products')) {
            // Check if out-of-stock products should be excluded from warmup
            $excludeOutOfStock = apply_filters('mamba_warmup_exclude_outofstock', false);
            $stockStatus = $excludeOutOfStock ? ['instock'] : [];
            
            // Selective mode: Only warm selected products
            if ($selectiveMode && !empty($selectedProducts)) {
                foreach ($selectedProducts as $productId) {
                    $permalink = get_permalink($productId);
                    if ($permalink && filter_var($permalink, FILTER_VALIDATE_URL)) {
                        $urls[] = self::cleanUrl($permalink);
                    }
                }
            } else {
                // Standard mode: Warm latest, featured, sale, and bestsellers
                $latest = wc_get_products(['limit'=>20,'orderby'=>'date','order'=>'DESC','status'=>'publish'] + ($stockStatus ? ['stock_status'=>$stockStatus] : []));
                foreach ($latest as $p) { 
                    $permalink = get_permalink($p->get_id());
                    if ($permalink && filter_var($permalink, FILTER_VALIDATE_URL)) {
                        $urls[] = self::cleanUrl($permalink);
                    }
                }
                
                // Featured products - add to priority queue if priority mode enabled
                $featured = wc_get_products(['limit'=>20,'status'=>'publish','featured'=>true] + ($stockStatus ? ['stock_status'=>$stockStatus] : []));
                foreach ($featured as $p) { 
                    $permalink = get_permalink($p->get_id());
                    if ($permalink && filter_var($permalink, FILTER_VALIDATE_URL)) {
                        $cleanUrl = self::cleanUrl($permalink);
                        if ($priorityMode) {
                            $priorityUrls[] = $cleanUrl;
                        } else {
                            $urls[] = $cleanUrl;
                        }
                    }
                }
                
                $sale = wc_get_products(['limit'=>20,'status'=>'publish','on_sale'=>true] + ($stockStatus ? ['stock_status'=>$stockStatus] : []));
                foreach ($sale as $p) { 
                    $permalink = get_permalink($p->get_id());
                    if ($permalink && filter_var($permalink, FILTER_VALIDATE_URL)) {
                        $urls[] = self::cleanUrl($permalink);
                    }
                }
                
                // Bestsellers - add to priority queue if priority mode enabled
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
                $bests = wc_get_products(['limit'=>20,'status'=>'publish','orderby'=>'meta_value_num','meta_key'=>'total_sales','order'=>'DESC'] + ($stockStatus ? ['stock_status'=>$stockStatus] : []));
                foreach ($bests as $p) { 
                    $permalink = get_permalink($p->get_id());
                    if ($permalink && filter_var($permalink, FILTER_VALIDATE_URL)) {
                        $cleanUrl = self::cleanUrl($permalink);
                        if ($priorityMode) {
                            $priorityUrls[] = $cleanUrl;
                        } else {
                            $urls[] = $cleanUrl;
                        }
                    }
                }
            }
        }
        
        // Categories and tags with enhanced filter coverage
        if (function_exists('get_terms')) {
            // Selective mode: Only warm selected categories
            if ($selectiveMode && !empty($selectedCategories)) {
                foreach ($selectedCategories as $catId) {
                    $term = get_term($catId, 'product_cat');
                    if ($term && !is_wp_error($term)) {
                        $baseLink = get_term_link($term);
                        if (!is_wp_error($baseLink) && filter_var($baseLink, FILTER_VALIDATE_URL)) {
                            $urls[] = self::cleanUrl($baseLink);
                            $filteredUrls = self::generateFilteredCategoryUrls($baseLink);
                            $urls = array_merge($urls, $filteredUrls);
                        }
                    }
                }
            } else {
                // Standard mode: Warm top categories by product count
                $cats = get_terms(['taxonomy'=>'product_cat','orderby'=>'count','order'=>'DESC','number'=>10,'hide_empty'=>true]);
                if (!is_wp_error($cats)) {
                    foreach ($cats as $c) { 
                        $baseLink = get_term_link($c); 
                        if (!is_wp_error($baseLink) && filter_var($baseLink, FILTER_VALIDATE_URL)) {
                            // Add base category URL
                            $urls[] = self::cleanUrl($baseLink);
                            
                            // Add filtered category URLs for common use cases
                            $filteredUrls = self::generateFilteredCategoryUrls($baseLink);
                            $urls = array_merge($urls, $filteredUrls);
                        }
                    }
                }
                
                $tags = get_terms(['taxonomy'=>'product_tag','orderby'=>'count','order'=>'DESC','number'=>10,'hide_empty'=>true]);
                if (!is_wp_error($tags)) {
                    foreach ($tags as $t) { 
                        $baseLink = get_term_link($t); 
                        if (!is_wp_error($baseLink) && filter_var($baseLink, FILTER_VALIDATE_URL)) {
                            // Add base tag URL
                            $urls[] = self::cleanUrl($baseLink);
                            
                            // Add filtered tag URLs for common use cases
                            $filteredUrls = self::generateFilteredCategoryUrls($baseLink);
                            $urls = array_merge($urls, $filteredUrls);
                        }
                    }
                }
            }
        }
        
        // Homepage
        $home = home_url('/');
        if (filter_var($home, FILTER_VALIDATE_URL)) {
            $urls[] = self::cleanUrl($home);
        }
        
        // Add Store API URLs for warmup
        $storeApiUrls = self::buildStoreApiUrls();
        $urls = array_merge($urls, $storeApiUrls);
        
        // Apply exclusion patterns (Premium feature)
        if (!empty($exclusionPatterns)) {
            $urls = self::applyExclusionPatterns($urls, $exclusionPatterns);
            $priorityUrls = self::applyExclusionPatterns($priorityUrls, $exclusionPatterns);
        }
        
        // Merge priority URLs first (they get warmed first)
        if ($priorityMode && !empty($priorityUrls)) {
            $priorityUrls = array_values(array_unique(array_filter($priorityUrls)));
            $urls = array_values(array_unique(array_filter($urls)));
            // Remove duplicates from regular URLs that are already in priority
            $urls = array_diff($urls, $priorityUrls);
            // Priority URLs first, then regular URLs
            $urls = array_merge($priorityUrls, $urls);
        }
        
        // Apply URL count guardrails to prevent explosion
        $urls = array_values(array_unique(array_filter($urls)));
        $maxUrls = (int) apply_filters('mamba_warmup_max_urls', 2500);
        if (count($urls) > $maxUrls) {
            $urls = array_slice($urls, 0, $maxUrls);
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('Mamba Warmup: URL count limited to ' . $maxUrls . ' to prevent explosion');
            }
        }
        
        return $urls;
    }
    
    /**
     * Build Store API URLs for warmup (conservative)
     * 
     * Safe-to-Cache Store API Endpoints:
     * ✅ ALWAYS WARM UP:
     * - /wp-json/wc/store/v1/products (main products endpoint)
     * - /wp-json/wc/store/v1/products/categories
     * - /wp-json/wc/store/v1/products/tags
     * - /wp-json/wc/store/v1/products/attributes
     * - /wp-json/wc/store/v1/products/collection-data
     * - /wp-json/wc/store/v1/products/{id} (individual products)
     * 
     * ⚠️ CACHE WITH SMART INVALIDATION:
     * - /wp-json/wc/store/v1/products/reviews
     * - /wp-json/wc/store/v1/products/{id}/reviews
     * 
     * 🚫 NEVER WARM UP:
     * - /wp-json/wc/store/v1/cart/* (user-specific)
     * - /wp-json/wc/store/v1/checkout/* (session-dependent)
     * - /wp-json/wc/store/v1/order/* (private order data)
     * - /wp-json/wc/store/v1/customers/* (personal customer data)
     * - /wp-json/wc/store/v1/account/* (account-specific)
     * - /wp-json/wc/store/v1/batch (batch operations)
     */
    private static function buildStoreApiUrls(): array {
        $urls = [];
        
        // Only build Store API URLs if WooCommerce is active
        if (!function_exists('wc_get_products') || !function_exists('rest_url')) {
            return $urls;
        }
        
        $restUrl = rest_url('wc/store/v1');
        
        // Core catalog endpoints (always warm up)
        $coreEndpoints = [
            '/products',
            '/products/categories',
            '/products/tags',
            '/products/attributes',
            '/products/collection-data'
        ];
        
        foreach ($coreEndpoints as $endpoint) {
            $urls[] = $restUrl . $endpoint;
        }
        
        // Check if out-of-stock products should be excluded from warmup
        $excludeOutOfStock = apply_filters('mamba_warmup_exclude_outofstock', false);
        $stockStatus = $excludeOutOfStock ? ['instock'] : [];
        
        // Individual product endpoints (top products only - conservative approach)
        $topProducts = wc_get_products([
            'limit' => 10, // Conservative limit
            'status' => 'publish',
            'orderby' => 'meta_value_num',
            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
            'meta_key' => 'total_sales',
            'order' => 'DESC'
        ] + ($stockStatus ? ['stock_status'=>$stockStatus] : []));
        
        foreach ($topProducts as $product) {
            $urls[] = $restUrl . '/products/' . $product->get_id();
        }
        
        // Featured products (individual endpoints)
        $featuredProducts = wc_get_products([
            'limit' => 5, // Conservative limit
            'status' => 'publish',
            'featured' => true
        ] + ($stockStatus ? ['stock_status'=>$stockStatus] : []));
        
        foreach ($featuredProducts as $product) {
            $urls[] = $restUrl . '/products/' . $product->get_id();
        }
        
        // Category-specific product listings (top categories only)
        $topCategories = get_terms([
            'taxonomy' => 'product_cat',
            'number' => 5, // Conservative limit
            'orderby' => 'count',
            'order' => 'DESC',
            'hide_empty' => true
        ]);
        
        if (!is_wp_error($topCategories)) {
            foreach ($topCategories as $category) {
                // Use scalar form for warmup URLs (Store API accepts scalar in URLs)
                $urls[] = $restUrl . '/products?category=' . $category->term_id;
            }
        }
        
        // Tag-specific product listings (top tags only)
        $topTags = get_terms([
            'taxonomy' => 'product_tag',
            'number' => 5, // Conservative limit
            'orderby' => 'count',
            'order' => 'DESC',
            'hide_empty' => true
        ]);
        
        if (!is_wp_error($topTags)) {
            foreach ($topTags as $tag) {
                // Use scalar form for warmup URLs (Store API accepts scalar in URLs)
                $urls[] = $restUrl . '/products?tag=' . $tag->term_id;
            }
        }
        
        // Common product count variations (matching frontend approach)
        $perPageVariations = [12, 24, 48];
        foreach ($perPageVariations as $perPage) {
            $urls[] = $restUrl . '/products?per_page=' . $perPage;
        }
        
        // Common sorting variations (Store API valid parameters only)
        $sortVariations = [
            'orderby=date',
            'orderby=price',
            'orderby=popularity',
            'orderby=rating',
            'orderby=menu_order'
        ];
        
        foreach ($sortVariations as $sort) {
            $urls[] = $restUrl . '/products?' . $sort;
        }
        
        // Combined variations (most common combinations)
        $combinedVariations = [
            'orderby=date&per_page=24',
            'orderby=popularity&per_page=24'
        ];
        
        foreach ($combinedVariations as $variation) {
            $urls[] = $restUrl . '/products?' . $variation;
        }
        
        // Additional low-cost enhancements for faster first user time
        // Page 2 for products (common pagination)
        $urls[] = $restUrl . '/products?page=2';
        
        // Stock status filter (common filter)
        $urls[] = $restUrl . '/products?stock_status=instock';
        
        // Dynamic attribute filters (conservative & filterable)
        // Discover product attribute taxonomies (pa_*) and pick top terms by count
        $maxAttributeTaxonomies = (int) apply_filters('mamba_warmup_max_attribute_taxonomies', 3);
        $termsPerAttribute = (int) apply_filters('mamba_warmup_attribute_terms_per_tax', 1);
        $termsPerAttribute = max(0, min($termsPerAttribute, 3)); // safety cap
        
        if ($termsPerAttribute > 0) {
            $allTaxonomies = get_taxonomies([], 'names');
            $attributeTaxonomies = [];
            if (is_array($allTaxonomies)) {
                foreach ($allTaxonomies as $tx) {
                    if (strpos($tx, 'pa_') === 0) {
                        $attributeTaxonomies[] = $tx;
                    }
                }
            }
            // Allow overriding discovered attribute taxonomies
            $attributeTaxonomies = (array) apply_filters('mamba_warmup_attribute_taxonomies', $attributeTaxonomies);
            // Cap number of attribute taxonomies
            $attributeTaxonomies = array_slice(array_unique(array_filter($attributeTaxonomies)), 0, max(0, $maxAttributeTaxonomies));
            
            foreach ($attributeTaxonomies as $tax) {
                $attrTerms = get_terms([
                    'taxonomy' => $tax,
                    'number' => $termsPerAttribute,
                    'orderby' => 'count',
                    'order' => 'DESC',
                    'hide_empty' => true
                ]);
                if (!is_wp_error($attrTerms) && !empty($attrTerms)) {
                    foreach ($attrTerms as $term) {
                        // Store API accepts scalar in query string for attributes
                        $urls[] = $restUrl . '/products?attributes[' . $tax . ']=' . $term->term_id;
                    }
                }
            }
        }
        
        // Allow filtering of generated Store API URLs
        $urls = apply_filters('mamba_warmup_store_api_urls', $urls, $restUrl);
        
        return array_values(array_unique(array_filter($urls)));
    }
    
    /**
     * Generate filtered category/tag URLs for common use cases
     * 
     * @param string $baseUrl Base category or tag URL
     * @return array Array of filtered URLs
     */
    private static function generateFilteredCategoryUrls(string $baseUrl): array {
        $filteredUrls = [];
        
        // Common sorting options that users frequently use
        $sortOptions = [
            'orderby=date' => 'Newest first',
            'orderby=price' => 'Price low to high',
            'orderby=price-desc' => 'Price high to low',
            'orderby=popularity' => 'Most popular',
            'orderby=rating' => 'Highest rated',
            'orderby=name' => 'Name A-Z',
            'orderby=name-desc' => 'Name Z-A'
        ];
        
        // Common product count options
        $productCountOptions = [
            'per_page=12' => '12 products per page',
            'per_page=24' => '24 products per page',
            'per_page=48' => '48 products per page'
        ];
        
        // Add sorting variations
        foreach ($sortOptions as $query => $description) {
            $filteredUrls[] = self::cleanUrl($baseUrl . '?' . $query);
        }
        
        // Add product count variations
        foreach ($productCountOptions as $query => $description) {
            $filteredUrls[] = self::cleanUrl($baseUrl . '?' . $query);
        }
        
        // Add combined variations (most common combinations)
        $combinedVariations = [
            'orderby=date&per_page=24',
            'orderby=price&per_page=24'
        ];
        
        foreach ($combinedVariations as $query) {
            $filteredUrls[] = self::cleanUrl($baseUrl . '?' . $query);
        }
        
        // Allow filtering of generated URLs
        $filteredUrls = apply_filters('mamba_warmup_filtered_urls', $filteredUrls, $baseUrl);
        
        return array_values(array_unique(array_filter($filteredUrls)));
    }
    
    /**
     * Apply exclusion patterns to filter out URLs
     * 
     * @param array $urls URLs to filter
     * @param array $patterns Exclusion patterns (supports wildcards)
     * @return array Filtered URLs
     */
    private static function applyExclusionPatterns(array $urls, array $patterns): array {
        if (empty($patterns)) {
            return $urls;
        }
        
        return array_filter($urls, function($url) use ($patterns) {
            // Handle array URLs (with ua, headers, etc.)
            $urlString = is_array($url) ? ($url['url'] ?? '') : $url;
            
            foreach ($patterns as $pattern) {
                $pattern = trim($pattern);
                if (empty($pattern)) continue;
                
                // Convert wildcard pattern to regex
                // * matches any characters, ? matches single character
                $regex = '/^' . str_replace(
                    ['\\*', '\\?'],
                    ['.*', '.'],
                    preg_quote($pattern, '/')
                ) . '$/i';
                
                if (preg_match($regex, $urlString)) {
                    return false; // Exclude this URL
                }
                
                // Also check for simple substring match
                if (stripos($urlString, $pattern) !== false) {
                    return false;
                }
            }
            
            return true; // Keep this URL
        });
    }
    
    /**
     * Clean URL by removing tracking parameters
     * 
     * @param string $url URL to clean
     * @return string Cleaned URL
     */
    private static function cleanUrl(string $url): string {
        $parts = wp_parse_url($url);
        if (!$parts) return $url;
        
        // Clean query parameters if present
        if (!empty($parts['query'])) {
            parse_str($parts['query'], $query);
            $query = \Mamba\Modules\Caching\Services\Paths::trimTrackingParams($query);
            $parts['query'] = http_build_query($query);
        }
        
        // Rebuild URL
        $scheme = isset($parts['scheme']) ? $parts['scheme'] . '://' : '';
        $host = $parts['host'] ?? '';
        $port = isset($parts['port']) ? ':' . $parts['port'] : '';
        $user = $parts['user'] ?? '';
        $pass = isset($parts['pass']) ? ':' . $parts['pass'] : '';
        $pass = ($user || $pass) ? "$pass@" : '';
        $path = $parts['path'] ?? '';
        $query = isset($parts['query']) ? '?' . $parts['query'] : '';
        $fragment = isset($parts['fragment']) ? '#' . $parts['fragment'] : '';
        
        return $scheme . $user . $pass . $host . $port . $path . $query . $fragment;
    }
    
    /**
     * Expand URLs with variants (device + currency/language/country with primary combo limiting)
     */
    public static function expandVariants(array $urls): array {
        $expanded = [];
        $primaryVariants = self::getPrimaryVariants();
        
        // Apply variant per URL limits to prevent explosion
        $maxVariantsPerUrl = (int) apply_filters('mamba_warmup_max_variants_per_url', 3);
        
        // Optimize variant expansion based on site size
        $totalUrls = count($urls);
        $maxVariants = apply_filters('mamba_warmup_max_variants', 3); // Limit variants for large sites
        
        // For large sites, prioritize desktop and mobile only
        if ($totalUrls > 100) {
            $devices = ['desktop', 'mobile'];
        } else {
            $devices = apply_filters('mamba_warmup_devices', ['desktop', 'mobile', 'tablet']);
        }
        
        // Limit devices to max variants per URL
        $devices = array_slice($devices, 0, $maxVariantsPerUrl);
        
        // Before the foreach($urls as $url) loop, decide which countries to warm:
        $countryVariants = [];
        if (!empty($primaryVariants['country'])) {
            // Default: warm just the primary country
            $countryVariants = [$primaryVariants['country']];
        }
        // Allow sites to opt into a few extra countries (kept small by caps)
        $countryVariants = (array) apply_filters('mamba_warmup_countries', $countryVariants);

        // Cap to prevent explosion (eg. 3 countries max by default)
        $maxCountryVariants = (int) apply_filters('mamba_warmup_max_country_variants', 3);
        $countryVariants = array_slice(array_unique(array_filter($countryVariants)), 0, $maxCountryVariants);

        // If none resolved, keep a single "no-country" pass
        if (empty($countryVariants)) $countryVariants = [''];
        
        foreach ($urls as $url) {
            foreach ($devices as $device) {
                foreach ($countryVariants as $countryCode) {
                    $target = [
                        'url' => $url,
                        'ua' => self::getUserAgentForDevice($device),
                    ];
                    $t = self::setVariantSignals($target, $device, $primaryVariants);
                    if ($countryCode !== '') {
                        $t['headers'] = $t['headers'] ?? [];
                        $t['headers']['MAMBA_GEO'] = $countryCode;
                    }
                    $expanded[] = $t;
                }
            }
        }
        
        return $expanded;
    }
    
    /**
     * Get appropriate user agent for device type
     */
    private static function getUserAgentForDevice(string $device): string {
        $deviceUA = '';
        switch ($device) {
            case 'mobile':
                $deviceUA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1';
                break;
            case 'tablet':
                $deviceUA = 'Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1';
                break;
            case 'desktop':
            default:
                $deviceUA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
                break;
        }
        
        // Always prefix with Mamba Cache Warmup Bot for proper identification
        return 'Mamba Cache Warmup Bot (' . $device . ') ' . $deviceUA;
    }
    
    /**
     * Get primary variants to limit warmup scope (prevent exponential growth)
     */
    private static function getPrimaryVariants(): array {
        // Currency: detect common multi-currency cookies before falling back
        $detectedCur = '';
        $cookieKeys = [
            'woocommerce_multicurrency_for_woo_currency', // Fox/ViWCM
            'woocs_current_currency',                     // WOOCS
            'wmc_current_currency',                       // Woo Multi-Currency
            'yay_currency',                               // YayCurrency
            'aelia_cs_selected_currency',                 // Aelia
        ];
        foreach ($cookieKeys as $ck) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized via sanitize_text_field
            if (!empty($_COOKIE[$ck])) { $detectedCur = sanitize_text_field(wp_unslash($_COOKIE[$ck])); break; }
        }
        if ($detectedCur === '') { $detectedCur = (string) get_option('woocommerce_currency','USD'); }
        
        $variants = [
            'currency' => $detectedCur,
            'language' => '',
            'country' => '',
            'tax_display' => get_option('woocommerce_tax_display_shop', 'incl')
        ];
        
        // Detect primary language
        if (function_exists('pll_current_language')) {
            $variants['language'] = pll_current_language() ?: '';
        } elseif (defined('ICL_LANGUAGE_CODE')) {
            $variants['language'] = ICL_LANGUAGE_CODE ?: '';
        }
        
        // Detect primary country (if geolocation enabled)
        if (get_option('woocommerce_default_customer_address') === 'geolocation_ajax') {
            // Use default country or first available
            $variants['country'] = get_option('woocommerce_default_country', 'US');
            if (strpos($variants['country'], ':') !== false) {
                $variants['country'] = explode(':', $variants['country'])[0];
            }
        }
        
        // Allow filtering
        return apply_filters('mamba_warmup_primary_variants', $variants);
    }
    
    /**
     * Set variant signals (cookies/headers) for warmup requests
     */
    private static function setVariantSignals(array $target, string $device, array $primaryVariants): array {
        $cookies = [];
        
        // Currency cookies (if different from default)
        if (!empty($primaryVariants['currency'])) {
            $cookieKeys = [
                'woocommerce_multicurrency_for_woo_currency',
                'woocs_current_currency',
                'wmc_current_currency',
                'yay_currency',
                'aelia_cs_selected_currency'
            ];
            
            foreach ($cookieKeys as $cookieKey) {
                $cookies[$cookieKey] = $primaryVariants['currency'];
            }
        }
        
        // Language cookies (if different from default)
        if (!empty($primaryVariants['language'])) {
            $cookies['pll_language'] = $primaryVariants['language'];
            $cookies['wpml_browser_redirect_lang'] = $primaryVariants['language'];
        }
        
        // Tax display cookies (for Store API and page cache compatibility)
        if (!empty($primaryVariants['tax_display'])) {
            $cookies['woocommerce_tax_display_shop'] = $primaryVariants['tax_display'];
        }
        
        // Country header (if geolocation enabled)
        if (!empty($primaryVariants['country'])) {
            $target['headers'] = $target['headers'] ?? [];
            $target['headers']['MAMBA_GEO'] = $primaryVariants['country'];
        }
        
        // Set cookies if any
        if (!empty($cookies)) {
            $target['cookies'] = $cookies;
        }
        
        return $target;
    }

    // Warmup job (async with status)
    public static function startJob(): array {
        // Check for existing lock to prevent concurrent jobs
        $existingLock = get_transient('mamba_warmup_lock');
        if ($existingLock) {
            // Smart stale lock detection: check if the job is actually running
            // by looking at the most recent job's updated_at timestamp
            $isStale = false;
            $staleThreshold = 5 * 60; // 5 minutes without update = stale
            
            // Get the most recent job metadata
            global $wpdb;
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $metaOption = $wpdb->get_var(
                $wpdb->prepare(
                    "SELECT option_name FROM {$wpdb->options} 
                     WHERE option_name LIKE %s 
                     ORDER BY option_id DESC 
                     LIMIT 1",
                    'mamba_warmup_job_meta_%'
                )
            );
            
            if ($metaOption) {
                $jobId = str_replace('mamba_warmup_job_meta_', '', $metaOption);
                $job = self::getJob($jobId);
                
                if ($job) {
                    $lastUpdate = (int)($job['updated_at'] ?? 0);
                    $timeSinceUpdate = time() - $lastUpdate;
                    
                    // If job is "running" but hasn't been updated in 5 minutes, it's stale
                    if ($job['status'] === 'running' && $timeSinceUpdate > $staleThreshold) {
                        $isStale = true;
                        if (defined('WP_DEBUG') && WP_DEBUG) {
                            error_log('Mamba Warmup: Detected stale job lock (last update: ' . $timeSinceUpdate . 's ago). Clearing lock.');
                        }
                    }
                    // If job is completed/cancelled/failed but lock exists, it's stale
                    elseif (in_array($job['status'], ['done', 'cancelled', 'failed', 'paused'])) {
                        $isStale = true;
                        if (defined('WP_DEBUG') && WP_DEBUG) {
                            error_log('Mamba Warmup: Detected orphan lock from completed job (status: ' . $job['status'] . '). Clearing lock.');
                        }
                    }
                } else {
                    // Job file doesn't exist but lock does = stale
                    $isStale = true;
                    if (defined('WP_DEBUG') && WP_DEBUG) {
                        error_log('Mamba Warmup: Job file not found but lock exists. Clearing stale lock.');
                    }
                }
            } else {
                // No job metadata but lock exists = stale lock
                $isStale = true;
                if (defined('WP_DEBUG') && WP_DEBUG) {
                    error_log('Mamba Warmup: No job metadata found but lock exists. Clearing orphan lock.');
                }
            }
            
            if ($isStale) {
                // Clear the stale lock so we can proceed
                delete_transient('mamba_warmup_lock');
            } else {
                // Lock is valid - a job is genuinely running
                return [
                    'status' => 'error',
                    'error' => 'job_already_running',
                    'message' => 'Another warmup job is already running'
                ];
            }
        }
        
        $urls = self::buildUrls();
        
        // If no URLs found, return empty job
        if (empty($urls)) {
            return [
                'status' => 'done',
                'queued' => [],
                'total' => 0,
                'success' => 0,
                'failed' => 0,
                'current_url' => '',
                'started_at' => time(),
                'updated_at' => time(),
                'concurrency' => 1,
            ];
        }
        
        // Expand variants per URL (device + currency/language/country with primary combo limiting)
        $expanded = self::expandVariants($urls);
        
        // Log for debugging
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('Mamba Warmup: Built ' . count($urls) . ' base URLs');
            error_log('Mamba Warmup: Expanded to ' . count($expanded) . ' total URLs');
        }
        
        // Get time budget settings
        $maxExecutionTime = (int) apply_filters('mamba_warmup_max_execution_time', 600); // 10 minutes default
        
        $job = [
            'status' => 'running',
            'queued' => array_values($expanded),
            'total' => count($expanded),
            'success' => 0,
            'failed' => 0,
            'current_url' => '',
            'started_at' => time(),
            'updated_at' => time(),
            'concurrency' => (int)get_option('mamba_preload_concurrency', 10), // Increased default concurrency
            'batch_size' => (int)get_option('mamba_warmup_batch_size', 50), // Increased default batch size
            'last_error' => '',
            'last_url' => '',
            'processed' => 0,
            'max_execution_time' => $maxExecutionTime,
            'paused_at' => null,
            'resumed_at' => null
        ];
        
        // Store job in file (non-autoloaded) and keep minimal metadata in options
        $jobId = uniqid('warmup_');
        $job['id'] = $jobId;
        
        if (self::storeJob($jobId, $job)) {
            // Store minimal metadata in non-autoloaded option
            update_option("mamba_warmup_job_meta_{$jobId}", [
                'id' => $jobId,
                'status' => $job['status'],
                'created_at' => $job['started_at'],
                'updated_at' => $job['updated_at'],
                'total_urls' => $job['total']
            ], false); // autoload = false
        }
        
        // Set global lock with job start time as value (for validation)
        set_transient('mamba_warmup_lock', $job['started_at'], 3600); // 1 hour timeout
        
        // Return the job immediately for async processing
        // The job will be processed via statusJob() when called
        return $job;
    }

    public static function statusJob(): array {
        // Get the most recent job metadata
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $metaOption = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT option_name FROM {$wpdb->options} 
                 WHERE option_name LIKE %s 
                 ORDER BY option_id DESC 
                 LIMIT 1",
                'mamba_warmup_job_meta_%'
            )
        );
        
        if (!$metaOption) {
            return ['status' => 'idle'];
        }
        
        $jobId = str_replace('mamba_warmup_job_meta_', '', $metaOption);
        $job = self::getJob($jobId);
        
        if (empty($job)) {
            return ['status' => 'idle'];
        }
        
        // If job is running, process a batch
        if ($job['status'] === 'running' && !empty($job['queued'])) {
            $job = self::processJobSynchronously($job);
        }
        
        return $job;
    }

    public static function cancelJob(): bool {
        // Get the most recent job
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $metaOption = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT option_name FROM {$wpdb->options} 
                 WHERE option_name LIKE %s 
                 ORDER BY option_id DESC 
                 LIMIT 1",
                'mamba_warmup_job_meta_%'
            )
        );
        
        if (!$metaOption) {
            return false;
        }
        
        $jobId = str_replace('mamba_warmup_job_meta_', '', $metaOption);
        $job = self::getJob($jobId);
        
        if (empty($job)) {
            return false;
        }
        
        $job['status'] = 'cancelled';
        $job['updated_at'] = time();
        $job['last_error'] = 'cancelled_by_user';
        
        if (self::storeJob($jobId, $job)) {
            // Update metadata
            update_option("mamba_warmup_job_meta_{$jobId}", [
                'id' => $jobId,
                'status' => $job['status'],
                'created_at' => $job['created_at'] ?? time(),
                'updated_at' => $job['updated_at'],
                'total_urls' => $job['total'] ?? 0
            ], false);
        }
        
        // Clear the global lock
        delete_transient('mamba_warmup_lock');
        
        return true;
    }
    
    /**
     * Resume a paused warmup job
     */
    public static function resumeJob(): bool {
        // Get the most recent job
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $metaOption = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT option_name FROM {$wpdb->options} 
                 WHERE option_name LIKE %s 
                 ORDER BY option_id DESC 
                 LIMIT 1",
                'mamba_warmup_job_meta_%'
            )
        );
        
        if (!$metaOption) {
            return false;
        }
        
        $jobId = str_replace('mamba_warmup_job_meta_', '', $metaOption);
        $job = self::getJob($jobId);
        
        if (empty($job) || $job['status'] !== 'paused') {
            return false;
        }
        
        $job['status'] = 'running';
        $job['resumed_at'] = time();
        $job['updated_at'] = time();
        $job['last_error'] = '';
        
        if (self::storeJob($jobId, $job)) {
            // Update metadata
            update_option("mamba_warmup_job_meta_{$jobId}", [
                'id' => $jobId,
                'status' => $job['status'],
                'created_at' => $job['created_at'] ?? time(),
                'updated_at' => $job['updated_at'],
                'total_urls' => $job['total'] ?? 0
            ], false);
        }
        
        return true;
    }

    /**
     * Get job storage directory
     */
    private static function getJobStorageDir(): string {
        $dir = WP_CONTENT_DIR . '/cache/mamba/jobs';
        if (!is_dir($dir)) {
            wp_mkdir_p($dir);
            // Create protection files
            file_put_contents($dir . '/index.html', '<!-- Directory access denied -->');
            file_put_contents($dir . '/.htaccess', "<IfModule mod_authz_core.c>\n  Require all denied\n</IfModule>\n<IfModule !mod_authz_core.c>\n  Deny from all\n</IfModule>\n");
        }
        return $dir;
    }
    
    /**
     * Store job data in file (non-autoloaded)
     */
    private static function storeJob(string $jobId, array $job): bool {
        $dir = self::getJobStorageDir();
        $file = $dir . '/' . $jobId . '.json';
        return file_put_contents($file, json_encode($job)) !== false;
    }
    
    /**
     * Retrieve job data from file
     */
    private static function getJob(string $jobId): ?array {
        $dir = self::getJobStorageDir();
        $file = $dir . '/' . $jobId . '.json';
        
        if (!file_exists($file)) {
            return null;
        }
        
        $content = file_get_contents($file);
        if ($content === false) {
            return null;
        }
        
        $job = json_decode($content, true);
        return is_array($job) ? $job : null;
    }
    
    /**
     * Delete job file
     */
    private static function deleteJob(string $jobId): bool {
        $dir = self::getJobStorageDir();
        $file = $dir . '/' . $jobId . '.json';
        
        if (file_exists($file)) {
            return unlink($file);
        }
        
        return true;
    }
    
    /**
     * Clean up old job files (older than 24 hours)
     */
    public static function cleanupOldJobs(): void {
        $dir = self::getJobStorageDir();
        if (!is_dir($dir)) {
            return;
        }
        
        $cutoff = time() - (24 * 60 * 60); // 24 hours ago
        
        $files = glob($dir . '/*.json');
        foreach ($files as $file) {
            if (filemtime($file) < $cutoff) {
                @unlink($file);
            }
        }
        
        // Also clean up old metadata options (safer approach)
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $oldOptions = $wpdb->get_col(
            $wpdb->prepare(
                "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s",
                'mamba_warmup_job_meta_%'
            )
        );
        
        if (!empty($oldOptions)) {
            foreach ($oldOptions as $optionName) {
                $optionValue = get_option($optionName);
                if (is_array($optionValue) && isset($optionValue['created_at'])) {
                    if ((int)$optionValue['created_at'] < $cutoff) {
                        delete_option($optionName);
                    }
                } elseif (is_array($optionValue) && isset($optionValue['updated_at'])) {
                    if ((int)$optionValue['updated_at'] < $cutoff) {
                        delete_option($optionName);
                    }
                }
            }
        }
    }
    
    /**
     * Process the warmup job synchronously without cron dependency
     */
    private static function processJobSynchronously(array $job): array {
        // Process only one batch at a time for async progress updates
        if ($job['status'] !== 'running' || empty($job['queued'])) {
            return $job;
        }
        
        // Check time budget to prevent excessive execution time
        $maxExecutionTime = (int)($job['max_execution_time'] ?? 600);
        $currentTime = time();
        $jobStartTime = (int)($job['started_at']);
        $resumedTime = (int)($job['resumed_at'] ?? $jobStartTime);
        $elapsedTime = $currentTime - $resumedTime;
        
        // If we've exceeded the time budget, pause the job
        if ($elapsedTime > $maxExecutionTime) {
            $job['status'] = 'paused';
            $job['paused_at'] = $currentTime;
            $job['updated_at'] = $currentTime;
            $job['last_error'] = 'time_budget_exceeded';
            
            // Store updated job
            if (isset($job['id'])) {
                self::storeJob($job['id'], $job);
                // Update metadata
                update_option("mamba_warmup_job_meta_{$job['id']}", [
                    'id' => $job['id'],
                    'status' => $job['status'],
                    'created_at' => $job['created_at'] ?? time(),
                    'updated_at' => $job['updated_at'],
                    'total_urls' => $job['total'] ?? 0
                ], false);
            }
            
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('Mamba Warmup: Job paused due to time budget (' . $maxExecutionTime . 's) exceeded');
            }
            return $job;
        }
        
        // Process next batch
        $batchSize = max(1, min(10, (int)($job['batch_size'] ?? 10))); // Smaller batch size for better progress updates
        $batch = array_slice($job['queued'], 0, $batchSize);
        
        // Remove the batch from the queue
        $job['queued'] = array_slice($job['queued'], $batchSize);
        
        // Set current URL for status display
        $job['current_url'] = is_array(end($batch)) ? (end($batch)['url'] ?? '') : (end($batch) ?: '');
        $job['last_url'] = $job['current_url'];
        $job['processed'] = (int)($job['processed']) + count($batch);
        
        // Process the batch with parallel processing
        $results = Warmup\Warmer::processBatchParallel($batch);
        $success = 0;
        $errors = [];
        
        foreach ($results as $index => $result) {
            if ($result === true) {
                $success++;
            } else {
                // Track error details for better debugging
                $target = $batch[$index] ?? '';
                $errorInfo = is_array($result) ? $result : [
                    'error' => 'unknown',
                    'url'   => is_array($target) ? ($target['url'] ?? 'unknown') : (string)$target,
                ];
                $errors[] = $errorInfo;
                // Update last_url with the current target for better observability
                $job['last_url'] = is_array($target) ? ($target['url'] ?? 'unknown') : $target;
            }
        }
        
        $job['success'] = (int)$job['success'] + $success;
        $job['failed'] = (int)$job['failed'] + (count($batch) - $success);
        $job['updated_at'] = time();
        
        // Track last error if any (improved error tracking)
        if (!empty($errors)) {
            $errorMessages = [];
            foreach (array_slice($errors, 0, 3) as $error) {
                if (is_array($error)) {
                    $errorMessages[] = ($error['error'] ?? 'unknown') . ':' . ($error['url'] ?? 'unknown');
                } else {
                    $errorMessages[] = $error;
                }
            }
            $job['last_error'] = 'batch_errors: ' . implode(', ', $errorMessages);
        }
        
        // If queue is now empty, mark done and update stats
        if (empty($job['queued'])) {
            $job['status'] = 'done';
            $job['updated_at'] = time();
            update_option('mamba_warmup_stats', [
                'last_run' => time(),
                'total_urls' => $job['total'],
                'success' => $job['success']
            ]);
            delete_transient('mamba_warmup_lock');
        }
        
        // Update job status
        if (isset($job['id'])) {
            self::storeJob($job['id'], $job);
            // Update metadata
            update_option("mamba_warmup_job_meta_{$job['id']}", [
                'id' => $job['id'],
                'status' => $job['status'],
                'created_at' => $job['created_at'] ?? time(),
                'updated_at' => $job['updated_at'],
                'total_urls' => $job['total'] ?? 0
            ], false);
        }
        
        return $job;
    }

}
