<?php

/**
 * Font Optimization Module
 *
 * Downloads and hosts Google Fonts locally for better privacy and performance
 *
 * @package ProRank\SEO\Modules\Performance
 * @since   1.0.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Modules\Performance;

use ProRank\SEO\Core\Buffer;
use ProRank\SEO\Modules\BaseModule;

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


/**
 * FontOptimizationModule class
 */


class FontOptimizationModule extends BaseModule {

    /**
     * Required tier for this module
     */
    protected string $feature_tier = 'free';
    
    /**
     * Local fonts directory
     *
     * @var string
     */
    private string $fonts_dir;
    
    /**
     * Local fonts URL
     *
     * @var string
     */
    private string $fonts_url;
    
    /**
     * Google Fonts API base URL
     *
     * @var string
     */
    private const GOOGLE_FONTS_API = 'https://fonts.googleapis.com/css';
    
    /**
     * User agent for font downloads
     *
     * @var string
     */
    private const 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';
    
    /**
     * Constructor
     */
    public function __construct() {
        $this->slug = 'font_optimization';
        $this->name = 'Font Optimization';
        $this->description = 'Host Google Fonts locally and optimize font loading';
        // CRITICAL: Set to 'free' so font-display:swap runs for ALL users
        // The Pro features (local font hosting) are gated by should_run() in init_hooks()
$this->parent_slug = 'performance';

        // Set up fonts directory
        $upload_dir = wp_upload_dir();
        $this->fonts_dir = $upload_dir['basedir'] . '/prorank-fonts';
        $this->fonts_url = $upload_dir['baseurl'] . '/prorank-fonts';

        // Hook for cleanup when module is disabled
        add_action('prorank_module_deactivated_' . $this->slug, [$this, 'cleanup_on_disable']);
    }

    /**
     * Get setting from unified asset settings
     * Overrides BaseModule to read from prorank_asset_optimization_settings
     *
     * @param string $key Setting key
     * @param mixed $default Default value
     * @return mixed
     */
    protected function get_setting(string $key, $default = null) {
        static $settings = null;
        if ($settings === null) {
            $settings = get_option('prorank_asset_optimization_settings', []);
        }
        return $settings[$key] ?? $default;
    }

    /**
     * Initialize module hooks
     *
     * @return void
     */
    public function init_hooks(): void {
        if (!$this->is_enabled()) {
            return;
        }

        // Don't run in admin
        if (is_admin()) {
            return;
        }

        // font-display: swap ALWAYS runs - this is critical for performance
        // It works independently of other font optimization features and license tier
        // Add preconnects EARLY in wp_head (priority 1)
        add_action('wp_head', [$this, 'output_preconnect_hints'], 1);

        // Use output buffering to change font-display: block to swap
        add_action('template_redirect', [$this, 'start_full_page_buffer'], 1);

        // Add display=swap parameter to Google Fonts URLs
        add_filter('style_loader_src', [$this, 'add_display_swap_to_google_fonts'], 10, 2);

        // Check if full module should run (Pro tier + local hosting enabled)
        if (!$this->should_run()) {
            return;
        }

        // Filter HTML to replace Google Fonts URLs
        add_filter('wp_head', [$this, 'capture_google_fonts'], 1);
        add_filter('wp_head', [$this, 'output_local_fonts'], 999);

        // Filter style tags
        add_filter('style_loader_tag', [$this, 'replace_google_fonts_url'], 10, 4);

        // AJAX handler for downloading fonts
        add_action('wp_ajax_prorank_download_fonts', [$this, 'handle_font_download']);

        // Clean up on deactivation
        add_action('prorank_seo_module_deactivated', [$this, 'maybe_cleanup_fonts']);
    }

    /**
     * Output preconnect hints and font preloads very early in <head>
     * Must run at wp_head priority 1 to be effective
     *
     * @return void
     */
    public function output_preconnect_hints(): void {
        // If we host/subset Google Fonts locally, preconnecting to Google Font origins is wasted.
        if ((bool) $this->get_setting('host_google_fonts_locally', false) || (bool) $this->get_setting('font_subsetting_enabled', false)) {
            return;
        }

        // Preconnect hints for common font CDNs - output immediately in head
        $preconnect_domains = [
            'fonts.googleapis.com',
            'fonts.gstatic.com',
        ];

        foreach ($preconnect_domains as $domain) {
            printf(
                '<link rel="preconnect" href="https://%s" crossorigin>' . "\n",
                esc_attr($domain)
            );
        }

        // DNS prefetch as fallback for browsers that don't support preconnect
        foreach ($preconnect_domains as $domain) {
            printf(
                '<link rel="dns-prefetch" href="https://%s">' . "\n",
                esc_attr($domain)
            );
        }

        // NOTE: Font preloading removed - it competes for bandwidth with critical resources
        // and actually hurt performance (LCP went from 3.8s to 4.6s in testing).
        // The proper fix is font-display:swap in CSS, which is handled by CssMinifierService.
    }

