<?php

/**
 * CSS Minifier Service
 *
 * Handles CSS minification using Matthias Mullie's Minify library
 *
 * @package ProRank\SEO\Core\Optimization\CSS
 * @since   1.0.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Core\Optimization\CSS;

use MatthiasMullie\Minify\CSS as CSSMinify;
use Exception;

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


/**
 * CssMinifierService class
 */


class CssMinifierService {
    
    /**
     * Cache directory for minified files
     *
     * @var string
     */
    private string $cache_dir;
    
    /**
     * Cache URL for minified files
     *
     * @var string
     */
    private string $cache_url;

    /**
     * Whether cache directory is available for writes.
     *
     * @var bool
     */
    private bool $cache_available = true;
    
    /**
     * Options for minification
     *
     * @var array
     */
    private array $options;
    
    /**
     * Constructor
     *
     * @param array $options Minification options
     */
    public function __construct(array $options = []) {
        $this->options = wp_parse_args($options, [
            'gzip' => true,
            'preserve_comments' => false,
            'remove_important_comments' => false,
        ]);

        // Keep cache path aligned with the public rewrite (/prorank-cache/css)
        $upload_dir = wp_upload_dir();
        $this->cache_dir = trailingslashit($upload_dir['basedir']) . 'prorank-cache/css/';
        $this->cache_url = trailingslashit($upload_dir['baseurl']) . 'prorank-cache/css/';
        
        $this->ensure_cache_directory();
    }
    
    /**
     * Minify CSS content
     *
     * @param string $css     CSS content to minify
     * @param string $source  Source file path (for path rewriting)
     * @return string Minified CSS
     * @throws Exception If minification fails
     */
    public function minify(string $css, string $source = ''): string {
        try {
            // IMPORTANT: Add font-display:swap BEFORE minification
            // This ensures all @font-face rules have optimal font loading behavior
            $css = $this->addFontDisplaySwap($css);

            // Check if library is available - return original if not
            if ( ! $this->isLibraryAvailable() ) {
                return $css;
            }

            $minifier = new CSSMinify($css);

            // Set options
            if ($this->options['preserve_comments']) {
                // This will preserve important comments (/*! ... */)
                $minifier->setImportExtensions([]);
            }

            $minified = $minifier->minify();
            $is_original_empty = trim($css) === '';
            $is_minified_empty = trim($minified) === '';

            // If minifier stripped everything but the source had content, keep original to avoid blank styles
            if ($is_minified_empty && !$is_original_empty) {
                if (defined('WP_DEBUG') && WP_DEBUG) {
                    prorank_log('ProRank CSS Minification Warning: Minification resulted in empty content, using original.');
                }
                return $css;
            }

            return $minified;
        } catch (Exception $e) {
            if ( defined( 'WP_DEBUG' ) && WP_DEBUG && function_exists( 'prorank_log' ) ) {
                prorank_log('ProRank CSS Minification Error: ' . $e->getMessage());
            }
            throw $e;
        }
    }

    /**
     * Ensure the Matthias Mullie minifier classes are loaded.
     *
     * @throws Exception When the library cannot be loaded.
     */
    private function ensureMinifyLibrary(): void {
        if ( class_exists( CSSMinify::class ) ) {
            return;
        }

        $base = dirname( __DIR__, 3 ) . '/lib/MatthiasMullie/Minify/';
        $path_converter_base = dirname( __DIR__, 3 ) . '/lib/MatthiasMullie/PathConverter/';
        $files = [
            'Exception.php',
            'Minify.php',
            'CSS.php',
            'JS.php',
        ];
        $path_converter_files = [
            'ConverterInterface.php',
            'NoConverter.php',
            'Converter.php',
        ];

        foreach ( $path_converter_files as $file ) {
            $path = $path_converter_base . $file;
            if ( file_exists( $path ) ) {
                require_once $path;
            }
        }

        foreach ( $files as $file ) {
            $path = $base . $file;
            if ( file_exists( $path ) ) {
                require_once $path;
            }
        }

        // Don't throw - let caller handle missing library gracefully
    }

