<?php

/**
 * Third-Party Script Defer/INP Guard
 *
 * Adds defer + data markers to heavy third-party scripts (analytics/ads/chat),
 * to reduce main-thread contention and improve INP.
 *
 * @package ProRank\SEO\Frontend
 */

declare(strict_types=1);

namespace ProRank\SEO\Frontend;

use ProRank\SEO\Core\Buffer;
use ProRank\SEO\Core\Compatibility\PerformanceCompatibilityRegistry;

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




class ThirdPartyDefer {

    /**
     * Whether the current request rendered a real Site Kit SIWG button target.
     *
     * Site Kit exposes a dedicated action whenever it renders a real button
     * placeholder for blocks, WooCommerce login, wp_login_form, or comments.
     * We can use that signal to suppress the footer runtime only when the page
     * never emitted a target.
     *
     * @var bool
     */
    private bool $site_kit_gsi_target_rendered = false;

    /**
     * Detect real Sign in with Google target markup outside script/style tags.
     *
     * Site Kit can emit the GSI client and bootstrap on pages that only carry
     * the related CSS and inline JS strings, but no actual button/container
     * markup. In that case the runtime is dead weight.
     *
     * @param string $html
     * @return bool
     */
    private function page_has_real_site_kit_gsi_targets(string $html): bool {
        if ($html === '' || stripos($html, 'googlesitekit-sign-in-with-google') === false) {
            return false;
        }

        $markup = preg_replace('~<(script|style)\b[^>]*>[\s\S]*?</\1>~i', '', $html);
        if (!is_string($markup) || $markup === '') {
            return false;
        }

        $patterns = [
            '~<[^>]+\bclass=(["\'])[^"\']*\bgooglesitekit-sign-in-with-google__frontend-output-button\b[^"\']*\1~i',
            '~<[^>]+\bclass=(["\'])[^"\']*\bgooglesitekit-sign-in-with-google__comments-form-button\b[^"\']*\1~i',
            '~<[^>]+\bid=(["\'])g_id_signin\1~i',
            '~<[^>]+\bclass=(["\'])[^"\']*\bg_id_signin\b[^"\']*\1~i',
            '~<[^>]+\bdata-googlesitekit-siwg-(?:shape|text|theme)=~i',
        ];

        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $markup) === 1) {
                return true;
            }
        }

        return false;
    }

    /**
     * Detect the Site Kit Sign in with Google bootstrap pair.
     *
     * @param string $html
     * @return bool
     */
    private function page_has_site_kit_gsi_bootstrap(string $html): bool {
        return $html !== ''
            && stripos($html, 'accounts.google.com/gsi/client') !== false
            && stripos($html, 'google.accounts.id.initialize') !== false
            && stripos($html, 'wp-login.php?action=googlesitekit_auth') !== false
            && stripos($html, 'googlesitekit-sign-in-with-google') !== false;
    }

    /**
     * Remove Site Kit's Sign in with Google runtime when no real targets exist.
     *
     * This keeps actual auth surfaces intact while stripping dead GSI payload
     * from pages that carry only Site Kit's bootstrap strings.
     *
     * @param string $html
     * @return string
     */
    private function prune_unused_site_kit_gsi_bootstrap(string $html): string {
        if (!$this->page_has_site_kit_gsi_bootstrap($html) || $this->page_has_real_site_kit_gsi_targets($html)) {
            return $html;
        }

        $updated = preg_replace(
            '~<script\b[^>]*\bsrc=(["\'])https://accounts\.google\.com/gsi/client\1[^>]*></script>\s*~i',
            '',
            $html,
            1
        );

        if (!is_string($updated)) {
            return $html;
        }

        $updated = preg_replace(
            '~<script\b[^>]*>[\s\S]*?wp-login\.php\?action=googlesitekit_auth[\s\S]*?google\.accounts\.id\.initialize[\s\S]*?googlesitekit-sign-in-with-google[\s\S]*?</script>\s*~i',
            '',
            $updated,
            1
        );

        if (!is_string($updated)) {
            return $html;
        }

        $updated = preg_replace(
            '~<!--\s*Sign in with Google button added by Site Kit\s*-->\s*(?:<style>\s*\.googlesitekit-sign-in-with-google__frontend-output-button\{[\s\S]*?</style>\s*)?~i',
            '',
            $updated,
            1
        );

        return is_string($updated) ? $updated : $html;
    }

    /**
     * GSI client is commonly followed by immediate inline bootstraps.
     *
     * Keep it synchronous unless ProRank grows a deeper dependency graph for
     * identity widgets. This follows the conservative competitor pattern for
     * auth/login runtimes.
     *
     * @param string $haystack
     * @return bool
     */
    private function is_gsi_client_haystack(string $haystack): bool {
        return $haystack !== '' && strpos($haystack, 'accounts.google.com/gsi/client') !== false;
    }

    /**
     * Detect immediate inline identity-widget bootstraps in the rendered page.
     *
     * @param string $html
     * @return bool
     */
    private function page_has_inline_identity_bootstrap(string $html): bool {
        if ($html === '' || stripos($html, 'google.accounts') === false) {
            return false;
        }

        $haystack = strtolower($html);
        foreach (PerformanceCompatibilityRegistry::get_identity_widget_inline_bootstrap_patterns() as $pattern) {
            if ($pattern !== '' && strpos($haystack, $pattern) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Detect inline Google measurement/bootstrap state in the rendered page.
     *
     * @param string $html
     * @return bool
     */
    private function page_has_inline_google_service_bootstrap(string $html): bool {
        if ($html === '') {
            return false;
        }

        $haystack = strtolower($html);
        foreach (PerformanceCompatibilityRegistry::get_google_service_inline_bootstrap_patterns() as $pattern) {
            if ($pattern !== '' && strpos($haystack, $pattern) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check whether the current script/tag matches protected delay/defer patterns.
     *
     * @param string $haystack
     * @return bool
     */
    private function is_delay_protected_haystack(string $haystack): bool {
        if ($haystack === '') {
            return false;
        }

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

        return false;
    }

    /**
     * Check whether the current script/tag matches protected defer patterns.
     *
     * @param string $haystack
     * @return bool
     */
    private function is_defer_protected_haystack(string $haystack): bool {
        if ($haystack === '') {
            return false;
        }

        foreach (PerformanceCompatibilityRegistry::get_defer_js_protected_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 = $this->get_runtime_advanced_settings();

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

        return $settings;
    }

    /**
     * Load advanced settings with legacy fallback for runtime governance.
     *
     * Some sites still carry values in the older `prorank_advanced_settings`
     * option. Runtime paths should honor those values when the newer
     * `prorank_seo_advanced_settings` option is missing keys.
     *
     * @return array<string, mixed>
     */
    private function get_runtime_advanced_settings(): array {
        $advanced = get_option('prorank_seo_advanced_settings', []);
        if (!is_array($advanced)) {
            $advanced = [];
        }

        $legacy = get_option('prorank_advanced_settings', []);
        if (!is_array($legacy) || empty($legacy)) {
            return $advanced;
        }

        foreach ([
            'delay_google_services',
            'delay_review_widgets',
            'defer_consent_scripts',
            'delay_identity_widgets',
        ] as $key) {
            if (!array_key_exists($key, $advanced) && array_key_exists($key, $legacy)) {
                $advanced[$key] = $legacy[$key];
            }
        }

        return $advanced;
    }

    /**
     * Whether the site explicitly opted into third-party governance lanes.
     *
     * This is stricter than get_third_party_governance_settings(), which uses
     * safe defaults. We use this when deciding whether governance should stay
     * active even if the generic js_defer lane is turned off.
     *
     * @return bool
     */
    private function has_explicit_governance_opt_in(): bool {
        $advanced = $this->get_runtime_advanced_settings();

        foreach ([
            'delay_google_services',
            'delay_review_widgets',
            'defer_consent_scripts',
            'delay_identity_widgets',
        ] as $key) {
            if (array_key_exists($key, $advanced) && !empty($advanced[$key])) {
                return true;
            }
        }

        return false;
    }

    /**
     * Whether generic low-risk tracker defer is allowed on this request.
     *
     * Explicit governance opt-ins should not silently widen into the generic
     * low-risk tracker lane when js_defer itself is off.
     *
     * @return bool
     */
    private function should_allow_generic_tracker_defer(): bool {
        $asset_settings = get_option('prorank_asset_optimization_settings', []);
        $cache_settings = get_option('prorank_cache_settings', []);

        return (is_array($asset_settings) && !empty($asset_settings['inp_optimizer_enabled']))
            || (is_array($cache_settings) && (
                !empty($cache_settings['js_defer'])
                || !empty($cache_settings['tracker_delay_enabled'])
                || !empty($cache_settings['js_delay'])
            ));
    }

    /**
     * Check whether a script belongs to the Google tracking/services category.
     */
    private function is_google_service_haystack(string $haystack): bool {
        foreach (PerformanceCompatibilityRegistry::get_google_service_patterns() as $pattern) {
            if (strpos($haystack, $pattern) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check whether a script belongs to the review-widget category.
     */
    private function is_review_widget_haystack(string $haystack): bool {
        foreach (PerformanceCompatibilityRegistry::get_review_widget_patterns() as $pattern) {
            if (strpos($haystack, $pattern) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check whether a script belongs to the consent category.
     */
    private function is_consent_script_haystack(string $haystack): bool {
        foreach (PerformanceCompatibilityRegistry::get_consent_script_patterns() as $pattern) {
            if (strpos($haystack, $pattern) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check whether a script belongs to the identity-widget category.
     */
    private function is_identity_widget_haystack(string $haystack): bool {
        foreach (PerformanceCompatibilityRegistry::get_identity_widget_patterns() as $pattern) {
            if (strpos($haystack, $pattern) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Determine whether an enqueued third-party script should be made non-blocking.
     */
    private function should_defer_enqueued_third_party(string $haystack): bool {
        $governance = $this->get_third_party_governance_settings();

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

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

        if ($this->is_google_service_haystack($haystack)) {
            return $governance['delay_google_services'];
        }

        if ($this->is_review_widget_haystack($haystack)) {
            return $governance['delay_review_widgets'];
        }

        if ($this->is_consent_script_haystack($haystack)) {
            return $governance['defer_consent_scripts'];
        }

        if ($this->is_identity_widget_haystack($haystack)) {
            return $governance['delay_identity_widgets'];
        }

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

        $patterns = PerformanceCompatibilityRegistry::get_low_risk_tracker_patterns();

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

        return false;
    }

    /**
     * Whether third-party defer should run for the current settings.
     */
    private function should_process(): bool {
        $asset_settings = get_option('prorank_asset_optimization_settings', []);
        $cache_settings = get_option('prorank_cache_settings', []);
        $governance = $this->get_third_party_governance_settings();

        $inp_enabled = is_array($asset_settings) && !empty($asset_settings['inp_optimizer_enabled']);
        $tracker_delay_enabled = is_array($cache_settings) && (
            !empty($cache_settings['tracker_delay_enabled'])
            || !empty($cache_settings['js_delay'])
        );
        $governance_opt_in_enabled = $this->has_explicit_governance_opt_in();
        $js_defer_governance_enabled = is_array($cache_settings)
            && (
                $governance_opt_in_enabled
                || (
                    !empty($cache_settings['js_defer'])
                    && (
                        $governance['delay_google_services']
                        || $governance['delay_review_widgets']
                        || $governance['defer_consent_scripts']
                        || $governance['delay_identity_widgets']
                    )
                )
            );

        return $inp_enabled || $tracker_delay_enabled || $js_defer_governance_enabled;
    }

    /**
     * Boot hooks
     */
    public function init(): void {
        if ( \ProRank\SEO\Plugin::is_elementor_runtime_request() ) {
            return;
        }

        add_filter( 'script_loader_tag', function($tag, $handle, $src) {
            $original_tag = is_string($tag) ? $tag : (string) $tag;

            try {
                return $this->defer_third_party(
                    $original_tag,
                    is_string($handle) ? $handle : (string) $handle,
                    is_string($src) ? $src : ''
                );
            } catch (\Throwable $e) {
                if (function_exists('prorank_log')) {
                    prorank_log('[ProRank ThirdPartyDefer] script_loader_tag failed: ' . $e->getMessage());
                }

                return $original_tag;
            }
        }, 10, 3 );
        Buffer::init();
        add_action( 'googlesitekit_render_sign_in_with_google_button', [ $this, 'mark_site_kit_gsi_target_rendered' ], 0 );
        add_action( 'wp_footer', [ $this, 'maybe_disable_unused_site_kit_gsi_footer_runtime' ], 0 );
        add_filter( 'prorank_output_buffer', [ $this, 'defer_raw_tracker_bootstraps' ], 12 );
        add_filter( 'prorank_output_buffer', [ $this, 'defer_non_enqueued_external_scripts' ], 13 );
        add_filter( 'prorank_output_buffer', [ $this, 'dedupe_trustindex_loader_scripts' ], 14 );
        add_filter( 'prorank_output_buffer', [ $this, 'localize_trustindex_loader_scripts' ], 15 );
        add_filter( 'prorank_output_buffer', [ $this, 'ensure_delayed_third_party_loader' ], 98 );
        add_filter( 'prorank_output_buffer', [ $this, 'prune_unused_site_kit_gsi_output' ], 99 );
    }

    /**
     * Mark that the current request rendered a real SIWG placeholder.
     *
     * @return void
     */
    public function mark_site_kit_gsi_target_rendered(): void {
        $this->site_kit_gsi_target_rendered = true;
    }

    /**
     * Remove Site Kit's dead footer runtime when the page rendered no targets.
     *
     * Site Kit's Web_Tag currently prints its GSI runtime sitewide in wp_footer
     * once the module is connected. If no real button placeholder was rendered
     * earlier in the request, the footer runtime is dead weight.
     *
     * @return void
     */
    public function maybe_disable_unused_site_kit_gsi_footer_runtime(): void {
        if (!$this->should_process() || $this->site_kit_gsi_target_rendered) {
            return;
        }

        $this->remove_site_kit_gsi_footer_callback('wp_footer');
    }

    /**
     * Remove Site Kit's Sign in with Google footer renderer from a hook.
     *
     * @param string $hook_name
     * @return void
     */
    private function remove_site_kit_gsi_footer_callback(string $hook_name): void {
        global $wp_filter;

        if (!isset($wp_filter[$hook_name]) || !($wp_filter[$hook_name] instanceof \WP_Hook)) {
            return;
        }

        foreach ($wp_filter[$hook_name]->callbacks as $priority => $callbacks) {
            foreach ($callbacks as $callback) {
                $fn = $callback['function'] ?? null;

                if ($fn instanceof \Closure) {
                    $reflection = new \ReflectionFunction($fn);
                    $closure_this = $reflection->getClosureThis();
                    $scope = $reflection->getClosureScopeClass();
                    $scope_name = $scope ? $scope->getName() : '';

                    if (
                        ($closure_this && is_a($closure_this, 'Google\\Site_Kit\\Modules\\Sign_In_With_Google\\Web_Tag'))
                        || $scope_name === 'Google\\Site_Kit\\Modules\\Sign_In_With_Google\\Web_Tag'
                    ) {
                        remove_action($hook_name, $fn, (int) $priority);
                    }

                    continue;
                }

                if (!is_array($fn) || !isset($fn[0], $fn[1]) || !is_object($fn[0]) || !is_string($fn[1])) {
                    continue;
                }

                if ($fn[1] !== 'render' || !is_a($fn[0], 'Google\\Site_Kit\\Modules\\Sign_In_With_Google\\Web_Tag')) {
                    continue;
                }

                remove_action($hook_name, [$fn[0], $fn[1]], (int) $priority);
            }
        }
    }

    /**
     * Late-pass pruning for Site Kit GSI payload with no real targets.
     *
     * Run this near the end of ProRank's buffer chain so dead GSI markup is
     * removed even when earlier JS-governance paths were bypassed or reverted.
     *
     * @param string $html
     * @return string
     */
    public function prune_unused_site_kit_gsi_output(string $html): string {
        if (!$this->should_process()) {
            return $html;
        }

        if ($html === '' || !$this->page_has_site_kit_gsi_bootstrap($html)) {
            return $html;
        }

        $html = $this->prune_unused_site_kit_gsi_bootstrap($html);
        return $html;
    }

    /**
     * Add defer + marker to known third-party scripts
     */
    public function defer_third_party( string $tag, string $handle, string $src ): string {
        if (!$this->should_process()) {
            return $tag;
        }

        // Respect WordPress-managed execution strategy. If core/plugin code has
        // already declared a strategy for this handle, ProRank should not try
        // to re-own the tag.
        if (stripos($tag, 'data-wp-strategy=') !== false) {
            return $tag;
        }

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

        if ($this->is_google_service_haystack($haystack) && stripos($tag, ' async') !== false) {
            return $tag;
        }

        if ( strpos( $tag, 'defer' ) === false ) {
            $tag = str_replace( '<script ', '<script defer ', $tag );
        }

        if ( strpos( $tag, 'data-prorank-3p' ) === false ) {
            $tag = str_replace( '<script ', '<script data-prorank-3p="true" ', $tag );
        }

        return $tag;
    }

    /**
     * Defer known raw tracker bootstraps emitted outside wp_enqueue_script().
     *
     * This is intentionally narrow:
     * - Google Tag Manager standard bootstrap
     * - Facebook Pixel standard bootstrap
     *
     * We keep the queue/bootstrap objects synchronous, but move the external
     * network fetch behind load/idle so they stop competing with page startup.
     *
     * @param string $html
     * @return string
     */
    public function defer_raw_tracker_bootstraps(string $html): string {
        if (!$this->should_process()) {
            return $html;
        }

        if ($html === '' || stripos($html, '<script') === false) {
            return $html;
        }

        if (
            stripos($html, 'googletagmanager.com/gtm.js') === false &&
            stripos($html, 'googletagmanager.com/gtag/js') === false &&
            stripos($html, 'connect.facebook.net/en_US/fbevents.js') === false &&
            stripos($html, 'static.getclicky.com') === false &&
            !$this->page_has_site_kit_gsi_bootstrap($html)
        ) {
            return $html;
        }

        $html = $this->prune_unused_site_kit_gsi_bootstrap($html);
        $original_html = $html;
        if ($this->get_third_party_governance_settings()['delay_google_services']) {
            $html = $this->defer_gtag_bootstrap($html);
            $html = $this->defer_gtm_bootstrap($html);
        }
        $html = $this->defer_facebook_pixel_bootstrap($html);
        $html = $this->defer_clicky_script($html);

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

    /**
     * Rewrite the standard Google tag bootstrap so only the external fetch is deferred.
     *
     * Keeps the inline dataLayer/gtag queue synchronous while moving the
     * network fetch for gtag.js to post-load/idle.
     *
     * @param string $html
     * @return string
     */
    private function defer_gtag_bootstrap(string $html): string {
        $pattern = '~<script\b(?![^>]*data-prorank-3p-inline=["\']gtag["\'])[^>]*\bsrc=(["\'])(https://www\.googletagmanager\.com/gtag/js\?id=[^"\']+)\1[^>]*></script>~i';
        $schedule_js = $this->build_google_tracker_schedule_js('w', 'loadGtag');

        $updated = preg_replace_callback(
            $pattern,
            function (array $matches) use ($schedule_js): string {
                $src = trim((string) ($matches[2] ?? ''));
                if ($src === '') {
                    return (string) ($matches[0] ?? '');
                }

                return '<script data-prorank-3p-inline="gtag">(function(w,d){var loadGtag=function(){if(w._prorankGtagLoaded){return;}w._prorankGtagLoaded=true;var s=d.createElement("script"),f=d.getElementsByTagName("script")[0];s.async=true;s.src="' . esc_js($src) . '";f.parentNode.insertBefore(s,f);};' . $schedule_js . '})(window,document);</script>';
            },
            $html
        );

        return is_string($updated) ? $updated : $html;
    }

    /**
     * Rewrite the standard GTM bootstrap so only the external fetch is deferred.
     *
     * @param string $html
     * @return string
     */
    private function defer_gtm_bootstrap(string $html): string {
        $pattern = '~<script\b(?![^>]*\bsrc=)(?![^>]*data-prorank-3p-inline=["\']gtm["\'])[^>]*>(.*?)</script>~is';
        $schedule_js = $this->build_google_tracker_schedule_js('w', 'loadGtm');

        $updated = preg_replace_callback(
            $pattern,
            function (array $matches) use ($schedule_js): string {
                $script = (string) ($matches[1] ?? '');
                if (
                    $script === ''
                    || stripos($script, 'gtm.start') === false
                    || stripos($script, 'googletagmanager.com/gtm.js') === false
                ) {
                    return (string) ($matches[0] ?? '');
                }

                if (!preg_match('~\}\s*\)\s*\(\s*window\s*,\s*document\s*,\s*[\'"]script[\'"]\s*,\s*[\'"]([^\'"]+)[\'"]\s*,\s*[\'"]([^\'"]+)[\'"]\s*\)\s*;?~i', $script, $call_matches)) {
                    return (string) ($matches[0] ?? '');
                }

                $layer = trim((string) ($call_matches[1] ?? 'dataLayer'));
                $container_id = trim((string) ($call_matches[2] ?? ''));

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

                return '<script data-prorank-3p-inline="gtm">(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({"gtm.start":new Date().getTime(),event:"gtm.js"});var loadGtm=function(){if(w._prorankGtmLoaded){return;}w._prorankGtmLoaded=true;var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!=="dataLayer"?"&l="+l:"";j.async=true;j.src="https://www.googletagmanager.com/gtm.js?id="+i+dl;f.parentNode.insertBefore(j,f);};' . $schedule_js . '})(window,document,"script","' . esc_js($layer) . '","' . esc_js($container_id) . '");</script>';
            },
            $html
        );

        return is_string($updated) ? $updated : $html;
    }

    /**
     * Rewrite the standard Facebook Pixel bootstrap so the queue stays sync
     * but the external loader waits until load/idle.
     *
     * @param string $html
     * @return string
     */
    private function defer_facebook_pixel_bootstrap(string $html): string {
        $pattern = '~<script\b(?![^>]*\bsrc=)(?![^>]*data-prorank-3p-inline=["\']facebook["\'])[^>]*>\s*(!function\(f,b,e,v,n,t,s\)(?:(?!</script>).)*?connect\.facebook\.net/en_US/fbevents\.js(?:(?!</script>).)*)</script>~is';
        $schedule_js = $this->build_facebook_tracker_schedule_js('f', '_prorankFbLoad', 'b');

        $updated = preg_replace_callback(
            $pattern,
            function (array $matches) use ($schedule_js): string {
                $content = (string) ($matches[1] ?? '');
                if ($content === '') {
                    return (string) ($matches[0] ?? '');
                }

                $replacement = preg_replace(
                    '~n\.queue=\[\];\s*t=b\.createElement\(e\);\s*t\.async=!0;\s*t\.src=v;\s*s=b\.getElementsByTagName\(e\)\[0\];\s*s\.parentNode\.insertBefore\(t,s\)~',
                    'n.queue=[];var _prorankFbLoad=function(){if(f._prorankFbLoaded){return;}f._prorankFbLoaded=!0;t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s);};' . $schedule_js,
                    $content,
                    1
                );

                if (!is_string($replacement) || $replacement === $content) {
                    return (string) ($matches[0] ?? '');
                }

                return '<script data-prorank-3p-inline="facebook">' . $replacement . '</script>';
            },
            $html
        );

        return is_string($updated) ? $updated : $html;
    }

    /**
     * Build tracker loader scheduling JS.
     *
     * When tracker-delay is enabled, use interaction-first loading with a long
     * timeout fallback. Otherwise keep the existing post-load/idle behavior.
     *
     * @param string $window_ref   Window variable name in the inline bootstrap.
     * @param string $load_fn      Loader function name.
     * @param string $document_ref Document variable name in the inline bootstrap.
     * @return string
     */
    private function build_tracker_schedule_js(string $window_ref, string $load_fn, string $document_ref = 'd'): string {
        if ($this->is_tracker_delay_enabled()) {
            return 'var _prorankTrackerRun=function(){if(' . $window_ref . '._prorankTrackerTriggered){return;}' . $window_ref . '._prorankTrackerTriggered=true;' . $load_fn . '();};var _prorankTrackerBind=function(){var evts=["click","keydown","touchstart","touchend","mousemove","scroll"];var opts={passive:true};var trigger=function(){evts.forEach(function(evt){' . $window_ref . '.removeEventListener(evt,trigger,opts);});_prorankTrackerRun();};evts.forEach(function(evt){' . $window_ref . '.addEventListener(evt,trigger,opts);});setTimeout(trigger,10000);};if(' . $document_ref . '.readyState==="complete"){setTimeout(_prorankTrackerBind,600);}else{' . $window_ref . '.addEventListener("load",function(){setTimeout(_prorankTrackerBind,600);},{once:true});}';
        }

        return 'var _prorankTrackerSchedule=function(){if("requestIdleCallback" in ' . $window_ref . '){' . $window_ref . '.requestIdleCallback(' . $load_fn . ',{timeout:5000});}else{setTimeout(' . $load_fn . ',1200);}};if(' . $document_ref . '.readyState==="complete"){setTimeout(_prorankTrackerSchedule,600);}else{' . $window_ref . '.addEventListener("load",function(){setTimeout(_prorankTrackerSchedule,600);},{once:true});}';
    }

    /**
     * Build a safe schedule for Google tracking bootstraps.
     *
     * By default, keep Google tags non-blocking but still load them after
     * page load/idle so Ads/analytics coverage is preserved for conservative
     * setups. When the explicit tracker-delay lane is enabled, honor it and
     * use the same interaction-first schedule as other low-risk trackers.
     *
     * @param string $window_ref   Window variable name in the inline bootstrap.
     * @param string $load_fn      Loader function name.
     * @param string $document_ref Document variable name in the inline bootstrap.
     * @return string
     */
    private function build_google_tracker_schedule_js(string $window_ref, string $load_fn, string $document_ref = 'd'): string {
        if ($this->is_tracker_delay_enabled()) {
            return $this->build_tracker_schedule_js($window_ref, $load_fn, $document_ref);
        }

        return 'var _prorankGoogleTrackerSchedule=function(){if("requestIdleCallback" in ' . $window_ref . '){' . $window_ref . '.requestIdleCallback(' . $load_fn . ',{timeout:2000});}else{setTimeout(' . $load_fn . ',250);}};if(' . $document_ref . '.readyState==="complete"){setTimeout(_prorankGoogleTrackerSchedule,50);}else{' . $window_ref . '.addEventListener("load",function(){setTimeout(_prorankGoogleTrackerSchedule,50);},{once:true});}';
    }

    /**
     * Build a safe schedule for Meta Pixel.
     *
     * The fbq queue must be available immediately for setup checks and
     * PageView measurement. Only the external fbevents.js request is delayed.
     *
     * @param string $window_ref   Window variable name in the inline bootstrap.
     * @param string $load_fn      Loader function name.
     * @param string $document_ref Document variable name in the inline bootstrap.
     * @return string
     */
    private function build_facebook_tracker_schedule_js(string $window_ref, string $load_fn, string $document_ref = 'd'): string {
        return 'var _prorankFacebookTrackerSchedule=function(){if("requestIdleCallback" in ' . $window_ref . '){' . $window_ref . '.requestIdleCallback(' . $load_fn . ',{timeout:2500});}else{setTimeout(' . $load_fn . ',350);}};if(' . $document_ref . '.readyState==="complete"){setTimeout(_prorankFacebookTrackerSchedule,100);}else{' . $window_ref . '.addEventListener("load",function(){setTimeout(_prorankFacebookTrackerSchedule,100);},{once:true});}';
    }

    /**
     * Whether tracker interaction delay is enabled.
     *
     * @return bool
     */
    private function is_tracker_delay_enabled(): bool {
        $cache_settings = get_option('prorank_cache_settings', []);
        if (!is_array($cache_settings)) {
            return false;
        }

        return !empty($cache_settings['tracker_delay_enabled']) || !empty($cache_settings['js_delay']);
    }

    /**
     * Convert the raw Clicky analytics tag into a post-load loader.
     *
     * @param string $html
     * @return string
     */
    private function defer_clicky_script(string $html): string {
        $pattern = '~<script\b(?![^>]*data-prorank-3p-inline=["\']clicky["\'])[^>]*\bsrc=(["\'])(//static\.getclicky\.com/66582699\.js)\1[^>]*></script>~i';

        $updated = preg_replace_callback(
            $pattern,
            static function (array $matches): string {
                $src = (string) ($matches[2] ?? '//static.getclicky.com/66582699.js');
                return '<script data-prorank-3p-inline="clicky">(function(w,d){var loadClicky=function(){if(w._prorankClickyLoaded){return;}w._prorankClickyLoaded=true;var s=d.createElement("script"),f=d.getElementsByTagName("script")[0];s.async=true;s.src="' . esc_js($src) . '";f.parentNode.insertBefore(s,f);};if(d.readyState==="complete"){setTimeout(loadClicky,1200);}else{w.addEventListener("load",function(){setTimeout(loadClicky,1200);},{once:true});}})(window,document);</script>';
            },
            $html
        );

        return is_string($updated) ? $updated : $html;
    }

    /**
     * Add `defer` to non-enqueued external scripts in the HTML buffer.
     *
     * WordPress's `script_loader_tag` only fires for scripts registered via
     * `wp_enqueue_script()`. Many plugins (Thrive Ultimatum, page-builder
     * add-ons, etc.) output `<script src="...">` tags directly. This pass
     * catches those and adds `defer` so they don't block HTML parsing.
     *
     * Intentionally conservative: only touches external scripts (has src),
     * skips scripts that already have defer/async, and protects jQuery,
     * reCAPTCHA, and other critical scripts via a safelist.
     *
     * @param string $html
     * @return string
     */
    public function defer_non_enqueued_external_scripts(string $html): string {
        $cache_settings = get_option('prorank_cache_settings', []);
        $governance_opt_in_enabled = $this->has_explicit_governance_opt_in();
        if ((!is_array($cache_settings) || empty($cache_settings['js_defer'])) && !$governance_opt_in_enabled) {
            return $html;
        }

        $governance = $this->get_third_party_governance_settings();

        if ($html === '' || stripos($html, '<script') === false) {
            return $html;
        }

        $html = $this->prune_unused_site_kit_gsi_bootstrap($html);
        $original_html = $html;
        $has_inline_identity_bootstrap = $this->page_has_inline_identity_bootstrap($html);
        $has_inline_google_service_bootstrap = $this->page_has_inline_google_service_bootstrap($html);

        if ($governance['defer_consent_scripts']) {
            $html = (string) preg_replace_callback(
                '~<script\b([^>]*)\bsrc\s*=\s*(["\'])([^"\']+)\2([^>]*)>~i',
                function (array $matches): string {
                    $before_src = $matches[1];
                    $after_src  = $matches[4];
                    $src        = $matches[3];
                    $full_tag   = $matches[0];
                    $attrs      = $before_src . $after_src;

                    if (!$this->is_consent_script_haystack(strtolower($src))) {
                        return $full_tag;
                    }

                    if (
                        preg_match('/\b(?:defer|async)\b/i', $attrs)
                        || stripos($attrs, 'data-no-defer') !== false
                        || stripos($attrs, 'data-cfasync="false"') !== false
                        || stripos($attrs, 'data-prorank-') !== false
                        || preg_match('/type=["\']module["\']/', $attrs)
                    ) {
                        return $full_tag;
                    }

                    return preg_replace('/(<script\b[^>]*)(>)/i', '$1 defer$2', $full_tag, 1);
                },
                $html
            );
        }

        $html = (string) preg_replace_callback(
            '~<script\b([^>]*)\bsrc\s*=\s*(["\'])([^"\']+)\2([^>]*)>~i',
            function (array $matches): string {
                $before_src = $matches[1];
                $after_src  = $matches[4];
                $src        = $matches[3];
                $full_tag   = $matches[0];
                $attrs      = $before_src . $after_src;

                // Already has defer or async.
                if (preg_match('/\b(?:defer|async)\b/i', $attrs)) {
                    return $full_tag;
                }

                // Already processed by script_loader_tag (has WordPress id).
                if (preg_match('/\bid=["\'][\w.\-]+-js["\']/', $attrs)) {
                    return $full_tag;
                }

                // Opt-out attributes.
                if (stripos($attrs, 'data-no-defer') !== false
                    || stripos($attrs, 'data-cfasync="false"') !== false
                    || stripos($attrs, 'data-prorank-') !== false) {
                    return $full_tag;
                }

                // Module scripts are already deferred by the browser.
                if (preg_match('/type=["\']module["\']/', $attrs)) {
                    return $full_tag;
                }

                if (stripos($attrs, 'data-wp-strategy=') !== false) {
                    return $full_tag;
                }

                // Safelist: scripts that must stay synchronous.
                $src_lc = strtolower($src);
                $tag_lc = strtolower($full_tag);

                $is_google_service = $this->is_google_service_haystack($src_lc);
                $is_review_widget  = $this->is_review_widget_haystack($src_lc);
                $is_consent_script = $this->is_consent_script_haystack($src_lc);
                $is_identity_widget = $this->is_identity_widget_haystack($src_lc);

                if ($this->is_defer_protected_haystack($src_lc . ' ' . $tag_lc)) {
                    return $full_tag;
                }

                if ($is_google_service) {
                    if ($has_inline_google_service_bootstrap) {
                        return $full_tag;
                    }

                    return $governance['delay_google_services']
                        ? preg_replace('/(<script\b[^>]*)(>)/i', '$1 defer$2', $full_tag, 1)
                        : $full_tag;
                }

                if ($is_review_widget) {
                    return $governance['delay_review_widgets']
                        ? preg_replace('/(<script\b[^>]*)(>)/i', '$1 defer$2', $full_tag, 1)
                        : $full_tag;
                }

                if ($is_consent_script) {
                    return $governance['defer_consent_scripts']
                        ? preg_replace('/(<script\b[^>]*)(>)/i', '$1 defer$2', $full_tag, 1)
                        : $full_tag;
                }

                if ($is_identity_widget) {
                    if ($this->is_gsi_client_haystack($src_lc) || $has_inline_identity_bootstrap) {
                        return $full_tag;
                    }

                    return $governance['delay_identity_widgets']
                        ? preg_replace('/(<script\b[^>]*)(>)/i', '$1 defer$2', $full_tag, 1)
                        : $full_tag;
                }

                $sync_patterns = [
                    'jquery', 'recaptcha', 'hcaptcha', 'turnstile',
                    'stripe', 'paypal', 'braintree', 'klarna',
                    'sumup', 'gateway.sumup.com', 'checkout.sumup.com',
                    'maps.googleapis.com', 'maps.gstatic.com',
                    'wp-includes/js/',
                    'bookly', 'amelia', 'latepoint', 'booknetic',
                    'simply-schedule-appointments',
                ];

                foreach ($sync_patterns as $p) {
                    if (strpos($src_lc, $p) !== false) {
                        return $full_tag;
                    }
                }

                // Add defer.
                return preg_replace('/(<script\b[^>]*)(>)/i', '$1 defer$2', $full_tag, 1);
            },
            $html
        );

        if (!is_string($html) || $html === '') {
            return $original_html;
        }

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

    /**
     * Collapse duplicate Trustindex loader fetches while preserving widget keys.
     *
     * Trustindex's loader scans the DOM for both `src` and `data-src` loader
     * tags, then initializes each widget key it finds. That lets ProRank keep
     * the first real network fetch and turn later duplicates into inert
     * placeholders, reducing duplicate JS transfer without breaking widgets.
     *
     * @param string $html
     * @return string
     */
    public function dedupe_trustindex_loader_scripts(string $html): string {
        if ($html === '' || stripos($html, 'cdn.trustindex.io/loader.js') === false) {
            return $html;
        }

        $seen_primary_loader = false;
        $updated = preg_replace_callback(
            '~<script\b([^>]*)\ssrc\s*=\s*(["\'])(https://cdn\.trustindex\.io/loader\.js\?[^"\']+)\2([^>]*)>\s*</script>~i',
            function (array $matches) use (&$seen_primary_loader): string {
                $before_src = (string) ($matches[1] ?? '');
                $src = trim((string) ($matches[3] ?? ''));
                $after_src = (string) ($matches[4] ?? '');
                $full_tag = (string) ($matches[0] ?? '');

                if ($src === '') {
                    return $full_tag;
                }

                if (!$seen_primary_loader) {
                    $seen_primary_loader = true;
                    $local_src = $this->get_local_trustindex_loader_url($src);
                    if (!$local_src) {
                        return $full_tag;
                    }

                    $attrs = trim(preg_replace('/\s+/', ' ', $before_src . ' ' . $after_src));
                    $attrs = (string) preg_replace('/\s+\b(?:data-prorank-trustindex-src|data-prorank-localized|data-src|src)\s*=\s*(["\']).*?\1/i', '', $attrs);
                    $attrs = trim((string) preg_replace('/\s+/', ' ', $attrs));

                    return $this->build_localized_trustindex_loader_markup($attrs, $local_src, $src);
                }

                $attrs = trim(preg_replace('/\s+/', ' ', $before_src . ' ' . $after_src));
                $delay_attr = preg_match('/\bdata-enable-delay-load\b/i', $attrs) === 1
                    ? ' data-enable-delay-load="1"'
                    : '';

                return '<script type="application/prorank-trustindex-placeholder" data-prorank-dedupe="trustindex-loader" data-prorank-trustindex-src="' . esc_attr($src) . '"' . $delay_attr . '></script>';
            },
            $html
        );

        return is_string($updated) ? $updated : $html;
    }

    /**
     * Serve the primary Trustindex loader from a local long-cache URL.
     *
     * Keep the original remote URL in data-src so Trustindex can still derive
     * the widget key from document.currentScript and DOM scans.
     *
     * @param string $html
     * @return string
     */
    public function localize_trustindex_loader_scripts(string $html): string {
        if ($html === '' || stripos($html, 'cdn.trustindex.io/loader.js') === false) {
            return $html;
        }

        $updated = preg_replace_callback(
            '~<script\b([^>]*)\ssrc\s*=\s*(["\'])(https://cdn\.trustindex\.io/loader\.js\?[^"\']+)\2([^>]*)>\s*</script>~i',
            function (array $matches): string {
                $before_src = (string) ($matches[1] ?? '');
                $remote_src = trim((string) ($matches[3] ?? ''));
                $after_src = (string) ($matches[4] ?? '');
                $full_tag = (string) ($matches[0] ?? '');

                if ($remote_src === '') {
                    return $full_tag;
                }

                $local_src = $this->get_local_trustindex_loader_url($remote_src);
                if (!$local_src) {
                    return $full_tag;
                }

                $attrs = trim(preg_replace('/\s+/', ' ', $before_src . ' ' . $after_src));
                $attrs = (string) preg_replace('/\s+\b(?:data-prorank-trustindex-src|data-prorank-localized|data-src|src)\s*=\s*(["\']).*?\1/i', '', $attrs);
                $attrs = trim((string) preg_replace('/\s+/', ' ', $attrs));

                return $this->build_localized_trustindex_loader_markup($attrs, $local_src, $remote_src);
            },
            $html,
            1
        );

        return is_string($updated) ? $updated : $html;
    }

    /**
     * Render a localized Trustindex loader plus a deferred ProRank init bridge.
     *
     * The vendor loader is kept local and long-cached, but its eager
     * `renderTrustindexWidgets()` call is suppressed via `data-skip-init`.
     * When review-widget governance or generic tracker delay is enabled, the
     * localized loader itself is emitted in ProRank's delayed-script format so
     * Trustindex no longer sits on the initial render/reflow path.
     *
     * @param string $attrs
     * @param string $local_src
     * @param string $remote_src
     * @return string
     */
    private function build_localized_trustindex_loader_markup(string $attrs, string $local_src, string $remote_src): string {
        $governance = $this->get_third_party_governance_settings();
        $interaction_only = !empty($governance['delay_review_widgets']) || $this->is_tracker_delay_enabled();
        $loader_attrs = $this->prepare_trustindex_loader_attrs($attrs, $interaction_only);
        $bridge_script = $this->get_trustindex_init_bridge_script($interaction_only);

        return sprintf(
            '<span class="prorank-trustindex-placeholder" aria-hidden="true" style="display:inline-block;width:220px;max-width:100%%;height:42px;border-radius:999px;background:#f3f4f6;vertical-align:middle;"></span><script%s src="%s" data-prorank-trustindex-src="%s" data-prorank-localized="trustindex-loader" data-skip-init="1"></script><script id="prorank-trustindex-init-bridge">%s</script>',
            $loader_attrs !== '' ? ' ' . $loader_attrs : '',
            esc_url($local_src),
            esc_url($remote_src),
            $bridge_script
        );

    }

    /**
     * Build the Trustindex bridge script.
     *
     * @param bool $interaction_only Whether reviews should wait for interaction.
     * @return string
     */
    private function get_trustindex_init_bridge_script(bool $interaction_only): string {
        $template = <<<'JS'
(function(){
if(window.prorankTrustindexInitBridge){return;}
window.prorankTrustindexInitBridge=1;
var started=false;
var scheduled=false;
var fallbackTimer=0;
var interactionOnly=%s;
var interactionEvents=["scroll","wheel","touchstart","keydown","pointerdown"];
var observer=null;
function isButtonWidget(instance){return !!(instance&&instance.widget&&Array.isArray(instance.widget.layoutCategory)&&instance.widget.layoutCategory.indexOf("button")!==-1);}
function patchTrustindex(){
if(window.prorankTrustindexPatched||typeof window.TrustindexWidget!=="function"){return;}
window.prorankTrustindexPatched=1;
var proto=window.TrustindexWidget.prototype;
if(proto&&typeof proto.resize==="function"){
var originalResize=proto.resize;
proto.resize=function(force){
if(isButtonWidget(this)){
if(this.widget&&typeof this.widget.toggleShow==="function"){this.widget.toggleShow();}
return;
}
return originalResize.call(this,force);
};
}
if(proto&&typeof proto.isOnViewport==="function"){proto.isOnViewport=function(){return true;};}
var darkProto=window.TrustindexDarkModeModule&&window.TrustindexDarkModeModule.prototype;
if(darkProto&&typeof darkProto.isLogoDark==="function"){
darkProto.isLogoDark=function(){
var setId=this.widget&&typeof this.widget.setId==="string"?this.widget.setId:"";
if(setId.indexOf("dark-")===0){return true;}
return !!(this.widget&&this.widget.getAttribute&&this.widget.getAttribute("data-auto-dark-mode")&&window.Trustindex&&typeof window.Trustindex.isDarkMode==="function"&&window.Trustindex.isDarkMode());
};
}
}
function findAnchor(){return document.querySelector('script[data-prorank-localized="trustindex-loader"]');}
function findTarget(){var anchor=findAnchor();if(!anchor){return null;}return anchor.closest(".elementor-widget-container,.ti-widget,.ti-widget-container,[data-layout-id]")||anchor.parentElement||anchor;}
function removePlaceholder(){var placeholder=document.querySelector(".prorank-trustindex-placeholder");if(placeholder&&placeholder.parentNode){placeholder.parentNode.removeChild(placeholder);}}
function cleanup(){for(var i=0;i<interactionEvents.length;i++){window.removeEventListener(interactionEvents[i],schedule,{passive:true});}if(observer){observer.disconnect();observer=null;}if(fallbackTimer){window.clearTimeout(fallbackTimer);fallbackTimer=0;}}
function flush(){if(started){return;}if(typeof window.renderTrustindexWidgets!=="function"||typeof window.TrustindexWidget!=="function"){window.setTimeout(flush,120);return;}started=true;cleanup();patchTrustindex();removePlaceholder();window.renderTrustindexWidgets();}
function schedule(){if(started||scheduled){return;}scheduled=true;var run=function(){scheduled=false;if("requestIdleCallback" in window){window.requestIdleCallback(flush,{timeout:2500});return;}window.setTimeout(flush,600);};run();}
function bindInteractions(){for(var i=0;i<interactionEvents.length;i++){window.addEventListener(interactionEvents[i],schedule,{once:true,passive:true});}}
function arm(){var target=findTarget();if(!interactionOnly&&target&&"IntersectionObserver" in window){observer=new IntersectionObserver(function(entries){for(var i=0;i<entries.length;i++){if(entries[i].isIntersecting||entries[i].intersectionRatio>0){schedule();return;}}},{rootMargin:"160px 0px"});observer.observe(target);}bindInteractions();if(!fallbackTimer){fallbackTimer=window.setTimeout(schedule,8000);}}
if(document.readyState==="complete"){arm();return;}
window.addEventListener("load",arm,{once:true});
})();
JS;

        return sprintf($template, $interaction_only ? 'true' : 'false');
    }

    /**
     * Prepare Trustindex loader attributes for the specialized bridge output.
     *
     * @param string $attrs
     * @param bool   $delay_loader
     * @return string
     */
    private function prepare_trustindex_loader_attrs(string $attrs, bool $delay_loader): string {
        $attrs = trim((string) preg_replace('/\s+/', ' ', $attrs));

        if (!$delay_loader) {
            return $attrs;
        }

        $attrs = (string) preg_replace('/(?:^|\s+)(?:async|defer)\b/i', ' ', $attrs);
        $attrs = (string) preg_replace(
            '/(?:^|\s+)(?:type|data-prorank-delay|data-prorank-type|data-prorank-delay-interaction|data-cfasync|data-no-optimize|data-no-defer|data-no-minify)\s*=\s*(["\']).*?\1/i',
            ' ',
            $attrs
        );

        $attrs = trim((string) preg_replace('/\s+/', ' ', $attrs));

        $delay_attrs = 'type="text/prorank-delayed" data-prorank-type="text/javascript" '
            . 'data-prorank-delay="true" data-prorank-delay-interaction="1" '
            . 'data-cfasync="false" data-no-optimize="1" data-no-defer="1" data-no-minify="1"';

        return trim($attrs . ' ' . $delay_attrs);
    }

    /**
     * Ensure delayed third-party tags still have a runtime when js_opt is off.
     *
     * ThirdPartyDefer can emit specialized delayed scripts independently from
     * JsOptimizeModule. If the full ProRank delay loader was not printed, add a
     * small compatible fallback so delayed widgets load when the delayed lane is
     * released.
     *
     * @param string $html
     * @return string
     */
    public function ensure_delayed_third_party_loader(string $html): string {
        if (
            $html === ''
            || stripos($html, 'text/prorank-delayed') === false
            || stripos($html, 'data-prorank-localized="trustindex-loader"') === false
            || stripos($html, 'id="prorank-delay-loader"') !== false
            || stripos($html, 'id="prorank-third-party-delay-loader"') !== false
        ) {
            return $html;
        }

        $loader = <<<'HTML'
<script id="prorank-third-party-delay-loader">(function(w,d){if(w.prorankThirdPartyDelayLoader||d.getElementById("prorank-delay-loader")){return;}w.prorankThirdPartyDelayLoader=1;var loading=false,timer=0;function getScripts(includeInteractionOnly){var list=Array.prototype.slice.call(d.querySelectorAll('script[type="text/prorank-delayed"]'));if(!includeInteractionOnly){list=list.filter(function(script){return script.getAttribute("data-prorank-delay-interaction")!=="1";});}return list;}function loadScript(script){return new Promise(function(resolve){var replacement=d.createElement("script");Array.prototype.slice.call(script.attributes).forEach(function(attr){if(["type","data-prorank-delay","data-prorank-type","data-prorank-delay-interaction","data-cfasync","data-no-optimize","data-no-defer","data-no-minify"].indexOf(attr.name)===-1){replacement.setAttribute(attr.name,attr.value);}});replacement.type=script.getAttribute("data-prorank-type")==="module"?"module":"text/javascript";if(!script.src&&script.innerHTML){replacement.innerHTML=script.innerHTML;if(script.parentNode){script.parentNode.replaceChild(replacement,script);}resolve();return;}replacement.onload=resolve;replacement.onerror=resolve;if(script.parentNode){script.parentNode.replaceChild(replacement,script);}else{resolve();}});}function run(includeInteractionOnly){if(loading){return;}var scripts=getScripts(includeInteractionOnly!==false);if(!scripts.length){return;}loading=true;if(timer){w.clearTimeout(timer);timer=0;}scripts.reduce(function(chain,script){return chain.then(function(){return loadScript(script);});},Promise.resolve()).then(function(){loading=false;}).catch(function(){loading=false;});}["mouseenter","mousemove","keydown","touchstart","touchend","scroll","click","pointerdown"].forEach(function(eventName){w.addEventListener(eventName,function(event){if(event&&typeof event.isTrusted==="boolean"&&event.isTrusted===false){return;}run(true);},{once:true,passive:true});});timer=w.setTimeout(function(){run(false);},10000);})(window,document);</script>
HTML;

        if (stripos($html, '</body>') !== false) {
            return (string) preg_replace('~</body>~i', $loader . '</body>', $html, 1);
        }

        return $html . $loader;
    }

    /**
     * Cache the Trustindex loader into uploads and return the local URL.
     *
     * A content-hashed filename lets the browser cache the asset aggressively
     * while still rotating cleanly if Trustindex changes the script body.
     *
     * @param string $remote_src
     * @return string|null
     */
    private function get_local_trustindex_loader_url(string $remote_src): ?string {
        $required_marker = 'prorank-ti-optimized-v4';
        $upload_dir = wp_upload_dir();
        $basedir = isset($upload_dir['basedir']) && is_string($upload_dir['basedir']) ? $upload_dir['basedir'] : '';
        $baseurl = isset($upload_dir['baseurl']) && is_string($upload_dir['baseurl']) ? $upload_dir['baseurl'] : '';

        if ($basedir === '' || $baseurl === '') {
            return null;
        }

        $cache_dir = trailingslashit($basedir) . 'prorank-seo/vendor-cache/trustindex';
        $cache_url = trailingslashit($baseurl) . 'prorank-seo/vendor-cache/trustindex';
        $source_hash = md5($remote_src);
        $cache_key = 'prorank_ti_loader_' . $source_hash;
        $cached = get_transient($cache_key);

        if (is_array($cached)) {
            $cached_file = isset($cached['file']) && is_string($cached['file']) ? basename($cached['file']) : '';
            $checked_at = isset($cached['checked_at']) ? (int) $cached['checked_at'] : 0;

            if (
                $cached_file !== '' &&
                file_exists($cache_dir . '/' . $cached_file) &&
                $checked_at > (time() - DAY_IN_SECONDS) &&
                strpos((string) @file_get_contents($cache_dir . '/' . $cached_file), $required_marker) !== false
            ) {
                return $cache_url . '/' . $cached_file;
            }
        }

        $existing_matches = glob($cache_dir . '/trustindex-loader-' . $source_hash . '-*.js');
        if (is_array($existing_matches) && !empty($existing_matches)) {
            rsort($existing_matches, SORT_STRING);
            $existing_file = basename((string) $existing_matches[0]);
            if (
                $existing_file !== '' &&
                file_exists($cache_dir . '/' . $existing_file) &&
                strpos((string) @file_get_contents($cache_dir . '/' . $existing_file), $required_marker) !== false
            ) {
                set_transient($cache_key, [
                    'file'       => $existing_file,
                    'checked_at' => time(),
                ], 14 * DAY_IN_SECONDS);

                return $cache_url . '/' . $existing_file;
            }
        }

        if (!is_dir($cache_dir) && !wp_mkdir_p($cache_dir)) {
            return null;
        }

        $response = wp_remote_get($remote_src, [
            'timeout'   => 8,
            'sslverify' => true,
            'headers'   => [
                'Accept' => 'application/javascript,text/javascript,*/*;q=0.1',
            ],
        ]);

        if (is_wp_error($response) || (int) wp_remote_retrieve_response_code($response) !== 200) {
            if (is_array($cached)) {
                $cached_file = isset($cached['file']) && is_string($cached['file']) ? basename($cached['file']) : '';
                if ($cached_file !== '' && file_exists($cache_dir . '/' . $cached_file)) {
                    return $cache_url . '/' . $cached_file;
                }
            }

            return null;
        }

        $body = (string) wp_remote_retrieve_body($response);
        if ($body === '') {
            return null;
        }

        $body = str_replace(
            "[src*=\".trustindex.\"][src*=\"'+t+'.js\"],[data-src*=\".trustindex.\"][data-src*=\"'+t+'.js\"]",
            "[src*=\".trustindex.\"][src*=\"'+t+'.js\"],[data-src*=\".trustindex.\"][data-src*=\"'+t+'.js\"],[data-prorank-trustindex-src*=\".trustindex.\"][data-prorank-trustindex-src*=\"'+t+'.js\"]",
            $body
        );
        $body = str_replace(
            'let e=t.getAttribute("data-src")||t.getAttribute("src"),i=e.replace(/.+\\?([^&]+)/,"$1");return i&&i!==e&&-1===i.indexOf("=")?i:null',
            'let e=t.getAttribute("data-prorank-trustindex-src")||t.getAttribute("data-src")||t.getAttribute("src"),i=e.replace(/.+\\?([^&]+)/,"$1");return i&&i!==e&&-1===i.indexOf("=")?i:null',
            $body
        );
        $body = str_replace(
            'static waitForVisibility(t,e,i=0){if(t.offsetWidth>0)return i?setTimeout(e,i):e();new ResizeObserver((function(t){t[0].target.offsetWidth>0&&(e(),this.disconnect())})).observe(t)}',
            'static waitForVisibility(t,e,i=0){let s=()=>{i?setTimeout(e,i):e()};if(!t||!t.isConnected)return s();if("IntersectionObserver"in window){let i=!1,r=()=>{i||(i=!0,s())},o=new IntersectionObserver((function(t){let e=t[0];e&&(e.isIntersecting||e.intersectionRatio>0)&&(o.disconnect(),r())}),{rootMargin:"256px 0px"});o.observe(t),window.addEventListener("load",(()=>setTimeout(r,1200)),{once:!0});return}requestAnimationFrame(s)}',
            $body
        );
        $body = str_replace(
            'i.setConstants();let t=document.body?document.body.offsetWidth:0,e=null;window.addEventListener("resize",(()=>{document.body.offsetWidth!==t&&(t=document.body.offsetWidth,e&&clearTimeout(e),window.tiWidgetInstances.forEach((t=>t.resize())))})),document.addEventListener("click",',
            'i.setConstants();let t=0,e=null;window.addEventListener("resize",(()=>{if(Array.isArray(window.tiWidgetInstances)&&window.tiWidgetInstances.length&&window.tiWidgetInstances.every((t=>t&&t.widget&&Array.isArray(t.widget.layoutCategory)&&-1!==t.widget.layoutCategory.indexOf("button"))))return;let i=window.innerWidth||document.documentElement.clientWidth||0;i!==t&&(t=i,e&&window.cancelAnimationFrame(e),e=window.requestAnimationFrame((()=>{window.tiWidgetInstances.forEach((t=>t.resize()))})))})),document.addEventListener("click",',
            $body
        );
        $body = str_replace(
            'isOnViewport(){let t=this.widget.container.getBoundingClientRect(),e=this.widget.container.offsetWidth-40,i=this.widget.container.offsetHeight-40;return t.top>=-1*i&&t.left>=-1*e&&t.right<=(window.innerWidth||document.documentElement.clientWidth)+i&&t.bottom<=(window.innerHeight||document.documentElement.clientHeight)+i}',
            'isOnViewport(){return true}',
            $body
        );
        if (strpos($body, $required_marker) === false) {
            $body .= "\n/* {$required_marker} */";
        }

        $content_hash = md5($body);
        $filename = sprintf('trustindex-loader-%s-%s.js', $source_hash, $content_hash);
        $filepath = $cache_dir . '/' . $filename;

        if (!file_exists($filepath) && file_put_contents($filepath, $body) === false) {
            if (is_array($cached)) {
                $cached_file = isset($cached['file']) && is_string($cached['file']) ? basename($cached['file']) : '';
                if ($cached_file !== '' && file_exists($cache_dir . '/' . $cached_file)) {
                    return $cache_url . '/' . $cached_file;
                }
            }

            return null;
        }

        set_transient($cache_key, [
            'file'       => $filename,
            'checked_at' => time(),
        ], 14 * DAY_IN_SECONDS);

        return $cache_url . '/' . $filename;
    }
}
