<?php

/**
 * Resource Hints Module
 *
 * Adds DNS prefetch, preconnect, and preload hints for improved 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;
}


/**
 * ResourceHintsModule class
 */


class ResourceHintsModule extends BaseModule {

    /**
     * Required tier for this module
     */
    protected string $feature_tier = 'free';
    
    /**
     * Constructor
     */
    public function __construct() {
        $this->slug = 'resource_hints';
        $this->name = 'Resource Hints';
        $this->description = 'Add DNS prefetch, preconnect, and preload hints for faster loading';
$this->parent_slug = 'performance';
    }

    /**
     * Get setting from unified cache settings
     * Overrides BaseModule to read from prorank_cache_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_cache_settings', []);
        }
        return $settings[$key] ?? $default;
    }

    /**
     * Initialize module hooks
     *
     * @return void
     */
    public function init_hooks(): void {
        // Check if module should run
        if (!$this->should_run()) {
            return;
        }
        
        // DNS Prefetch
        if ($this->get_setting('dns_prefetch_enabled', false)) {
            add_filter('wp_resource_hints', [$this, 'add_dns_prefetch_hints'], 10, 2);

            // Discover external origins from the final HTML and inject a small
            // preconnect budget automatically. This helps builder/plugin-heavy
            // pages where third-party hosts are introduced late or inline.
            if ($this->is_truthy_setting($this->get_setting('dynamic_resource_hints_enabled', true))) {
                Buffer::init();
                add_filter('prorank_output_buffer', [$this, 'inject_dynamic_resource_hints'], 98);
            }
        }
        
        // Font Preloading
        if ($this->get_setting('font_preload_enabled', false)) {
            add_action('wp_head', [$this, 'add_font_preload_links'], 2);
        }
        
        // Link Preloading
        if ($this->get_setting('link_preload_enabled', false)) {
            add_action('wp_enqueue_scripts', [$this, 'enqueue_link_preload_script']);
        }
    }
    
    /**
     * Check if module should run
     *
     * @return bool
     */
    private function should_run(): bool {
        // Check if module is enabled
        if (!$this->is_enabled()) {
            return false;
        }
        
        // Don't run in admin
        if (is_admin()) {
            return false;
        }
        
        return true;
    }
    
    /**
     * Add DNS prefetch hints
     *
     * @param array  $hints Array of resource hints
     * @param string $relation_type Type of resource hint
     * @return array Modified hints array
     */
    public function add_dns_prefetch_hints(array $hints, string $relation_type): array {
        if ($relation_type !== 'dns-prefetch') {
            return $hints;
        }
        
        $hosts = $this->get_setting('dns_prefetch_hosts', '');
        if (empty($hosts)) {
            return $hints;
        }
        
        // Parse hosts from textarea
        $hosts_array = array_filter(array_map('trim', explode("\n", $hosts)));
        
        foreach ($hosts_array as $host) {
            // Validate host
            if ($this->is_valid_host($host)) {
                // Ensure protocol is removed
                $host = str_replace(['http://', 'https://'], '', $host);
                $host = rtrim($host, '/');

                if ($this->is_font_hint_managed_by_font_optimization($host)) {
                    continue;
                }
                
                if (!in_array($host, $hints, true)) {
                    $hints[] = $host;
                }
            }
        }
        
        return $hints;
    }

