<?php
/**
 * Cache Exclusions class.
 *
 * This class handles exclusions for popular caching/optimization plugins
 * to prevent our plugin's scripts from being minified or combined.
 *
 * @package SaveToWishlist\Classes\Integrations
 * @since 1.0.1
 */
namespace SaveToWishlist\Classes\Integrations;

use SaveToWishlist\Abstracts\Singleton;

/**
 * Class Cache_Exclusions
 *
 * This class manages script exclusions for various caching plugins.
 */
class Cache_Exclusions extends Singleton {

    /**
     * List of script handles to exclude.
     *
     * @var array
     */
    private $script_handles = array();

    /**
     * List of script URL patterns to exclude.
     *
     * @var array
     */
    private $script_patterns = array();

    /**
     * List of Vite module script handles. Subset of $script_handles.
     *
     * Used to scope force_clean_module_tag() so type="module" is only ever
     * forced on actual ES module bundles, not on jQuery-dependent scripts.
     *
     * @var array
     */
    private $module_script_handles = array();

    /**
     * List of Vite module script URL patterns. Subset of $script_patterns.
     *
     * @var array
     */
    private $module_script_patterns = array();

    /**
     * Constructor.
     */
    public function __construct() {
        // Vite app handles — the four ES module bundles emitted by the Vite build.
        $vite_app_handles = array(
            'saveto-wishlist-frontend-app-scripts',
            'saveto-wishlist-wishlists-app-scripts',
            'saveto-wishlist-pages-app-scripts',
            'saveto-wishlist-settings-app-scripts',
        );

        // Default script handles to exclude.
        $default_handles = array_merge( array( 'stwlite_frontend_js' ), $vite_app_handles );

        /**
         * Filter the script handles to exclude from caching plugins.
         *
         * @since 1.0.1
         *
         * @param array $handles Array of script handles to exclude.
         */
        $this->script_handles = apply_filters( 'stwlite_cache_exclusion_script_handles', $default_handles );

        // Default script URL patterns to exclude. Vite app handles are matched via $script_handles, not here.
        $default_patterns = array(
            'savetowishlist-frontend-scripts.js',
            'savetowishlist-backend-scripts.js',
            'saveto-wishlist-lite-for-woocommerce/dist',
            'saveto-wishlist-lite-for-woocommerce/assets/js',
        );

        /**
         * Filter the script URL patterns to exclude from caching plugins.
         *
         * @since 1.0.1
         *
         * @param array $patterns Array of script URL patterns to exclude.
         */
        $this->script_patterns = apply_filters( 'stwlite_cache_exclusion_script_patterns', $default_patterns );

        // Vite module-only handles — must NOT include classic jQuery scripts like stwlite_frontend_js.
        $default_module_handles = $vite_app_handles;

        /**
         * Filter the Vite module script handles eligible for type="module" enforcement.
         *
         * @since 1.0.7
         *
         * @param array $handles Array of module script handles.
         */
        $this->module_script_handles = apply_filters( 'stwlite_cache_exclusion_module_script_handles', $default_module_handles );

        // Vite module-only URL patterns — only the dist build output. Module handles are matched via $module_script_handles.
        $default_module_patterns = array( 'saveto-wishlist-lite-for-woocommerce/dist' );

        /**
         * Filter the Vite module script URL patterns eligible for type="module" enforcement.
         *
         * @since 1.0.7
         *
         * @param array $patterns Array of module script URL patterns.
         */
        $this->module_script_patterns = apply_filters( 'stwlite_cache_exclusion_module_script_patterns', $default_module_patterns );
    }

    /**
     * Run the class.
     *
     * @since 1.0.1
     * @access public
     */
    public function run() {
        // WP Rocket exclusions.
        add_filter( 'rocket_exclude_js', array( $this, 'wp_rocket_exclude_js' ) );
        add_filter( 'rocket_minify_excluded_external_js', array( $this, 'wp_rocket_exclude_js' ) );
        add_filter( 'rocket_exclude_defer_js', array( $this, 'wp_rocket_exclude_js' ) );

        // SiteGround Optimizer exclusions.
        add_filter( 'sgo_js_minify_exclude', array( $this, 'siteground_exclude_js' ), 99999 );
        add_filter( 'sgo_javascript_combine_exclude', array( $this, 'siteground_exclude_js' ), 99999 );
        add_filter( 'sgo_javascript_combine_excluded_inline_content', array( $this, 'siteground_exclude_inline_js' ) );
        add_filter( 'sgo_html_minify_exclude_params', array( $this, 'siteground_exclude_module_scripts' ) );
        add_filter( 'sgo_js_async_exclude', array( $this, 'siteground_exclude_js' ), 99999 );
        add_filter( 'script_loader_tag', array( $this, 'prevent_module_script_minification' ), 20, 3 );
        add_filter( 'script_loader_tag', array( $this, 'force_clean_module_tag' ), 99999, 3 );

        // LiteSpeed Cache exclusions.
        add_filter( 'litespeed_optimize_js_excludes', array( $this, 'litespeed_exclude_js' ), 99999 );

        // Autoptimize exclusions.
        add_filter( 'autoptimize_filter_js_exclude', array( $this, 'autoptimize_exclude_js' ), 99999 );

        // W3 Total Cache exclusions.
        add_filter( 'w3tc_minify_js_do_tag_minification', array( $this, 'w3tc_exclude_js' ), 10, 2 );

        // WP Fastest Cache exclusions.
        add_filter( 'wpfc_exclude_current_page', array( $this, 'wpfc_exclude_pages' ), 99999 );

        // Asset CleanUp exclusions.
        add_filter( 'wpacu_skip_assets_from_rules_and_list_for_optimization', array( $this, 'asset_cleanup_exclude_js' ), 10, 2 );

        // WP-Optimize exclusions.
        add_filter( 'wp-optimize-minify-default-exclusions', array( $this, 'wp_optimize_exclude_js' ) );

        // Breeze exclusions.
        add_filter( 'breeze_minify_exclude_js', array( $this, 'breeze_exclude_js' ) );
    }