    /**
     * Start full page output buffering to modify font-display values
     * This runs on template_redirect to capture entire page output
     *
     * @return void
     */
    public function start_full_page_buffer(): void {
        ob_start([$this, 'process_full_page_fonts']);
    }

    /**
     * Process the full page HTML to fix font-display issues
     * This handles both inline styles and references to external CSS
     *
     * @param string $html Full page HTML
     * @return string Modified HTML
     */
    public function process_full_page_fonts(string $html): string {
        if (empty($html)) {
            return $html;
        }

        $original_html = $html;

        // Some plugins output CSS both via wp_enqueue_style (which ProRank
        // converts to preload+swap) AND via direct wp_head output. Remove the
        // duplicate blocking <link rel="stylesheet"> when an async preload for
        // the same URL already exists in the page.
        $html = $this->strip_duplicate_blocking_stylesheets($html);

        // When another local-font plugin already hosts Google Fonts (for
        // example OMGF), remove duplicate remote Google Fonts imports/links
        // that builders inject into inline CSS blocks.
        $html = $this->strip_duplicate_google_font_requests($html);

        // When local Google-font replacements are present, preload the first
        // likely-above-the-fold local font files to reduce late font swaps.
        $html = $this->inject_local_font_preloads($html);

        // 1. Replace font-display: block/auto/fallback with font-display: swap in all inline styles
        // This catches Font Awesome, WooCommerce Inter font, and other fonts that use non-swap values
        // block = invisible text until font loads (bad for FCP)
        // auto = browser decides (often behaves like block)
        // fallback = brief swap period, then invisible (still causes render-blocking)
        $html = preg_replace(
            '/font-display\s*:\s*(block|auto|fallback)/i',
            'font-display:swap',
            $html
        );

        // 2. Find all @font-face rules and add font-display: swap if not present
        $html = preg_replace_callback(
            '/@font-face\s*\{([^}]*)\}/is',
            function($matches) {
                $font_face_content = $matches[1];

                // Skip if font-display is already set (we already converted block to swap above)
                if (stripos($font_face_content, 'font-display') !== false) {
                    return $matches[0];
                }

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

        // 3. Inject CSS override for external Font Awesome and icon fonts
        // This forces font-display:swap on fonts loaded from external CSS files
        if (strpos($html, '</head>') !== false) {
            $font_override_css = $this->get_font_display_override_css();
            $html = str_replace('</head>', $font_override_css . '</head>', $html);
        }

        return Buffer::guard_structural_containers($original_html, $html, 'font_optimization');
    }

    /**
     * Remove duplicate blocking stylesheets when an async preload for the same
     * URL already exists in the page.
     *
     * Some plugins (e.g. Cookie Law Info) output their CSS both via
     * wp_enqueue_style AND via a direct wp_head action. ProRank's CSS async
     * fallback converts the enqueued version to preload+swap, but the
     * hardcoded duplicate stays as a render-blocking stylesheet.
     *
     * @param string $html
     * @return string
     */
    private function strip_duplicate_blocking_stylesheets(string $html): string {
        // Collect all URLs that appear as async preloads.
        preg_match_all(
            '~<link\b[^>]+rel=["\']preload["\'][^>]+as=["\']style["\'][^>]+href=["\']([^"\']+)["\'][^>]*>~i',
            $html,
            $preload_matches
        );

        if (empty($preload_matches[1])) {
            return $html;
        }

        $preloaded_urls = array_map('strtolower', $preload_matches[1]);

        // For each preloaded URL, remove blocking <link rel="stylesheet"> tags
        // that reference the same URL (keeping the async preload version).
        foreach ($preloaded_urls as $url) {
            $escaped = preg_quote($url, '~');
            $html = (string) preg_replace(
                '~<link\b[^>]+rel=["\']stylesheet["\'][^>]+href=["\']' . $escaped . '["\'][^>]*>\s*~i',
                '',
                $html
            );
        }

        return $html;
    }

    /**
     * Strip duplicate remote Google Fonts requests when a local replacement is
     * already present in the document.
     *
     * @param string $html
     * @return string
     */
    private function strip_duplicate_google_font_requests(string $html): string {
        if (!$this->has_local_google_font_stylesheet($html)) {
            return $html;
        }

        $patterns = [
            // Direct Google Fonts link tags.
            '~<link\b[^>]+href=(["\'])(?:https?:)?//fonts\.googleapis\.com/[^"\']+\1[^>]*>\s*~i',
            // Preconnect / DNS-prefetch hints that are wasted once fonts are local.
            '~<link\b[^>]+href=(["\'])(?:https?:)?//fonts\.(?:googleapis|gstatic)\.com[^"\']*\1[^>]*>\s*~i',
            // Inline @import Google Fonts rules from builders/symbol CSS.
            '~@import\s+url\(\s*([\'"])?(?:https?:)?//fonts\.googleapis\.com/[^)]+\)\s*;?\s*~i',
        ];

        foreach ($patterns as $pattern) {
            $html = (string) preg_replace($pattern, '', $html);
        }

        return $html;
    }

    /**
     * Detect whether the HTML already contains a locally hosted Google Fonts stylesheet.
     *
     * @param string $html
     * @return bool
     */
    private function has_local_google_font_stylesheet(string $html): bool {
        $markers = [
            '/wp-content/uploads/omgf/',
            'omgf-stylesheet',
            'host-webfonts-local',
            '/wp-content/uploads/host-webfonts-local/',
            '/wp-content/uploads/prorank-fonts/',
            '/prorank-fonts/google-fonts-',
        ];

        $haystack = strtolower($html);
        foreach ($markers as $marker) {
            if (strpos($haystack, strtolower($marker)) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Inject preload links for the first local font files referenced by a
     * local-hosted Google Fonts stylesheet.
     *
     * @param string $html
     * @return string
     */
    private function inject_local_font_preloads(string $html): string {
        if (stripos($html, '</head>') === false) {
            return $html;
        }

        $font_urls = $this->collect_local_font_preload_urls($html);
        if (empty($font_urls)) {
            return $html;
        }

        $preloads = '';
        foreach ($font_urls as $font_url) {
            $preloads .= sprintf(
                '<link rel="preload" as="font" href="%s" type="font/woff2" crossorigin>' . "\n",
                esc_url($font_url)
            );
        }

        return str_replace('</head>', $preloads . '</head>', $html);
    }

    /**
     * Extract preload-worthy local WOFF2 font URLs from local font stylesheets.
     *
     * @param string $html
     * @return array<int,string>
     */
    private function collect_local_font_preload_urls(string $html): array {
        $matches = [];
        preg_match_all(
            '~<link\b[^>]+href=(["\'])([^"\']+(?:omgf-stylesheet|host-webfonts-local|prorank-fonts)[^"\']*\.css[^"\']*)\1[^>]*>~i',
            $html,
            $matches
        );

        if (empty($matches[2]) || !is_array($matches[2])) {
            return [];
        }

        $font_urls = [];
        foreach ($matches[2] as $css_url) {
            foreach ($this->extract_local_font_urls_from_stylesheet((string) $css_url) as $font_url) {
                $key = strtolower((string) preg_replace('~\?.*$~', '', $font_url));
                if (!isset($font_urls[$key])) {
                    $font_urls[$key] = $font_url;
                }
                if (count($font_urls) >= 4) {
                    break 2;
                }
            }
        }

        return array_values($font_urls);
    }

    /**
     * Read a local font stylesheet from uploads and extract preload candidates.
     *
     * @param string $css_url
     * @return array<int,string>
     */
    private function extract_local_font_urls_from_stylesheet(string $css_url): array {
        $css_path = $this->map_upload_url_to_path($css_url);
        if ($css_path === '' || !is_readable($css_path)) {
            return [];
        }

        $css = @file_get_contents($css_path);
        if (!is_string($css) || $css === '') {
            return [];
        }

        preg_match_all('~url\((["\']?)([^)"\']+\.woff2[^)"\']*)\1\)~i', $css, $matches);
        if (empty($matches[2]) || !is_array($matches[2])) {
            return [];
        }

        $upload_dir = wp_upload_dir();
        $baseurl = (string) ($upload_dir['baseurl'] ?? '');
        $urls = [];

        foreach ($matches[2] as $raw_url) {
            $font_url = trim((string) $raw_url, " \t\n\r\0\x0B'\"");
            if ($font_url === '') {
                continue;
            }

            if (strpos(strtolower($font_url), 'latin-ext') !== false) {
                continue;
            }

            if (strpos($font_url, '//') === 0) {
                $scheme = is_ssl() ? 'https:' : 'http:';
                $font_url = $scheme . $font_url;
            } elseif (strpos($font_url, '/') === 0 && $baseurl !== '') {
                $parts = wp_parse_url($baseurl);
                $host = isset($parts['host']) ? ($parts['scheme'] ?? 'https') . '://' . $parts['host'] : '';
                if (!empty($parts['port'])) {
                    $host .= ':' . $parts['port'];
                }
                if ($host !== '') {
                    $font_url = $host . $font_url;
                }
            }

            $urls[] = $font_url;
        }

        return $urls;
    }

    /**
     * Map an uploads URL back to the local filesystem path.
     *
     * @param string $url
     * @return string
     */
    private function map_upload_url_to_path(string $url): string {
        $upload_dir = wp_upload_dir();
        $baseurl = (string) ($upload_dir['baseurl'] ?? '');
        $basedir = (string) ($upload_dir['basedir'] ?? '');
        if ($baseurl === '' || $basedir === '' || $url === '') {
            return '';
        }

        $url_path = (string) wp_parse_url($url, PHP_URL_PATH);
        $base_path = (string) wp_parse_url($baseurl, PHP_URL_PATH);
        if ($url_path === '' || $base_path === '' || strpos($url_path, $base_path) !== 0) {
            return '';
        }

        $relative = ltrim(substr($url_path, strlen($base_path)), '/');
        if ($relative === '') {
            return '';
        }

        return trailingslashit($basedir) . $relative;
    }

    /**
     * Get CSS override to force font-display:optional on common icon fonts
     * Uses font-display:optional to prevent CLS entirely - if font doesn't load quickly,
     * it uses the fallback font for the entire page session (no swap = no layout shift)
     *
     * For text fonts, we use font-display:swap to ensure text is always visible
     *
     * @return string
     */
    private function get_font_display_override_css(): string {
        // Use CSS property override with high specificity
        // The unicode-range:U+0-10FFFF covers all characters, making this the "winning" rule
        // Icon fonts use 'optional' to prevent CLS (icons will use fallback if font is slow)
        // Text fonts use 'swap' to ensure text is always visible
        return '<style id="prorank-font-display-override">
/* Font-display optional for icon fonts prevents CLS - icons use fallback if font loads slowly */
@font-face{font-family:"Font Awesome 5 Free";font-display:optional;src:local("Font Awesome 5 Free");unicode-range:U+0-10FFFF}
@font-face{font-family:"Font Awesome 5 Brands";font-display:optional;src:local("Font Awesome 5 Brands");unicode-range:U+0-10FFFF}
@font-face{font-family:"Font Awesome 5 Pro";font-display:optional;src:local("Font Awesome 5 Pro");unicode-range:U+0-10FFFF}
@font-face{font-family:"Font Awesome 6 Free";font-display:optional;src:local("Font Awesome 6 Free");unicode-range:U+0-10FFFF}
@font-face{font-family:"Font Awesome 6 Brands";font-display:optional;src:local("Font Awesome 6 Brands");unicode-range:U+0-10FFFF}
@font-face{font-family:"Font Awesome 6 Pro";font-display:optional;src:local("Font Awesome 6 Pro");unicode-range:U+0-10FFFF}
@font-face{font-family:FontAwesome;font-display:optional;src:local(FontAwesome);unicode-range:U+0-10FFFF}
@font-face{font-family:eicons;font-display:optional;src:local(eicons);unicode-range:U+0-10FFFF}
@font-face{font-family:"Elementor Icons";font-display:optional;src:local("Elementor Icons");unicode-range:U+0-10FFFF}
@font-face{font-family:ETmodules;font-display:optional;src:local(ETmodules);unicode-range:U+0-10FFFF}
@font-face{font-family:"Material Icons";font-display:optional;src:local("Material Icons");unicode-range:U+0-10FFFF}
</style>';
    }

    /**
     * Start output buffering for font-display injection (legacy method)
     *
     * @return void
     */
    public function start_font_display_buffer(): void {
        ob_start([$this, 'inject_font_display_swap']);
    }

    /**
     * End output buffering for font-display injection (legacy method)
     *
     * @return void
     */
    public function end_font_display_buffer(): void {
        if (ob_get_level() > 0) {
            ob_end_flush();
        }
    }

    /**
     * Inject font-display: swap into all @font-face rules (legacy method)
     * Kept for backwards compatibility
     *
     * @param string $html HTML content
     * @return string Modified HTML
     */
    public function inject_font_display_swap(string $html): string {
        return $this->process_full_page_fonts($html);
    }

    /**
     * Add display=swap to Google Fonts URLs via style_loader_src filter
     * This is the most reliable way to add font-display: swap to Google Fonts
     *
     * @param string $src The source URL of the enqueued style
     * @param string $handle The style's registered handle
     * @return string Modified URL
     */
    public function add_display_swap_to_google_fonts(string $src, string $handle): string {
        if (strpos($src, 'fonts.googleapis.com') === false) {
            return $src;
        }

        if (!$this->get_setting('font_display_swap', false)) {
            return $src;
        }

        // Force display=swap even when a theme sets display=fallback.
        $src = remove_query_arg('display', $src);
        $src = add_query_arg('display', 'swap', $src);

        return $src;
    }
    
    /**
     * Check if module should run
     *
     * @return bool
     */
    private function should_run(): bool {
        // Check if module is enabled
        if (!$this->is_enabled()) {
            return false;
        }
        
        // Check if local fonts hosting is enabled
        return (bool) $this->get_setting('host_google_fonts_locally', false);
    }
    
    /**
     * Capture Google Fonts URLs
     *
     * @return void
     */
    public function capture_google_fonts(): void {
        ob_start();
    }
    
    /**
     * Output local fonts and process captured content
     *
     * @return void
     */
    public function output_local_fonts(): void {
        $content = ob_get_clean();
        
        // Find all Google Fonts links
        preg_match_all('/<link[^>]+href=[\'"]([^\'"]+fonts\.googleapis\.com[^\'"]+)[\'"][^>]*>/i', $content, $matches);
        
        if (!empty($matches[1])) {
            foreach ($matches[1] as $google_url) {
                $local_css = $this->get_or_download_font($google_url);
                if ($local_css) {
                    // Replace the Google Fonts URL with local version
                    $content = str_replace($google_url, $local_css, $content);
                }
            }
        }
        
        // Find @import rules for Google Fonts
        preg_match_all('/@import\s+url\([\'"]?([^\'"]+fonts\.googleapis\.com[^\'"]+)[\'"]?\)/i', $content, $import_matches);
        
        if (!empty($import_matches[1])) {
            foreach ($import_matches[1] as $google_url) {
                $local_css = $this->get_or_download_font($google_url);
                if ($local_css) {
                    $content = str_replace($google_url, $local_css, $content);
                }
            }
        }

        // Global optimization: inline tiny local Google-font CSS files generated
        // by this module to remove remaining render-blocking font stylesheet RTT.
        $content = $this->inline_small_local_font_styles($content);
        
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- full HTML output buffer
        echo $content;
    }

    /**
     * Inline small local Google-font stylesheet links from prorank-fonts.
     *
     * @param string $content
     * @return string
     */
    private function inline_small_local_font_styles(string $content): string {
        if ($content === '') {
            return $content;
        }

        $max_bytes = (int) apply_filters('prorank_font_inline_max_bytes', 8192);
        if ($max_bytes < 512) {
            $max_bytes = 512;
        }

        $pattern = '/<link\\b([^>]*\\bhref=(["\'])([^"\']*\\/wp-content\\/uploads\\/prorank-fonts\\/google-fonts-[^"\']+\\.css(?:\\?[^"\']*)?)\\2[^>]*)>/i';

        $result = preg_replace_callback(
            $pattern,
            function (array $matches) use ($max_bytes): string {
                $attrs = (string) ($matches[1] ?? '');
                $href = (string) ($matches[3] ?? '');
                if ($href === '') {
                    return (string) ($matches[0] ?? '');
                }

                $path = preg_replace('#^https?://[^/]+#i', '', $href);
                if (!is_string($path) || $path === '') {
                    return (string) ($matches[0] ?? '');
                }
                $path = strtok($path, '?#');
                if (!is_string($path) || $path === '') {
                    return (string) ($matches[0] ?? '');
                }

                $candidate = wp_normalize_path(trailingslashit(ABSPATH) . ltrim(rawurldecode($path), '/'));
                if (!file_exists($candidate) || !is_readable($candidate)) {
                    return (string) ($matches[0] ?? '');
                }

                // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_filesize
                $size = @filesize($candidate);
                if (!is_int($size) || $size <= 0 || $size > $max_bytes) {
                    return (string) ($matches[0] ?? '');
                }

                $css = trim((string) file_get_contents($candidate));
                if ($css === '') {
                    return '';
                }

                $css = str_ireplace('</style', '<\\/style', $css);

                $id = 'prorank-local-google-fonts';
                if (preg_match('/\\bid=(["\'])([^"\']+)\\1/i', $attrs, $id_match) && !empty($id_match[2])) {
                    $id = (string) $id_match[2];
                }

                $media_attr = '';
                if (preg_match('/\\bmedia=(["\'])([^"\']+)\\1/i', $attrs, $media_match) && !empty($media_match[2])) {
                    $media = strtolower((string) $media_match[2]);
                    if ($media !== 'all') {
                        $media_attr = ' media="' . esc_attr($media_match[2]) . '"';
                    }
                }

                return sprintf(
                    '<style id="%s-inline-css" data-prorank-inline-font="true"%s>%s</style>',
                    esc_attr($id),
                    $media_attr,
                    $css // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted local CSS content
                );
            },
            $content
        );

        return is_string($result) ? $result : $content;
    }
    
    /**
     * Replace Google Fonts URLs in style tags
     *
     * @param string $tag    The style tag HTML
     * @param string $handle The style handle
     * @param string $href   The stylesheet URL
     * @param string $media  The media attribute
     * @return string Modified style tag
     */
    public function replace_google_fonts_url(string $tag, string $handle, string $href, string $media): string {
        // Only process if the Google URL is still in the tag (FontSubsettingModule may
        // have already replaced it at a lower priority).
        if (strpos($href, 'fonts.googleapis.com') !== false
            && strpos($tag, 'fonts.googleapis.com') !== false
        ) {
            $local_css = $this->get_or_download_font($href);
            if ($local_css) {
                // Theme output can encode query separators (e.g. &#038;), so exact
                // str_replace($href, ...) is brittle. Replace the full href value by
                // pattern whenever it points to fonts.googleapis.com.
                $tag = (string) preg_replace(
                    '/href=(["\'])[^"\']*fonts\\.googleapis\\.com[^"\']*\\1/i',
                    'href=$1' . esc_url($local_css) . '$1',
                    $tag,
                    1
                );
            }
        }
        // font-display:swap is handled by the full-page buffer (process_full_page_fonts)
        // so we don't need to inject data-font-display attributes here.

        return $tag;
    }
    
    /**
     * Get or download font CSS
     *
     * @param string $google_url Google Fonts URL
     * @return string|false Local CSS URL or false on failure
     */
    private function get_or_download_font(string $google_url) {
        $google_url = $this->normalize_google_fonts_url($google_url);

        // Generate unique filename based on URL
        $url_hash = md5($google_url);
        $css_filename = 'google-fonts-' . $url_hash . '.css';
        $css_path = $this->fonts_dir . '/' . $css_filename;
        $css_url = $this->fonts_url . '/' . $css_filename;
        
        // Check if already downloaded
        if (file_exists($css_path)
            && filemtime($css_path) > (time() - 30 * DAY_IN_SECONDS)
            && $this->is_cached_font_css_healthy($css_path)
        ) {
            return $css_url;
        }
        
        // Download the CSS
        $css_content = $this->download_font_css($google_url);
        if (!$css_content) {
            return false;
        }
        
        // Process and save CSS
        $processed_css = $this->process_font_css($css_content, $url_hash);
        if ($this->save_font_file($css_filename, $processed_css)) {
            return $css_url;
        }
        
        return false;
    }
    
    /**
     * Download font CSS from Google
     *
     * @param string $url Google Fonts URL
     * @return string|false CSS content or false on failure
     */
    private function download_font_css(string $url) {
        $args = [
            'timeout' => 10,
            'user-agent' => self::USER_AGENT,
            'sslverify' => true,
            'headers' => [
                'Accept' => 'text/css,*/*;q=0.1',
                'Accept-Encoding' => 'gzip, deflate',
            ],
        ];
        
        $response = wp_remote_get($url, $args);
        
        if (is_wp_error($response)) {
            prorank_log('ProRank SEO: Failed to download Google Fonts CSS: ' . $response->get_error_message());
            return false;
        }
        
        $body = wp_remote_retrieve_body($response);
        if (empty($body)) {
            return false;
        }
        
        return $body;
    }
    
    /**
     * Process font CSS and download font files
     *
     * @param string $css      CSS content
     * @param string $url_hash URL hash for subdirectory
     * @return string Processed CSS
     */
    private function process_font_css(string $css, string $url_hash): string {
        try {
            // Prefer woff2: if Google returns multiple formats, keep only woff2 rules.
            $css = $this->keep_only_woff2_font_faces($css);

            // Find all font URLs in the CSS
            preg_match_all('/url\(([^)]+)\)/i', $css, $matches);

            if (empty($matches[1])) {
                return $css;
            }

            $font_dir = $this->fonts_dir . '/' . $url_hash;
            $font_url = $this->fonts_url . '/' . $url_hash;

            // Create font directory
            if (!is_dir($font_dir)) {
                wp_mkdir_p($font_dir);
            }

            foreach ($matches[1] as $font_url_match) {
                // Clean up the URL
                $font_url_clean = trim($font_url_match, '\'"');

                // Skip data URLs
                if (strpos($font_url_clean, 'data:') === 0) {
                    continue;
                }

                // Download the font file
                $font_filename = basename(wp_parse_url($font_url_clean, PHP_URL_PATH));
                if (empty($font_filename)) {
                    $font_filename = md5($font_url_clean) . '.woff2';
                }

                $local_font_path = $font_dir . '/' . $font_filename;
                $local_font_url = $font_url . '/' . $font_filename;

                // Download if not exists
                if (!file_exists($local_font_path)) {
                    $font_data = $this->download_font_file($font_url_clean);
                    if ($font_data) {
                        file_put_contents($local_font_path, $font_data);
                    }
                }

                // Replace URL in CSS
                if (file_exists($local_font_path)) {
                    $css = str_replace($font_url_match, '"' . $local_font_url . '"', $css);
                }
            }

            if ($this->get_setting('font_display_swap', false)) {
                // Replace non-optimal values and add font-display: swap when missing.
                $css = preg_replace('/font-display\\s*:\\s*(block|auto|fallback)\\b/i', 'font-display: swap', $css);
                $css = preg_replace_callback('/@font-face\\s*\\{([^}]*)\\}/is', function($matches) {
                    $rule = $matches[1];
                    if (stripos($rule, 'font-display') !== false) {
                        return $matches[0];
                    }
                    return '@font-face{font-display: swap;' . $rule . '}';
                }, $css);
            }

            return $css;
        } catch (\Throwable $e) {
            prorank_log('ProRank SEO: Failed to process font CSS - ' . $e->getMessage());
            return $css;
        }
    }
    
    /**
     * Download font file
     *
     * @param string $url Font file URL
     * @return string|false Font data or false on failure
     */
    private function download_font_file(string $url) {
        $args = [
            'timeout' => 30,
            'user-agent' => self::USER_AGENT,
            'sslverify' => true,
            'headers' => [
                'Accept' => 'font/woff2,*/*;q=0.1',
                'Accept-Encoding' => 'gzip, deflate',
            ],
        ];
        
        $response = wp_remote_get($url, $args);
        
        if (is_wp_error($response)) {
            prorank_log('ProRank SEO: Failed to download font file: ' . $response->get_error_message());
            return false;
        }
        
        return wp_remote_retrieve_body($response);
    }

    private function normalize_google_fonts_url(string $url): string {
        $url = html_entity_decode(trim($url), ENT_QUOTES | ENT_HTML5, 'UTF-8');

        // WordPress/theme output often uses protocol-relative URLs for Google Fonts.
        // wp_remote_get() expects an absolute URL, so normalize to HTTPS first.
        if (strpos($url, '//') === 0) {
            $url = 'https:' . $url;
        } elseif (strpos($url, 'http://') === 0) {
            $url = 'https://' . substr($url, 7);
        }

        if (strpos($url, 'fonts.googleapis.com') === false) {
            return $url;
        }

        if (!$this->get_setting('font_display_swap', false)) {
            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 is_cached_font_css_healthy(string $css_path): bool {
        $content = @file_get_contents($css_path);
        if (!is_string($content) || $content === '') {
            return false;
        }

        if (stripos($content, '@font-face') === false) {
            return false;
        }

        $has_woff2 = stripos($content, '.woff2') !== false
            || (bool) preg_match('/format\\(\\s*[\"\\\']woff2[\"\\\']\\s*\\)/i', $content);

        if (!$has_woff2) {
            return false;
        }

        if (stripos($content, '.ttf') !== false || stripos($content, 'truetype') !== false) {
            return false;
        }

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

        return true;
    }

    private function keep_only_woff2_font_faces(string $css): string {
        $has_woff2 = stripos($css, '.woff2') !== false
            || (bool) preg_match('/format\\(\\s*[\"\\\']woff2[\"\\\']\\s*\\)/i', $css);

        if (!$has_woff2) {
            return $css;
        }

        $filtered = preg_replace_callback('/@font-face\\s*\\{[^}]*\\}/is', function($matches) {
            $match = $matches[0];
            $is_woff2 = stripos($match, '.woff2') !== false
                || (bool) preg_match('/format\\(\\s*[\"\\\']woff2[\"\\\']\\s*\\)/i', $match);

            return $is_woff2 ? $match : '';
        }, $css);

        if (!is_string($filtered) || stripos($filtered, '@font-face') === false) {
            return $css;
        }

        return $filtered;
    }
    
    /**
     * Save font file
     *
     * @param string $filename Filename
     * @param string $content  File content
     * @return bool Success status
     */
    private function save_font_file(string $filename, string $content): bool {
        try {
            // Create fonts directory if needed
            if (!is_dir($this->fonts_dir)) {
                wp_mkdir_p($this->fonts_dir);

                // Add index.php for security
                file_put_contents($this->fonts_dir . '/index.php', '<?php // Silence is golden');
            }

            $filepath = $this->fonts_dir . '/' . $filename;
            return (bool) file_put_contents($filepath, $content);
        } catch (\Throwable $e) {
            prorank_log('ProRank SEO: Failed to save font file - ' . $e->getMessage());
            return false;
        }
    }
    
    /**
     * Add font preconnect hints
     *
     * @param array  $hints Array of resource hints
     * @param string $relation_type Type of hint
     * @return array Modified hints
     */
    public function add_font_preconnect(array $hints, string $relation_type): array {
        if ('preconnect' === $relation_type) {
            // Add local fonts URL
            $hints[] = $this->fonts_url;
        }
        
        return $hints;
    }
    
    /**
     * Handle AJAX font download request
     *
     * @return void
     */
    public function handle_font_download(): void {
        // Check nonce
        if (!check_ajax_referer('prorank_font_optimization', 'nonce', false)) {
            wp_send_json_error(__('Invalid nonce', 'prorank-seo'));
        }
        
        // Check capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(__('Insufficient permissions', 'prorank-seo'));
        }
        
        // Get all Google Fonts URLs from the site
        $fonts = $this->scan_for_google_fonts();
        
        if (empty($fonts)) {
            wp_send_json_success([
                'message' => __('No Google Fonts found on your site', 'prorank-seo'),
                'count' => 0,
            ]);
        }
        
        $downloaded = 0;
        foreach ($fonts as $font_url) {
            if ($this->get_or_download_font($font_url)) {
                $downloaded++;
            }
        }
        
        wp_send_json_success([
            'message' => sprintf(
                /* translators: %d: number of fonts downloaded */
                __('Downloaded %1$d Google Fonts', 'prorank-seo'),
                $downloaded
            ),
            'count' => $downloaded,
        ]);
    }
    
    /**
     * Scan site for Google Fonts
     *
     * @return array Array of Google Fonts URLs
     */
    private function scan_for_google_fonts(): array {
        $fonts = [];
        
        // Check theme mods
        $theme_mods = get_theme_mods();
        if (is_array($theme_mods)) {
            array_walk_recursive($theme_mods, function($value) use (&$fonts) {
                if (is_string($value) && strpos($value, 'fonts.googleapis.com') !== false) {
                    $fonts[] = $value;
                }
            });
        }
        
        // Check registered styles
        global $wp_styles;
        if (!empty($wp_styles->registered)) {
            foreach ($wp_styles->registered as $handle => $style) {
                if (!empty($style->src) && is_string($style->src) && strpos($style->src, 'fonts.googleapis.com') !== false) {
                    $fonts[] = $style->src;
                }
            }
        }
        
        return array_unique($fonts);
    }
    
    /**
     * Maybe cleanup fonts on module deactivation
     *
     * @param string $module_slug Module slug
     * @return void
     */
    public function maybe_cleanup_fonts(string $module_slug): void {
        if ($module_slug !== $this->slug) {
            return;
        }
        
        // Only cleanup if setting is enabled
        if (!$this->get_setting('cleanup_on_deactivate', false)) {
            return;
        }
        
        // Remove fonts directory
        if (is_dir($this->fonts_dir)) {
            $this->remove_directory($this->fonts_dir);
        }
    }
    
    /**
     * Recursively remove directory
     *
     * @param string $dir Directory path
     * @return bool Success status
     */
    private function remove_directory(string $dir): bool {
        if (!is_dir($dir)) {
            return false;
        }
        
        $files = array_diff(scandir($dir), ['.', '..']);
        foreach ($files as $file) {
            $path = $dir . '/' . $file;
            if (is_dir($path)) {
                $this->remove_directory($path);
            } else {
                \ProRank\SEO\Core\FilesystemHelper::delete_file($path);
            }
        }
        
        return \ProRank\SEO\Core\FilesystemHelper::delete_dir($dir);
    }
    
    /**
     * Get font statistics
     *
     * @return array Statistics
     */
    public function get_stats(): array {
        $stats = [
            'total_fonts' => 0,
            'total_size' => 0,
            'google_fonts_found' => count($this->scan_for_google_fonts()),
        ];
        
        if (!is_dir($this->fonts_dir)) {
            return $stats;
        }
        
        // Count files and calculate size
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($this->fonts_dir)
        );
        
        foreach ($iterator as $file) {
            if ($file->isFile() && $file->getFilename() !== 'index.php') {
                $stats['total_fonts']++;
                $stats['total_size'] += $file->getSize();
            }
        }
        
        return $stats;
    }
}