    /**
     * Inject dynamic preconnect and dns-prefetch hints discovered from final HTML.
     *
     * @param string $html
     * @return string
     */
    public function inject_dynamic_resource_hints(string $html): string {
        if ($html === '' || stripos($html, '</head>') === false) {
            return $html;
        }

        $budget = (int) apply_filters('prorank_dynamic_preconnect_budget', 4);
        if ($budget < 1) {
            return $html;
        }

        $site_host = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
        if ($site_host === '') {
            return $html;
        }

        $existing = $this->extract_existing_hint_hosts($html);
        $existing_preconnect_count = count($existing['preconnect']);
        if ($existing_preconnect_count >= $budget) {
            return $html;
        }

        $remaining_budget = max(0, $budget - $existing_preconnect_count);
        if ($remaining_budget < 1) {
            return $html;
        }

        $delayed_hosts = $this->extract_delayed_runtime_hosts($html, $site_host);
        $candidates = $this->collect_dynamic_preconnect_candidates($html, $site_host, $delayed_hosts);
        if (empty($candidates)) {
            return $html;
        }

        arsort($candidates);

        $tags = [];
        $emitted = 0;
        foreach ($candidates as $host => $score) {
            if (!is_string($host) || $host === '' || isset($existing['preconnect'][$host])) {
                continue;
            }

            if ($emitted >= $remaining_budget) {
                break;
            }

            $tags[] = sprintf(
                '<link rel="preconnect" href="%s"%s />',
                esc_url('https://' . $host),
                $this->host_needs_crossorigin_preconnect($host) ? ' crossorigin' : ''
            );

            $emitted++;
        }

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

        $insertion = implode("\n", $tags) . "\n";
        $updated = preg_replace('/<\/head>/i', $insertion . '</head>', $html, 1);
        return is_string($updated) && $updated !== '' ? $updated : $html;
    }
    
    /**
     * Add font preload links
     *
     * @return void
     */
    public function add_font_preload_links(): void {
        $font_urls = $this->get_setting('font_preload_urls', '');
        if (empty($font_urls)) {
            return;
        }
        
        // Parse URLs from textarea
        $urls_array = array_filter(array_map('trim', explode("\n", $font_urls)));
        
        $printed = [];
        foreach ($urls_array as $url) {
            if (!$this->is_valid_font_url($url)) {
                continue;
            }

            $resolved_url = $this->resolve_font_preload_url($url);
            if (in_array($resolved_url, $printed, true)) {
                continue;
            }

                // Determine crossorigin attribute
                printf(
                    '<link rel="preload" href="%s" as="font" type="font/%s"%s>%s',
                    esc_url($resolved_url),
                    esc_attr($this->get_font_type($resolved_url)),
                    $this->is_external_url($resolved_url) ? ' crossorigin="' . esc_attr('anonymous') . '"' : '',
                    "\n"
                );
                $printed[] = $resolved_url;
        }
    }

    /**
     * Resolve preload URL to local hosted font URL when local font mode is active.
     *
     * @param string $url
     * @return string
     */
    private function resolve_font_preload_url(string $url): string {
        if ($url === '') {
            return $url;
        }

        if (!$this->is_local_font_mode_enabled()) {
            return $url;
        }

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

        $path = (string) wp_parse_url($url, PHP_URL_PATH);
        $filename = basename($path);
        if ($filename === '' || strpos($filename, '.woff') === false) {
            return $url;
        }

        $local_url = $this->find_local_font_url_by_filename($filename);
        if ($local_url !== null) {
            return $local_url;
        }

        return $url;
    }