    /**
     * WP Rocket - Exclude JavaScript files.
     *
     * @param array $excluded_files Array of excluded files.
     *
     * @since 1.0.1
     * @return array
     */
    public function wp_rocket_exclude_js( $excluded_files = array() ) {
        if ( ! is_array( $excluded_files ) ) {
            $excluded_files = array();
        }

        return array_merge( $excluded_files, $this->script_patterns );
    }

    /**
     * SiteGround Optimizer - Exclude JavaScript files.
     *
     * @param array $excluded_files Array of excluded files.
     *
     * @since 1.0.1
     * @return array
     */
    public function siteground_exclude_js( $excluded_files = array() ) {
        if ( ! is_array( $excluded_files ) ) {
            $excluded_files = array();
        }

        return array_merge( $excluded_files, $this->script_patterns );
    }

    /**
     * SiteGround Optimizer - Exclude inline JavaScript.
     *
     * @param array $excluded_inline Array of excluded inline content.
     *
     * @since 1.0.1
     * @return array
     */
    public function siteground_exclude_inline_js( $excluded_inline = array() ) {
        if ( ! is_array( $excluded_inline ) ) {
            $excluded_inline = array();
        }

        // Exclude inline scripts that contain our localized variables.
        $excluded_inline[] = 'stwlite_frontend_js_vars';
        $excluded_inline[] = 'savetowishlistObj';

        return $excluded_inline;
    }

    /**
     * SiteGround Optimizer - Exclude module scripts from minification.
     *
     * @param array $params Array of exclusion parameters.
     *
     * @since 1.0.1
     * @return array
     */
    public function siteground_exclude_module_scripts( $params = array() ) {
        if ( ! is_array( $params ) ) {
            $params = array();
        }

        if ( ! isset( $params['exclude_scripts'] ) ) {
            $params['exclude_scripts'] = array();
        }

        // Add patterns to exclude.
        $params['exclude_scripts'] = array_merge( $params['exclude_scripts'], $this->script_patterns );

        // Exclude all scripts with type="module" attribute.
        if ( ! isset( $params['script_attributes'] ) ) {
            $params['script_attributes'] = array();
        }
        $params['script_attributes'][] = 'type="module"';

        return $params;
    }

    /**
     * LiteSpeed Cache - Exclude JavaScript files.
     *
     * @param array $excluded_files Array of excluded files.
     *
     * @since 1.0.1
     * @return array
     */
    public function litespeed_exclude_js( $excluded_files = array() ) {
        if ( ! is_array( $excluded_files ) ) {
            $excluded_files = array();
        }

        return array_merge( $excluded_files, $this->script_patterns );
    }

    /**
     * Autoptimize - Exclude JavaScript files.
     *
     * @param string $excluded_js Comma-separated list of excluded files.
     *
     * @since 1.0.1
     * @return string
     */
    public function autoptimize_exclude_js( $excluded_js = '' ) {
        $excluded_array = ! empty( $excluded_js ) ? explode( ',', $excluded_js ) : array();
        $excluded_array = array_merge( $excluded_array, $this->script_patterns );

        return implode( ',', array_unique( $excluded_array ) );
    }

    /**
     * W3 Total Cache - Exclude JavaScript files.
     *
     * @param bool   $do_tag_minification Whether to minify.
     * @param string $script_tag           The script tag.
     *
     * @since 1.0.1
     * @return bool
     */
    public function w3tc_exclude_js( $do_tag_minification, $script_tag ) {
        foreach ( $this->script_patterns as $pattern ) {
            if ( str_contains( $script_tag, $pattern ) ) {
                return false;
            }
        }

        return $do_tag_minification;
    }

    /**
     * WP Fastest Cache - Exclude pages with our scripts.
     *
     * @param bool $exclude Whether to exclude current page.
     *
     * @since 1.0.1
     * @return bool
     */
    public function wpfc_exclude_pages( $exclude ) {
        // Check if any of our scripts are enqueued.
        foreach ( $this->script_handles as $handle ) {
            if ( wp_script_is( $handle, 'enqueued' ) || wp_script_is( $handle, 'registered' ) ) {
                return true;
            }
        }

        return $exclude;
    }

