<?php

/**
 * External Asset Cache Service
 *
 * Handles caching of external CSS assets locally
 *
 * @package ProRank\SEO\Core\Optimization\CSS
 * @since   1.0.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Core\Optimization\CSS;

use Exception;

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


/**
 * ExternalAssetCacheService class
 */


class ExternalAssetCacheService {

    /**
     * Google Fonts responds differently based on user-agent; use a modern browser UA to get woff2.
     */
    private const GOOGLE_FONTS_CSS_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
    
    /**
     * Cache directory for external assets
     *
     * @var string
     */
    private string $cache_dir;
    
    /**
     * Cache URL for external assets
     *
     * @var string
     */
    private string $cache_url;
    
    /**
     * Cache expiration time in seconds
     *
     * @var int
     */
    private int $cache_expiration;
    
    /**
     * User agent for requests
     *
     * @var string
     */
    private string $user_agent;
    
    /**
     * Constructor
     *
     * @param int $cache_expiration Cache expiration in seconds (default 7 days)
     */
    public function __construct(int $cache_expiration = 604800) {
        $upload_dir = wp_upload_dir();
        // Align with public cache path used by CSS cache server for consistency
        $this->cache_dir = trailingslashit($upload_dir['basedir']) . 'prorank-cache/external-css/';
        $this->cache_url = trailingslashit($upload_dir['baseurl']) . 'prorank-cache/external-css/';
        $this->cache_expiration = $cache_expiration;
        $this->user_agent = 'ProRank SEO/' . PRORANK_SEO_VERSION . ' External Asset Cache';
        
        $this->ensure_cache_directory();
    }
    
    /**
     * Get cached version of external CSS file
     *
     * @param string $url External CSS URL
     * @return array|null Cached file info or null if caching fails
     */
    public function get(string $url): ?array {
        // Validate URL
        if (!$this->isValidUrl($url)) {
            return null;
        }

        // If another ProRank module is already hosting/subsetting Google Fonts locally,
        // don't cache fonts.googleapis.com CSS here (it still references fonts.gstatic.com).
        if ($this->isGoogleFontsCssUrl($url) && $this->shouldSkipGoogleFontsCssCache()) {
            return null;
        }

        $url = $this->maybeForceGoogleFontsDisplaySwap($url);
        
        // Generate cache filename
        $cache_info = $this->getCacheInfo($url);
        
        // Check if cached and valid
        if ($this->isCacheValid($cache_info['path'])) {
            // Auto-heal legacy Google Fonts cache entries (TTF + display=fallback)
            if (!$this->isGoogleFontsCssUrl($url) || $this->isGoogleFontsCacheHealthy($cache_info['path'])) {
                return $cache_info;
            }
        }
        
        // Fetch and cache the asset
        try {
            $content = $this->fetchAsset($url);
            
            if (empty($content)) {
                return null;
            }
            
            // Validate content type
            if (!$this->isValidCssContent($content)) {
                return null;
            }
            
            // Process content (fix relative URLs, etc.)
            $content = $this->processContent($content, $url);
            
            // Save to cache
            $this->saveToCache($content, $cache_info['path']);
            
            // Update cache info
            $cache_info['size'] = strlen($content);
            $cache_info['cached_at'] = time();
            
            return $cache_info;
        } catch (Exception $e) {
            prorank_log('ProRank External CSS Cache Error: ' . $e->getMessage());
            return null;
        }
    }
    
    /**
     * Get cache info for URL
     *
     * @param string $url External URL
     * @return array Cache file info
     */
    private function getCacheInfo(string $url): array {
        $parsed = wp_parse_url($url);
        $host = $parsed['host'] ?? 'unknown';
        $path = $parsed['path'] ?? '/index';
        
        // Clean up path for filename
        $path = str_replace(['/', '\\', '..'], '-', $path);
        $path = trim($path, '-');
        
        // Generate filename
        $filename = $host . '-' . $path;
        
        // Add hash for uniqueness (handles query strings)
        $hash = substr(md5($url), 0, 8);
        $filename = $filename . '-' . $hash . '.css';
        
        // Sanitize filename
        $filename = preg_replace('/[^a-zA-Z0-9\-_\.]/', '', $filename);
        
        return [
            'path' => $this->cache_dir . $filename,
            'url' => $this->cache_url . $filename,
            'filename' => $filename,
            'original_url' => $url,
        ];
    }
    
