<?php

/**
 * WooCommerce performance compatibility shim.
 *
 * Keeps the page cache and asset optimisers safe on WooCommerce stores:
 *   - never serve or store cached HTML for cart, checkout, my-account,
 *     order-pay/order-received pages, Woo AJAX, or the Store API
 *     (block cart/checkout) routes;
 *   - bypass the cache whenever a WooCommerce cart/session cookie is present;
 *   - keep dynamic WooCommerce URLs (incl. add/remove/update-cart and Store
 *     API) out of the cache preloader;
 *   - protect critical WooCommerce + payment-gateway scripts from JS
 *     delay/defer/minify so carts, fragments, and checkout keep working;
 *   - optionally vary the cache key by currency/country/tax-display cookies
 *     (only when the woocommerce_cache_variants setting is on) — never by cart
 *     contents or personalised checkout state.
 *
 * Entirely self-gating: nothing is registered unless WooCommerce is active
 * AND the woocommerce_safe_cache_enabled toggle is on (default true), so
 * non-WooCommerce sites are completely unaffected.
 *
 * @package ProRank\SEO\Core\Compatibility
 */

declare(strict_types=1);

namespace ProRank\SEO\Core\Compatibility;

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

/**
 * WooCommercePerformanceCompatibility class.
 */
final class WooCommercePerformanceCompatibility {

    /**
     * Cache settings option + toggle key.
     */
    private const OPTION  = 'prorank_cache_settings';
    private const SETTING = 'woocommerce_safe_cache_enabled';

    /**
     * Whether WooCommerce is active on this site.
     *
     * @return bool
     */
    public static function is_woocommerce_active(): bool {
        return class_exists( 'WooCommerce' ) || function_exists( 'WC' );
    }

    /**
     * Whether the WooCommerce cache-safety toggle is enabled (default true).
     *
     * @return bool
     */
    public static function is_enabled(): bool {
        $settings = get_option( self::OPTION, [] );
        if ( ! is_array( $settings ) || ! array_key_exists( self::SETTING, $settings ) ) {
            return true; // safe default for fresh installs.
        }
        return ! empty( $settings[ self::SETTING ] );
    }

    /**
     * Register the shim's hooks if (and only if) it should run.
     *
     * Idempotent and self-gating, so it is safe to call from module boot.
     *
     * @return void
     */
    public static function boot(): void {
        static $booted = false;
        if ( $booted ) {
            return;
        }

        if ( ! self::is_woocommerce_active() || ! self::is_enabled() ) {
            return;
        }

        $booted = true;
        ( new self() )->register();
    }

    /**
     * Hook into the cache + asset-optimisation decision points.
     *
     * @return void
     */
    public function register(): void {
        // Page cache bypass.
        add_filter( 'prorank_should_cache_page', [ $this, 'filter_should_cache' ] );
        add_action( 'template_redirect', [ $this, 'maybe_set_donotcache' ], 0 );

        // Cache preloader exclusions.
        add_filter( 'prorank_cache_preload_urls', [ $this, 'filter_preload_urls' ] );

        // Asset optimisation: never delay/defer/minify critical Woo + payment JS.
        add_filter( 'prorank_delay_js_protected_patterns', [ $this, 'filter_protected_js_patterns' ] );
        add_filter( 'prorank_defer_js_protected_patterns', [ $this, 'filter_protected_js_patterns' ] );
        add_filter( 'prorank_js_minify_exclude', [ $this, 'filter_minify_exclude' ], 10, 3 );

        // Conservative cache variation (only when woocommerce_cache_variants is on).
        add_filter( 'prorank_cache_context_suffix_parts', [ $this, 'filter_cache_variation_parts' ] );
    }

    /* ----------------------- cache bypass ----------------------- */

    /**
     * Veto caching of dynamic WooCommerce requests.
     *
     * @param mixed $allowed Current allow decision.
     * @return mixed
     */
    public function filter_should_cache( $allowed ) {
        if ( $this->should_skip_cache() ) {
            return false;
        }
        return $allowed;
    }

    /**
     * Also set DONOTCACHEPAGE so any other cache layer honours the bypass.
     *
     * @return void
     */
    public function maybe_set_donotcache(): void {
        if ( $this->should_skip_cache() && ! defined( 'DONOTCACHEPAGE' ) ) {
            define( 'DONOTCACHEPAGE', true );
        }
    }

