<?php

/**
 * JavaScript Optimization Module
 *
 * Implements JavaScript optimization features using proper DOM manipulation.
 *
 * @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;
}


/**
 * JsOptimize module class
 *
 * Uses DOMDocument for reliable script tag manipulation instead of fragile string replacement.
 */


class JsOptimizeModule extends BaseModule {

    /**
     * Required tier for this module
     */
    protected string $feature_tier = 'free';

    /**
     * Excluded scripts cache
     *
     * @var array
     */
    private array $excluded_scripts = [];

    /**
     * Delay load script added flag
     *
     * @var bool
     */
    private bool $delay_script_added = false;

    /**
     * Scripts collected for combining
     *
     * @var array<string, array{handle: string, src: string, path: string}>
     */
    private array $combinable_scripts = [];

    /**
     * Combined scripts output guard
     *
     * @var bool
     */
    private bool $combined_output_done = false;

    /**
     * Cache directory for combined JS files
     *
     * @var string
     */
    private string $combine_cache_dir = '';

    /**
     * Cache URL base for combined JS files
     *
     * @var string
     */
    private string $combine_cache_url = '';

    /**
     * Scripts that must never be combined (order-sensitive or critical)
     */
    private const COMBINE_EXCLUDE = [
        'jquery',
        'jquery-core',
        'jquery-migrate',
        'wp-polyfill',
        'regenerator-runtime',
        'wp-hooks',
        'wp-i18n',
        'react',
        'react-dom',
        'wp-element',
        'wp-components',
        'lodash',
    ];

    /**
     * Constructor
     */
    public function __construct() {
        $this->slug = 'js_opt';
        $this->name = 'JavaScript Optimization';
        $this->description = 'Optimize JavaScript loading for better performance';
$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;
        }

        // Set up combining cache if JS combine is enabled
        if ($this->is_combine_enabled()) {
            $this->setup_combine_cache();
            add_action('prorank_clear_cache', [$this, 'clear_combine_cache']);
            add_action('switch_theme', [$this, 'clear_combine_cache']);
        }

        // Hook into script loading
        add_filter('script_loader_tag', [$this, 'optimize_script_tag'], 10, 3);
        add_filter('script_loader_tag', [$this, 'enforce_safe_script_tag'], 9999, 3);

        // Output combined scripts in footer
        if ($this->is_combine_enabled()) {
            add_action('wp_footer', [$this, 'output_combined_scripts'], 9999);
        }

        // Some plugins output <script ...> tags directly (no WP handle), so
        // `script_loader_tag` never runs. Route those through ProRank's
        // centralized buffer instead of opening a second full-page buffer.
        if ($this->is_delay_enabled() || $this->get_setting('js_defer', false)) {
            Buffer::init();
            add_filter('prorank_output_buffer', [$this, 'delay_non_enqueued_module_scripts'], 20);
        } elseif ($this->is_tracker_delay_enabled()) {
            Buffer::init();
            add_filter('prorank_output_buffer', [$this, 'delay_non_enqueued_tracker_scripts'], 20);
        }