    /**
     * Check if cache is valid
     *
     * @param string $cache_path Cache file path
     * @return bool
     */
    private function isCacheValid(string $cache_path): bool {
        if (!file_exists($cache_path)) {
            return false;
        }
        
        $file_age = time() - filemtime($cache_path);
        
        return $file_age < $this->cache_expiration;
    }
    
    /**
     * Fetch asset from external URL
     *
     * @param string $url URL to fetch
     * @return string Content
     * @throws Exception If fetch fails
     */
    private function fetchAsset(string $url): string {
        $args = [
            'timeout' => 30,
            'user-agent' => $this->getUserAgentForUrl($url),
            'sslverify' => true,
            'headers' => [
                'Accept' => 'text/css,*/*;q=0.1',
                'Accept-Encoding' => 'gzip, deflate',
            ],
        ];
        
        $response = wp_remote_get($url, $args);
        
        if (is_wp_error($response)) {
            $error_message = sanitize_text_field( $response->get_error_message() );
            throw new Exception(
                sprintf(
                    'Failed to fetch external CSS: %s',
                    esc_html( $error_message )
                )
            );
        }
        
        $response_code = wp_remote_retrieve_response_code($response);
        if ($response_code !== 200) {
            $safe_code = absint( $response_code );
            throw new Exception(
                sprintf(
                    'HTTP error %s when fetching external CSS',
                    esc_html( (string) $safe_code )
                )
            );
        }
        
        $content = wp_remote_retrieve_body($response);
        
        if (empty($content)) {
            throw new Exception('Empty response from external CSS');
        }
        
        return $content;
    }
    