    /**
     * Whether the current request must bypass the page cache.
     *
     * @return bool
     */
    public function should_skip_cache(): bool {
        // WooCommerce conditional tags (front-end only).
        foreach ( [ 'is_cart', 'is_checkout', 'is_account_page' ] as $conditional ) {
            if ( function_exists( $conditional ) && $conditional() ) {
                return true;
            }
        }

        if ( function_exists( 'is_wc_endpoint_url' ) ) {
            foreach ( [ 'order-pay', 'order-received', 'add-payment-method' ] as $endpoint ) {
                if ( is_wc_endpoint_url( $endpoint ) ) {
                    return true;
                }
            }
        }

        $uri     = isset( $_SERVER['REQUEST_URI'] ) ? (string) wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
        $cookies = array_keys( is_array( $_COOKIE ) ? $_COOKIE : [] );

        return self::should_skip_for_request( $uri, $cookies );
    }

    /**
     * Pure URI + cookie check (no WooCommerce runtime needed) so it can be
     * unit tested and reused. Bypasses cache for Woo AJAX, the Store API,
     * the standard Woo page paths, cart mutation queries, and any active
     * WooCommerce cart/session cookie.
     *
     * @param string            $uri     Request URI (path + optional query).
     * @param array<int,string> $cookies Cookie names present on the request.
     * @return bool
     */
    public static function should_skip_for_request( string $uri, array $cookies ): bool {
        $path  = (string) ( parse_url( $uri, PHP_URL_PATH ) ?? $uri );
        $query = (string) ( parse_url( $uri, PHP_URL_QUERY ) ?? '' );

        if ( self::is_dynamic_woo_path( $path, $query ) ) {
            return true;
        }

        foreach ( $cookies as $name ) {
            if ( self::is_woo_bypass_cookie( (string) $name ) ) {
                return true;
            }
        }

        return false;
    }

    /**
     * Whether a cookie name signals active cart/session state (bypass cache).
     *
     * @param string $name Cookie name.
     * @return bool
     */
    public static function is_woo_bypass_cookie( string $name ): bool {
        $name = (string) $name;
        return 'woocommerce_items_in_cart' === $name
            || 'woocommerce_cart_hash' === $name
            || 0 === strpos( $name, 'wp_woocommerce_session_' )
            || 0 === strpos( $name, 'wc_cart_hash_' )
            || 0 === strpos( $name, 'wc_fragments_' );
    }

    /**
     * Whether a path/query represents a dynamic WooCommerce surface.
     *
     * @param string $path  URL path.
     * @param string $query URL query string.
     * @return bool
     */
    private static function is_dynamic_woo_path( string $path, string $query ): bool {
        if ( $query !== '' ) {
            parse_str( $query, $query_vars );
            foreach ( [
                'wc-ajax',
                'add-to-cart',
                'remove_item',
                'removed_item',
                'undo_item',
                'update_cart',
                'apply_coupon',
                'remove_coupon',
                'coupon_code',
            ] as $key ) {
                if ( array_key_exists( $key, $query_vars ) ) {
                    return true;
                }
            }
            foreach ( array_keys( $query_vars ) as $key ) {
                if ( 0 === strpos( (string) $key, 'attribute_pa_' ) ) {
                    return true;
                }
            }
            // Store API via the plain-permalink REST route (?rest_route=/wc/store/...).
            if (
                isset( $query_vars['rest_route'] )
                && preg_match( '~^/?wc/store(?:/v\d+)?/(?:cart|checkout)(?:/|$)~i', (string) $query_vars['rest_route'] )
            ) {
                return true;
            }
        }

        $path = '/' . ltrim( $path, '/' );

        // WooCommerce Store API (block cart/checkout) — never page-cacheable.
        if ( preg_match( '~^/wp-json/wc/store(?:/v\d+)?/(?:cart|checkout)(?:/|$)~i', $path ) ) {
            return true;
        }

        return (bool) preg_match(
            '~/(cart|checkout|my-account|add-to-cart|order-pay|order-received|add-payment-method|view-order|edit-account|edit-address|lost-password|customer-logout)(/|$)~i',
            $path
        );
    }

    /* ----------------------- preload exclusions ----------------------- */

    /**
     * Strip dynamic WooCommerce URLs from the preload queue.
     *
     * @param mixed $urls URL list.
     * @return mixed
     */
    public function filter_preload_urls( $urls ) {
        if ( ! is_array( $urls ) ) {
            return $urls;
        }

        return array_values( array_filter( $urls, function ( $url ) {
            if ( ! is_string( $url ) ) {
                return false;
            }
            $path  = (string) ( wp_parse_url( $url, PHP_URL_PATH ) ?? '' );
            $query = (string) ( wp_parse_url( $url, PHP_URL_QUERY ) ?? '' );
            return ! self::is_dynamic_woo_path( $path, $query );
        } ) );
    }

    /* ----------------------- asset optimisation exclusions ----------------------- */

