<?php
/**
 * Storefront script attribute and optimizer-exclusion helpers.
 *
 * @package Carticy\Conversions\Support
 */

declare(strict_types=1);

namespace Carticy\Conversions\Support;

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

/**
 * Keeps the plugin's storefront scripts working under JS optimizers.
 *
 * Tracking scripts need to survive common optimization plugins without asking
 * merchants to disable site-wide performance tooling. The plugin opts its own
 * handles out of Cloudflare Rocket Loader, Autoptimize, WP Rocket, LiteSpeed,
 * and Perfmatters.
 */
final class ScriptAttributes {

	/**
	 * Handles already registered for optimizer exclusion.
	 *
	 * @var array<string, bool>
	 */
	private static array $handles = array();

	/**
	 * Whether the optimizer exclusion filters have been added.
	 *
	 * @var bool
	 */
	private static bool $filters_added = false;

	/**
	 * Register a script handle for optimizer exclusion.
	 *
	 * @param string $handle Script handle.
	 */
	public static function exclude_from_optimizers( string $handle ): void {
		self::$handles[ $handle ] = true;

		if ( self::$filters_added || ! function_exists( 'add_filter' ) ) {
			return;
		}

		self::$filters_added = true;

		$exclusions = static function ( $excluded ) {
			$excluded = is_array( $excluded ) ? $excluded : array();
			foreach ( array_keys( self::$handles ) as $handle ) {
				$excluded[] = $handle;
				$excluded[] = $handle . '.js';
			}

			return array_values( array_unique( $excluded ) );
		};

		// WP Rocket: delay-JS and defer exclusions match on URL/handle substrings.
		add_filter( 'rocket_delay_js_exclusions', $exclusions );
		add_filter( 'rocket_exclude_defer_js', $exclusions );
		add_filter( 'rocket_exclude_js', $exclusions );
		// LiteSpeed Cache JS optimization exclusions.
		add_filter( 'litespeed_optimize_js_excludes', $exclusions );
		add_filter( 'litespeed_optm_js_defer_exc', $exclusions );
		// Perfmatters delay/defer exclusions.
		add_filter( 'perfmatters_delay_js_exclusions', $exclusions );
		add_filter( 'perfmatters_defer_js_exclusions', $exclusions );
		// Autoptimize (attribute-based data-no-optimize is primary; this
		// covers aggregated bundles).
		add_filter(
			'autoptimize_filter_js_exclude',
			static function ( $exclude ) {
				$handles = implode( ',', array_keys( self::$handles ) );

				return is_string( $exclude ) && '' !== $exclude ? $exclude . ',' . $handles : $handles;
			}
		);
	}

	/**
	 * Add attributes to a rendered script tag without rebuilding it.
	 *
	 * Rebuilding the tag from scratch discards attributes other filters
	 * (CSP nonces, integrity) already added; injecting into the existing
	 * tag preserves them.
	 *
	 * @param string                $tag Rendered script tag.
	 * @param array<string, string> $attributes Attribute => value ('' for boolean attributes).
	 * @return string
	 */
	public static function with_attributes( string $tag, array $attributes ): string {
		foreach ( $attributes as $name => $value ) {
			if ( false !== strpos( $tag, ' ' . $name ) ) {
				continue;
			}

			$attribute = '' === $value
				? ' ' . $name
				: ' ' . $name . '="' . ( function_exists( 'esc_attr' ) ? esc_attr( $value ) : htmlspecialchars( $value, ENT_QUOTES ) ) . '"';

			$tag = (string) preg_replace( '/<script\b/', '<script' . $attribute, $tag, 1 );
		}

		return $tag;
	}
}
