<?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;

    /**
     * Output-buffer level used when capturing wp_head font markup.
     *
     * @var int|null
     */
    private ?int $google_fonts_capture_level = null;
    
    /**
     * 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;
        }

        if (\ProRank\SEO\Plugin::is_elementor_runtime_request()) {
            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, 'safe_output_preconnect_hints'], 1);

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

        // Add display=swap parameter to Google Fonts URLs
        add_filter('style_loader_src', [$this, 'safe_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, 'safe_capture_google_fonts'], 1);
        add_filter('wp_head', [$this, 'safe_output_local_fonts'], 999);

        // Filter style tags
        add_filter('style_loader_tag', [$this, 'safe_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']);
    }

    /**
     * Fail-open wrapper for font preconnect output.
     *
     * @return void
     */
    public function safe_output_preconnect_hints(): void {
        try {
            $this->output_preconnect_hints();
        } catch (\Throwable $e) {
            $this->log_frontend_callback_error('output_preconnect_hints', $e);
        }
    }

    /**
     * Start full-page font processing with a fail-open callback.
     *
     * @return void
     */
    public function safe_start_full_page_buffer(): void {
        try {
            ob_start([$this, 'safe_process_full_page_fonts']);
        } catch (\Throwable $e) {
            $this->log_frontend_callback_error('start_full_page_buffer', $e);
        }
    }

    /**
     * Fail-open output-buffer callback for font-display rewrites.
     *
     * @param mixed $html
     * @return string
     */
    public function safe_process_full_page_fonts($html): string {
        $fallback = is_string($html) ? $html : '';

        try {
            return $this->process_full_page_fonts($fallback);
        } catch (\Throwable $e) {
            $this->log_frontend_callback_error('process_full_page_fonts', $e);
            return $fallback;
        }
    }

    /**
     * Fail-open wrapper for Google Fonts URL rewriting.
     *
     * @param mixed $src
     * @param mixed $handle
     * @return string
     */
    public function safe_add_display_swap_to_google_fonts($src, $handle): string {
        $fallback = is_string($src) ? $src : '';

        try {
            return $this->add_display_swap_to_google_fonts(
                $fallback,
                is_string($handle) ? $handle : (string) $handle
            );
        } catch (\Throwable $e) {
            $this->log_frontend_callback_error('add_display_swap_to_google_fonts', $e);
            return $fallback;
        }
    }

    /**
     * Fail-open wrapper for starting the Google Fonts wp_head capture buffer.
     *
     * @return void
     */
    public function safe_capture_google_fonts(): void {
        try {
            ob_start();
            $this->google_fonts_capture_level = ob_get_level();
        } catch (\Throwable $e) {
            $this->google_fonts_capture_level = null;
            $this->log_frontend_callback_error('capture_google_fonts', $e);
        }
    }

    /**
     * Fail-open wrapper for local font output.
     *
     * @return void
     */
    public function safe_output_local_fonts(): void {
        if ($this->google_fonts_capture_level === null || ob_get_level() < $this->google_fonts_capture_level) {
            return;
        }

        try {
            $this->output_local_fonts();
            $this->google_fonts_capture_level = null;
        } catch (\Throwable $e) {
            $content = '';
            if (ob_get_level() >= $this->google_fonts_capture_level) {
                $captured = ob_get_clean();
                $content = is_string($captured) ? $captured : '';
            }
            $this->google_fonts_capture_level = null;
            $this->log_frontend_callback_error('output_local_fonts', $e);
            echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
        }
    }

    /**
     * Fail-open wrapper for stylesheet tag font URL replacement.
     *
     * @param mixed $tag
     * @param mixed $handle
     * @param mixed $href
     * @param mixed $media
     * @return string
     */
    public function safe_replace_google_fonts_url($tag, $handle, $href, $media): string {
        $fallback = is_string($tag) ? $tag : '';

        try {
            return $this->replace_google_fonts_url(
                $fallback,
                is_string($handle) ? $handle : (string) $handle,
                is_string($href) ? $href : (string) $href,
                is_string($media) ? $media : (string) $media
            );
        } catch (\Throwable $e) {
            $this->log_frontend_callback_error('replace_google_fonts_url', $e);
            return $fallback;
        }
    }

    /**
     * Log frontend optimization failures without taking the page down.
     *
     * @param string     $callback
     * @param \Throwable $e
     * @return void
     */
    private function log_frontend_callback_error(string $callback, \Throwable $e): void {
        if (function_exists('prorank_log')) {
            prorank_log('[ProRank FontOptimizationModule] ' . $callback . ' failed: ' . $e->getMessage());
        }
    }

    /**
     * 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);

        // Keep a single local font owner in the final HTML. Some themes/plugins
        // inline one local font lane while ProRank hosts the same family from a
        // second local path. That bloats the head and creates duplicate font
        // dependency chains.
        $html = $this->deduplicate_local_font_face_ownership($html);

        // Some builder/plugin styles bypass or later overwrite style_loader_tag
        // filters. Rewrite known local icon-font styles in the final HTML too
        // so the served stylesheet itself carries font-display:optional.
        $html = $this->rewrite_local_icon_font_stylesheet_urls_in_html($html);

        // Elementor can enqueue the full Font Awesome bundle plus the shared
        // base and split solid/brands lanes together. Once all.min.css is in
        // the page, those extra family styles are redundant.
        $html = $this->deduplicate_font_awesome_family_stylesheets($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-',
            '/wp-content/fonts/',
            '/wp-content/cache/fonts/',
        ];

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

        // Some themes/plugins inline @font-face blocks that already point to
        // locally hosted font files. In that case, Google preconnect/prefetch
        // hints are wasted and should be stripped from the final HTML.
        if (
            stripos($haystack, '@font-face') !== false
            && preg_match('~src:\s*url\((["\']?)(?:https?:)?//[^)]+/wp-content/(?:fonts|cache/fonts|uploads/[^)]+fonts)[^)]+\.woff2~i', $html) === 1
        ) {
            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 {
        $candidates = [];
        $max_preloads = (int) apply_filters('prorank_font_preload_family_limit', 2);
        if ($max_preloads < 1) {
            $max_preloads = 1;
        }

        $css_urls = $this->collect_local_font_stylesheet_urls($html);
        foreach ($css_urls as $css_url) {
            foreach ($this->extract_local_font_faces_from_stylesheet((string) $css_url) as $font_face) {
                $normalized_url = strtolower((string) preg_replace('~\?.*$~', '', (string) ($font_face['url'] ?? '')));
                if ($normalized_url === '' || isset($candidates[$normalized_url])) {
                    continue;
                }

                $candidates[$normalized_url] = [
                    'family' => (string) ($font_face['family'] ?? ''),
                    'url' => (string) ($font_face['url'] ?? ''),
                    'priority' => 999,
                    'owner_priority' => $this->get_local_font_owner_priority((string) ($font_face['url'] ?? '')),
                    'index' => count($candidates),
                ];
            }
        }

        foreach ($this->extract_inlined_local_font_faces($html) as $font_face) {
            $normalized_url = strtolower((string) preg_replace('~\?.*$~', '', (string) ($font_face['url'] ?? '')));
            if ($normalized_url === '' || isset($candidates[$normalized_url])) {
                continue;
            }

            $candidates[$normalized_url] = [
                'family' => (string) ($font_face['family'] ?? ''),
                'url' => (string) ($font_face['url'] ?? ''),
                'priority' => 999,
                'owner_priority' => $this->get_local_font_owner_priority((string) ($font_face['url'] ?? '')),
                'index' => count($candidates),
            ];
        }

        if (empty($candidates)) {
            return [];
        }

        $family_priority = $this->collect_critical_font_family_priority($html);
        foreach ($candidates as $key => $candidate) {
            $family = $this->normalize_font_family_name((string) ($candidate['family'] ?? ''));
            $candidates[$key]['priority'] = $family_priority[$family] ?? 999;
        }

        uasort(
            $candidates,
            static function (array $left, array $right): int {
                $priority_compare = ((int) ($left['priority'] ?? 999)) <=> ((int) ($right['priority'] ?? 999));
                if ($priority_compare !== 0) {
                    return $priority_compare;
                }

                $owner_compare = ((int) ($left['owner_priority'] ?? 99)) <=> ((int) ($right['owner_priority'] ?? 99));
                if ($owner_compare !== 0) {
                    return $owner_compare;
                }

                return ((int) ($left['index'] ?? 0)) <=> ((int) ($right['index'] ?? 0));
            }
        );

        $selected = [];
        $seen_families = [];

        foreach ($candidates as $candidate) {
            $url = (string) ($candidate['url'] ?? '');
            $family = $this->normalize_font_family_name((string) ($candidate['family'] ?? ''));
            if ($url === '' || isset($seen_families[$family])) {
                continue;
            }

            $selected[] = $url;
            $seen_families[$family] = true;

            if (count($selected) >= $max_preloads) {
                return $selected;
            }
        }

        foreach ($candidates as $candidate) {
            $url = (string) ($candidate['url'] ?? '');
            if ($url === '' || in_array($url, $selected, true)) {
                continue;
            }

            $selected[] = $url;
            if (count($selected) >= $max_preloads) {
                break;
            }
        }

        return $selected;
    }

    /**
     * Collect local font stylesheet URLs from both link tags and @import rules.
     *
     * @param string $html
     * @return array<int,string>
     */
    private function collect_local_font_stylesheet_urls(string $html): array {
        $css_urls = [];
        $patterns = [
            '~<link\b[^>]+href=(["\'])([^"\']+(?:omgf-stylesheet|host-webfonts-local|prorank-fonts)[^"\']*\.css[^"\']*)\1[^>]*>~i',
            '~@import\s+url\((["\']?)([^)"\']+(?:omgf-stylesheet|host-webfonts-local|prorank-fonts)[^)"\']*\.css[^)"\']*)\1\)~i',
        ];

        foreach ($patterns as $pattern) {
            $matches = [];
            preg_match_all($pattern, $html, $matches);
            if (empty($matches[2]) || !is_array($matches[2])) {
                continue;
            }

            foreach ($matches[2] as $css_url) {
                $normalized = trim((string) $css_url);
                if ($normalized === '') {
                    continue;
                }

                $key = strtolower((string) preg_replace('~\?.*$~', '', $normalized));
                if (!isset($css_urls[$key])) {
                    $css_urls[$key] = $normalized;
                }
            }
        }

        return array_values($css_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 {
        $faces = $this->extract_local_font_faces_from_stylesheet($css_url);
        if (empty($faces)) {
            return [];
        }

        return array_values(array_filter(array_map(
            static fn(array $face): string => (string) ($face['url'] ?? ''),
            $faces
        )));
    }

    /**
     * Read a local font stylesheet from uploads and extract preload-worthy font faces.
     *
     * @param string $css_url
     * @return array<int,array{family:string,url:string}>
     */
    private function extract_local_font_faces_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 [];
        }

        return $this->extract_local_font_faces_from_css($css, $css_url);
    }

    /**
     * Extract preload-worthy local font faces from inline CSS already present in the HTML.
     *
     * @param string $html
     * @return array<int,array{family:string,url:string}>
     */
    private function extract_inlined_local_font_faces(string $html): array {
        if (stripos($html, '@font-face') === false || stripos($html, '.woff2') === false) {
            return [];
        }

        return $this->extract_local_font_faces_from_css($html);
    }

    /**
     * Extract preload-worthy local font faces from CSS content.
     *
     * @param string $css
     * @param string $context_url
     * @return array<int,array{family:string,url:string}>
     */
    private function extract_local_font_faces_from_css(string $css, string $context_url = ''): array {
        if ($css === '' || stripos($css, '.woff2') === false) {
            return [];
        }

        preg_match_all('~@font-face\s*\{[^}]*font-family\s*:\s*(["\']?)([^;"\'}]+)\1[^}]*src\s*:\s*([^}]*)\}~is', $css, $matches, PREG_SET_ORDER);
        if (empty($matches)) {
            return [];
        }

        $faces = [];
        $seen = [];

        foreach ($matches as $match) {
            $family = $this->normalize_font_family_name((string) ($match[2] ?? ''));
            if ($family === '' || $this->is_icon_font_family($family)) {
                continue;
            }

            $src_block = (string) ($match[3] ?? '');
            if ($src_block === '') {
                continue;
            }

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

            foreach ($url_matches[2] as $raw_url) {
                $font_url = $this->normalize_local_font_asset_url((string) $raw_url, $context_url);
                if ($font_url === '') {
                    continue;
                }

                $key = strtolower((string) preg_replace('~\?.*$~', '', $font_url));
                if (isset($seen[$key])) {
                    continue;
                }

                $seen[$key] = true;
                $faces[] = [
                    'family' => $family,
                    'url' => $font_url,
                ];
            }
        }

        return $faces;
    }

    /**
     * Deduplicate semantically equivalent local @font-face rules across the
     * final HTML and keep the best owner.
     *
     * Prefer ProRank-hosted uploads font assets over generic local font paths
     * when both define the same family/style/weight/unicode-range tuple.
     *
     * @param string $html
     * @return string
     */
    private function deduplicate_local_font_face_ownership(string $html): string {
        if (stripos($html, '@font-face') === false || stripos($html, '/wp-content/') === false) {
            return $html;
        }

        preg_match_all('/@font-face\s*\{[^}]+\}\s*/is', $html, $matches, PREG_OFFSET_CAPTURE);
        if (empty($matches[0]) || !is_array($matches[0])) {
            return $html;
        }

        $keepers = [];
        $removals = [];

        foreach ($matches[0] as $match) {
            if (!is_array($match) || !isset($match[0], $match[1])) {
                continue;
            }

            $block = (string) $match[0];
            $offset = (int) $match[1];
            $signature = $this->extract_local_font_face_signature($block);
            if ($signature === null) {
                continue;
            }

            $key = md5(implode('|', [
                $signature['family'],
                $signature['style'],
                $signature['weight'],
                $signature['stretch'],
                $signature['asset_name'],
            ]));

            $candidate = [
                'offset' => $offset,
                'length' => strlen($block),
                'priority' => $this->get_local_font_owner_priority($signature['url']),
            ];

            if (!isset($keepers[$key])) {
                $keepers[$key] = $candidate;
                continue;
            }

            $current = $keepers[$key];
            if ($candidate['priority'] < $current['priority']) {
                $removals[] = [
                    'offset' => (int) $current['offset'],
                    'length' => (int) $current['length'],
                ];
                $keepers[$key] = $candidate;
                continue;
            }

            $removals[] = [
                'offset' => $offset,
                'length' => strlen($block),
            ];
        }

        if (empty($removals)) {
            return $html;
        }

        usort(
            $removals,
            static function (array $left, array $right): int {
                return ((int) $right['offset']) <=> ((int) $left['offset']);
            }
        );

        foreach ($removals as $removal) {
            $html = substr_replace($html, '', (int) $removal['offset'], (int) $removal['length']);
        }

        return $html;
    }

    /**
     * Extract a semantic signature from one local @font-face block.
     *
     * @param string $block
     * @return array{family:string,style:string,weight:string,stretch:string,unicode_range:string,asset_name:string,url:string}|null
     */
    private function extract_local_font_face_signature(string $block): ?array {
        if (
            !preg_match('/font-family\s*:\s*(["\']?)([^;"\'}]+)\1/i', $block, $family_match)
            || !preg_match('/src\s*:\s*[^;]*url\((["\']?)([^)"\']+\.(?:woff2?|ttf|otf)[^)"\']*)\1\)/i', $block, $url_match)
        ) {
            return null;
        }

        $url = $this->normalize_local_font_asset_url((string) ($url_match[2] ?? ''));
        if ($url === '' || !$this->is_local_font_asset_url($url)) {
            return null;
        }

        return [
            'family' => $this->normalize_font_family_name((string) ($family_match[2] ?? '')),
            'style' => $this->extract_font_face_property($block, 'font-style', 'normal'),
            'weight' => $this->extract_font_face_property($block, 'font-weight', '400'),
            'stretch' => $this->extract_font_face_property($block, 'font-stretch', 'normal'),
            'unicode_range' => $this->extract_font_face_property($block, 'unicode-range', ''),
            'asset_name' => strtolower((string) basename((string) wp_parse_url($url, PHP_URL_PATH))),
            'url' => strtolower((string) preg_replace('~\?.*$~', '', $url)),
        ];
    }

    /**
     * Extract a single property value from one @font-face block.
     *
     * @param string $block
     * @param string $property
     * @param string $default
     * @return string
     */
    private function extract_font_face_property(string $block, string $property, string $default = ''): string {
        if (preg_match('/' . preg_quote($property, '/') . '\s*:\s*([^;]+);?/i', $block, $match) !== 1) {
            return strtolower(trim($default));
        }

        return strtolower(trim((string) ($match[1] ?? $default)));
    }

    /**
     * Score one local font owner. Lower is better.
     *
     * @param string $url
     * @return int
     */
    private function get_local_font_owner_priority(string $url): int {
        $url = strtolower($url);

        if (strpos($url, '/wp-content/uploads/prorank-fonts/') !== false) {
            return 0;
        }

        if (strpos($url, '/wp-content/fonts/') !== false) {
            return 1;
        }

        if (strpos($url, '/wp-content/cache/fonts/') !== false) {
            return 2;
        }

        if (strpos($url, '/wp-content/uploads/') !== false) {
            return 3;
        }

        return 9;
    }

    /**
     * Determine whether a font URL points to one local WordPress font asset.
     *
     * @param string $url
     * @return bool
     */
    private function is_local_font_asset_url(string $url): bool {
        return preg_match(
            '~(?:/wp-content/(?:fonts|uploads/[^"\')]*fonts|cache/fonts)/[^"\')]+\.(?:woff2?|ttf|otf))~i',
            $url
        ) === 1;
    }

    /**
     * Normalize a font family name for matching.
     *
     * @param string $family
     * @return string
     */
    private function normalize_font_family_name(string $family): string {
        $family = trim(str_replace(['"', "'"], '', $family));
        return strtolower($family);
    }

    /**
     * Whether a family is an icon font that should not be preloaded here.
     *
     * @param string $family
     * @return bool
     */
    private function is_icon_font_family(string $family): bool {
        if ($family === '') {
            return true;
        }

        foreach ([
            'font awesome',
            'fontawesome',
            'eicons',
            'elementor icons',
            'etmodules',
            'material icons',
        ] as $needle) {
            if (strpos($family, $needle) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Collect a priority map for font families already used in critical/early CSS.
     *
     * @param string $html
     * @return array<string,int>
     */
    private function collect_critical_font_family_priority(string $html): array {
        $head = $html;
        if (preg_match('~<head[^>]*>(.*)</head>~is', $html, $match) === 1) {
            $head = (string) ($match[1] ?? $html);
        }

        $priority = [];
        $position = 0;
        preg_match_all(
            '/(?:font-family|--e-global-typography-[^:]+-font-family)\s*:\s*([\'"]?)([^;"\',}]+)\1/i',
            $head,
            $matches
        );

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

        foreach ($matches[2] as $family) {
            $normalized = $this->normalize_font_family_name((string) $family);
            if ($normalized === '' || $this->is_icon_font_family($normalized) || isset($priority[$normalized])) {
                continue;
            }

            $priority[$normalized] = $position++;
        }

        return $priority;
    }

    /**
     * Normalize a local font asset URL from CSS to an absolute URL when possible.
     *
     * @param string $font_url
     * @param string $context_url
     * @return string
     */
    private function normalize_local_font_asset_url(string $font_url, string $context_url = ''): string {
        $font_url = trim($font_url, " \t\n\r\0\x0B'\"");
        if ($font_url === '' || strpos(strtolower($font_url), 'latin-ext') !== false) {
            return '';
        }

        if (strpos($font_url, '//') === 0) {
            return (is_ssl() ? 'https:' : 'http:') . $font_url;
        }

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

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

        if (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'];
            }

            return $host !== '' ? $host . $font_url : '';
        }

        if ($context_url !== '') {
            return $this->resolve_relative_asset_url($font_url, $context_url);
        }

        return '';
    }

    /**
     * Resolve a relative asset URL against a stylesheet URL.
     *
     * @param string $asset_url
     * @param string $context_url
     * @return string
     */
    private function resolve_relative_asset_url(string $asset_url, string $context_url): string {
        $asset_url = trim($asset_url, " \t\n\r\0\x0B'\"");
        if ($asset_url === '') {
            return '';
        }

        $asset_parts = wp_parse_url($asset_url);
        $asset_path = is_array($asset_parts) ? (string) ($asset_parts['path'] ?? '') : '';
        if ($asset_path === '') {
            return '';
        }

        $context_parts = wp_parse_url($context_url);
        $context_parts = is_array($context_parts) ? $context_parts : [];
        $context_path = (string) ($context_parts['path'] ?? '');
        if ($context_path === '') {
            return '';
        }

        $resolved_path = str_starts_with($asset_path, '/')
            ? $asset_path
            : rtrim(dirname($context_path), '/') . '/' . $asset_path;
        $resolved_path = $this->normalize_url_path($resolved_path);

        $origin_parts = $context_parts;
        if (empty($origin_parts['host'])) {
            $fallback_parts = wp_parse_url(home_url('/'));
            $origin_parts = is_array($fallback_parts) ? $fallback_parts : [];
        }

        if (empty($origin_parts['host'])) {
            return '';
        }

        $origin = ($origin_parts['scheme'] ?? 'https') . '://' . $origin_parts['host'];
        if (!empty($origin_parts['port'])) {
            $origin .= ':' . $origin_parts['port'];
        }

        $suffix = '';
        if (isset($asset_parts['query'])) {
            $suffix .= '?' . $asset_parts['query'];
        }
        if (isset($asset_parts['fragment'])) {
            $suffix .= '#' . $asset_parts['fragment'];
        }

        return $origin . '/' . ltrim($resolved_path, '/') . $suffix;
    }

    /**
     * Normalize a URL path while preserving real path segments.
     *
     * @param string $path
     * @return string
     */
    private function normalize_url_path(string $path): string {
        $segments = [];

        foreach (explode('/', $path) as $segment) {
            if ($segment === '' || $segment === '.') {
                continue;
            }

            if ($segment === '..') {
                array_pop($segments);
                continue;
            }

            $segments[] = $segment;
        }

        return '/' . implode('/', $segments);
    }

    /**
     * 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;
        }

        if (\ProRank\SEO\Plugin::is_elementor_runtime_request()) {
            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] ?? '');
                }
                $css = $this->get_inlineable_local_font_css($href, $max_bytes);
                if ($css === null) {
                    return (string) ($matches[0] ?? '');
                }

                $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
        );

        if (!is_string($result) || $result === '') {
            return $content;
        }

        $import_pattern = '/@import\\s+url\\((["\']?)([^)"\']*\\/wp-content\\/uploads\\/prorank-fonts\\/google-fonts-[^)"\']+\\.css(?:\\?[^)"\']*)?)\\1\\)\\s*;?/i';
        $result = preg_replace_callback(
            $import_pattern,
            function (array $matches) use ($max_bytes): string {
                $href = (string) ($matches[2] ?? '');
                if ($href === '') {
                    return (string) ($matches[0] ?? '');
                }

                $css = $this->get_inlineable_local_font_css($href, $max_bytes);
                if ($css === null) {
                    return (string) ($matches[0] ?? '');
                }

                return $css;
            },
            $result
        );

        return is_string($result) ? $result : $content;
    }

    /**
     * Return inlineable local Google-font CSS when the generated file is small.
     *
     * @param string $href
     * @param int    $max_bytes
     * @return string|null
     */
    private function get_inlineable_local_font_css(string $href, int $max_bytes): ?string {
        $path = preg_replace('#^https?://[^/]+#i', '', $href);
        if (!is_string($path) || $path === '') {
            return null;
        }

        $path = strtok($path, '?#');
        if (!is_string($path) || $path === '') {
            return null;
        }

        $candidate = wp_normalize_path(trailingslashit(ABSPATH) . ltrim(rawurldecode($path), '/'));
        if (!file_exists($candidate) || !is_readable($candidate)) {
            return null;
        }

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

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

        return str_ireplace('</style', '<\\/style', $css);
    }
    
    /**
     * 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
                );
            }
        }

        if ($this->get_setting('font_display_swap', false) && $this->is_local_icon_font_stylesheet_url($href, $handle)) {
            $optimized_icon_css = $this->get_or_build_local_icon_font_stylesheet($href);
            if ($optimized_icon_css) {
                $tag = (string) preg_replace(
                    '/href=(["\'])[^"\']+\1/i',
                    'href=$1' . esc_url($optimized_icon_css) . '$1',
                    $tag,
                    1
                );
                $tag = $this->convert_icon_font_stylesheet_tag_to_async($tag, $handle, $optimized_icon_css, $media);
            }
        }

        // 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;
    }

    /**
     * Determine whether a stylesheet URL is a local icon-font CSS candidate.
     *
     * @param string $href
     * @param string $handle
     * @return bool
     */
    private function is_local_icon_font_stylesheet_url(string $href, string $handle = ''): bool {
        if ($href === '' || strpos($href, '.css') === false) {
            return false;
        }

        $haystack = strtolower($handle . ' ' . html_entity_decode($href, ENT_QUOTES | ENT_HTML5, 'UTF-8'));
        $markers = [
            'elementor-icons',
            'eicons',
            'fontawesome',
            'font-awesome/css/all',
            'font-awesome/css/brands',
            'font-awesome/css/solid',
            'font-awesome/css/regular',
            'fa-brands',
            'fa-solid',
            'fa-regular',
        ];

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

        return false;
    }

    /**
     * Build or fetch a rewritten local icon-font stylesheet with
     * font-display:optional applied directly in CSS.
     *
     * @param string $href
     * @return string|false
     */
    private function get_or_build_local_icon_font_stylesheet(string $href) {
        $source_path = $this->map_site_stylesheet_url_to_path($href);
        if ($source_path === '' || !is_readable($source_path)) {
            return false;
        }

        $normalized_href = html_entity_decode(trim($href), ENT_QUOTES | ENT_HTML5, 'UTF-8');
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_filemtime
        $mtime = @filemtime($source_path) ?: 0;
        $hash = md5($normalized_href . '|' . $mtime . '|icon-font-display-v2');
        $filename = 'icon-fonts-' . $hash . '.css';
        $cached_path = $this->fonts_dir . '/' . $filename;
        $cached_url = $this->fonts_url . '/' . $filename;

        if (file_exists($cached_path) && $this->is_cached_icon_font_css_healthy($cached_path)) {
            return $cached_url;
        }

        $css = @file_get_contents($source_path);
        if (!is_string($css) || $css === '' || !$this->is_icon_font_stylesheet_css($css)) {
            return false;
        }

        $processed_css = $this->process_icon_font_stylesheet_css($css, $normalized_href);
        if ($processed_css === '') {
            return false;
        }

        if ($this->save_font_file($filename, $processed_css)) {
            return $cached_url;
        }

        return false;
    }

    /**
     * Map a same-origin stylesheet URL to a local filesystem path.
     *
     * @param string $url
     * @return string
     */
    private function map_site_stylesheet_url_to_path(string $url): string {
        $url = html_entity_decode(trim($url), ENT_QUOTES | ENT_HTML5, 'UTF-8');
        if ($url === '') {
            return '';
        }

        if (strpos($url, '//') === 0) {
            $url = (is_ssl() ? 'https:' : 'http:') . $url;
        }

        $site_host = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
        $url_host = strtolower((string) wp_parse_url($url, PHP_URL_HOST));
        if ($url_host !== '' && $site_host !== '' && $url_host !== $site_host) {
            return '';
        }

        $path = (string) wp_parse_url($url, PHP_URL_PATH);
        if ($path === '') {
            return '';
        }

        $candidate = wp_normalize_path(trailingslashit(ABSPATH) . ltrim(rawurldecode($path), '/'));
        return is_readable($candidate) ? $candidate : '';
    }

    /**
     * Whether CSS content looks like an icon-font stylesheet.
     *
     * @param string $css
     * @return bool
     */
    private function is_icon_font_stylesheet_css(string $css): bool {
        if ($css === '' || stripos($css, '@font-face') === false) {
            return false;
        }

        return (bool) preg_match(
            '/font-family\s*:\s*(["\']?)(?:Font Awesome|FontAwesome|eicons|Elementor Icons|Material Icons)[^;"\'}]*\1/i',
            $css
        );
    }

    /**
     * Rewrite a local icon-font stylesheet so font-display is explicit and
     * relative asset URLs remain valid after moving it to ProRank uploads.
     *
     * @param string $css
     * @param string $context_url
     * @return string
     */
    private function process_icon_font_stylesheet_css(string $css, string $context_url): string {
        $css = preg_replace_callback(
            '/url\((["\']?)([^)"\']+)\1\)/i',
            function (array $matches) use ($context_url): string {
                $raw_url = trim((string) ($matches[2] ?? ''));
                if ($raw_url === '' || str_starts_with($raw_url, 'data:') || str_starts_with($raw_url, 'blob:')) {
                    return (string) ($matches[0] ?? '');
                }

                $resolved = $this->normalize_stylesheet_asset_url($raw_url, $context_url);
                if ($resolved === '') {
                    return (string) ($matches[0] ?? '');
                }

                return 'url("' . esc_url_raw($resolved) . '")';
            },
            $css
        );

        $css = preg_replace_callback(
            '/@font-face\s*\{([^}]*)\}/is',
            function (array $matches): string {
                $block = (string) ($matches[0] ?? '');
                if ($block === '' || !$this->is_icon_font_stylesheet_css($block)) {
                    return $block;
                }

                if (preg_match('/font-display\s*:\s*[^;]+;?/i', $block) === 1) {
                    $updated = preg_replace('/font-display\s*:\s*[^;]+;?/i', 'font-display:optional;', $block, 1);
                    return is_string($updated) ? $updated : $block;
                }

                $updated = preg_replace('/@font-face\s*\{/i', '@font-face{font-display:optional;', $block, 1);
                return is_string($updated) ? $updated : $block;
            },
            $css
        );

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

    /**
     * Resolve an asset URL referenced from a local stylesheet to an absolute URL.
     *
     * @param string $asset_url
     * @param string $context_url
     * @return string
     */
    private function normalize_stylesheet_asset_url(string $asset_url, string $context_url): string {
        $asset_url = trim($asset_url, " \t\n\r\0\x0B'\"");
        if ($asset_url === '') {
            return '';
        }

        if (strpos($asset_url, '//') === 0) {
            return (is_ssl() ? 'https:' : 'http:') . $asset_url;
        }

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

        if (strpos($asset_url, '/') === 0) {
            $parts = wp_parse_url(home_url('/'));
            $origin = isset($parts['host']) ? (($parts['scheme'] ?? 'https') . '://' . $parts['host']) : '';
            if (!empty($parts['port'])) {
                $origin .= ':' . $parts['port'];
            }

            return $origin !== '' ? $origin . $asset_url : '';
        }

        return $this->resolve_relative_asset_url($asset_url, $context_url);
    }

    /**
     * Ensure a cached icon-font stylesheet still has explicit font-display.
     *
     * @param string $css_path
     * @return bool
     */
    private function is_cached_icon_font_css_healthy(string $css_path): bool {
        $content = @file_get_contents($css_path);
        if (!is_string($content) || $content === '' || !$this->is_icon_font_stylesheet_css($content)) {
            return false;
        }

        if (stripos($content, '/css/fonts/') !== false) {
            return false;
        }

        return preg_match('/font-display\s*:\s*optional\b/i', $content) === 1;
    }

    /**
     * Rewrite local icon-font stylesheet hrefs in final HTML to ProRank copies
     * with explicit font-display values.
     *
     * @param string $html
     * @return string
     */
    private function rewrite_local_icon_font_stylesheet_urls_in_html(string $html): string {
        if ($html === '' || stripos($html, '.css') === false) {
            return $html;
        }

        $noscript_blocks = [];
        $html = (string) preg_replace_callback(
            '~<noscript\b[^>]*>.*?</noscript>~is',
            function (array $matches) use (&$noscript_blocks): string {
                $key = '%%PRORANK_NOSCRIPT_' . count($noscript_blocks) . '%%';
                $noscript_blocks[$key] = (string) ($matches[0] ?? '');
                return $key;
            },
            $html
        );

        $result = preg_replace_callback(
            '/<link\b([^>]*\bhref=(["\'])([^"\']+\.css(?:\?[^"\']*)?)\2[^>]*)>/i',
            function (array $matches): string {
                $href = (string) ($matches[3] ?? '');
                if (!$this->is_local_icon_font_stylesheet_url($href, (string) ($matches[1] ?? ''))) {
                    return (string) ($matches[0] ?? '');
                }

                if ($this->is_generated_icon_font_stylesheet_url($href)) {
                    return $this->convert_icon_font_stylesheet_tag_to_async(
                        (string) ($matches[0] ?? ''),
                        '',
                        $href,
                        $this->extract_link_attribute((string) ($matches[0] ?? ''), 'media') ?: 'all'
                    );
                }

                $optimized_href = $this->get_or_build_local_icon_font_stylesheet($href);
                if (!$optimized_href) {
                    return (string) ($matches[0] ?? '');
                }

                $rebuilt = preg_replace(
                    '/\bhref=(["\'])[^"\']+\1/i',
                    'href=$1' . esc_url($optimized_href) . '$1',
                    (string) ($matches[0] ?? ''),
                    1
                );

                if (!is_string($rebuilt)) {
                    return (string) ($matches[0] ?? '');
                }

                return $this->convert_icon_font_stylesheet_tag_to_async(
                    $rebuilt,
                    '',
                    $optimized_href,
                    $this->extract_link_attribute($rebuilt, 'media') ?: 'all'
                );
            },
            $html
        );

        $result = is_string($result) ? $result : $html;

        return $noscript_blocks !== [] ? strtr($result, $noscript_blocks) : $result;
    }

    /**
     * Determine whether a stylesheet URL already points to a generated
     * ProRank icon-font CSS file.
     *
     * @param string $href
     * @return bool
     */
    private function is_generated_icon_font_stylesheet_url(string $href): bool {
        $href = strtolower(html_entity_decode($href, ENT_QUOTES | ENT_HTML5, 'UTF-8'));

        return strpos($href, '/wp-content/uploads/prorank-fonts/icon-fonts-') !== false;
    }

    /**
     * Convert generated icon-font CSS to non-render-blocking delivery.
     *
     * @param string $tag
     * @param string $handle
     * @param string $href
     * @param string $media
     * @return string
     */
    private function convert_icon_font_stylesheet_tag_to_async(string $tag, string $handle, string $href, string $media): string {
        if ($tag === '' || stripos($tag, 'data-prorank-async') !== false) {
            return $tag;
        }

        $id = $this->extract_link_attribute($tag, 'id');
        if ($id === '') {
            $id = $handle !== '' ? $handle . '-css' : 'prorank-icon-font-css';
        }

        $target_media = trim($media) !== '' ? trim($media) : 'all';
        if (strtolower($target_media) === 'print') {
            $target_media = 'all';
        }

        $async = sprintf(
            '<link rel="stylesheet" id="%s" href="%s" media="print" onload="this.onload=null;this.media=\'%s\'" onerror="this.onerror=null;this.media=\'%s\'" data-prorank-async="true" data-prorank-css-owner="font_opt_icon" />',
            esc_attr($id),
            esc_url($href),
            esc_attr($target_media),
            esc_attr($target_media)
        );
        $noscript = sprintf(
            '<noscript><link rel="stylesheet" id="%s-noscript" href="%s" media="%s" /></noscript>',
            esc_attr($id),
            esc_url($href),
            esc_attr($target_media)
        );

        return $async . $noscript;
    }

    /**
     * Extract an attribute value from a link tag.
     *
     * @param string $tag
     * @param string $attribute
     * @return string
     */
    private function extract_link_attribute(string $tag, string $attribute): string {
        $attribute = preg_quote($attribute, '/');
        if (preg_match('/\b' . $attribute . '\s*=\s*(["\'])(.*?)\1/i', $tag, $matches) !== 1) {
            return '';
        }

        return html_entity_decode((string) ($matches[2] ?? ''), ENT_QUOTES | ENT_HTML5, 'UTF-8');
    }

    /**
     * Remove redundant Elementor Font Awesome family stylesheets when the
     * full all.min.css bundle is already present.
     *
     * @param string $html
     * @return string
     */
    private function deduplicate_font_awesome_family_stylesheets(string $html): string {
        if ($html === '' || !$this->has_full_font_awesome_bundle_in_html($html)) {
            return $html;
        }

        $patterns = [
            '/<link\b(?=[^>]*\bid=(["\'])elementor-icons-shared-0-css\1)[^>]*>\s*/i',
            '/<link\b(?=[^>]*\bid=(["\'])elementor-icons-fa-solid-css\1)[^>]*>\s*/i',
            '/<link\b(?=[^>]*\bid=(["\'])elementor-icons-fa-brands-css\1)[^>]*>\s*/i',
            '/<link\b(?=[^>]*\bid=(["\'])elementor-icons-fa-regular-css\1)[^>]*>\s*/i',
            '/<link\b[^>]*href=(["\'])[^"\']*\/font-awesome\/css\/fontawesome\.min\.css(?:\?[^"\']*)?\1[^>]*>\s*/i',
            '/<link\b[^>]*href=(["\'])[^"\']*\/font-awesome\/css\/solid\.min\.css(?:\?[^"\']*)?\1[^>]*>\s*/i',
            '/<link\b[^>]*href=(["\'])[^"\']*\/font-awesome\/css\/brands\.min\.css(?:\?[^"\']*)?\1[^>]*>\s*/i',
            '/<link\b[^>]*href=(["\'])[^"\']*\/font-awesome\/css\/regular\.min\.css(?:\?[^"\']*)?\1[^>]*>\s*/i',
        ];

        foreach ($patterns as $pattern) {
            $updated = preg_replace($pattern, '', $html);
            if (is_string($updated)) {
                $html = $updated;
            }
        }

        return $html;
    }

    /**
     * Detect whether the full Font Awesome bundle is already present in HTML.
     *
     * @param string $html
     * @return bool
     */
    private function has_full_font_awesome_bundle_in_html(string $html): bool {
        return preg_match(
            '/<link\b(?=[^>]*(?:\bid=(["\'])font-awesome-5-all-css\1|\bhref=(["\'])[^"\']*\/font-awesome\/css\/all\.min\.css(?:\?[^"\']*)?\2))[^>]*>/i',
            $html
        ) === 1;
    }
    
    /**
     * 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;
    }
}