    /**
     * Validate CSS content
     *
     * @param string $content Content to validate
     * @return bool
     */
    private function isValidCssContent(string $content): bool {
        // Basic CSS validation - check for common CSS patterns
        $css_patterns = [
            '/[a-zA-Z\-]+\s*:\s*[^;]+;/', // property: value;
            '/@[a-zA-Z\-]+/', // @rules
            '/\.[a-zA-Z\-]+/', // .classes
            '/#[a-zA-Z\-]+/', // #ids
            '/[a-zA-Z]+\s*\{/', // selectors {
        ];
        
        foreach ($css_patterns as $pattern) {
            if (preg_match($pattern, $content)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Process content before caching
     *
     * @param string $content CSS content
     * @param string $base_url Original URL for relative paths
     * @return string Processed content
     */
    private function processContent(string $content, string $base_url): string {
        // Add source comment
        $content = "/* ProRank CSS - Cached from: {$base_url} */\n" . $content;
        
        // Fix relative URLs in the content
        $content = $this->fixRelativeUrls($content, $base_url);
        
        if ($this->isFontDisplaySwapEnabled()) {
            // Force font-display:swap for performance audits (and override display=fallback).
            $content = $this->addFontDisplaySwap($content);
        }
        
        return $content;
    }
    
    /**
     * Fix relative URLs in CSS content
     *
     * @param string $content CSS content
     * @param string $base_url Base URL for relative paths
     * @return string Fixed content
     */
    private function fixRelativeUrls(string $content, string $base_url): string {
        $base_parts = wp_parse_url($base_url);
        $base_dir = dirname($base_parts['path'] ?? '/');
        $base_scheme = $base_parts['scheme'] ?? 'https';
        $base_host = $base_parts['host'] ?? '';
        
        // Fix url() references
        $content = preg_replace_callback(
            '/url\(\s*["\']?(?!data:|https?:|\/\/)([^"\')]+)["\']?\s*\)/i',
            function($matches) use ($base_scheme, $base_host, $base_dir) {
                $url = $matches[1];
                
                // Handle root-relative URLs
                if (strpos($url, '/') === 0) {
                    $absolute = $base_scheme . '://' . $base_host . $url;
                } else {
                    // Handle relative URLs
                    $absolute = $base_scheme . '://' . $base_host . $base_dir . '/' . $url;
                }
                
                return 'url("' . $absolute . '")';
            },
            $content
        );
        
        return $content;
    }
    
    /**
     * Add font-display: swap to @font-face rules
     * Also replaces font-display: block/auto with swap (they cause render blocking)
     *
     * @param string $content CSS content
     * @return string Modified content
     */
    private function addFontDisplaySwap(string $content): string {
        return preg_replace_callback(
            '/@font-face\s*\{([^}]+)\}/i',
            function($matches) {
                $rule = $matches[1];
                $original_rule = $rule;

                // Replace non-optimal values (block/auto/fallback) with swap.
                $rule = preg_replace('/font-display\s*:\s*(block|auto|fallback)/i', 'font-display: swap', $rule);

                // If font-display already exists (swap/optional/etc), keep it (but return modifications).
                if (stripos($rule, 'font-display') !== false) {
                    if ($rule !== $original_rule) {
                        return '@font-face {' . $rule . '}';
                    }
                    return $matches[0];
                }

                // Add font-display: swap if missing entirely
                $rule = rtrim($rule, '; ') . '; font-display: swap;';

                return '@font-face {' . $rule . '}';
            },
            $content
        );
    }

    private function isGoogleFontsCssUrl(string $url): bool {
        return (wp_parse_url($url, PHP_URL_HOST) ?? '') === 'fonts.googleapis.com';
    }

    private function shouldSkipGoogleFontsCssCache(): bool {
        static $asset_settings = null;
        if ($asset_settings === null) {
            $asset_settings = get_option('prorank_asset_optimization_settings', []);
            if (!is_array($asset_settings)) {
                $asset_settings = [];
            }
        }

        return !empty($asset_settings['font_subsetting_enabled'])
            || !empty($asset_settings['host_google_fonts_locally']);
    }

    private function getUserAgentForUrl(string $url): string {
        if ($this->isGoogleFontsCssUrl($url)) {
            return self::GOOGLE_FONTS_CSS_USER_AGENT;
        }
        return $this->user_agent;
    }

    private function isFontDisplaySwapEnabled(): bool {
        // Prefer unified Asset Optimization setting.
        $asset_settings = get_option('prorank_asset_optimization_settings', []);
        if (!empty($asset_settings['font_display_swap'])) {
            return true;
        }

        // Back-compat: CSS Optimization screen stores this under cache settings.
        $cache_settings = get_option('prorank_cache_settings', []);
        if (!empty($cache_settings['css_font_display_swap'])) {
            return true;
        }

        return false;
    }

    private function maybeForceGoogleFontsDisplaySwap(string $url): string {
        if (!$this->isGoogleFontsCssUrl($url) || !$this->isFontDisplaySwapEnabled()) {
            return $url;
        }

        if (!function_exists('remove_query_arg') || !function_exists('add_query_arg')) {
            return $url;
        }

        $url = remove_query_arg('display', $url);
        return add_query_arg('display', 'swap', $url);
    }

    private function isGoogleFontsCacheHealthy(string $cache_path): bool {
        $content = @file_get_contents($cache_path);
        if ($content === false || $content === '') {
            return false;
        }

        // If we cached a non-browser response, Google Fonts will be TTF-only; force refresh to woff2.
        $has_woff2 = stripos($content, '.woff2') !== false
            || stripos($content, 'format(\'woff2\')') !== false
            || stripos($content, 'format(\"woff2\")') !== false;

        if (!$has_woff2) {
            return false;
        }

        if ($this->isFontDisplaySwapEnabled() && preg_match('/font-display\s*:\s*(block|auto|fallback)/i', $content)) {
            return false;
        }

        return true;
    }
    
    /**
     * Save content to cache
     *
     * @param string $content Content to save
     * @param string $path File path
     * @throws Exception If save fails
     */
    private function saveToCache(string $content, string $path): void {
        $result = file_put_contents($path, $content, LOCK_EX);
        
        if ($result === false) {
            $safe_path = sanitize_text_field( $path );
            throw new Exception(
                sprintf(
                    'Failed to save external CSS to cache: %s',
                    esc_html( $safe_path )
                )
            );
        }
        
        // Create gzip version
        $compressed = gzencode($content, 9);
        if ($compressed !== false) {
            file_put_contents($path . '.gz', $compressed, LOCK_EX);
        }
    }
    
    /**
     * Validate URL
     *
     * @param string $url URL to validate
     * @return bool
     */
    private function isValidUrl(string $url): bool {
        // Must be absolute URL
        if (!preg_match('/^https?:\/\//i', $url)) {
            return false;
        }
        
        // Validate URL format
        if (!filter_var($url, FILTER_VALIDATE_URL)) {
            return false;
        }

        // WP.org-safe scope: only allow Google Fonts CSS.
        $host = strtolower((string) wp_parse_url($url, PHP_URL_HOST));
        if ($host !== 'fonts.googleapis.com') {
            return false;
        }
        
        // Check file extension (optional, but recommended)
        $ext = pathinfo(wp_parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
        if (!empty($ext) && $ext !== 'css') {
            return false;
        }
        
        return true;
    }
    
    /**
     * Clear cache
     *
     * @param bool $expired_only Only clear expired files
     * @return int Number of files deleted
     */
    public function clearCache(bool $expired_only = true): int {
        $deleted = 0;
        
        if (!is_dir($this->cache_dir)) {
            return 0;
        }
        
        $files = glob($this->cache_dir . '*.css');
        $gzip_files = glob($this->cache_dir . '*.css.gz');
        $all_files = array_merge($files ?: [], $gzip_files ?: []);
        
        foreach ($all_files as $file) {
            if ($expired_only && $this->isCacheValid($file)) {
                continue;
            }
            
            if (\ProRank\SEO\Core\FilesystemHelper::delete_file($file)) {
                $deleted++;
            }
        }
        
        return $deleted;
    }
    
    /**
     * Get cache statistics
     *
     * @return array Statistics
     */
    public function getStats(): array {
        if (!is_dir($this->cache_dir)) {
            return [
                'files' => 0,
                'size' => 0,
                'size_formatted' => '0 B',
            ];
        }
        
        $files = glob($this->cache_dir . '*.css');
        $total_size = 0;
        $expired = 0;
        
        foreach ($files ?: [] as $file) {
            $total_size += filesize($file);
            
            if (!$this->isCacheValid($file)) {
                $expired++;
            }
        }
        
        return [
            'files' => count($files ?: []),
            'size' => $total_size,
            'size_formatted' => size_format($total_size),
            'expired' => $expired,
            'directory' => $this->cache_dir,
        ];
    }
    
    /**
     * Ensure cache directory exists
     */
    private function ensure_cache_directory(): void {
        if (!is_dir($this->cache_dir)) {
            wp_mkdir_p($this->cache_dir);
            
            // Add index.php for security
            $index_content = '<?php // Silence is golden';
            file_put_contents($this->cache_dir . 'index.php', $index_content);
            
            // Add .htaccess to allow CSS files
            $htaccess_content = "# ProRank SEO External CSS Cache\n";
            $htaccess_content .= "<FilesMatch \"\\.css(\\.gz)?$\">\n";
            $htaccess_content .= "  Order Allow,Deny\n";
            $htaccess_content .= "  Allow from all\n";
            $htaccess_content .= "</FilesMatch>\n";
            file_put_contents($this->cache_dir . '.htaccess', $htaccess_content);
        }
    }
}
