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

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




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

    /**
     * Check whether a script belongs to the Google tracking/services category.
     */
    private function is_google_service_haystack(string $haystack): bool {
        $patterns = [
            'gtag',
            'googlesitekit',
            'googletagmanager.com',
            'google-analytics.com',
            'googlesyndication.com',
            'doubleclick.net',
            'google_gtagjs',
        ];

        foreach ($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 {
        return strpos($haystack, 'cdn.trustindex.io/loader.js') !== false
            || strpos($haystack, 'trustindex') !== false;
    }

    /**
     * Check whether a script belongs to the consent category.
     */
    private function is_consent_script_haystack(string $haystack): bool {
        $patterns = [
            'cdn-cookieyes.com',
            'cookieyes',
            'cookie-law-info',
            'consent.cookiebot.com',
            'cookiebot.com',
            'complianz',
            'cmplz',
        ];

        foreach ($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 {
        $patterns = [
            'accounts.google.com/gsi/client',
            'google.accounts.id',
            'g_id_onload',
        ];

        foreach ($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_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'];
        }

        $patterns = [
            'facebook.net',
            'connect.facebook.net',
            'static.hotjar.com',
            'cdn.segment.com',
            'js.hs-analytics.net',
            'cdn.mouseflow.com',
            'cdn.fullstory.com',
            'snap.licdn.com',
            'js.adsrvr.org',
            'fbq',
            'hotjar',
            'segment',
            'clarity',
            'hs-analytics',
            'mouseflow',
            'fullstory',
            'linkedin_insight',
        ];

        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', []);

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

        return $inp_enabled || $tracker_delay_enabled;
    }

    /**
     * Boot hooks
     */
    public function init(): void {
        add_filter( 'script_loader_tag', [ $this, 'defer_third_party' ], 10, 3 );
        Buffer::init();
        add_filter( 'prorank_output_buffer', [ $this, 'defer_raw_tracker_bootstraps' ], 12 );
        add_filter( 'prorank_output_buffer', [ $this, 'defer_non_enqueued_external_scripts' ], 13 );
    }

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

        $haystack = strtolower(trim($handle . ' ' . $src));
        if ($haystack === '' || !$this->should_defer_enqueued_third_party($haystack)) {
            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, 'connect.facebook.net/en_US/fbevents.js') === false &&
            stripos($html, 'static.getclicky.com') === false
        ) {
            return $html;
        }

        $original_html = $html;
        if ($this->get_third_party_governance_settings()['delay_google_services']) {
            $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 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(?![^>]*data-prorank-3p-inline=["\']gtm["\'])[^>]*>([\s\S]*?googletagmanager\.com/gtm\.js[\s\S]*?)</script>~i';

        $updated = preg_replace_callback(
            $pattern,
            static function (array $matches): string {
                $script = (string) ($matches[1] ?? '');
                if ($script === '' || stripos($script, 'gtm.start') === 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);};var scheduleGtm=function(){if("requestIdleCallback" in w){w.requestIdleCallback(loadGtm,{timeout:5000});}else{setTimeout(loadGtm,1200);}};if(d.readyState==="complete"){setTimeout(scheduleGtm,600);}else{w.addEventListener("load",function(){setTimeout(scheduleGtm,600);},{once:true});}})(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(?![^>]*data-prorank-3p-inline=["\']facebook["\'])[^>]*>\s*(!function\(f,b,e,v,n,t,s\)[\s\S]*?connect\.facebook\.net/en_US/fbevents\.js[\s\S]*?)</script>~i';

        $updated = preg_replace_callback(
            $pattern,
            static function (array $matches): 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);};var _prorankFbSchedule=function(){if("requestIdleCallback" in f){f.requestIdleCallback(_prorankFbLoad,{timeout:5000});}else{setTimeout(_prorankFbLoad,1200);}};if(b.readyState==="complete"){setTimeout(_prorankFbSchedule,600);}else{f.addEventListener("load",function(){setTimeout(_prorankFbSchedule,600);},{once:true});}',
                    $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;
    }

    /**
     * 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', []);
        if (!is_array($cache_settings) || empty($cache_settings['js_defer'])) {
            return $html;
        }

        $governance = $this->get_third_party_governance_settings();

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

        $original_html = $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;
                }

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

                if (
                    ($this->is_google_service_haystack($src_lc) && !$governance['delay_google_services']) ||
                    ($this->is_review_widget_haystack($src_lc) && !$governance['delay_review_widgets']) ||
                    ($this->is_consent_script_haystack($src_lc) && !$governance['defer_consent_scripts']) ||
                    ($this->is_identity_widget_haystack($src_lc) && !$governance['delay_identity_widgets'])
                ) {
                    return $full_tag;
                }

                $sync_patterns = [
                    'jquery', 'recaptcha', 'hcaptcha', 'turnstile',
                    'stripe', 'paypal', 'braintree', 'klarna',
                    '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');
    }
}