        // Add delay load script if needed (no longer requires beta flag)
        if ($this->is_delay_enabled() || $this->is_tracker_delay_enabled()) {
            // Add WP namespace shim before anything else to capture wp.i18n/wp.hooks
            // calls from deferred scripts whose dependencies are delayed
            add_action('wp_head', [$this, 'add_wp_namespace_shim'], 0);

            // Add jQuery facade early in head to capture calls before jQuery loads
            add_action('wp_head', [$this, 'add_jquery_facade'], 1);
            add_action('wp_footer', [$this, 'add_delay_load_script'], 1);
        }
    }

    /**
     * Start full page output buffering to delay non-enqueued module scripts.
     *
     * @return void
     */
    public function start_full_page_delay_buffer(): void {
        if (!$this->is_delay_enabled() && !$this->is_tracker_delay_enabled() && !$this->get_setting('js_defer', false)) {
            return;
        }

        // Only buffer real frontend HTML responses.
        if (wp_doing_ajax() || wp_doing_cron() || is_feed() || is_robots() || is_trackback()) {
            return;
        }

        ob_start([$this, 'delay_non_enqueued_module_scripts']);
    }

    /**
     * Delay selected non-enqueued scripts output directly in HTML.
     *
     * This covers:
     * - Known heavy third-party module scripts (SureCart)
     * - Third-party tracking scripts injected outside WP enqueue
     * - Inline tracker bootstraps (GTM/Pixel/analytics loaders)
     *
     * @param string $html Full page HTML.
     * @return string Modified HTML.
     */
    public function delay_non_enqueued_module_scripts(string $html): string {
        if ($html === '' || stripos($html, '<script') === false) {
            return $html;
        }

        $original_html = $html;

        if (!$this->should_delay_third_party_modules_on_this_request()) {
            return $html;
        }

        // If no captcha widget markup exists on the page, remove standalone
        // captcha API boot scripts entirely to avoid downloading large unused JS.
        $has_captcha_markup = $this->page_has_captcha_markup($html);

        $match_count = preg_match_all('/<script\\b([^>]*)>(.*?)<\\/script>/is', $html, $matches, PREG_SET_ORDER);
        if ($match_count < 1 || empty($matches)) {
            return $html;
        }

        $replacements = [];
        foreach ($matches as $match) {
            $tag = (string) ($match[0] ?? '');
            $attrs = (string) ($match[1] ?? '');
            $content = (string) ($match[2] ?? '');
            if ($tag === '') {
                continue;
            }

            $replacement = $this->transform_non_enqueued_script_tag($tag, $attrs, $content, $has_captcha_markup);
            if ($replacement !== $tag) {
                $replacements[$tag] = $replacement;
            }
        }

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

        $updated_html = strtr($html, $replacements);

        return Buffer::guard_structural_containers($original_html, $updated_html, 'js_optimize');
    }

    /**
     * Delay only low-risk non-enqueued tracker scripts.
     *
     * This is intentionally narrower than the full module-script path so the
     * tracker-delay setting can be used without pulling in broader JS mutation.
     *
     * @param string $html
     * @return string
     */
    public function delay_non_enqueued_tracker_scripts(string $html): string {
        if ($html === '' || stripos($html, '<script') === false || !$this->is_tracker_delay_enabled()) {
            return $html;
        }

        $original_html = $html;

        $match_count = preg_match_all('/<script\\b([^>]*)>(.*?)<\\/script>/is', $html, $matches, PREG_SET_ORDER);
        if ($match_count < 1 || empty($matches)) {
            return $html;
        }

        $replacements = [];
        foreach ($matches as $match) {
            $tag = (string) ($match[0] ?? '');
            $attrs = (string) ($match[1] ?? '');
            $content = (string) ($match[2] ?? '');
            if ($tag === '') {
                continue;
            }

            $replacement = $this->transform_non_enqueued_tracker_script_tag($tag, $attrs, $content);
            if ($replacement !== $tag) {
                $replacements[$tag] = $replacement;
            }
        }

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

        $updated_html = strtr($html, $replacements);

        return Buffer::guard_structural_containers($original_html, $updated_html, 'js_optimize');
    }

    /**
     * Transform a raw non-enqueued script tag when it qualifies for defer/delay.
     *
     * @param string $tag
     * @param string $attrs
     * @param string $content
     * @param bool   $has_captcha_markup
     * @return string
     */
    private function transform_non_enqueued_script_tag(string $tag, string $attrs, string $content, bool $has_captcha_markup): string {
        // Skip if already delayed (by us or another pass).
        if (stripos($attrs, 'data-prorank-delay') !== false || stripos($attrs, 'type="text/prorank-delayed"') !== false) {
            return $tag;
        }

        $src = '';
        if (preg_match('/\\bsrc\\s*=\\s*([\"\\\'])([^\"\\\']+)\\1/i', $attrs, $srcMatch)) {
            $src = html_entity_decode((string) ($srcMatch[2] ?? ''), ENT_QUOTES | ENT_HTML5, 'UTF-8');
        }

        // Strip captcha API script when no captcha field/widget exists
        // in rendered markup for this page.
        if (
            $src !== ''
            && $this->is_captcha_non_enqueued_script(strtolower($src))
            && !$has_captcha_markup
        ) {
            return '';
        }

        // Non-enqueued module scripts (SureCart) that are safe to delay.
        if (($this->is_delay_enabled() || $this->should_use_interaction_delay_for_tracker_haystack(strtolower(trim($src . ' ' . $attrs)))) && $src !== '' && preg_match('/\\btype\\s*=\\s*([\"\\\']?)module\\1/i', $attrs)) {
            if (
                stripos($src, '/wp-content/plugins/surecart/') !== false &&
                preg_match('~/(?:cart|checkout|line-item-note)/index\\.js~i', $src)
            ) {
                return $this->convert_non_enqueued_script_tag_to_delayed($tag, 'module');
            }

            return $tag;
        }

        // Non-enqueued consent/captcha scripts: make non-blocking via defer,
        // but do not push to interaction-based delay.
        if ($src !== '' && $this->should_defer_non_enqueued_external_script($src, $attrs)) {
            return $this->add_defer_to_non_enqueued_script_tag($tag);
        }

        // Third-party external tracker scripts injected outside WP enqueue.
        if (($this->is_delay_enabled() || $this->should_use_interaction_delay_for_tracker_haystack(strtolower(trim($src . ' ' . $attrs)))) && $src !== '' && $this->should_delay_non_enqueued_external_script($src, $attrs)) {
            return $this->convert_non_enqueued_script_tag_to_delayed($tag, 'text/javascript');
        }

        // Inline tracker bootstraps (GTM/Pixel/etc.) injected in head.
        if (($this->is_delay_enabled() || $this->should_use_interaction_delay_for_tracker_haystack(strtolower($content . ' ' . $attrs))) && $src === '' && $this->should_delay_non_enqueued_inline_script($content, $attrs)) {
            return $this->convert_non_enqueued_script_tag_to_delayed($tag, 'text/javascript');
        }

        return $tag;
    }

    /**
     * Transform only known tracker scripts in raw HTML.
     *
     * @param string $tag
     * @param string $attrs
     * @param string $content
     * @return string
     */
    private function transform_non_enqueued_tracker_script_tag(string $tag, string $attrs, string $content): string {
        if (stripos($attrs, 'data-prorank-delay') !== false || stripos($attrs, 'type="text/prorank-delayed"') !== false) {
            return $tag;
        }

        $src = '';
        if (preg_match('/\\bsrc\\s*=\\s*([\"\\\'])([^\"\\\']+)\\1/i', $attrs, $srcMatch)) {
            $src = html_entity_decode((string) ($srcMatch[2] ?? ''), ENT_QUOTES | ENT_HTML5, 'UTF-8');
        }

        if ($src !== '') {
            if ($this->should_use_interaction_delay_for_tracker_haystack(strtolower(trim($src . ' ' . $attrs))) && $this->should_delay_non_enqueued_external_script($src, $attrs)) {
                return $this->convert_non_enqueued_script_tag_to_delayed($tag, 'text/javascript');
            }

            return $tag;
        }

        if ($this->should_use_interaction_delay_for_tracker_haystack(strtolower($content . ' ' . $attrs)) && $this->should_delay_non_enqueued_inline_script($content, $attrs)) {
            return $this->convert_non_enqueued_script_tag_to_delayed($tag, 'text/javascript');
        }

        return $tag;
    }

    /**
     * Detect whether rendered HTML contains captcha widget markup.
     *
     * @param string $html
     * @return bool
     */
    private function page_has_captcha_markup(string $html): bool {
        $markers = [
            'g-recaptcha',
            'grecaptcha-badge',
            'data-sitekey=',
            'elementor-field-type-recaptcha',
            'wpcf7-recaptcha',
            'h-captcha',
            'data-hcaptcha',
        ];

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

        return false;
    }

    /**
     * Decide whether to delay third-party module scripts on this request.
     *
     * We avoid delaying on likely checkout/cart URLs to prevent breaking payment flows.
     *
     * @return bool
     */
    private function should_delay_third_party_modules_on_this_request(): bool {
        $path = (string) (wp_parse_url((string) ($_SERVER['REQUEST_URI'] ?? ''), PHP_URL_PATH) ?? '');
        if ($path === '') {
            return true;
        }

        if (preg_match('~/(?:checkout|cart)(?:/|$)~i', $path)) {
            return false;
        }

        return true;
    }

    /**
     * Convert a raw script tag to ProRank delayed script format.
     *
     * @param string $tag          Original script tag HTML.
     * @param string $originalType Original script type value.
     * @return string
     */
    private function convert_non_enqueued_script_tag_to_delayed(string $tag, string $originalType = 'text/javascript'): string {
        $normalizedType = strtolower(trim($originalType));
        if ($normalizedType === '') {
            $normalizedType = 'text/javascript';
        }

        $interaction_only = $this->should_use_interaction_delay_for_tracker_haystack(strtolower($tag))
            || $this->is_captcha_non_enqueued_script(strtolower($tag));

        if (preg_match('/\\btype\\s*=\\s*([\"\\\'])([^\"\\\']+)\\1/i', $tag, $typeMatch)) {
            $declaredType = strtolower(trim((string) ($typeMatch[2] ?? '')));
            if ($declaredType !== '' && $declaredType !== 'text/prorank-delayed') {
                $normalizedType = $declaredType;
            }

            $tag = preg_replace(
                '/\\btype\\s*=\\s*([\"\\\'])([^\"\\\']+)\\1/i',
                'type="text/prorank-delayed" data-prorank-type="' . esc_attr($normalizedType) . '"',
                $tag,
                1
            );
        } else {
            $tag = preg_replace(
                '/<script\\b/i',
                '<script type="text/prorank-delayed" data-prorank-type="' . esc_attr($normalizedType) . '"',
                $tag,
                1
            );
        }

        if (stripos($tag, 'data-prorank-delay') === false) {
            $tag = preg_replace(
                '/<script\\b/i',
                '<script data-prorank-delay="true" data-cfasync="false" data-no-optimize="1" data-no-defer="1" data-no-minify="1"',
                $tag,
                1
            );
        }

        if ($interaction_only && stripos($tag, 'data-prorank-delay-interaction') === false) {
            $tag = preg_replace(
                '/<script\\b/i',
                '<script data-prorank-delay-interaction="1"',
                $tag,
                1
            );
        }

        return $tag;
    }

    /**
     * Determine whether a non-enqueued external script is safe and useful to delay.
     *
     * @param string $src
     * @param string $attrs
     * @return bool
     */
    private function should_delay_non_enqueued_external_script(string $src, string $attrs): bool {
        $haystack = strtolower(trim($src . ' ' . $attrs));
        if ($haystack === '') {
            return false;
        }

        // Captcha bundles are large and usually unused until user interaction.
        // Delay them on normal pages, but avoid interaction-delay on high-risk
        // auth/commerce endpoints.
        if ($this->is_captcha_non_enqueued_script($haystack)) {
            return !$this->should_avoid_captcha_delay_on_request();
        }

        if ($this->is_non_enqueued_script_high_risk($haystack)) {
            return false;
        }

        return $this->is_low_risk_tracker_haystack($haystack);
    }

    /**
     * Determine whether a non-enqueued external script should be deferred (not delayed).
     *
     * @param string $src
     * @param string $attrs
     * @return bool
     */
    private function should_defer_non_enqueued_external_script(string $src, string $attrs): bool {
        $haystack = strtolower(trim($src . ' ' . $attrs));
        if ($haystack === '') {
            return false;
        }

        $governance = $this->get_third_party_governance_settings();

        // For captcha scripts:
        // - when delay is enabled and request is safe: let delay path handle them
        // - otherwise: keep non-blocking via defer
        if ($this->is_captcha_non_enqueued_script($haystack)) {
            if (!$this->is_delay_enabled()) {
                return true;
            }

            return $this->should_avoid_captcha_delay_on_request();
        }

        if (!$governance['defer_consent_scripts']) {
            return false;
        }

        return $this->is_consent_script_haystack($haystack);
    }

    /**
     * Detect captcha scripts in non-enqueued HTML script tags.
     *
     * @param string $haystack
     * @return bool
     */
    private function is_captcha_non_enqueued_script(string $haystack): bool {
        return
            strpos($haystack, 'recaptcha/api.js') !== false ||
            strpos($haystack, 'google.com/recaptcha') !== false ||
            strpos($haystack, 'gstatic.com/recaptcha') !== false ||
            strpos($haystack, 'grecaptcha') !== false ||
            strpos($haystack, 'hcaptcha.com') !== false ||
            strpos($haystack, 'hcaptcha') !== false;
    }

    /**
     * High-risk request paths where captcha should not wait for interaction delay.
     *
     * @return bool
     */
    private function should_avoid_captcha_delay_on_request(): bool {
        $uri = strtolower((string) ($_SERVER['REQUEST_URI'] ?? ''));
        if ($uri === '') {
            return false;
        }

        $high_risk_paths = [
            'wp-login.php',
            '/checkout',
            '/my-account',
            '/wc-api',
            '/cart',
            'action=register',
            'action=lostpassword',
        ];

        foreach ($high_risk_paths as $needle) {
            if (strpos($uri, $needle) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Add defer to a non-enqueued external script tag, unless it already has async/defer.
     *
     * @param string $tag
     * @return string
     */
    private function add_defer_to_non_enqueued_script_tag(string $tag): string {
        $lowerTag = strtolower($tag);

        if (
            strpos($lowerTag, ' defer') !== false ||
            strpos($lowerTag, ' async') !== false ||
            strpos($lowerTag, 'data-prorank-delay') !== false ||
            strpos($lowerTag, 'text/prorank-delayed') !== false
        ) {
            return $tag;
        }

        return (string) preg_replace('/<script\\b/i', '<script defer data-prorank-defer="true"', $tag, 1);
    }

    /**
     * Determine whether a non-enqueued inline script is a tracking bootstrap.
     *
     * @param string $content
     * @param string $attrs
     * @return bool
     */
    private function should_delay_non_enqueued_inline_script(string $content, string $attrs): bool {
        $attrsLower = strtolower($attrs);
        if (
            strpos($attrsLower, 'application/ld+json') !== false ||
            strpos($attrsLower, 'application/json') !== false ||
            strpos($attrsLower, 'text/template') !== false ||
            strpos($attrsLower, 'importmap') !== false ||
            strpos($attrsLower, 'speculationrules') !== false
        ) {
            return false;
        }

        $contentLower = strtolower($content);
        if ($contentLower === '') {
            return false;
        }

        if ($this->is_non_enqueued_script_high_risk($contentLower)) {
            return false;
        }

        return $this->is_low_risk_tracker_haystack($contentLower);
    }

    /**
     * Safety filter for non-enqueued scripts that should never be delayed globally.
     *
     * @param string $haystack
     * @return bool
     */
    private function is_non_enqueued_script_high_risk(string $haystack): bool {
        $patterns = [
            'recaptcha',
            'grecaptcha',
            'hcaptcha',
            'maps.googleapis.com',
            'maps.gstatic.com',
            'cookieyes',
            'cookie-law-info',
            'complianz',
            'borlabs',
            'real-cookie-banner',
            'moove-gdpr',
            'stripe',
            'paypal',
            'klarna',
            'braintree',
            'squareup',
            'woocommerce-checkout',
            '/checkout',
            '/cart',
        ];

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

        return false;
    }

    /**
     * Add WP namespace shim to capture wp.i18n/wp.hooks calls
     *
     * When WP core scripts (wp-hooks, wp-i18n, wp-api-fetch, etc.) are delayed,
     * deferred scripts with inline translations still try to call wp.i18n.setLocaleData()
     * synchronously. This shim provides a stub wp namespace that captures those calls
     * and replays them when the real packages load.
     *
     * @return void
     */
    public function add_wp_namespace_shim(): void {
        ?>
        <script id="prorank-wp-shim">
        (function(w) {
            if (w.wp && w.wp.i18n && !w.wp.i18n._prorank_shim) return;
            var q = { locale: [], hooks: [] };
            w.wp = w.wp || {};

            // i18n shim — captures setLocaleData, passes through translation functions
            w.wp.i18n = {
                _prorank_shim: true,
                setLocaleData: function(data, domain) { q.locale.push([data, domain]); },
                __: function(text) { return text; },
                _x: function(text) { return text; },
                _n: function(single, plural, number) { return number === 1 ? single : plural; },
                _nx: function(single, plural, number) { return number === 1 ? single : plural; },
                sprintf: function() {
                    var args = arguments, fmt = args[0] || '';
                    return fmt.replace(/%[sd]/g, function() { return args[1] || ''; });
                },
                isRTL: function() { return false; },
                getLocaleData: function() { return {}; },
                subscribe: function() { return function() {}; },
                resetLocaleData: function() {}
            };

            // hooks shim — captures addFilter/addAction, no-ops everything else
            w.wp.hooks = w.wp.hooks || {
                addFilter: function() { q.hooks.push(['addFilter', Array.prototype.slice.call(arguments)]); },
                addAction: function() { q.hooks.push(['addAction', Array.prototype.slice.call(arguments)]); },
                removeFilter: function() {},
                removeAction: function() {},
                removeAllFilters: function() {},
                removeAllActions: function() {},
                applyFilters: function(name, value) { return value; },
                doAction: function() {},
                doingFilter: function() { return false; },
                doingAction: function() { return false; },
                didFilter: function() { return 0; },
                didAction: function() { return 0; },
                hasFilter: function() { return false; },
                hasAction: function() { return false; },
                currentFilter: function() { return null; },
                currentAction: function() { return null; }
            };

            // Store queue for replay by the delay loader
            w._prorank_wp_shim_queue = q;
        })(window);
        </script>
        <?php
    }

    /**
     * Add jQuery facade to capture calls before real jQuery loads
     * This allows jQuery to be delayed while maintaining compatibility
     *
     * Inspired by Perfmatters: Also captures clicks and events for replay
     * Enhanced with jQuery.fn.extend interception
     *
     * @return void
     */
    public function add_jquery_facade(): void {
        ?>
        <script id="prorank-jquery-facade">
        (function(w, d) {
            'use strict';
            if (w.jQuery && !w.jQuery._prorank_facade) return; // Real jQuery already loaded

            // Queues for various callbacks
            var readyQueue = [];
            var eventQueue = []; // Store {selector, event, handler} for .on() calls
            var clickQueue = []; // Store click events to replay
            var extendQueue = []; // Store jQuery.fn.extend calls for replay
            var ajaxPrefilterQueue = []; // Store [dataTypes, handler] for $.ajaxPrefilter
            var ajaxSetupQueue = []; // Store options objects for $.ajaxSetup
            var ajaxTransportQueue = []; // Store [dataTypes, handler] for $.ajaxTransport

            w._prorank_ready_queue = readyQueue;
            w._prorank_event_queue = eventQueue;
            w._prorank_click_queue = clickQueue;
            w._prorank_extend_queue = extendQueue;
            w._prorank_ajax_prefilter_queue = ajaxPrefilterQueue;
            w._prorank_ajax_setup_queue = ajaxSetupQueue;
            w._prorank_ajax_transport_queue = ajaxTransportQueue;
            w._prorank_scripts_loaded = false;

            // Intercept addEventListener to capture early event bindings
            var originalAddEventListener = EventTarget.prototype.addEventListener;
            var capturedListeners = [];
            w._prorank_captured_listeners = capturedListeners;

            EventTarget.prototype.addEventListener = function(type, listener, options) {
                // If scripts haven't loaded yet and this is an interaction event, capture it
                if (!w._prorank_scripts_loaded &&
                    ['click', 'submit', 'change', 'input', 'focus', 'blur'].indexOf(type) > -1) {
                    capturedListeners.push({
                        target: this,
                        type: type,
                        listener: listener,
                        options: options
                    });
                }
                return originalAddEventListener.call(this, type, listener, options);
            };

            // Capture clicks on the document for replay (with outerHTML verification - Perfmatters pattern)
            d.addEventListener('click', function(e) {
                if (!w._prorank_scripts_loaded && e.target) {
                    clickQueue.push({
                        target: e.target,
                        outerHTML: e.target.outerHTML, // For verification during replay
                        x: e.clientX,
                        y: e.clientY,
                        timestamp: Date.now()
                    });
                }
            }, true);

            function hasRealJquery() {
                return w.jQuery && !w.jQuery._prorank_facade;
            }

            function queueReady(fn) {
                if (typeof fn !== 'function') return;
                if (hasRealJquery()) {
                    try {
                        w.jQuery(fn);
                        return;
                    } catch(e) {}
                }
                readyQueue.push(fn);
            }

            function bindQueuedEvent(item) {
                if (!item || !item.events) return;
                var ctx = (item.context === undefined || item.context === null || item.context === '') ? d : item.context;
                var $ctx = w.jQuery(ctx);
                var method = item.once ? 'one' : 'on';

                if (item.selector) {
                    $ctx[method](item.events, item.selector, item.handler);
                } else if (item.handler) {
                    $ctx[method](item.events, item.handler);
                }
            }

            function queueOrBindEvent(item) {
                if (!item || !item.events) return;
                if (hasRealJquery()) {
                    try {
                        bindQueuedEvent(item);
                        return;
                    } catch(e) {}
                }
                eventQueue.push(item);
            }

            function replayAjaxQueues() {
                if (!hasRealJquery()) return false;

                if (ajaxSetupQueue.length > 0 && typeof w.jQuery.ajaxSetup === 'function') {
                    var setupItems = ajaxSetupQueue.slice();
                    ajaxSetupQueue.length = 0;
                    setupItems.forEach(function(opts) {
                        try { w.jQuery.ajaxSetup(opts || {}); } catch(e) {}
                    });
                }

                if (ajaxPrefilterQueue.length > 0 && typeof w.jQuery.ajaxPrefilter === 'function') {
                    var prefilterItems = ajaxPrefilterQueue.slice();
                    ajaxPrefilterQueue.length = 0;
                    prefilterItems.forEach(function(item) {
                        try {
                            w.jQuery.ajaxPrefilter(item.dataTypes || '*', item.handler);
                        } catch(e) {}
                    });
                }

                if (ajaxTransportQueue.length > 0 && typeof w.jQuery.ajaxTransport === 'function') {
                    var transportItems = ajaxTransportQueue.slice();
                    ajaxTransportQueue.length = 0;
                    transportItems.forEach(function(item) {
                        try {
                            w.jQuery.ajaxTransport(item.dataTypes || '*', item.handler);
                        } catch(e) {}
                    });
                }

                return true;
            }

            // Create chainable jQuery-like object
            function createChainable(selector) {
                var obj = {
                    selector: selector,
                    length: 0,
                    ready: function(fn) {
                        queueReady(fn);
                        return this;
                    },
                    on: function(events, selector, handler) {
                        if (typeof selector === 'function') {
                            handler = selector;
                            selector = null;
                        }
                        queueOrBindEvent({
                            events: events,
                            selector: selector,
                            handler: handler,
                            context: this.selector
                        });
                        return this;
                    },
                    off: function() { return this; },
                    one: function(events, selector, handler) {
                        if (typeof selector === 'function') {
                            handler = selector;
                            selector = null;
                        }
                        queueOrBindEvent({
                            events: events,
                            selector: selector,
                            handler: handler,
                            context: this.selector,
                            once: true
                        });
                        return this;
                    },
                    trigger: function() { return this; },
                    triggerHandler: function() { return this; },
                    click: function(fn) {
                        if (fn) queueOrBindEvent({ events: 'click', handler: fn, context: this.selector });
                        return this;
                    },
                    submit: function(fn) {
                        if (fn) queueOrBindEvent({ events: 'submit', handler: fn, context: this.selector });
                        return this;
                    },
                    focus: function(fn) {
                        if (fn) queueOrBindEvent({ events: 'focus', handler: fn, context: this.selector });
                        return this;
                    },
                    blur: function(fn) {
                        if (fn) queueOrBindEvent({ events: 'blur', handler: fn, context: this.selector });
                        return this;
                    },
                    change: function(fn) {
                        if (fn) queueOrBindEvent({ events: 'change', handler: fn, context: this.selector });
                        return this;
                    },
                    keydown: function(fn) {
                        if (fn) queueOrBindEvent({ events: 'keydown', handler: fn, context: this.selector });
                        return this;
                    },
                    keyup: function(fn) {
                        if (fn) queueOrBindEvent({ events: 'keyup', handler: fn, context: this.selector });
                        return this;
                    },
                    mouseenter: function(fn) {
                        if (fn) queueOrBindEvent({ events: 'mouseenter', handler: fn, context: this.selector });
                        return this;
                    },
                    mouseleave: function(fn) {
                        if (fn) queueOrBindEvent({ events: 'mouseleave', handler: fn, context: this.selector });
                        return this;
                    },
                    scroll: function(fn) {
                        if (fn) queueOrBindEvent({ events: 'scroll', handler: fn, context: this.selector });
                        return this;
                    },
                    resize: function(fn) {
                        if (fn) queueOrBindEvent({ events: 'resize', handler: fn, context: this.selector });
                        return this;
                    },
                    load: function(fn) {
                        if (typeof fn === 'function') queueOrBindEvent({ events: 'load', handler: fn, context: this.selector });
                        return this;
                    },
                    hover: function(fnIn, fnOut) {
                        if (fnIn) queueOrBindEvent({ events: 'mouseenter', handler: fnIn, context: this.selector });
                        if (fnOut) queueOrBindEvent({ events: 'mouseleave', handler: fnOut, context: this.selector });
                        return this;
                    },
                    each: function(fn) { return this; },
                    map: function(fn) { return this; },
                    filter: function() { return this; },
                    not: function() { return this; },
                    is: function() { return false; },
                    has: function() { return this; },
                    eq: function() { return this; },
                    first: function() { return this; },
                    last: function() { return this; },
                    slice: function() { return this; },
                    addClass: function() { return this; },
                    removeClass: function() { return this; },
                    toggleClass: function() { return this; },
                    hasClass: function() { return false; },
                    css: function(prop) {
                        if (typeof prop === 'string') return '';
                        return this;
                    },
                    attr: function(name) {
                        if (arguments.length === 1) return '';
                        return this;
                    },
                    removeAttr: function() { return this; },
                    prop: function(name) {
                        if (arguments.length === 1) return undefined;
                        return this;
                    },
                    removeProp: function() { return this; },
                    data: function(key) {
                        if (arguments.length <= 1) return undefined;
                        return this;
                    },
                    removeData: function() { return this; },
                    find: function() { return createChainable(); },
                    parent: function() { return this; },
                    parents: function() { return this; },
                    parentsUntil: function() { return this; },
                    children: function() { return this; },
                    siblings: function() { return this; },
                    closest: function() { return this; },
                    next: function() { return this; },
                    nextAll: function() { return this; },
                    prev: function() { return this; },
                    prevAll: function() { return this; },
                    contents: function() { return this; },
                    add: function() { return this; },
                    end: function() { return this; },
                    html: function(val) {
                        if (arguments.length === 0) return '';
                        return this;
                    },
                    text: function(val) {
                        if (arguments.length === 0) return '';
                        return this;
                    },
                    val: function(val) {
                        if (arguments.length === 0) return '';
                        return this;
                    },
                    append: function() { return this; },
                    prepend: function() { return this; },
                    after: function() { return this; },
                    before: function() { return this; },
                    appendTo: function() { return this; },
                    prependTo: function() { return this; },
                    insertAfter: function() { return this; },
                    insertBefore: function() { return this; },
                    wrap: function() { return this; },
                    wrapAll: function() { return this; },
                    wrapInner: function() { return this; },
                    unwrap: function() { return this; },
                    clone: function() { return this; },
                    remove: function() { return this; },
                    detach: function() { return this; },
                    empty: function() { return this; },
                    replaceWith: function() { return this; },
                    replaceAll: function() { return this; },
                    show: function() { return this; },
                    hide: function() { return this; },
                    toggle: function() { return this; },
                    fadeIn: function(dur, cb) { if (cb) cb(); return this; },
                    fadeOut: function(dur, cb) { if (cb) cb(); return this; },
                    fadeTo: function() { return this; },
                    fadeToggle: function() { return this; },
                    slideUp: function(dur, cb) { if (cb) cb(); return this; },
                    slideDown: function(dur, cb) { if (cb) cb(); return this; },
                    slideToggle: function() { return this; },
                    animate: function(props, dur, ease, cb) {
                        if (typeof cb === 'function') cb();
                        else if (typeof ease === 'function') ease();
                        else if (typeof dur === 'function') dur();
                        return this;
                    },
                    stop: function() { return this; },
                    delay: function() { return this; },
                    queue: function() { return this; },
                    dequeue: function() { return this; },
                    clearQueue: function() { return this; },
                    width: function(val) {
                        if (arguments.length === 0) return 0;
                        return this;
                    },
                    height: function(val) {
                        if (arguments.length === 0) return 0;
                        return this;
                    },
                    innerWidth: function() { return 0; },
                    innerHeight: function() { return 0; },
                    outerWidth: function() { return 0; },
                    outerHeight: function() { return 0; },
                    offset: function() { return { top: 0, left: 0 }; },
                    position: function() { return { top: 0, left: 0 }; },
                    scrollTop: function(val) {
                        if (arguments.length === 0) return 0;
                        return this;
                    },
                    scrollLeft: function(val) {
                        if (arguments.length === 0) return 0;
                        return this;
                    },
                    serialize: function() { return ''; },
                    serializeArray: function() { return []; },
                    get: function(i) { return undefined; },
                    index: function() { return -1; },
                    toArray: function() { return []; },
                    promise: function() {
                        return { done: function(fn) { fn(); return this; }, then: function(fn) { fn(); return this; } };
                    }
                };
                return obj;
            }

            // jQuery facade
            var jq = function(selector) {
                // If real jQuery is now loaded, proxy calls for scripts that captured the facade reference.
                if (hasRealJquery() && w.jQuery !== jq) {
                    return w.jQuery.apply(w, arguments);
                }

                if (typeof selector === 'function') {
                    queueReady(selector);
                    return jq;
                }
                return createChainable(selector);
            };

            // Static methods
            jq.ready = function(fn) {
                queueReady(fn);
                return jq;
            };

            // jQuery.fn with extend support (Perfmatters pattern)
            jq.fn = createChainable();
            jq.fn.extend = function(obj) {
                // Queue the extend call for replay when real jQuery loads
                if (obj) {
                    if (hasRealJquery()) {
                        try {
                            w.jQuery.fn.extend(obj);
                        } catch(e) {}
                    } else {
                        extendQueue.push(obj);
                    }
                    // Also add methods to facade for immediate chainability
                    for (var key in obj) {
                        if (obj.hasOwnProperty(key) && typeof obj[key] === 'function') {
                            jq.fn[key] = function() { return this; };
                        }
                    }
                }
                return this;
            };
            jq.fn.jquery = '3.7.1-prorank-facade';
            jq.fn.init = jq;

            // jQuery.extend
            jq.extend = function(deep, target) {
                var args = Array.prototype.slice.call(arguments);
                if (typeof deep === 'boolean') {
                    target = args[1] || {};
                    args = args.slice(2);
                } else {
                    target = deep || {};
                    args = args.slice(1);
                }
                args.forEach(function(src) {
                    if (src) {
                        for (var key in src) {
                            if (src.hasOwnProperty(key)) {
                                target[key] = src[key];
                            }
                        }
                    }
                });
                return target;
            };

            // Utility methods
            jq.noConflict = function() { return jq; };
            jq.ajax = function(opts) {
                return {
                    done: function(fn) { return this; },
                    fail: function(fn) { return this; },
                    always: function(fn) { return this; },
                    then: function(fn) { return this; },
                    success: function(fn) { return this; },
                    error: function(fn) { return this; }
                };
            };
            jq.get = jq.post = jq.getJSON = jq.getScript = jq.ajax;
            jq.ajaxSetup = function(options) {
                if (hasRealJquery() && typeof w.jQuery.ajaxSetup === 'function') {
                    try {
                        w.jQuery.ajaxSetup(options || {});
                    } catch(e) {}
                    return jq;
                }
                ajaxSetupQueue.push(options || {});
                return jq;
            };
            jq.ajaxPrefilter = function(dataTypes, handler) {
                if (typeof dataTypes === 'function') {
                    handler = dataTypes;
                    dataTypes = '*';
                }

                if (hasRealJquery() && typeof w.jQuery.ajaxPrefilter === 'function') {
                    try {
                        w.jQuery.ajaxPrefilter(dataTypes || '*', handler);
                    } catch(e) {}
                    return jq;
                }

                if (typeof handler === 'function') {
                    ajaxPrefilterQueue.push({
                        dataTypes: dataTypes || '*',
                        handler: handler
                    });
                }
                return jq;
            };
            jq.ajaxTransport = function(dataTypes, handler) {
                if (typeof dataTypes === 'function') {
                    handler = dataTypes;
                    dataTypes = '*';
                }

                if (hasRealJquery() && typeof w.jQuery.ajaxTransport === 'function') {
                    try {
                        w.jQuery.ajaxTransport(dataTypes || '*', handler);
                    } catch(e) {}
                    return jq;
                }

                if (typeof handler === 'function') {
                    ajaxTransportQueue.push({
                        dataTypes: dataTypes || '*',
                        handler: handler
                    });
                }
                return jq;
            };
            jq.each = function(arr, fn) {
                if (arr && arr.length !== undefined) {
                    for (var i = 0; i < arr.length; i++) {
                        if (fn.call(arr[i], i, arr[i]) === false) break;
                    }
                } else if (arr) {
                    for (var key in arr) {
                        if (arr.hasOwnProperty(key)) {
                            if (fn.call(arr[key], key, arr[key]) === false) break;
                        }
                    }
                }
                return arr;
            };
            jq.map = function(arr, fn) {
                var result = [];
                if (arr) {
                    for (var i = 0; i < arr.length; i++) {
                        var val = fn(arr[i], i);
                        if (val != null) result.push(val);
                    }
                }
                return result;
            };
            jq.grep = function(arr, fn, inv) {
                var result = [];
                if (arr) {
                    for (var i = 0; i < arr.length; i++) {
                        var match = !!fn(arr[i], i);
                        if (inv ? !match : match) result.push(arr[i]);
                    }
                }
                return result;
            };
            jq.merge = function(first, second) {
                var len = +second.length, j = 0, i = first.length;
                for (; j < len; j++) first[i++] = second[j];
                first.length = i;
                return first;
            };
            jq.makeArray = function(arr) { return Array.prototype.slice.call(arr); };
            jq.isFunction = function(f) { return typeof f === 'function'; };
            jq.isArray = Array.isArray;
            jq.isPlainObject = function(obj) { return obj && typeof obj === 'object' && obj.constructor === Object; };
            jq.isEmptyObject = function(obj) { for (var k in obj) return false; return true; };
            jq.isNumeric = function(n) { return !isNaN(parseFloat(n)) && isFinite(n); };
            jq.isWindow = function(obj) { return obj && obj === obj.window; };
            jq.inArray = function(v, a, i) { return a ? a.indexOf(v, i) : -1; };
            jq.trim = function(s) { return s ? s.trim() : ''; };
            jq.type = function(obj) {
                if (obj == null) return obj + '';
                return typeof obj;
            };
            jq.proxy = function(fn, ctx) { return fn.bind(ctx); };
            jq.now = Date.now;
            jq.noop = function() {};
            jq.globalEval = function(code) { eval(code); };
            jq.parseJSON = JSON.parse;
            jq.parseHTML = function(html) { return []; };
            jq.Deferred = function() {
                var callbacks = { done: [], fail: [], always: [] };
                return {
                    resolve: function() { callbacks.done.forEach(function(fn) { fn(); }); callbacks.always.forEach(function(fn) { fn(); }); return this; },
                    reject: function() { callbacks.fail.forEach(function(fn) { fn(); }); callbacks.always.forEach(function(fn) { fn(); }); return this; },
                    done: function(fn) { callbacks.done.push(fn); return this; },
                    fail: function(fn) { callbacks.fail.push(fn); return this; },
                    always: function(fn) { callbacks.always.push(fn); return this; },
                    then: function(done, fail) { if (done) callbacks.done.push(done); if (fail) callbacks.fail.push(fail); return this; },
                    promise: function() { return this; }
                };
            };
            jq.when = function() {
                return { done: function(fn) { fn(); return this; }, then: function(fn) { fn(); return this; } };
            };
            jq.Callbacks = function() {
                var list = [];
                return {
                    add: function(fn) { list.push(fn); return this; },
                    remove: function(fn) { list = list.filter(function(f) { return f !== fn; }); return this; },
                    fire: function() { var args = arguments; list.forEach(function(fn) { fn.apply(null, args); }); return this; },
                    empty: function() { list = []; return this; },
                    has: function(fn) { return list.indexOf(fn) > -1; }
                };
            };
            jq.event = { special: {} };
            jq.expr = { ':': {} };
            jq.support = {};
            jq.cssHooks = {};
            jq.cssNumber = {};

            // Store references for delay loader
            jq._prorank_ready_queue = readyQueue;
            jq._prorank_event_queue = eventQueue;
            jq._prorank_extend_queue = extendQueue;
            jq._prorank_facade = true;

            // Provide a stable reference for any code that captured the facade before real jQuery loads.
            w._prorank_jquery_facade = jq;

            // Flush queued ready callbacks and event bindings as soon as real jQuery is available.
            var flushed = false;
            function flushJqueryQueues() {
                if (flushed || w._prorank_jquery_queue_flushed) return;
                if (!hasRealJquery()) return;

                flushed = true;
                w._prorank_jquery_queue_flushed = true;

                // Replay jQuery.fn.extend calls first.
                if (extendQueue.length > 0) {
                    var toExtend = extendQueue.slice();
                    extendQueue.length = 0;
                    toExtend.forEach(function(obj) {
                        try {
                            w.jQuery.fn.extend(obj);
                        } catch(e) {}
                    });
                }

                // Replay AJAX setup/prefilter/transport registrations.
                replayAjaxQueues();

                // Bind queued events.
                if (eventQueue.length > 0) {
                    var toBind = eventQueue.slice();
                    eventQueue.length = 0;
                    toBind.forEach(function(item) {
                        try {
                            bindQueuedEvent(item);
                        } catch(e) {}
                    });
                }

                // Register queued ready callbacks.
                if (readyQueue.length > 0) {
                    var toReady = readyQueue.slice();
                    readyQueue.length = 0;
                    toReady.forEach(function(fn) {
                        try {
                            w.jQuery(fn);
                        } catch(e) {}
                    });
                }
            }

            d.addEventListener('DOMContentLoaded', flushJqueryQueues, { once: true });
            w.addEventListener('load', flushJqueryQueues, { once: true });
            setTimeout(flushJqueryQueues, 0);

            w.jQuery = w.$ = jq;
        })(window, document);
        </script>
        <?php
    }

    /**
     * Check if module should run
     *
     * @return bool
     */
    private function should_run(): bool {
        // Module is available for free tier
        if (!$this->is_enabled()) {
            return false;
        }

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

        // Skip wp-login to avoid interfering with auth/runtime-critical scripts.
        if (($GLOBALS['pagenow'] ?? '') === 'wp-login.php') {
            return false;
        }

        // Don't run on preview
        if (is_preview() || is_customize_preview()) {
            return false;
        }

        // Check if any optimization is enabled
        return $this->get_setting('js_defer', false) ||
               $this->get_setting('js_delay', false) ||
               $this->is_tracker_delay_enabled() ||
               $this->get_setting('js_combine', false);
    }

    /**
     * Optimize script tag using DOMDocument
     *
     * Separates defer and delay exclusions:
     * - Defer: Applied to almost all scripts (except critical WP core)
     * - Delay: More restrictive, excludes jQuery so it loads early for dependent scripts
     *
     * @param string $tag    Script tag HTML
     * @param string $handle Script handle
     * @param string $src    Script source URL
     * @return string Modified script tag
     */
    public function optimize_script_tag(string $tag, string $handle, string $src): string {
        // Skip critical scripts that should never be modified (our own loaders, WP internals)
        if ($this->is_script_excluded_from_all($handle, $src)) {
            return $tag;
        }

        // Hard safety guard for UI bootstrap/runtime scripts that can cause
        // hidden content or broken widgets when moved to interaction-delay.
        $force_no_delay = $this->is_force_no_delay_script($handle, $src);

        // Skip inline scripts
        if (empty($src)) {
            return $tag;
        }

        // JS combining: collect local scripts and suppress their individual tags
        if ($this->is_combine_enabled() && $this->is_script_combinable($handle, $src)) {
            $file_path = $this->url_to_path($src);
            if ($file_path && file_exists($file_path)) {
                $this->combinable_scripts[$handle] = [
                    'handle' => $handle,
                    'src'    => $src,
                    'path'   => $file_path,
                ];
                return '<!-- ProRank JS combined: ' . esc_html($handle) . ' -->';
            }
        }

        // Parse the script tag using DOMDocument
        $script_element = $this->parse_script_tag($tag);

        if ($script_element === null) {
            return $tag;
        }

        $dom = $script_element['dom'];
        $script = $script_element['script'];

        // Check if script should be excluded from defer
        $exclude_from_defer = $this->is_script_excluded_from_defer($handle, $src);
        $auto_tracker_delay = $this->should_auto_delay_tracker_script($handle, $src);

        // If delay mode is OFF, only a small high-risk subset of scripts with
        // inline companions should stay synchronous. Broadly disabling defer for
        // all inline-companion handles breaks mixed runtime graphs (e.g. Elementor
        // runtime chunks deferred while frontend bootstrap stays sync).
        if (!$this->is_delay_enabled() && $this->script_has_inline_companions($handle)) {
            $h = strtolower($handle);
            $high_risk_inline_handles = [
                'jquery',
                'jquery-core',
                'jquery-migrate',
                'wp-hooks',
                'wp-i18n',
                'wp-api-fetch',
                'wp-url',
                'wp-dom-ready',
                'wp-a11y',
                'wp-date',
                'wp-data',
                'react',
                'react-dom',
                'wp-element',
                'wp-components',
                'moment',
            ];

            $is_wp_dist = strpos($src, '/wp-includes/js/dist/') !== false;
            if (in_array($h, $high_risk_inline_handles, true) || $is_wp_dist) {
                $exclude_from_defer = true;
            }
        }

        // Never remove defer that WordPress itself added - WP knows its own
        // script dependency graph and only adds defer when safe to do so.

        // Skip scripts that already have defer or async
        if ($script->hasAttribute('defer') || $script->hasAttribute('async')) {
            // Strip pre-existing defer when this handle must remain synchronous.
            if ($script->hasAttribute('defer') && $exclude_from_defer) {
                $script->removeAttribute('defer');
                return $this->serialize_script_element($dom, $script);
            }

            // But still check if we should apply delay to deferred scripts
            if (($this->is_delay_enabled() || $auto_tracker_delay) && !$force_no_delay && !$this->is_script_excluded_from_delay($handle, $src)) {
                $this->convert_to_delayed_script_element($script, $handle, $src);
                $this->delay_sibling_inline_scripts($dom, $script);
                return $this->serialize_script_element($dom, $script);
            }
            return $tag;
        }

        $modified = false;

        // Check if script should be excluded from delay
        $exclude_from_delay = $this->is_script_excluded_from_delay($handle, $src);

        // Apply defer attribute (unless specifically excluded from defer)
        if ($this->get_setting('js_defer', false) && !$exclude_from_defer) {
            $script->setAttribute('defer', '');
            $modified = true;
        }

        // Apply delay execution ONLY if not excluded from delay
        // jQuery gets defer but NOT delay (so it loads early but non-blocking)
        if (($this->is_delay_enabled() || $auto_tracker_delay) && !$force_no_delay && !$exclude_from_delay) {
            $this->convert_to_delayed_script_element($script, $handle, $src);
            $this->delay_sibling_inline_scripts($dom, $script);
            $modified = true;
        }

        if (!$modified) {
            return $tag;
        }

        // Return the modified tag
        return $this->serialize_script_element($dom, $script);
    }

    /**
     * Final safety pass: strip unsafe defer attributes after all other filters.
     *
     * Some plugins/themes re-apply defer later in the filter chain. This guard
     * ensures scripts with inline companions (or explicit defer exclusions) remain
     * synchronous when delay mode is disabled.
     *
     * @param string $tag    Script tag HTML
     * @param string $handle Script handle
     * @param string $src    Script source URL
     * @return string
     */
    public function enforce_safe_script_tag(string $tag, string $handle, string $src): string {
        if (empty($src) || strpos($tag, 'defer') === false) {
            return $tag;
        }

        $should_strip_defer = $this->is_script_excluded_from_defer($handle, $src);
        if (!$this->is_delay_enabled() && $this->script_has_inline_companions($handle)) {
            // Inline companions are common and usually safe with defer (config objects).
            // Restrict defer stripping to high-risk handles only; otherwise defer gets
            // removed from almost everything and optimization becomes ineffective.
            $h = strtolower($handle);
            $high_risk_inline_handles = [
                'jquery',
                'jquery-core',
                'jquery-migrate',
                'wp-hooks',
                'wp-i18n',
                'wp-api-fetch',
                'wp-url',
                'wp-dom-ready',
                'wp-a11y',
                'wp-date',
                'wp-data',
                'react',
                'react-dom',
                'wp-element',
                'wp-components',
                'moment',
            ];

            $is_wp_dist = strpos($src, '/wp-includes/js/dist/') !== false;
            if (in_array($h, $high_risk_inline_handles, true) || $is_wp_dist) {
                $should_strip_defer = true;
            }
        }

        if (!$should_strip_defer) {
            return $tag;
        }

        $parsed = $this->parse_script_tag($tag);
        if ($parsed === null) {
            return str_replace(' defer', '', $tag);
        }

        $dom = $parsed['dom'];
        $script = $parsed['script'];
        if ($script->hasAttribute('defer')) {
            $script->removeAttribute('defer');
            return $this->serialize_script_element($dom, $script);
        }

        return $tag;
    }

    /**
     * Parse script tag into DOMDocument
     *
     * @param string $tag Script tag HTML
     * @return array|null Array with 'dom' and 'script' keys, or null on failure
     */
    private function parse_script_tag(string $tag): ?array {
        $dom = new \DOMDocument();

        // Suppress warnings for HTML5 elements
        libxml_use_internal_errors(true);

        // Wrap in a container to ensure proper parsing
        $wrapped = '<!DOCTYPE html><html><head>' . $tag . '</head></html>';

        if (!$dom->loadHTML($wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD)) {
            libxml_clear_errors();
            return null;
        }

        libxml_clear_errors();

        $scripts = $dom->getElementsByTagName('script');

        if ($scripts->length === 0) {
            return null;
        }

        // WordPress 6.9+ concatenates inline before/after scripts with the
        // external script into a single $tag. Find the external <script> (with
        // src attribute) so we only modify the correct element.
        $target = null;
        for ($i = 0; $i < $scripts->length; $i++) {
            if ($scripts->item($i)->hasAttribute('src')) {
                $target = $scripts->item($i);
                break;
            }
        }

        // Fallback to first script if no external script found (single inline)
        if ($target === null) {
            $target = $scripts->item(0);
        }

        return [
            'dom' => $dom,
            'script' => $target,
        ];
    }

    /**
     * Serialize script element back to HTML
     *
     * @param \DOMDocument $dom    DOM document
     * @param \DOMElement  $script Script element
     * @return string Script tag HTML
     */
    private function serialize_script_element(\DOMDocument $dom, \DOMElement $script): string {
        // WordPress 6.9+ concatenates multiple <script> tags (inline before +
        // external + inline after) into a single $tag. Serialize ALL elements
        // in the <head> to preserve them, not just the one we modified.
        $head = $dom->getElementsByTagName('head')->item(0);

        if ($head !== null && $head->childNodes->length > 1) {
            $html = '';
            foreach ($head->childNodes as $child) {
                $part = $dom->saveHTML($child);
                if ($part !== false) {
                    $html .= $part;
                }
            }
            return $html;
        }

        // Single script element — original path
        $html = $dom->saveHTML($script);

        if ($html === false) {
            return '';
        }

        // Clean up the output - DOMDocument may add closing tags differently
        // Ensure proper self-closing or closing format
        $html = preg_replace('/<script([^>]*)><\/script>/', '<script$1></script>', $html);

        return $html;
    }

    /**
     * Convert script element to delayed execution
     *
     * Inspired by Perfmatters: Store original type for ES module detection
     *
     * @param \DOMElement $script Script element to modify
     * @return void
     */
    private function convert_to_delayed_script_element(\DOMElement $script, string $handle = '', string $src = ''): void {
        // Store original type for ES module detection (Perfmatters pattern)
        if ($script->hasAttribute('type')) {
            $originalType = $script->getAttribute('type');
            // Store original type for later restoration
            if (in_array($originalType, ['module', 'text/javascript', 'application/javascript'], true)) {
                $script->setAttribute('data-prorank-type', $originalType);
            }
        } else {
            // No type attribute means standard JavaScript
            $script->setAttribute('data-prorank-type', 'text/javascript');
        }

        // Set type to prevent immediate execution
        $script->setAttribute('type', 'text/prorank-delayed');

        // Add data attribute for delay loader
        $script->setAttribute('data-prorank-delay', 'true');

        // Add vendor bypass attributes to prevent double-optimization (Perfmatters pattern)
        $script->setAttribute('data-cfasync', 'false');      // Cloudflare Rocket Loader
        $script->setAttribute('data-no-optimize', '1');      // WP Rocket
        $script->setAttribute('data-no-defer', '1');         // Other defer plugins
        $script->setAttribute('data-no-minify', '1');        // Minification plugins

        // Captcha scripts are often large and only needed when users interact
        // with forms. Keep them out of idle timeout loading on normal pages.
        if ($this->is_interaction_only_delayed_script($handle, $src)) {
            $script->setAttribute('data-prorank-delay-interaction', '1');
        }
    }

    /**
     * Determine whether delayed script should wait for interaction.
     *
     * @param string $handle
     * @param string $src
     * @return bool
     */
    private function is_interaction_only_delayed_script(string $handle, string $src): bool {
        $haystack = strtolower(trim($handle . ' ' . $src));
        if ($haystack === '') {
            return false;
        }

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

        if (!$this->is_captcha_non_enqueued_script($haystack)) {
            return false;
        }

        return !$this->should_avoid_captcha_delay_on_request();
    }

    /**
     * Also delay inline sibling scripts in the same tag
     *
     * WordPress 6.9+ concatenates translation/data inline scripts with the
     * external script into a single $tag. If we delay the external script,
     * we must also delay the inline siblings to prevent "undefined" errors
     * (e.g., wp.i18n.setLocaleData() calling wp.i18n before it loads).
     *
     * The delay loader handles inline delayed scripts correctly — it copies
     * innerHTML and executes them sequentially alongside external scripts.
     *
     * @param \DOMDocument $dom        The parsed DOM containing all sibling scripts
     * @param \DOMElement  $mainScript The external script being delayed
     * @return void
     */
    private function delay_sibling_inline_scripts(\DOMDocument $dom, \DOMElement $mainScript): void {
        $head = $dom->getElementsByTagName('head')->item(0);
        if ($head === null) {
            return;
        }

        foreach ($head->childNodes as $child) {
            // Skip the main script we already converted
            if ($child === $mainScript) {
                continue;
            }

            // Only process script elements
            if (!($child instanceof \DOMElement) || $child->tagName !== 'script') {
                continue;
            }

            // Only delay inline scripts (no src attribute)
            if ($child->hasAttribute('src')) {
                continue;
            }

            // Skip if already delayed
            if ($child->getAttribute('type') === 'text/prorank-delayed') {
                continue;
            }

            // Convert inline script to delayed
            if ($child->hasAttribute('type')) {
                $child->setAttribute('data-prorank-type', $child->getAttribute('type'));
            } else {
                $child->setAttribute('data-prorank-type', 'text/javascript');
            }
            $child->setAttribute('type', 'text/prorank-delayed');
            $child->setAttribute('data-prorank-delay', 'true');
        }
    }

    /**
     * Add delay load script to footer
     *
     * Inspired by Perfmatters: Includes click replay and event queue processing
     *
     * @return void
     */
    public function add_delay_load_script(): void {
        if ($this->delay_script_added) {
            return;
        }

        $this->delay_script_added = true;

        ?>
        <script id="prorank-delay-loader">
        (function(w, d) {
            'use strict';

            var loading = false;
            var timer = null;
            var readyQueue = w._prorank_ready_queue || [];
            var eventQueue = w._prorank_event_queue || [];
            var clickQueue = w._prorank_click_queue || [];
            var extendQueue = w._prorank_extend_queue || [];
            var ajaxPrefilterQueue = w._prorank_ajax_prefilter_queue || [];
            var ajaxSetupQueue = w._prorank_ajax_setup_queue || [];
            var ajaxTransportQueue = w._prorank_ajax_transport_queue || [];

            // Preload script for faster execution (Perfmatters pattern)
            function preloadScript(script) {
                if (!script.src) return;
                var link = d.createElement('link');
                var originalType = script.getAttribute('data-prorank-type');

                // Use modulepreload for ES modules (Perfmatters pattern)
                if (originalType === 'module') {
                    link.rel = 'modulepreload';
                } else {
                    link.rel = 'preload';
                    link.as = 'script';
                }
                link.href = script.src;
                d.head.appendChild(link);
            }

            function loadScript(script) {
                return new Promise(function(resolve) {
                    var newScript = d.createElement('script');

                    // Copy attributes except type, data-prorank-delay, and vendor bypass attrs
                    var skipAttrs = ['type', 'data-prorank-delay', 'data-prorank-type',
                                     'data-cfasync', 'data-no-optimize', 'data-no-defer', 'data-no-minify'];
                    Array.from(script.attributes).forEach(function(attr) {
                        if (skipAttrs.indexOf(attr.name) === -1) {
                            newScript.setAttribute(attr.name, attr.value);
                        }
                    });

                    // Restore original type for ES modules (Perfmatters pattern)
                    var originalType = script.getAttribute('data-prorank-type');
                    if (originalType === 'module') {
                        newScript.type = 'module';
                    } else {
                        newScript.type = 'text/javascript';
                    }

                    // Copy content if inline script
                    if (!script.src && script.innerHTML) {
                        newScript.innerHTML = script.innerHTML;
                        script.parentNode.replaceChild(newScript, script);
                        resolve();
                    } else {
                        newScript.onload = resolve;
                        newScript.onerror = resolve;
                        script.parentNode.replaceChild(newScript, script);
                    }
                });
            }

            function executeExtendQueue() {
                // Replay jQuery.fn.extend calls (Perfmatters pattern)
                if (!w.jQuery || w.jQuery._prorank_facade) {
                    return false;
                }

                if (extendQueue.length > 0) {
                    var toExtend = extendQueue.slice();
                    extendQueue.length = 0;
                    toExtend.forEach(function(obj) {
                        try {
                            w.jQuery.fn.extend(obj);
                        } catch(e) {
                            console.error('ProRank: Error replaying jQuery.fn.extend', e);
                        }
                    });
                }
                return true;
            }

            function executeAjaxQueue() {
                if (!w.jQuery || w.jQuery._prorank_facade) {
                    return false;
                }

                if (ajaxSetupQueue.length > 0 && typeof w.jQuery.ajaxSetup === 'function') {
                    var setups = ajaxSetupQueue.slice();
                    ajaxSetupQueue.length = 0;
                    setups.forEach(function(opts) {
                        try { w.jQuery.ajaxSetup(opts || {}); } catch(e) {}
                    });
                }

                if (ajaxPrefilterQueue.length > 0 && typeof w.jQuery.ajaxPrefilter === 'function') {
                    var prefilters = ajaxPrefilterQueue.slice();
                    ajaxPrefilterQueue.length = 0;
                    prefilters.forEach(function(item) {
                        try { w.jQuery.ajaxPrefilter(item.dataTypes || '*', item.handler); } catch(e) {}
                    });
                }

                if (ajaxTransportQueue.length > 0 && typeof w.jQuery.ajaxTransport === 'function') {
                    var transports = ajaxTransportQueue.slice();
                    ajaxTransportQueue.length = 0;
                    transports.forEach(function(item) {
                        try { w.jQuery.ajaxTransport(item.dataTypes || '*', item.handler); } catch(e) {}
                    });
                }

                return true;
            }

            function executeReadyQueue() {
                // Execute queued jQuery ready callbacks only when real jQuery is available
                if (!w.jQuery || w.jQuery._prorank_facade) {
                    return false; // Real jQuery not loaded yet
                }

                // Replay queued AJAX registrations before callbacks run.
                executeAjaxQueue();

                // First replay any jQuery.fn.extend calls
                executeExtendQueue();

                // Execute all queued ready callbacks
                if (readyQueue.length > 0) {
                    var toExecute = readyQueue.slice();
                    readyQueue.length = 0;
                    toExecute.forEach(function(fn) {
                        try {
                            if (typeof fn === 'function') {
                                w.jQuery(fn);
                            }
                        } catch(e) {
                            console.error('ProRank: Error executing queued callback', e);
                        }
                    });
                }
                return true;
            }

            function executeEventQueue() {
                // Re-bind queued .on() event handlers with real jQuery
                if (!w.jQuery || w.jQuery._prorank_facade) {
                    return;
                }

                if (eventQueue.length > 0) {
                    var toExecute = eventQueue.slice();
                    eventQueue.length = 0;
                    toExecute.forEach(function(item) {
                        try {
                            var ctx = (item.context === undefined || item.context === null || item.context === '') ? d : item.context;
                            var $ctx = w.jQuery(ctx);
                            var method = item.once ? 'one' : 'on';

                            if (item.selector) {
                                $ctx[method](item.events, item.selector, item.handler);
                            } else if (item.handler) {
                                $ctx[method](item.events, item.handler);
                            }
                        } catch(e) {
                            console.error('ProRank: Error binding queued event', e);
                        }
                    });
                }
            }

            function replayClicks() {
                // Replay captured clicks (Perfmatters-style click replay)
                if (clickQueue.length === 0) return;

                // Only replay recent clicks (within last 3 seconds)
                var now = Date.now();
                var recentClicks = clickQueue.filter(function(click) {
                    return (now - click.timestamp) < 3000;
                });

                clickQueue.length = 0;

                // Small delay to let event handlers bind
                setTimeout(function() {
                    recentClicks.forEach(function(click) {
                        try {
                            // Check if target still exists in DOM
                            if (!d.body.contains(click.target)) return;

                            // Verify outerHTML matches to prevent wrong element clicks (Perfmatters pattern)
                            if (click.outerHTML && click.target.outerHTML !== click.outerHTML) {
                                // Element has changed, skip replay
                                return;
                            }

                            // Create and dispatch a new click event
                            var event = new MouseEvent('click', {
                                bubbles: true,
                                cancelable: true,
                                clientX: click.x,
                                clientY: click.y,
                                view: w
                            });
                            click.target.dispatchEvent(event);
                        } catch(e) {
                            console.error('ProRank: Error replaying click', e);
                        }
                    });
                }, 100);
            }

            function triggerDOMReady() {
                // Dispatch DOMContentLoaded-like events for scripts that listen for them
                try {
                    var event = new Event('DOMContentLoaded', { bubbles: true });
                    d.dispatchEvent(event);
                } catch(e) {}

                // Also trigger jQuery document ready if available
                if (w.jQuery && !w.jQuery._prorank_facade) {
                    try {
                        w.jQuery(d).trigger('ready');
                    } catch(e) {}
                }
            }

            function loadDelayedScripts(includeInteractionOnly) {
                if (loading) return;
                if (includeInteractionOnly === undefined) {
                    includeInteractionOnly = true;
                }

                if (timer && includeInteractionOnly) {
                    clearTimeout(timer);
                }

                // Get all delayed scripts
                var scripts = Array.from(d.querySelectorAll('script[type="text/prorank-delayed"]'));
                if (!includeInteractionOnly) {
                    scripts = scripts.filter(function(script) {
                        return script.getAttribute('data-prorank-delay-interaction') !== '1';
                    });
                }
                if (scripts.length === 0) {
                    if (d.querySelectorAll('script[type="text/prorank-delayed"]').length === 0) {
                        w._prorank_scripts_loaded = true;
                    }
                    return;
                }

                loading = true;

                // Sort: jQuery first, then jquery-migrate, then others
                scripts.sort(function(a, b) {
                    var aId = a.id || '';
                    var bId = b.id || '';
                    var aSrc = a.src || '';
                    var bSrc = b.src || '';

                    var aIsJquery = aId.indexOf('jquery-core') > -1 || aSrc.indexOf('jquery.min.js') > -1 || aSrc.indexOf('jquery/jquery') > -1;
                    var bIsJquery = bId.indexOf('jquery-core') > -1 || bSrc.indexOf('jquery.min.js') > -1 || bSrc.indexOf('jquery/jquery') > -1;
                    var aIsMigrate = aId.indexOf('jquery-migrate') > -1 || aSrc.indexOf('jquery-migrate') > -1;
                    var bIsMigrate = bId.indexOf('jquery-migrate') > -1 || bSrc.indexOf('jquery-migrate') > -1;

                    if (aIsJquery && !bIsJquery) return -1;
                    if (!aIsJquery && bIsJquery) return 1;
                    if (aIsMigrate && !bIsMigrate) return -1;
                    if (!aIsMigrate && bIsMigrate) return 1;
                    return 0;
                });

                // Preload all scripts first for faster execution (Perfmatters pattern)
                scripts.forEach(preloadScript);

                // Load scripts sequentially
                scripts.reduce(function(promise, script) {
                    return promise.then(function() {
                        return loadScript(script);
                    }).then(function() {
                        // Check if jQuery loaded and execute queued callbacks
                        if (executeReadyQueue()) {
                            executeEventQueue();
                        }
                    });
                }, Promise.resolve()).then(function() {
                    // Final execution of any remaining queued items
                    executeReadyQueue();
                    executeEventQueue();

                    // Mark scripts as loaded only when nothing delayed remains.
                    if (d.querySelectorAll('script[type="text/prorank-delayed"]').length === 0) {
                        w._prorank_scripts_loaded = true;
                    }

                    loading = false;

                    // Replay WP namespace shim captured calls (i18n locale data, hooks)
                    if (w._prorank_wp_shim_queue) {
                        var sq = w._prorank_wp_shim_queue;
                        if (sq.locale.length && w.wp && w.wp.i18n && !w.wp.i18n._prorank_shim) {
                            sq.locale.forEach(function(args) {
                                try { w.wp.i18n.setLocaleData(args[0], args[1]); } catch(e) {}
                            });
                        }
                        if (sq.hooks.length && w.wp && w.wp.hooks && !w.wp.hooks._prorank_shim) {
                            sq.hooks.forEach(function(item) {
                                try { w.wp.hooks[item[0]].apply(null, item[1]); } catch(e) {}
                            });
                        }
                        delete w._prorank_wp_shim_queue;
                    }

                    // Trigger DOM ready events
                    triggerDOMReady();

                    // Replay any captured clicks
                    replayClicks();
                }).catch(function() {
                    loading = false;
                });
            }

            // Trigger on user interaction
            ['mouseenter', 'mousemove', 'keydown', 'touchstart', 'touchend', 'scroll', 'click'].forEach(function(event) {
                w.addEventListener(event, function(e) {
                    // Ignore synthetic/programmatic events; only real user
                    // interaction should unlock interaction-only scripts.
                    if (e && typeof e.isTrusted === 'boolean' && e.isTrusted === false) {
                        return;
                    }
                    loadDelayedScripts(true);
                }, { once: true, passive: true });
            });

            // Also trigger after 10 seconds as fallback (keeps Lighthouse/RUM clean while still loading for idle users)
            timer = setTimeout(function() {
                loadDelayedScripts(false);
            }, 10000);
        })(window, document);
        </script>
        <?php
    }
    
    /**
     * Check if script should be excluded from ALL optimization
     *
     * Only our own loader scripts and critical WP internals
     *
     * @param string $handle Script handle
     * @param string $src    Script source URL
     * @return bool
     */
    private function is_script_excluded_from_all(string $handle, string $src): bool {
        // Only exclude ProRank's own internal loader/facade scripts that inject
        // inline code and must remain synchronous and unmodified.
        $loader_scripts = [
            'prorank-delay-loader',
            'prorank-jquery-facade',
            'prorank-wp-shim',
            'prorank-cls-fix',
            'prorank-async-css-loader',
            'prorank-rum-beacon',
            'prorank-lcp-beacon',
            'prorank-inp-monitor',
            'wp-polyfill',
            'regenerator-runtime',
        ];

        return in_array($handle, $loader_scripts, true);
    }

    /**
     * Check if script should be excluded from defer
     *
     * Very few scripts need to be excluded from defer - mostly just
     * scripts that absolutely must run synchronously
     *
     * @param string $handle Script handle
     * @param string $src    Script source URL
     * @return bool
     */
    private function is_script_excluded_from_defer(string $handle, string $src): bool {
        if ($this->is_high_risk_runtime_script($handle, $src, false)) {
            return true;
        }

        // Check Quick Exclusions database for defer exclusions
        if (QuickExclusions::should_exclude_from_defer($handle, $src)) {
            return true;
        }

        // Note: User's js_exclude setting only affects DELAY, not defer.
        // defer is safe for all scripts as it preserves document execution order.
        // jQuery and other excluded scripts still load non-blocking with defer.

        return false;
    }

    /**
     * Check whether a registered script has inline before/after/data payloads.
     *
     * @param string $handle Script handle.
     * @return bool
     */
    private function script_has_inline_companions(string $handle): bool {
        if ($handle === '') {
            return false;
        }

        $wp_scripts = wp_scripts();
        if (!is_object($wp_scripts) || !isset($wp_scripts->registered[$handle])) {
            return false;
        }

        $extra = $wp_scripts->registered[$handle]->extra ?? [];
        if (!is_array($extra)) {
            return false;
        }

        return !empty($extra['before']) || !empty($extra['after']) || !empty($extra['data']);
    }

    /**
     * Check if script should be excluded from delay
     *
     * More scripts are excluded from delay than defer.
     * jQuery and related scripts should load early but can be deferred.
     *
     * @param string $handle Script handle
     * @param string $src    Script source URL
     * @return bool
     */
    private function is_script_excluded_from_delay(string $handle, string $src): bool {
        if ($this->is_high_risk_runtime_script($handle, $src, true)) {
            return true;
        }

        // When tracker-delay is explicitly enabled, allow low-risk analytics
        // scripts to bypass broad compatibility exclusions like Site Kit.
        if ($this->should_bypass_delay_exclusions_for_tracker($handle, $src)) {
            return false;
        }

        // Check Quick Exclusions database for delay exclusions
        // This includes jQuery, polyfills, and plugin-specific scripts.
        // WP core scripts (wp-hooks, wp-i18n, etc.) are intentionally NOT excluded —
        // they are safely delayed alongside their inline siblings (translations/data).
        if (QuickExclusions::should_exclude_from_delay($handle, $src)) {
            return true;
        }

        // Check user-defined exclusions
        $this->load_user_exclusions();
        foreach ($this->excluded_scripts as $pattern) {
            if ($handle === $pattern || (!empty($src) && strpos($src, $pattern) !== false)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Allow low-risk tracker scripts to bypass generic compatibility
     * exclusions when tracker-delay is enabled.
     *
     * @param string $handle
     * @param string $src
     * @return bool
     */
    private function should_bypass_delay_exclusions_for_tracker(string $handle, string $src): bool {
        if (!$this->is_tracker_delay_enabled()) {
            return false;
        }

        $haystack = strtolower(trim($handle . ' ' . $src));
        if ($haystack === '' || !$this->is_low_risk_tracker_haystack($haystack)) {
            return false;
        }

        return $this->get_third_party_governance_settings()['delay_google_services']
            && $this->is_google_service_haystack($haystack);
    }

    /**
     * Global safety net for scripts that frequently break when delayed/deferred.
     *
     * @param string $handle
     * @param string $src
     * @param bool   $for_delay True for delay path, false for defer path
     * @return bool
     */
    private function is_high_risk_runtime_script(string $handle, string $src, bool $for_delay): bool {
        $haystack = strtolower(trim($handle . ' ' . $src));
        if ($haystack === '') {
            return false;
        }

        // Captcha scripts should be delay/defer-eligible on normal pages and
        // only protected on auth/commerce endpoints.
        if ($this->is_captcha_non_enqueued_script($haystack)) {
            return $this->should_avoid_captcha_delay_on_request();
        }

        $common_patterns = [
            'bookly',
            '/bookly/',
            'maps.googleapis.com/maps/api/js',
            'maps.gstatic.com',
            'google.com/maps',
            'stripe',
            'paypal',
            'klarna',
            'braintree',
            'squareup',
            'woocommerce-checkout',
            'wc-checkout',
            '/woocommerce/assets/js/frontend/checkout',
            'amelia',
            'latepoint',
            'booknetic',
            'simply-schedule-appointments',
        ];

        // Delay is more aggressive, so keep a wider protection list.
        $delay_only_patterns = [
            'jquery-ui-datepicker',
            'flatpickr',
            'timepicker',
            'calendar',
            // Elementor addon carousels/sliders can stay hidden until init.
            // Keep these out of interaction-delay to avoid missing cards/widgets.
            'premium-addons-for-elementor',
            'premium-addons-elementor',
            '/uploads/premium-addons-elementor/',
            'pafe-',
            'pa-slick',
            'pa-elements-handler',
            'pro-elements-handlers',
            'elements-handler.min.js',
            'lite-youtube.js',
            'lite-vimeo.js',
        ];

        $patterns = $for_delay ? array_merge($common_patterns, $delay_only_patterns) : $common_patterns;
        foreach ($patterns as $pattern) {
            if ($pattern !== '' && strpos($haystack, $pattern) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Scripts that should never use interaction-delay because they initialize
     * visible UI components (sliders/carousels/widget handlers).
     *
     * @param string $handle
     * @param string $src
     * @return bool
     */
    private function is_force_no_delay_script(string $handle, string $src): bool {
        $haystack = strtolower(trim($handle . ' ' . $src));
        if ($haystack === '') {
            return false;
        }

        $patterns = [
            'premium-addons-for-elementor',
            'premium-addons-elementor',
            '/uploads/premium-addons-elementor/',
            'pafe-',
            'pa-slick',
            'pa-elements-handler',
            'pro-elements-handlers',
            'elements-handlers.min.js',
            'swiper',
            'slick.min.js',
            'lite-youtube.js',
            'lite-vimeo.js',
        ];

        foreach ($patterns as $pattern) {
            if ($pattern !== '' && strpos($haystack, $pattern) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Determine whether tracker-specific automatic delay is enabled.
     *
     * @return bool
     */
    private function is_tracker_delay_enabled(): bool {
        $value = $this->get_setting('tracker_delay_enabled', null);
        if ($value === null || $value === '') {
            return $this->is_delay_enabled();
        }

        if (is_string($value)) {
            return in_array(strtolower($value), ['true', '1', 'yes', 'on'], true);
        }

        return (bool) $value;
    }

    /**
     * Whether an enqueued script should use tracker-specific interaction delay.
     *
     * @param string $handle
     * @param string $src
     * @return bool
     */
    private function should_auto_delay_tracker_script(string $handle, string $src): bool {
        if (!$this->is_tracker_delay_enabled()) {
            return false;
        }

        return $this->is_low_risk_tracker_haystack(strtolower(trim($handle . ' ' . $src)));
    }

    /**
     * Whether a raw HTML script tag should use interaction delay for trackers.
     *
     * @param string $haystack
     * @return bool
     */
    private function should_use_interaction_delay_for_tracker_haystack(string $haystack): bool {
        return $this->is_tracker_delay_enabled() && $this->is_low_risk_tracker_haystack($haystack);
    }

    /**
     * Detect low-risk analytics and ad trackers that are safe to push behind interaction.
     *
     * @param string $haystack
     * @return bool
     */
    private function is_low_risk_tracker_haystack(string $haystack): bool {
        if ($haystack === '') {
            return false;
        }

        $governance = $this->get_third_party_governance_settings();
        $patterns = [];

        if ($governance['delay_google_services']) {
            $patterns = array_merge($patterns, [
                'google_gtagjs',
                'googlesitekit',
                'googletagmanager.com/gtag/js',
                'googletagmanager.com/gtm.js',
                'window.datalayer',
                'gtm.start',
                'gtag(',
                'google-analytics.com/analytics.js',
                'google-analytics.com/ga.js',
                'google-analytics.com/g/collect',
                'googlesyndication.com',
                'doubleclick.net',
            ]);
        }

        if ($governance['delay_identity_widgets']) {
            $patterns = array_merge($patterns, [
                'accounts.google.com/gsi/client',
                'google.accounts.id',
                'g_id_onload',
            ]);
        }

        if ($governance['delay_review_widgets']) {
            $patterns[] = 'cdn.trustindex.io/loader.js';
        }

        $patterns = array_merge($patterns, [
            'connect.facebook.net/en_us/fbevents.js',
            'fbq(',
            'fbevents.js',
            'static.getclicky.com',
            'clicky.init',
            'clicky_site_ids',
            'clarity.ms/tag',
            'clarity(',
            'hotjar.com',
            'hj(',
            'cdn.segment.com',
            'analytics.tiktok.com',
            'bat.bing.com/bat.js',
            'snapchat.com/scevent',
            'web-2-tel.com/org-sdk',
            'xg4ken.com/js/v2/ktag.js',
        ]);

        foreach ($patterns as $pattern) {
            if ($pattern !== '' && strpos($haystack, $pattern) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Read UI-managed third-party governance settings with safe defaults.
     *
     * @return array<string, bool>
     */
    private function get_third_party_governance_settings(): array {
        static $settings = null;

        if (is_array($settings)) {
            return $settings;
        }

        $advanced = get_option('prorank_seo_advanced_settings', []);
        if (!is_array($advanced)) {
            $advanced = [];
        }

        $settings = [
            'delay_google_services' => !array_key_exists('delay_google_services', $advanced) || !empty($advanced['delay_google_services']),
            'delay_review_widgets' => !array_key_exists('delay_review_widgets', $advanced) || !empty($advanced['delay_review_widgets']),
            'defer_consent_scripts' => !array_key_exists('defer_consent_scripts', $advanced) || !empty($advanced['defer_consent_scripts']),
            'delay_identity_widgets' => !array_key_exists('delay_identity_widgets', $advanced) || !empty($advanced['delay_identity_widgets']),
        ];

        return $settings;
    }

    /**
     * Identify consent-manager scripts handled by ProRank's non-blocking path.
     *
     * @param string $haystack
     * @return bool
     */
    private function is_consent_script_haystack(string $haystack): bool {
        $patterns = [
            'cdn-cookieyes.com',
            'cookieyes',
            'cookie-law-info',
            'consent.cookiebot.com',
            'cookiebot.com',
            'complianz',
            'borlabs',
            'real-cookie-banner',
            'moove-gdpr',
        ];

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

        return false;
    }

    /**
     * Identify Google tracking/service scripts controlled by the UI.
     *
     * @param string $haystack
     * @return bool
     */
    private function is_google_service_haystack(string $haystack): bool {
        $patterns = [
            'google_gtagjs',
            'googlesitekit',
            'googletagmanager.com/gtag/js',
            'googletagmanager.com/gtm.js',
            'google-analytics.com',
            'googlesyndication.com',
            'doubleclick.net',
            'window.datalayer',
            'gtm.start',
            'gtag(',
        ];

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

        return false;
    }

    /**
     * Load user-defined exclusions (cached)
     *
     * @return void
     */
    private function load_user_exclusions(): void {
        if (!empty($this->excluded_scripts)) {
            return;
        }

        $excluded = $this->get_setting('js_exclude', '');
        if (!empty($excluded)) {
            $this->excluded_scripts = array_filter(
                array_map('trim', explode("\n", $excluded))
            );
        }
    }
    
    /**
     * Check if delay is enabled
     *
     * @return bool
     */
    private function is_delay_enabled(): bool {
        $value = $this->get_setting('js_delay', false);
        // Handle string values from WP options (e.g., "false", "true", "1", "0")
        if (is_string($value)) {
            return in_array(strtolower($value), ['true', '1', 'yes', 'on'], true);
        }
        return (bool) $value;
    }

    /**
     * Clear combined JS cache
     *
     * @return void
     */
    public function clear_combine_cache(): void {
        $upload_dir = wp_upload_dir();
        $cache_dir  = trailingslashit($upload_dir['basedir']) . 'prorank-seo/js-combined/';

        if (!is_dir($cache_dir)) {
            return;
        }

        $files = glob($cache_dir . 'combined-*.js');
        if ($files) {
            foreach ($files as $file) {
                \ProRank\SEO\Core\FilesystemHelper::delete_file((string) $file);
            }
        }
    }

    /**
     * Check if JS combining is enabled
     *
     * @return bool
     */
    private function is_combine_enabled(): bool {
        $value = $this->get_setting('js_combine', false);
        if (is_string($value)) {
            return in_array(strtolower($value), ['true', '1', 'yes', 'on'], true);
        }
        return (bool) $value;
    }

    /**
     * Set up cache directories for combined JS files
     *
     * @return void
     */
    private function setup_combine_cache(): void {
        $upload_dir = wp_upload_dir();

        $this->combine_cache_dir = trailingslashit($upload_dir['basedir']) . 'prorank-seo/js-combined/';
        $this->combine_cache_url = trailingslashit($upload_dir['baseurl']) . 'prorank-seo/js-combined/';

        if (!file_exists($this->combine_cache_dir)) {
            wp_mkdir_p($this->combine_cache_dir);
            file_put_contents($this->combine_cache_dir . 'index.php', '<?php // Silence is golden');
        }
    }

    /**
     * Check if a script can be safely combined
     *
     * Only local, non-module, non-critical scripts qualify.
     *
     * @param string $handle Script handle
     * @param string $src    Script source URL
     * @return bool
     */
    private function is_script_combinable(string $handle, string $src): bool {
        // Never combine WordPress package and translation scripts.
        if ($this->is_wordpress_core_script($handle, $src)) {
            return false;
        }

        // Never combine critical/order-sensitive scripts
        if (in_array($handle, self::COMBINE_EXCLUDE, true)) {
            return false;
        }

        // Never combine ProRank's own scripts
        if (strpos($handle, 'prorank-') === 0 || strpos($handle, 'prorank_') === 0) {
            return false;
        }

        // Only combine local scripts (same host)
        $src_host  = wp_parse_url($src, PHP_URL_HOST);
        $site_host = wp_parse_url(site_url(), PHP_URL_HOST);
        if ($src_host && $src_host !== $site_host) {
            return false;
        }

        // Skip scripts with inline extra data (before/after) — combining would lose them
        $wp_scripts = wp_scripts();
        if (isset($wp_scripts->registered[$handle])) {
            $data = $wp_scripts->registered[$handle];
            if (!empty($data->extra['before']) || !empty($data->extra['after']) || !empty($data->extra['data'])) {
                return false;
            }
        }

        // Check user exclusions
        $this->load_user_exclusions();
        foreach ($this->excluded_scripts as $pattern) {
            if ($handle === $pattern || strpos($src, $pattern) !== false) {
                return false;
            }
        }

        return true;
    }

    /**
     * Determine whether a script is a WordPress core/package or translation script.
     *
     * These scripts rely on strict execution order and should not be deferred, delayed, or combined.
     *
     * @param string $handle Script handle
     * @param string $src    Script source URL
     * @return bool
     */
    private function is_wordpress_core_script(string $handle, string $src): bool {
        if (strpos($handle, 'wp-') === 0) {
            return true;
        }

        if (strpos($handle, '-translations') !== false || strpos($handle, 'translations') !== false) {
            return true;
        }

        if ($src !== '') {
            if (strpos($src, '/wp-includes/js/dist/') !== false || strpos($src, '/wp-content/languages/') !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Convert a URL to a local file path
     *
     * @param string $url Script URL
     * @return string|null Local file path or null if not resolvable
     */
    private function url_to_path(string $url): ?string {
        // Strip query strings
        $url = strtok($url, '?');
        if (!$url) {
            return null;
        }

        // Try content_url → WP_CONTENT_DIR mapping
        $content_url = content_url();
        if (strpos($url, $content_url) === 0) {
            $relative = substr($url, strlen($content_url));
            $path     = WP_CONTENT_DIR . $relative;
            return file_exists($path) ? $path : null;
        }

        // Try includes_url → ABSPATH mapping
        $includes_url = includes_url();
        if (strpos($url, $includes_url) === 0) {
            $relative = substr($url, strlen($includes_url));
            $path     = ABSPATH . WPINC . '/' . $relative;
            return file_exists($path) ? $path : null;
        }

        // Try site_url → ABSPATH mapping
        $site_url = site_url();
        if (strpos($url, $site_url) === 0) {
            $relative = substr($url, strlen($site_url));
            $home_path = function_exists('prorank_get_home_path')
                ? \prorank_get_home_path()
                : trailingslashit(wp_normalize_path(ABSPATH));
            $path     = $home_path . ltrim($relative, '/');
            return file_exists($path) ? $path : null;
        }

        return null;
    }

    /**
     * Output combined scripts in a single tag
     *
     * Called at wp_footer priority 9999 to collect all suppressed scripts
     * and emit a single combined file.
     *
     * @return void
     */
    public function output_combined_scripts(): void {
        if ($this->combined_output_done || empty($this->combinable_scripts)) {
            return;
        }
        $this->combined_output_done = true;

        // Need at least 2 scripts to make combining worthwhile
        if (count($this->combinable_scripts) < 2) {
            foreach ($this->combinable_scripts as $script) {
                echo '<script src="' . esc_url($script['src']) . '"></script>' . "\n"; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
            }
            return;
        }

        // Build a cache key from all file paths + modification times
        $hash_parts = [];
        foreach ($this->combinable_scripts as $script) {
            $hash_parts[] = $script['path'] . ':' . filemtime($script['path']);
        }
        $cache_key  = md5(implode('|', $hash_parts));
        $cache_file = $this->combine_cache_dir . 'combined-' . $cache_key . '.js';
        $cache_url  = $this->combine_cache_url . 'combined-' . $cache_key . '.js';

        // Generate combined file if not cached
        if (!file_exists($cache_file)) {
            $combined = '';
            foreach ($this->combinable_scripts as $script) {
                $content = file_get_contents($script['path']);
                if ($content === false) {
                    continue;
                }
                // Add semicolon separator to prevent ASI issues
                $combined .= '/* ' . basename($script['path']) . " */\n";
                $combined .= rtrim($content) . ";\n";
            }

            if ($combined === '') {
                // Fallback: output individual tags
                foreach ($this->combinable_scripts as $script) {
                    echo '<script src="' . esc_url($script['src']) . '"></script>' . "\n"; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
                }
                return;
            }

            file_put_contents($cache_file, $combined);
        }

        // Output the combined script tag
        $tag = sprintf(
            '<script id="prorank-combined-js" src="%s" defer></script>', // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
            esc_url($cache_url)
        );

        echo $tag . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
    }
}