    /**
     * Check if minify library is available
     *
     * @return bool
     */
    public function isLibraryAvailable(): bool {
        $this->ensureMinifyLibrary();
        return class_exists( CSSMinify::class );
    }

    /**
     * Add font-display: swap to all @font-face rules
     * This is critical for preventing render-blocking font loading
     *
     * - Replaces font-display: block/auto with swap (these cause FOIT - Flash of Invisible Text)
     * - Adds font-display: swap to @font-face rules that don't have it
     *
     * @param string $css CSS content
     * @return string Modified CSS with font-display: swap
     */
    private function addFontDisplaySwap(string $css): string {
        // First, replace font-display: block or auto with swap
        // These values cause render-blocking behavior
        $css = preg_replace(
            '/font-display\s*:\s*(block|auto)\s*;?/i',
            'font-display:swap;',
            $css
        );

        // Then, add font-display: swap to @font-face rules that don't have it
        $css = preg_replace_callback(
            '/@font-face\s*\{([^}]+)\}/i',
            function($matches) {
                $rule = $matches[1];

                // If font-display already exists (swap, fallback, or optional), leave it
                if (stripos($rule, 'font-display') !== false) {
                    return $matches[0];
                }

                // Add font-display: swap at the beginning
                return '@font-face{font-display:swap;' . $rule . '}';
            },
            $css
        );

        return $css;
    }

    /**
     * Rewrite background URL declarations to include AVIF/WebP image-set fallbacks.
     *
     * Example output:
     * background:url(hero.jpg);background-image:image-set(url(hero.avif) type("image/avif"),url(hero.webp) type("image/webp"),url(hero.jpg) type("image/jpeg"));
     *
     * @param string $css CSS content
     * @param string $source_file Source CSS file path
     * @return string
     */
    private function rewriteBackgroundImageUrls(string $css, string $source_file): string {
        if ($css === '' || stripos($css, 'background') === false || stripos($css, 'url(') === false) {
            return $css;
        }

        $enabled = (bool) apply_filters('prorank_css_background_image_set_enabled', true);
        if (!$enabled) {
            return $css;
        }

        $pattern = '/(^|[;{])\s*(background(?:-image)?)\s*:\s*([^;{}]*url\((["\']?)([^)\'"]+)\4\)[^;{}]*)(;?)/im';

        $rewritten = preg_replace_callback(
            $pattern,
            function (array $matches) use ($source_file): string {
                $prefix = (string) ($matches[1] ?? '');
                $property = (string) ($matches[2] ?? 'background-image');
                $value = trim((string) ($matches[3] ?? ''));
                $original_url = trim((string) ($matches[5] ?? ''));

                if ($value === '' || $original_url === '') {
                    return (string) ($matches[0] ?? '');
                }

                if (stripos($value, 'image-set(') !== false) {
                    return (string) ($matches[0] ?? '');
                }

                if (preg_match_all('/url\(\s*([\'"]?)([^)\'"]+)\1\s*\)/i', $value, $url_matches) && count($url_matches[2] ?? []) !== 1) {
                    return (string) ($matches[0] ?? '');
                }

                if (
                    stripos($original_url, 'data:') === 0
                    || stripos($original_url, 'blob:') === 0
                    || stripos($original_url, 'javascript:') === 0
                    || strpos($original_url, '#') === 0
                ) {
                    return (string) ($matches[0] ?? '');
                }

                $image_set = $this->buildBackgroundImageSetValue($original_url, $source_file);
                if ($image_set === null) {
                    return (string) ($matches[0] ?? '');
                }

                $important = stripos($value, '!important') !== false ? ' !important' : '';
                $fallback_value = trim((string) preg_replace('/\s*!important\s*/i', '', $value));

                return $prefix
                    . $property . ':' . $fallback_value . $important . ';'
                    . 'background-image:image-set(' . $image_set . ')' . $important . ';';
            },
            $css
        );

        return is_string($rewritten) ? $rewritten : $css;
    }