    /**
     * Whether local Google font hosting/subsetting is active.
     *
     * @return bool
     */
    private function is_local_font_mode_enabled(): bool {
        static $enabled = null;
        if ($enabled !== null) {
            return $enabled;
        }

        $asset_settings = get_option('prorank_asset_optimization_settings', []);
        if (!is_array($asset_settings)) {
            $asset_settings = [];
        }

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

    /**
     * Find locally hosted font URL by filename under uploads/prorank-fonts.
     *
     * @param string $filename
     * @return string|null
     */
    private function find_local_font_url_by_filename(string $filename): ?string {
        static $map = null;
        if ($map === null) {
            $map = [];
            $upload = wp_upload_dir();
            $base_dir = trailingslashit((string) ($upload['basedir'] ?? '')) . 'prorank-fonts';
            $base_url = trailingslashit((string) ($upload['baseurl'] ?? '')) . 'prorank-fonts';

            if (is_dir($base_dir)) {
                try {
                    $iterator = new \RecursiveIteratorIterator(
                        new \RecursiveDirectoryIterator($base_dir, \RecursiveDirectoryIterator::SKIP_DOTS)
                    );

                    foreach ($iterator as $file) {
                        if (!$file->isFile()) {
                            continue;
                        }

                        $name = $file->getFilename();
                        if (stripos($name, '.woff') === false) {
                            continue;
                        }

                        $absolute = wp_normalize_path($file->getPathname());
                        $relative = ltrim(str_replace(wp_normalize_path($base_dir), '', $absolute), '/');
                        if ($relative === '') {
                            continue;
                        }

                        if (!isset($map[$name])) {
                            $map[$name] = trailingslashit($base_url) . str_replace('\\', '/', $relative);
                        }
                    }
                } catch (\Throwable $e) {
                    // Ignore and fall back to original URL
                }
            }
        }

        return $map[$filename] ?? null;
    }
    
    /**
     * Enqueue link preload script
     *
     * @return void
     */
    public function enqueue_link_preload_script(): void {
        $delay = $this->get_setting('link_preload_delay', 50);
        
        $script = sprintf(
            '
            (function() {
                var delay = %d;
                var preloaded = new Set();
                var prefetcher = document.createElement("link");
                
                function preloadLink(url) {
                    if (preloaded.has(url)) return;
                    
                    preloaded.add(url);
                    var link = document.createElement("link");
                    link.rel = "prefetch";
                    link.href = url;
                    document.head.appendChild(link);
                }
                
                function handleHover(e) {
                    var link = e.target.closest("a");
                    if (!link) return;
                    
                    var url = link.href;
                    if (!url || url.indexOf("#") === 0) return;
                    
                    // Skip external links
                    if (link.hostname !== window.location.hostname) return;
                    
                    // Skip non-HTML links
                    if (link.pathname.match(/\.(jpg|jpeg|png|gif|svg|webp|ico|pdf|zip|rar|gz|7z|exe|dmg|mp3|mp4|avi|mov)$/i)) return;
                    
                    // Skip admin links
                    if (link.pathname.indexOf("/wp-admin/") === 0) return;
                    
                    clearTimeout(link.preloadTimeout);
                    link.preloadTimeout = setTimeout(function() {
                        preloadLink(url);
                    }, delay);
                }
                
                function handleLeave(e) {
                    var link = e.target.closest("a");
                    if (!link) return;
                    
                    clearTimeout(link.preloadTimeout);
                }
                
                // Attach event listeners
                document.addEventListener("mouseover", handleHover, { passive: true });
                document.addEventListener("mouseout", handleLeave, { passive: true });
                
                // Mobile: preload on touchstart
                document.addEventListener("touchstart", function(e) {
                    var link = e.target.closest("a");
                    if (!link) return;
                    
                    var url = link.href;
                    if (url && url.indexOf("#") !== 0 && link.hostname === window.location.hostname) {
                        preloadLink(url);
                    }
                }, { passive: true });
            })();
            ',
            intval($delay)
        );
        
        $handle = 'prorank-resource-hints-runtime';
        if (!wp_script_is($handle, 'registered')) {
            wp_register_script($handle, '', [], PRORANK_VERSION, true);
        }
        if (!wp_script_is($handle, 'enqueued')) {
            wp_enqueue_script($handle);
        }

        wp_add_inline_script($handle, $script, 'after');
    }
    
    /**
     * Validate host
     *
     * @param string $host Host to validate
     * @return bool
     */
    private function is_valid_host(string $host): bool {
        $host = str_replace(['http://', 'https://'], '', $host);
        $host = rtrim($host, '/');
        
        // Basic validation
        return preg_match('/^[a-z0-9.-]+\.[a-z]{2,}$/i', $host) === 1;
    }

    /**
     * Collect external origins worth preconnecting to from the final HTML.
     *
     * @param string $html
     * @param string $site_host
     * @param array<string, bool> $delayed_hosts
     * @return array<string, int>
     */
    private function collect_dynamic_preconnect_candidates(string $html, string $site_host, array $delayed_hosts = []): array {
        $scores = [];

        if (preg_match_all('/<link\b[^>]*>/i', $html, $link_matches) === 1 || !empty($link_matches[0])) {
            foreach ($link_matches[0] as $tag) {
                $href = $this->extract_tag_attribute($tag, 'href');
                $rel = strtolower(trim($this->extract_tag_attribute($tag, 'rel')));
                $as = strtolower(trim($this->extract_tag_attribute($tag, 'as')));

                if ($href === '' || $rel === '') {
                    continue;
                }

                if (strpos($rel, 'preconnect') !== false || strpos($rel, 'dns-prefetch') !== false) {
                    continue;
                }

                $score = 0;
                if (strpos($rel, 'stylesheet') !== false) {
                    $score = 50;
                } elseif (strpos($rel, 'preload') !== false) {
                    $score = in_array($as, ['font', 'style', 'script'], true) ? 48 : 30;
                }

                if ($score > 0) {
                    $this->add_candidate_score($scores, $href, $site_host, $score, $delayed_hosts);
                }
            }
        }

        if (preg_match_all('/<(script|iframe|img|source)\b[^>]*(?:src|poster|data-src)=([\'"])(.*?)\2[^>]*>/is', $html, $asset_matches, PREG_SET_ORDER) === 1 || !empty($asset_matches)) {
            foreach ($asset_matches as $match) {
                $tag_name = strtolower((string) ($match[1] ?? ''));
                $url = (string) ($match[3] ?? '');
                $score = match ($tag_name) {
                    'script' => 45,
                    'iframe' => 28,
                    'source', 'img' => 18,
                    default => 15,
                };

                $this->add_candidate_score($scores, $url, $site_host, $score, $delayed_hosts);
            }
        }

        if (preg_match_all('/<script\b[^>]*>(.*?)<\/script>/is', $html, $script_blocks) === 1 || !empty($script_blocks[1])) {
            foreach ($script_blocks[1] as $script) {
                if (!is_string($script) || trim($script) === '' || stripos($script, 'http') === false) {
                    continue;
                }

                if (preg_match_all('~https?:\/\/[^"\'<>\s)]+~i', $script, $url_matches) !== 1 || empty($url_matches[0])) {
                    continue;
                }

                foreach ($url_matches[0] as $url) {
                    if ($this->is_hintable_inline_script_url($url)) {
                        $this->add_candidate_score($scores, $url, $site_host, 42, $delayed_hosts);
                    }
                }
            }
        }

        return $scores;
    }

    /**
     * Add a candidate origin score if the URL is preconnect-worthy.
     *
     * @param array<string, int> $scores
     * @param string             $url
     * @param string             $site_host
     * @param int                $score
     * @param array<string, bool> $delayed_hosts
     * @return void
     */
    private function add_candidate_score(array &$scores, string $url, string $site_host, int $score, array $delayed_hosts = []): void {
        $host = $this->normalize_dynamic_hint_host($url, $site_host, $delayed_hosts);
        if ($host === null) {
            return;
        }

        if (!isset($scores[$host])) {
            $scores[$host] = 0;
        }

        $scores[$host] += max(1, $score);
    }

    /**
     * Normalize an external host candidate from a URL string.
     *
     * @param string $url
     * @param string $site_host
     * @param array<string, bool> $delayed_hosts
     * @return string|null
     */
    private function normalize_dynamic_hint_host(string $url, string $site_host, array $delayed_hosts = []): ?string {
        $url = html_entity_decode(trim($url), ENT_QUOTES | ENT_HTML5, 'UTF-8');
        if ($url === '' || str_starts_with($url, '#') || str_starts_with($url, 'data:') || str_starts_with($url, 'blob:') || str_starts_with($url, 'javascript:')) {
            return null;
        }

        if (str_starts_with($url, '//')) {
            $url = 'https:' . $url;
        }

        $host = strtolower((string) wp_parse_url($url, PHP_URL_HOST));
        if ($host === '' || $host === strtolower($site_host) || !$this->is_valid_host($host)) {
            return null;
        }

        if ($this->should_skip_dynamic_hint_host($host, $delayed_hosts)) {
            return null;
        }

        return $host;
    }

    /**
     * Detect delayed third-party origins that should not receive preconnect.
     *
     * DNS prefetch is still fine for these origins, but preconnect defeats part
     * of the point of interaction/load-delayed runtime ownership.
     *
     * @param string $html
     * @param string $site_host
     * @return array<string, bool>
     */
    private function extract_delayed_runtime_hosts(string $html, string $site_host): array {
        $hosts = [];

        if (
            preg_match_all(
                '/<script\b[^>]*type=(["\'])text\/prorank-delayed\1[^>]*\bsrc=(["\'])(.*?)\2[^>]*>/is',
                $html,
                $matches,
                PREG_SET_ORDER
            ) === 1 || !empty($matches)
        ) {
            foreach ($matches as $match) {
                $url = (string) ($match[3] ?? '');
                $host = $this->normalize_dynamic_hint_host($url, $site_host);
                if ($host !== null) {
                    $hosts[$host] = true;
                }
            }
        }

        if (
            stripos($html, 'data-prorank-3p-inline="gtm"') !== false ||
            stripos($html, "data-prorank-3p-inline='gtm'") !== false
        ) {
            $hosts['www.googletagmanager.com'] = true;
        }

        if (
            stripos($html, 'data-prorank-3p-inline="gtag"') !== false ||
            stripos($html, "data-prorank-3p-inline='gtag'") !== false
        ) {
            $hosts['www.googletagmanager.com'] = true;
        }

        if (
            stripos($html, 'data-prorank-3p-inline="facebook"') !== false ||
            stripos($html, "data-prorank-3p-inline='facebook'") !== false
        ) {
            $hosts['connect.facebook.net'] = true;
            $hosts['www.facebook.com'] = true;
            $hosts['facebook.com'] = true;
        }

        return $hosts;
    }

    /**
     * Whether a URL found inside inline scripts is worth preconnecting to.
     *
     * @param string $url
     * @return bool
     */
    private function is_hintable_inline_script_url(string $url): bool {
        $path = strtolower((string) wp_parse_url($url, PHP_URL_PATH));
        if ($path === '') {
            return false;
        }

        return (bool) preg_match('/\.(js|mjs|css|woff2?|ttf|otf|eot)(?:$|\?)/i', $path);
    }

    /**
     * Check whether a discovered host should be skipped.
     *
     * @param string $host
     * @param array<string, bool> $delayed_hosts
     * @return bool
     */
    private function should_skip_dynamic_hint_host(string $host, array $delayed_hosts = []): bool {
        if (isset($delayed_hosts[$host])) {
            return true;
        }

        if ($this->is_font_hint_managed_by_font_optimization($host)) {
            return true;
        }

        return false;
    }

    /**
     * Whether Google font origins are already managed by Font Optimization.
     *
     * When ProRank font optimization is active, it already emits font-origin
     * preconnect and dns-prefetch hints directly. Resource Hints should not
     * duplicate those same origins later in the page.
     *
     * @param string $host
     * @return bool
     */
    private function is_font_hint_managed_by_font_optimization(string $host): bool {
        if (!in_array($host, ['fonts.googleapis.com', 'fonts.gstatic.com'], true)) {
            return false;
        }

        if ($this->is_local_font_mode_enabled()) {
            return true;
        }

        $asset_settings = get_option('prorank_asset_optimization_settings', []);
        if (!is_array($asset_settings)) {
            $asset_settings = [];
        }

        return !empty($asset_settings['font_display_swap']);
    }

    /**
     * Extract existing preconnect and dns-prefetch hosts from HTML.
     *
     * @param string $html
     * @return array{preconnect: array<string, bool>, dns-prefetch: array<string, bool>}
     */
    private function extract_existing_hint_hosts(string $html): array {
        $existing = [
            'preconnect' => [],
            'dns-prefetch' => [],
        ];

        if (preg_match_all('/<link\b[^>]*>/i', $html, $matches) < 1 || empty($matches[0])) {
            return $existing;
        }

        foreach ($matches[0] as $tag) {
            $rel = strtolower(trim($this->extract_tag_attribute($tag, 'rel')));
            $href = $this->extract_tag_attribute($tag, 'href');
            if ($rel === '' || $href === '') {
                continue;
            }

            // Existing hints must count even when the same host would be skipped
            // for dynamic emission later, such as Google Fonts origins that are
            // already managed by the font optimization path.
            $host = $this->extract_hint_host($href);
            if ($host === null) {
                continue;
            }

            if (strpos($rel, 'preconnect') !== false) {
                $existing['preconnect'][$host] = true;
            }

            if (strpos($rel, 'dns-prefetch') !== false) {
                $existing['dns-prefetch'][$host] = true;
            }
        }

        return $existing;
    }

    /**
     * Extract an external host from an existing hint href without applying
     * dynamic-skip logic.
     *
     * @param string $href
     * @return string|null
     */
    private function extract_hint_host(string $href): ?string {
        $href = html_entity_decode(trim($href), ENT_QUOTES | ENT_HTML5, 'UTF-8');
        if ($href === '' || str_starts_with($href, '#') || str_starts_with($href, 'data:') || str_starts_with($href, 'blob:') || str_starts_with($href, 'javascript:')) {
            return null;
        }

        if (str_starts_with($href, '//')) {
            $href = 'https:' . $href;
        }

        $host = strtolower((string) wp_parse_url($href, PHP_URL_HOST));
        if ($host === '' || !$this->is_valid_host($host)) {
            return null;
        }

        return $host;
    }

    /**
     * Extract a quoted HTML attribute value.
     *
     * @param string $tag
     * @param string $attribute
     * @return string
     */
    private function extract_tag_attribute(string $tag, string $attribute): string {
        $pattern = '/\b' . preg_quote($attribute, '/') . '\s*=\s*([\'"])(.*?)\1/i';
        if (preg_match($pattern, $tag, $match) !== 1) {
            return '';
        }

        return (string) ($match[2] ?? '');
    }

    /**
     * Whether a host should use crossorigin on preconnect.
     *
     * @param string $host
     * @return bool
     */
    private function host_needs_crossorigin_preconnect(string $host): bool {
        return (bool) preg_match('/(?:^|\.)(gstatic\.com|googleapis\.com|fontawesome\.com|typekit\.net)$/i', $host);
    }

    /**
     * Normalize mixed setting values into booleans.
     *
     * @param mixed $value
     * @return bool
     */
    private function is_truthy_setting($value): bool {
        if (is_string($value)) {
            return in_array(strtolower($value), ['1', 'true', 'yes', 'on'], true);
        }

        return (bool) $value;
    }
    
    /**
     * Validate font URL
     *
     * @param string $url URL to validate
     * @return bool
     */
    private function is_valid_font_url(string $url): bool {
        // Check if it's a valid URL or relative path
        if (!filter_var($url, FILTER_VALIDATE_URL) && !preg_match('/^\/[^\/]/', $url)) {
            return false;
        }
        
        // Check if it has a font extension
        return preg_match('/\.(woff2?|ttf|otf|eot)$/i', $url) === 1;
    }
    
    /**
     * Check if URL is external
     *
     * @param string $url URL to check
     * @return bool
     */
    private function is_external_url(string $url): bool {
        $site_host = wp_parse_url(home_url(), PHP_URL_HOST);
        $url_host = wp_parse_url($url, PHP_URL_HOST);
        
        return $url_host && $url_host !== $site_host;
    }
    
    /**
     * Get font type from URL
     *
     * @param string $url Font URL
     * @return string Font type
     */
    private function get_font_type(string $url): string {
        if (preg_match('/\.woff2$/i', $url)) {
            return 'woff2';
        } elseif (preg_match('/\.woff$/i', $url)) {
            return 'woff';
        } elseif (preg_match('/\.ttf$/i', $url)) {
            return 'truetype';
        } elseif (preg_match('/\.otf$/i', $url)) {
            return 'opentype';
        } elseif (preg_match('/\.eot$/i', $url)) {
            return 'embedded-opentype';
        }
        
        return 'woff2'; // Default
    }
}