    /**
     * Asset CleanUp - Exclude JavaScript files.
     *
     * @param bool   $skip   Whether to skip optimization.
     * @param string $handle The script handle.
     *
     * @since 1.0.1
     * @return bool
     */
    public function asset_cleanup_exclude_js( $skip, $handle ) {
        if ( in_array( $handle, $this->script_handles, true ) ) {
            return true;
        }

        return $skip;
    }

    /**
     * WP-Optimize - Exclude JavaScript files.
     *
     * @param array $excluded_files Array of excluded files.
     *
     * @since 1.0.1
     * @return array
     */
    public function wp_optimize_exclude_js( $excluded_files = array() ) {
        if ( ! is_array( $excluded_files ) ) {
            $excluded_files = array();
        }

        return array_merge( $excluded_files, $this->script_patterns );
    }

    /**
     * Breeze - Exclude JavaScript files.
     *
     * @param array $excluded_files Array of excluded files.
     *
     * @since 1.0.1
     * @return array
     */
    public function breeze_exclude_js( $excluded_files = array() ) {
        if ( ! is_array( $excluded_files ) ) {
            $excluded_files = array();
        }

        return array_merge( $excluded_files, $this->script_patterns );
    }

    /**
     * Prevent our scripts from being minified by adding data attributes.
     *
     * Runs after Vite_App::add_script_tag_attributes (priority 10) has added
     * type="module", so the data attributes are applied to the final tag.
     *
     * @param string $tag    The script tag.
     * @param string $handle The script handle.
     * @param string $src    The script source URL.
     *
     * @since 1.0.1
     * @return string Modified script tag.
     */
    public function prevent_module_script_minification( $tag, $handle, $src ) {
        if ( ! $this->is_our_script( $handle, $src ) ) {
            return $tag;
        }

        // Add data attributes to signal caching/optimization plugins to leave the tag alone.
        if ( ! str_contains( $tag, 'data-no-minify' ) ) {
            $tag = str_replace( '<script', '<script data-no-minify="1" data-cfasync="false"', $tag );
        }

        return $tag;
    }

    /**
     * Force a clean type="module" attribute on our script tags.
     *
     * Runs at a very late priority to override security/optimization plugins
     * (e.g. Wordfence) that strip or duplicate the type="module" attribute on
     * our scripts. Removes any existing type attribute first to avoid duplicates,
     * then re-applies type="module".
     *
     * @param string $tag    The script tag.
     * @param string $handle The script handle.
     * @param string $src    The script source URL.
     *
     * @since 1.0.7
     * @return string Modified script tag.
     */
    public function force_clean_module_tag( $tag, $handle, $src ) {
        if ( ! $this->is_our_module_script( $handle, $src ) ) {
            return $tag;
        }

        // Remove any existing type attribute (fixes duplicates if a plugin injected its own).
        // Require leading whitespace so we never match attributes ending in -type (e.g. data-type).
        // Handles double-quoted, single-quoted, and unquoted values (e.g. type=module).
        $tag = preg_replace( '/\s+type=("[^"]*"|\'[^\']*\'|[^\s>]+)/', '', $tag );

        // Force the module type cleanly.
        $tag = str_replace( '<script', '<script type="module"', $tag );

        return $tag;
    }

    /**
     * Check whether a script tag belongs to this plugin.
     *
     * @param string $handle The script handle.
     * @param string $src    The script source URL.
     *
     * @since 1.0.7
     * @return bool True if the script is one of ours.
     */
    private function is_our_script( $handle, $src ) {
        return $this->matches_lists( $handle, $src, $this->script_handles, $this->script_patterns );
    }

    /**
     * Check whether a script tag belongs to one of our Vite module bundles.
     *
     * Stricter than is_our_script(): excludes classic (non-module) scripts so we never
     * force type="module" onto a jQuery-dependent file, which would break it.
     *
     * @param string $handle The script handle.
     * @param string $src    The script source URL.
     *
     * @since 1.0.7
     * @return bool True if the script is one of our module bundles.
     */
    private function is_our_module_script( $handle, $src ) {
        return $this->matches_lists( $handle, $src, $this->module_script_handles, $this->module_script_patterns );
    }

    /**
     * Check whether a handle or URL matches the given handle/pattern lists.
     *
     * @param string $handle   The script handle.
     * @param string $src      The script source URL.
     * @param array  $handles  List of handles to match exactly.
     * @param array  $patterns List of URL substrings to match.
     *
     * @since 1.0.7
     * @return bool
     */
    private function matches_lists( $handle, $src, array $handles, array $patterns ) {
        if ( in_array( $handle, $handles, true ) ) {
            return true;
        }

        foreach ( $patterns as $pattern ) {
            if ( str_contains( (string) $src, $pattern ) ) {
                return true;
            }
        }

        return false;
    }
}