    /**
     * Build image-set declaration for a background image URL.
     *
     * @param string $original_url Original image URL
     * @param string $source_file Source CSS path
     * @return string|null
     */
    private function buildBackgroundImageSetValue(string $original_url, string $source_file): ?string {
        $normalized_original_url = $this->normalizeCssAssetUrl($original_url, $source_file);
        if ($normalized_original_url === null) {
            return null;
        }

        $local_path = $this->resolveCssUrlToLocalPath($normalized_original_url, $source_file);
        if ($local_path === null || !file_exists($local_path)) {
            return null;
        }

        $ext = strtolower((string) pathinfo($local_path, PATHINFO_EXTENSION));
        if (!in_array($ext, ['jpg', 'jpeg', 'png'], true)) {
            return null;
        }

        $base = preg_replace('/\.(?:jpe?g|png)$/i', '', $local_path);
        if (!is_string($base) || $base === '') {
            return null;
        }

        $entries = [];
        $variants = [
            'avif' => $base . '.avif',
            'webp' => $base . '.webp',
        ];

        foreach ($variants as $variant_ext => $variant_path) {
            if (!file_exists($variant_path)) {
                continue;
            }

            $variant_url = $this->convertLocalPathToUrl($variant_path);
            if ($variant_url === null) {
                continue;
            }

            $entries[] = 'url("' . esc_url_raw($variant_url) . '") type("image/' . $variant_ext . '")';
        }

        if (empty($entries)) {
            return null;
        }

        $original_mime = $this->mimeTypeForPath($local_path);
        if ($original_mime === '') {
            return null;
        }

        $entries[] = 'url("' . esc_url_raw($normalized_original_url) . '") type("' . $original_mime . '")';

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

    /**
     * Normalize a CSS asset URL into an absolute site URL.
     *
     * @param string $url Asset URL from CSS
     * @param string $source_file Source CSS path
     * @return string|null
     */
    private function normalizeCssAssetUrl(string $url, string $source_file): ?string {
        $url = trim($url);
        if ($url === '') {
            return null;
        }

        // Strip query/fragment for filesystem checks; keep normalized URL clean.
        $url_without_query = preg_replace('/[#?].*$/', '', $url);
        if (!is_string($url_without_query) || $url_without_query === '') {
            return null;
        }

        if (strpos($url_without_query, '//') === 0) {
            return 'https:' . $url_without_query;
        }

        if (preg_match('#^https?://#i', $url_without_query) === 1) {
            return $url_without_query;
        }

        if (strpos($url_without_query, '/') === 0) {
            return home_url($url_without_query);
        }

        $source_dir = dirname($source_file);
        $candidate = wp_normalize_path($source_dir . '/' . $url_without_query);
        $candidate_url = $this->convertLocalPathToUrl($candidate);
        if ($candidate_url !== null) {
            return $candidate_url;
        }

        return null;
    }

    /**
     * Resolve a CSS URL to a local filesystem path.
     *
     * @param string $url Absolute or root-relative URL
     * @param string $source_file Source CSS path (for relative fallback)
     * @return string|null
     */
    private function resolveCssUrlToLocalPath(string $url, string $source_file): ?string {
        $url = trim($url);
        if ($url === '') {
            return null;
        }

        if (preg_match('#^https?://#i', $url) === 1) {
            $site_host = wp_parse_url(home_url('/'), PHP_URL_HOST);
            $asset_host = wp_parse_url($url, PHP_URL_HOST);
            if (!empty($site_host) && !empty($asset_host) && strtolower((string) $site_host) !== strtolower((string) $asset_host)) {
                return null;
            }
            $path = (string) wp_parse_url($url, PHP_URL_PATH);
        } elseif (strpos($url, '/') === 0) {
            $path = $url;
        } else {
            return wp_normalize_path(dirname($source_file) . '/' . $url);
        }

        if ($path === '') {
            return null;
        }

        $home_path = (string) wp_parse_url(home_url('/'), PHP_URL_PATH);
        if ($home_path !== '' && $home_path !== '/' && strpos($path, $home_path) === 0) {
            $path = substr($path, strlen($home_path));
            if ($path === false) {
                return null;
            }
            $path = '/' . ltrim($path, '/');
        }

        $absolute = wp_normalize_path(ABSPATH . ltrim($path, '/'));
        return $absolute;
    }

    /**
     * Convert a local path to a public URL.
     *
     * @param string $path Local filesystem path
     * @return string|null
     */
    private function convertLocalPathToUrl(string $path): ?string {
        $path = wp_normalize_path($path);
        if ($path === '') {
            return null;
        }

        $uploads = wp_upload_dir();
        $uploads_basedir = isset($uploads['basedir']) ? wp_normalize_path((string) $uploads['basedir']) : '';
        $uploads_baseurl = isset($uploads['baseurl']) ? (string) $uploads['baseurl'] : '';

        if ($uploads_basedir !== '' && strpos($path, $uploads_basedir) === 0 && $uploads_baseurl !== '') {
            $relative = ltrim(substr($path, strlen($uploads_basedir)), '/');
            return trailingslashit($uploads_baseurl) . str_replace('%2F', '/', rawurlencode($relative));
        }

        $content_dir = wp_normalize_path(WP_CONTENT_DIR);
        if ($content_dir !== '' && strpos($path, $content_dir) === 0) {
            $relative = ltrim(substr($path, strlen($content_dir)), '/');
            return content_url(str_replace('%2F', '/', rawurlencode($relative)));
        }

        $abs = wp_normalize_path(ABSPATH);
        if ($abs !== '' && strpos($path, $abs) === 0) {
            $relative = ltrim(substr($path, strlen($abs)), '/');
            return home_url('/' . str_replace('%2F', '/', rawurlencode($relative)));
        }

        return null;
    }

    /**
     * Resolve MIME type for image-set type() values.
     *
     * @param string $path Local file path
     * @return string
     */
    private function mimeTypeForPath(string $path): string {
        $ext = strtolower((string) pathinfo($path, PATHINFO_EXTENSION));
        switch ($ext) {
            case 'jpg':
            case 'jpeg':
                return 'image/jpeg';
            case 'png':
                return 'image/png';
            default:
                return '';
        }
    }
    
    /**
     * Minify CSS file and save to cache
     *
     * @param string $file_path Path to CSS file
     * @return array Array with 'path' and 'url' of minified file
     * @throws Exception If file cannot be read or minified
     */
    public function minifyFile(string $file_path): array {
        if (!file_exists($file_path) || !is_readable($file_path)) {
            $safe_path = sanitize_text_field( $file_path );
            throw new Exception(
                sprintf(
                    'Cannot read CSS file: %s',
                    esc_html( $safe_path )
                )
            );
        }
        
        $content = file_get_contents($file_path);
        if ($content === false) {
            $safe_path = sanitize_text_field( $file_path );
            throw new Exception(
                sprintf(
                    'Failed to read CSS file: %s',
                    esc_html( $safe_path )
                )
            );
        }
        
        // Generate cache filename
        $file_hash = md5($file_path . filemtime($file_path));
        $cache_filename = $file_hash . '.min.css';
        $cache_path = $this->cache_dir . $cache_filename;
        $cache_url = $this->cache_url . $cache_filename;
        
        // Check if already cached
        if (file_exists($cache_path)) {
            // Use CssCacheServer for pretty URLs if available
            if (class_exists(CssCacheServer::class)) {
                $cache_url = CssCacheServer::get_cache_url($cache_filename);
            }
            
            return [
                'path' => $cache_path,
                'url' => $cache_url,
                'size_before' => filesize($file_path),
                'size_after' => filesize($cache_path),
            ];
        }

        // Rewrite relative URLs for the new cache file location.
        try {
            $path_rewriter = new CssPathRewriterService();
            $content = $path_rewriter->rewrite($content, $file_path, $cache_path);
        } catch (\Throwable $e) {
            if (defined('WP_DEBUG') && WP_DEBUG && function_exists('prorank_log')) {
                prorank_log('ProRank CSS Minify Rewrite Warning: ' . $e->getMessage());
            }
        }

        // Add modern background image fallbacks where AVIF/WebP variants exist.
        $content = $this->rewriteBackgroundImageUrls($content, $file_path);
        
        // Minify content
        $minified = $this->minify($content, $file_path);
        
        // Save to cache
        $this->saveToCache($minified, $cache_path);
        
        // Create gzip version if enabled
        if ($this->options['gzip']) {
            $this->createGzipVersion($minified, $cache_path . '.gz');
        }
        
        // Use CssCacheServer for pretty URLs if available
        if (class_exists(CssCacheServer::class)) {
            $cache_url = CssCacheServer::get_cache_url($cache_filename);
        }
        
        $size_before = strlen($content);
        $size_after = strlen($minified);
        $compression_ratio = 0.0;
        if ($size_before > 0) {
            $compression_ratio = round((1 - ($size_after / $size_before)) * 100, 2);
        }

        return [
            'path' => $cache_path,
            'url' => $cache_url,
            'size_before' => $size_before,
            'size_after' => $size_after,
            'compression_ratio' => $compression_ratio,
        ];
    }
    
    /**
     * Combine multiple CSS files
     *
     * @param array $files Array of file paths
     * @return array Combined file info
     * @throws Exception If combination fails
     */
    public function combineFiles(array $files): array {
        $combined_content = '';
        $total_original_size = 0;
        $rewrite_target_path = $this->cache_dir . 'combined-placeholder.min.css';
        $path_rewriter = new CssPathRewriterService();
        
        foreach ($files as $file) {
            if (!file_exists($file) || !is_readable($file)) {
                continue;
            }
            
            $content = file_get_contents($file);
            if ($content === false) {
                continue;
            }

            // Keep asset URLs valid after moving CSS into uploads/prorank-cache/css.
            try {
                $content = $path_rewriter->rewrite($content, $file, $rewrite_target_path);
            } catch (\Throwable $e) {
                if (defined('WP_DEBUG') && WP_DEBUG && function_exists('prorank_log')) {
                    prorank_log('ProRank CSS Combine Rewrite Warning: ' . $e->getMessage());
                }
            }

            // Add modern background image fallbacks where AVIF/WebP variants exist.
            $content = $this->rewriteBackgroundImageUrls($content, $file);
            
            $total_original_size += strlen($content);
            
            // Add file marker for debugging
            $combined_content .= "\n/* ProRank CSS - Source: " . basename($file) . " */\n";
            $combined_content .= $content;
        }
        
        if (empty($combined_content)) {
            throw new Exception('No content to combine');
        }
        
        // Generate cache filename for combined file
        $files_hash = md5(implode('', $files) . $combined_content);
        $cache_filename = 'combined-' . $files_hash . '.min.css';
        $cache_path = $this->cache_dir . $cache_filename;
        $cache_url = $this->cache_url . $cache_filename;
        
        // Check if already cached
        if (file_exists($cache_path)) {
            return [
                'path' => $cache_path,
                'url' => $cache_url,
                'files_count' => count($files),
            ];
        }
        
        // Minify combined content
        $minified = $this->minify($combined_content);
        
        // Save to cache
        $this->saveToCache($minified, $cache_path);
        
        // Create gzip version if enabled
        if ($this->options['gzip']) {
            $this->createGzipVersion($minified, $cache_path . '.gz');
        }
        
        $size_after = strlen($minified);
        $compression_ratio = 0.0;
        if ($total_original_size > 0) {
            $compression_ratio = round((1 - ($size_after / $total_original_size)) * 100, 2);
        }

        return [
            'path' => $cache_path,
            'url' => $cache_url,
            'files_count' => count($files),
            'size_before' => $total_original_size,
            'size_after' => $size_after,
            'compression_ratio' => $compression_ratio,
        ];
    }
    
    /**
     * Clear CSS cache
     *
     * @param bool $force Force clear all files
     * @return int Number of files deleted
     */
    public function clearCache(bool $force = false): 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) {
            // Skip if not force and file is recent (less than 30 days old)
            if (!$force && (time() - filemtime($file) < 30 * DAY_IN_SECONDS)) {
                continue;
            }
            
            if (\ProRank\SEO\Core\FilesystemHelper::delete_file($file)) {
                $deleted++;
            }
        }
        