    /**
     * Critical WooCommerce + payment-gateway script patterns that must never be
     * delayed, deferred, or minified in a way that breaks cart/checkout. Matched
     * as case-insensitive substrings against a script's handle or src.
     *
     * @return array<int,string>
     */
    public static function get_protected_script_patterns(): array {
        return [
            // Core WooCommerce cart/checkout behaviour.
            'wc-cart-fragments',
            'wc-add-to-cart',
            'wc-add-to-cart-variation',
            'wc-cart',
            'wc-checkout',
            'wc-country-select',
            'wc-address-i18n',
            'woocommerce',
            // Block cart/checkout (Store API driven).
            'wc-blocks',
            'wc-blocks-checkout',
            'wc-blocks-registry',
            'wc-settings',
            // Payment gateways — must stay synchronous and unminified-safe.
            'stripe',
            'wc-stripe',
            'paypal',
            'ppcp',
            'wc-gateway-ppec',
            'square',
            'wc-square',
            'braintree',
            'mollie',
            'klarna',
            'razorpay',
            'authorize-net',
        ];
    }

    /**
     * Append the protected Woo/payment patterns to a JS delay/defer
     * protected-pattern list (reuses the registry filter mechanism).
     *
     * @param mixed $patterns Existing patterns.
     * @return array<int,string>
     */
    public function filter_protected_js_patterns( $patterns ) {
        $patterns = is_array( $patterns ) ? $patterns : [];
        return array_values( array_unique( array_merge( $patterns, self::get_protected_script_patterns() ) ) );
    }

    /**
     * Keep critical Woo/payment scripts out of JS minification.
     *
     * @param mixed  $exclude Current exclude decision.
     * @param string $handle  Script handle.
     * @param string $src     Script src.
     * @return bool
     */
    public function filter_minify_exclude( $exclude, $handle = '', $src = '' ) {
        if ( $exclude ) {
            return true;
        }
        return self::matches_protected_script( (string) $handle, (string) $src );
    }

    /**
     * Whether a handle/src matches a protected Woo/payment pattern.
     *
     * @param string $handle Script handle.
     * @param string $src    Script src.
     * @return bool
     */
    public static function matches_protected_script( string $handle, string $src ): bool {
        $haystack = strtolower( $handle . ' ' . $src );
        if ( trim( $haystack ) === '' ) {
            return false;
        }
        foreach ( self::get_protected_script_patterns() as $pattern ) {
            if ( $pattern !== '' && strpos( $haystack, strtolower( $pattern ) ) !== false ) {
                return true;
            }
        }
        return false;
    }

    /* ----------------------- conservative cache variation ----------------------- */

    /**
     * Append a currency/country/tax variation signature to the cache-key parts
     * when the woocommerce_cache_variants setting is on. Never varies by cart
     * contents or session (those bypass the cache entirely).
     *
     * @param mixed $parts Existing suffix parts.
     * @return array<int,string>
     */
    public function filter_cache_variation_parts( $parts ) {
        $parts = is_array( $parts ) ? $parts : [];

        $settings = get_option( self::OPTION, [] );
        if ( ! is_array( $settings ) || empty( $settings['woocommerce_cache_variants'] ) ) {
            return $parts; // conservative: only when explicitly enabled.
        }

        $signature = self::variation_signature( is_array( $_COOKIE ) ? $_COOKIE : [] );
        if ( $signature !== '' ) {
            $parts[] = $signature;
        }

        return $parts;
    }

    /**
     * Build a compact, value-hashed variation signature from currency / country
     * / tax-display cookies. Pure (no WordPress) so it can be unit tested.
     * Cart and session cookies are explicitly excluded — they bypass cache.
     *
     * @param array<string,mixed> $cookies Cookie name => value map.
     * @return string Empty when no relevant cookies are present.
     */
    public static function variation_signature( array $cookies ): string {
        $relevant = [];
        foreach ( $cookies as $name => $value ) {
            $lname = strtolower( (string) $name );

            if ( self::is_woo_bypass_cookie( (string) $name ) ) {
                continue; // never a variation key — these bypass the cache.
            }

            if ( preg_match( '~(currenc|country|geoip|geo_country|tax_display|price_display|locat)~i', $lname ) ) {
                $relevant[ $lname ] = (string) ( is_scalar( $value ) ? $value : '' );
            }
        }

        if ( empty( $relevant ) ) {
            return '';
        }

        ksort( $relevant );
        $pairs = [];
        foreach ( $relevant as $key => $val ) {
            $pairs[] = $key . '=' . $val;
        }

        return 'wcvar-' . substr( md5( implode( '|', $pairs ) ), 0, 10 );
    }
}