        return $deleted;
    }
    
    /**
     * Get cache statistics
     *
     * @return array Cache statistics
     */
    public function getCacheStats(): array {
        if (!is_dir($this->cache_dir)) {
            return [
                'files' => 0,
                'size' => 0,
                'size_formatted' => '0 B',
            ];
        }
        
        $files = glob($this->cache_dir . '*.css');
        $gzip_files = glob($this->cache_dir . '*.css.gz');
        $all_files = array_merge($files ?: [], $gzip_files ?: []);
        
        $total_size = 0;
        foreach ($all_files as $file) {
            $total_size += filesize($file);
        }
        
        return [
            'files' => count($all_files),
            'size' => $total_size,
            'size_formatted' => size_format($total_size),
            'directory' => $this->cache_dir,
        ];
    }
    
    /**
     * Save content to cache file
     *
     * @param string $content Content to save
     * @param string $path    File path
     * @throws Exception If save fails
     */
    private function saveToCache(string $content, string $path): void {
        $dir = dirname($path);
        if (!$this->ensureWritableDirectory($dir)) {
            $safe_dir = sanitize_text_field($dir);
            throw new Exception(
                sprintf(
                    'CSS cache directory is not writable: %s',
                    esc_html($safe_dir)
                )
            );
        }

        $result = @file_put_contents($path, $content, LOCK_EX);
        
        if ($result === false) {
            $safe_path = sanitize_text_field( $path );
            throw new Exception(
                sprintf(
                    'Failed to save minified CSS to cache: %s',
                    esc_html( $safe_path )
                )
            );
        }
    }
    
    /**
     * Create gzip version of file
     *
     * @param string $content Content to compress
     * @param string $path    Gzip file path
     */
    private function createGzipVersion(string $content, string $path): void {
        $compressed = gzencode($content, 9);
        
        if ($compressed !== false) {
            $dir = dirname($path);
            if (!$this->ensureWritableDirectory($dir)) {
                return;
            }
            @file_put_contents($path, $compressed, LOCK_EX);
        }
    }
    
    /**
     * Ensure cache directory exists
     */
    private function ensure_cache_directory(): void {
        $this->cache_available = $this->ensureWritableDirectory($this->cache_dir);

        if ($this->cache_available && !file_exists($this->cache_dir . 'index.php')) {
            // Add index.php for security
            $index_content = '<?php // Silence is golden';
            @file_put_contents($this->cache_dir . 'index.php', $index_content);
        }
    }

    /**
     * Ensure target directory exists and is writable.
     *
     * @param string $dir Directory path.
     * @return bool
     */
    private function ensureWritableDirectory(string $dir): bool {
        if ($dir === '') {
            return false;
        }

        if (!is_dir($dir)) {
            if (!wp_mkdir_p($dir)) {
                return false;
            }
        }

        if (!wp_is_writable($dir)) {
            return false;
        }

        return true;
    }
}
