<?php
/**
 * AssestCache class
 *
 * @package   PopupMaker
 * @copyright Copyright (c) 2024, Code Atlantic LLC
 */

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

/**
 * AssetCache class.
 */
class PUM_AssetCache {

	/**
	 * Cache directory.
	 *
	 * @var string
	 */
	public static $cache_dir;

	/**
	 * Suffix for minified assets.
	 *
	 * @var string
	 */
	public static $suffix;

	/**
	 * Asset url.
	 *
	 * @var string
	 */
	public static $asset_url;

	/**
	 * JS Url.
	 *
	 * @var string
	 */
	public static $js_url;

	/**
	 * CSS Url.
	 *
	 * @var string
	 */
	public static $css_url;

	/**
	 * Should we disable asset caching?
	 *
	 * @var bool
	 */
	public static $disabled = true;

	/**
	 * Should we output debug information?
	 *
	 * @var bool
	 */
	public static $debug;

	/**
	 * Check to see if the cache has been initialized.
	 *
	 * @var boolean
	 */
	public static $initialized = false;

	/**
	 * Initialize asset cache.
	 */
	public static function init() {
		if ( ! self::$initialized ) {
			self::$cache_dir = self::get_cache_dir();
			self::$debug     = Popup_Maker::debug_mode() || ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG );
			self::$suffix    = self::$debug ? '' : '.min';
			self::$asset_url = Popup_Maker::$URL . 'assets/';
			self::$js_url    = self::$asset_url . 'js/';
			self::$css_url   = self::$asset_url . 'css/';
			if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
				self::$disabled = true;
			} else {
				self::$disabled = pum_get_option( 'disable_asset_caching', false );
			}

			add_action( 'pum_extension_updated', [ __CLASS__, 'reset_cache' ] );
			add_action( 'pum_extension_deactivated', [ __CLASS__, 'reset_cache' ] );
			add_action( 'pum_extension_activated', [ __CLASS__, 'reset_cache' ] );
			add_action( 'pum_regenerate_asset_cache', [ __CLASS__, 'reset_cache' ] );
			add_action( 'pum_save_settings', [ __CLASS__, 'reset_cache' ] );
			add_action( 'pum_save_popup', [ __CLASS__, 'reset_cache' ] );
			add_action( 'pum_save_theme', [ __CLASS__, 'reset_cache' ] );
			add_action( 'pum_update_core_version', [ __CLASS__, 'reset_cache' ] );

			add_action( 'pum_update_core_version', [ __CLASS__, 'maybe_reset_asset_cache_notices' ] );

			if ( isset( $_GET['flush_popup_cache'] ) && check_admin_referer( 'flush_popup_cache' ) ) {
				add_action( 'init', [ __CLASS__, 'reset_cache' ] );
			}

			add_filter( 'pum_alert_list', [ __CLASS__, 'cache_alert' ] );

			add_action( 'pum_styles', [ __CLASS__, 'global_custom_styles' ] );

			if ( null === get_option( 'pum_files_writeable', null ) ) {
				add_option( 'pum_files_writeable', true );
				add_option( '_pum_writeable_notice_dismissed', true );
				pum_reset_assets();
			}

			if ( is_admin() && current_user_can( 'edit_posts' ) ) {
				add_action( 'init', [ __CLASS__, 'admin_notice_check' ] );
			}

			// Run our own action for enqueuing scripts via our API.
			add_action( 'wp_enqueue_scripts', function () {
				do_action( 'pum_register_scripts' );
			}, 0 );

			// Run our own action for enqueuing scripts via our API.
			add_action( 'wp_enqueue_scripts', function () {
				do_action( 'pum_enqueue_scripts' );
			}, 10 );

			// Run our own action for enqueuing scripts via our API.
			add_action( 'admin_enqueue_scripts', function () {
				do_action( 'pum_register_scripts' );
				do_action( 'pum_admin_register_scripts' );
			}, 0 );

			// Run our own action for enqueuing scripts via our API.
			add_action( 'admin_enqueue_scripts', function () {
				do_action( 'pum_admin_enqueue_scripts' );
			}, 10 );

			add_action( 'wp_print_scripts', [ __CLASS__, 'localize_bundled_scripts' ], 10 );

			// Hook into WordPress script/style tag filters for ID obfuscation.
			add_filter( 'script_loader_tag', [ __CLASS__, 'obfuscate_script_tag' ], 10, 3 );
			add_filter( 'style_loader_tag', [ __CLASS__, 'obfuscate_style_tag' ], 10, 4 );
			add_filter( 'wp_inline_script_attributes', [ __CLASS__, 'obfuscate_inline_script_id' ], 10, 2 );

			// Prevent reinitialization.
			self::$initialized = true;
		}
	}

	/**
	 * Checks if Asset caching is possible and enabled.
	 *
	 * @return bool
	 */
	public static function enabled() {
		if ( defined( 'PUM_ASSET_CACHE' ) && ! PUM_ASSET_CACHE ) {
			return false;
		}

		return self::writeable() && ! self::$disabled;
	}

	/**
	 * Is the cache directory writeable?
	 *
	 * @return bool True if directory is writeable
	 */
	public static function writeable() {
		if ( self::$disabled ) {
			return false;
		}

		// If we have already determined files to not be writeable, go ahead and return.
		if ( true !== (bool) get_option( 'pum_files_writeable', true ) ) {
			return false;
		}

		/**
		 * Filesystem.
		 *
		 * @var \WP_Filesystem_Base $wp_filesystem
		 */
		global $wp_filesystem;

		if ( ! function_exists( 'WP_Filesystem' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}

		$results = WP_Filesystem();

		if ( true !== $results ) {
			// Prevents this from running again and set to show the admin notice.
			update_option( 'pum_files_writeable', false );
			update_option( '_pum_writeable_notice_dismissed', false );
			if ( ! is_null( $results ) && isset( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
				$error = $wp_filesystem->errors->get_error_message();
				pum_log_message( sprintf( 'Cache directory is not writeable due to filesystem error. Error given: %s', esc_html( $error ) ) );
			} else {
				pum_log_message( 'Cache directory is not writeable due to incorrect filesystem method.' );
			}
			return false;
		}

		// Checks and create cachedir.
		if ( false !== self::$cache_dir && ! is_dir( self::$cache_dir ) ) {
			$wp_filesystem->mkdir( self::$cache_dir );
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		return false !== self::$cache_dir && $wp_filesystem->is_writable( self::$cache_dir ) && ! isset( $_POST['wp_customize'] );
	}

	/**
	 * Regenerate cache on demand.
	 */
	public static function regenerate_cache() {
		self::cache_js();
		self::cache_css();
	}

	/**
	 * Gets the directory caching should be stored in.
	 *
	 * Accounts for various adblock bypass options.
	 *
	 * @return array|string
	 */
	public static function get_cache_dir() {
		$upload_dir = PUM_Helpers::get_upload_dir_path();
		if ( false === $upload_dir ) {
			return false;
		}

		if ( ! pum_get_option( 'bypass_adblockers', false ) ) {
			return trailingslashit( $upload_dir ) . 'pum';
		}

		return $upload_dir;
	}

	/**
	 * Generates a cache filename based on the current adblock bypass settings.
	 *
	 * @param string $filename Filename.
	 *
	 * @return string
	 */
	public static function generate_cache_filename( $filename ) {
		if ( ! pum_get_option( 'bypass_adblockers', false ) ) {
			global $blog_id;
			$is_multisite = ( is_multisite() ) ? '-' . $blog_id : '';

			return $filename . $is_multisite;
		}

		$site_url = get_site_url();

		switch ( pum_get_option( 'adblock_bypass_url_method', 'custom' ) ) {
			case 'random':
				$filename = md5( $site_url . $filename );
				break;
			case 'custom':
				$filename = preg_replace( '/[^a-z0-9]+/', '-', pum_get_option( 'adblock_bypass_custom_filename', 'pm-' . $filename ) );

				break;
		}

		return $filename;
	}

	/**
	 * Generate JS cache file.
	 */
	public static function cache_js() {
		if ( false === self::$cache_dir ) {
			return;
		}
		$js_file = self::generate_cache_filename( 'pum-site-scripts' ) . '.js';

		$js  = "/**\n";
		$js .= " * Do not touch this file! This file created by the Popup Maker plugin using PHP\n";
		$js .= ' * Last modified time: ' . ( function_exists( 'wp_date' ) ? wp_date( 'M d Y, h:i:s' ) : date( 'M d Y, h:i:s', current_time( 'timestamp' ) ) ) . "\n";
		$js .= " */\n\n\n";
		$js .= self::generate_js();

		if ( ! self::cache_file( $js_file, $js ) ) {
			update_option( 'pum-has-cached-js', false );
		} else {
			update_option( 'pum-has-cached-js', strtotime( 'now' ) );
		}
	}

	/**
	 * Generate CSS cache file.
	 */
	public static function cache_css() {
		if ( false === self::$cache_dir ) {
			return;
		}
		$css_file = self::generate_cache_filename( 'pum-site-styles' ) . '.css';

		$css  = "/**\n";
		$css .= " * Do not touch this file! This file created by the Popup Maker plugin using PHP\n";
		$css .= ' * Last modified time: ' . ( function_exists( 'wp_date' ) ? wp_date( 'M d Y, h:i:s' ) : date( 'M d Y, h:i:s', current_time( 'timestamp' ) ) ) . "\n";
		$css .= " */\n\n\n";
		$css .= self::generate_css();

		if ( ! self::cache_file( $css_file, $css ) ) {
			update_option( 'pum-has-cached-css', false );
		} else {
			update_option( 'pum-has-cached-css', strtotime( 'now' ) );
		}
	}

	/**
	 * Generate custom JS
	 *
	 * @return string
	 */
	public static function generate_js() {
		/**
		 * Filesystem.
		 *
		 * @var \WP_Filesystem_Base $wp_filesystem
		 */
		global $wp_filesystem;

		// Load core scripts so we can eliminate another stylesheet.
		$core_js = $wp_filesystem->get_contents( Popup_Maker::$DIR . 'dist/assets/site.js' );

		/**
		 *  0 Core
		 *  5 Extensions
		 *  8 Integrations
		 * 10 Per Popup JS
		 */
		$js = [
			'core' => [
				'content'  => $core_js,
				'priority' => 0,
			],
		];

		$popups = pum_get_all_popups();

		if ( ! empty( $popups ) ) {
			foreach ( $popups as $popup ) {
				// Set this popup as the global $current.
				pum()->current_popup = $popup;

				ob_start();

				// Allow per popup JS additions.
				do_action( 'pum_generate_popup_js', $popup->ID );

				$popup_js = ob_get_clean();

				if ( ! empty( $popup_js ) ) {
					$js[ 'popup-' . $popup->ID ] = [
						'content' => $popup_js,
					];
				}
			}

			// Clear the global $current.
			pum()->current_popup = null;
		}

		$js = apply_filters( 'pum_generated_js', $js );

		foreach ( $js as $key => $code ) {
			$js[ $key ] = wp_parse_args(
				$code,
				[
					'content'  => '',
					'priority' => 10,
				]
			);
		}

		uasort( $js, [ 'PUM_Helpers', 'sort_by_priority' ] );

		$js_code = '';
		foreach ( $js as $key => $code ) {
			if ( ! empty( $code['content'] ) ) {
				$js_code .= $code['content'] . "\n\n";
			}
		}

		return $js_code;
	}

	/**
	 * Cache file contents.
	 *
	 * @param string $filename Filename of file to generate.
	 * @param string $contents Contents to put into file.
	 *
	 * @return bool
	 */
	public static function cache_file( $filename, $contents ) {
		/**
		 * WP Filesystem.
		 *
		 * @var \WP_Filesystem_Base $wp_filesystem
		 */
		global $wp_filesystem;

		if ( false === self::$cache_dir ) {
			return false;
		}

		if ( ! function_exists( 'WP_Filesystem' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}

		$file = trailingslashit( self::$cache_dir ) . $filename;

		WP_Filesystem();

		$results = $wp_filesystem->put_contents( $file, $contents, defined( 'FS_CHMOD_FILE' ) ? FS_CHMOD_FILE : false );

		// If the file is generated and is accessible...
		if ( true === $results && self::is_file_accessible( $filename ) ) {
			return true;
		} else {
			// ... else, let's set our flags to prevent cache running again for now.
			update_option( 'pum_files_writeable', false );
			update_option( '_pum_writeable_notice_dismissed', false );
			return false;
		}
	}

	/**
	 * Generate Custom Styles
	 *
	 * @return string
	 */
	public static function generate_css() {
		/**
		 * WP Filesystem.
		 *
		 * @var \WP_Filesystem_Base $wp_filesystem
		 */
		global $wp_filesystem;

		// Include core styles so we can eliminate another stylesheet.
		$core_css = $wp_filesystem->get_contents( Popup_Maker::$DIR . 'dist/assets/site' . ( is_rtl() ? '-rtl' : '' ) . '.css' );

		/**
		 *  0 Core
		 *  1 Popup Themes
		 *  5 Extensions
		 * 10 Per Popup CSS
		 */
		$css = [
			'imports' => [
				'content'  => self::generate_font_imports(),
				'priority' => - 1,
			],
			'core'    => [
				'content'  => $core_css,
				'priority' => 0,
			],
			'themes'  => [
				'content'  => self::generate_popup_theme_styles(),
				'priority' => 1,
			],
			'popups'  => [
				'content'  => self::generate_popup_styles(),
				'priority' => 15,
			],
			'custom'  => [
				'content'  => self::custom_css(),
				'priority' => 20,
			],
		];

		$css = apply_filters( 'pum_generated_css', $css );

		foreach ( $css as $key => $code ) {
			$css[ $key ] = wp_parse_args(
				$code,
				[
					'content'  => '',
					'priority' => 10,
				]
			);
		}

		uasort( $css, [ 'PUM_Helpers', 'sort_by_priority' ] );

		$css_code = '';
		foreach ( $css as $key => $code ) {
			if ( ! empty( $code['content'] ) ) {
				$css_code .= $code['content'] . "\n\n";
			}
		}

		return $css_code;
	}

	/**
	 * Render global custom styles.
	 */
	public static function global_custom_styles() {
		if ( pum_get_option( 'adjust_body_padding' ) ) {
			echo 'html.pum-open.pum-open-overlay.pum-open-scrollable body > *:not([aria-modal="true"]) { padding-right: ' . esc_attr( pum_get_option( 'body_padding_override', '15px' ) ) . '!important; }';
		}
	}

	/**
	 * Generate Popup Styles
	 *
	 * @return string
	 */
	public static function generate_popup_styles() {
		$popup_css = '';

		$popups = pum_get_all_popups();

		if ( ! empty( $popups ) ) {
			foreach ( $popups as $popup ) {
				// Set this popup as the global $current.
				pum()->current_popup = $popup;

				$popup = pum_get_popup( $popup->ID );

				if ( ! pum_is_popup( $popup ) ) {
					continue;
				}

				ob_start();

				if ( $popup->get_setting( 'zindex', false ) ) {
					$zindex = esc_attr( $popup->get_setting( 'zindex' ) );
					printf( "#pum-%d {z-index: %d}\r\n", esc_attr( $popup->ID ), esc_attr( $zindex ) );
				}

				// Allow per popup CSS additions.
				do_action( 'pum_generate_popup_css', $popup->ID );

				$popup_css .= ob_get_clean();
			}

			// Clear the global $current.
			pum()->current_popup = null;
		}

		return $popup_css;
	}

	/**
	 * Used when asset cache is not enabled.
	 *
	 * @return string
	 */
	public static function inline_css() {
		ob_start();

		// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
		echo self::generate_font_imports();
		echo self::generate_popup_theme_styles();

		// Render popup styles.
		echo self::generate_popup_styles();

		// Render any extra styles globally added.
		echo self::custom_css();

		// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
		return ob_get_clean();
	}

	/**
	 * Generate Popup Theme Styles
	 *
	 * @return string
	 */
	public static function custom_css() {
		// Reset ob.
		ob_start();

		// Render any extra styles globally added.
		if ( ! empty( $GLOBALS['pum_extra_styles'] ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			echo $GLOBALS['pum_extra_styles'];
		}

		// Allows rendering extra css via action.
		do_action( 'pum_styles' );

		return ob_get_clean();
	}

	/**
	 * Generate Popup Theme Styles
	 *
	 * @return mixed|string
	 */
	public static function generate_font_imports() {
		$imports = '';

		$google_fonts = [];

		foreach ( pum_get_all_themes() as $theme ) {
			$google_fonts = array_merge( $google_fonts, pum_get_theme( $theme->ID )->get_google_fonts_used() );
		}

		if ( ! empty( $google_fonts ) && ! pum_get_option( 'disable_google_font_loading', false ) ) {
			$link = '//fonts.googleapis.com/css?family=';
			foreach ( $google_fonts as $font_family => $variants ) {
				if ( '//fonts.googleapis.com/css?family=' !== $link ) {
					$link .= '|';
				}
				$link .= $font_family;
				if ( is_array( $variants ) ) {
					if ( implode( ',', $variants ) !== '' ) {
						$link .= ':';
						$link .= trim( implode( ',', $variants ), ':' );
					}
				}
			}

			$imports = "/* Popup Google Fonts */\r\n@import url('$link');\r\n\r\n" . $imports;
		}

		$imports = apply_filters( 'pum_generate_font_imports', $imports );

		return $imports;
	}

	/**
	 * Generate Popup Theme Styles
	 *
	 * @return mixed|string
	 */
	public static function generate_popup_theme_styles() {
		$styles = '';

		$themes = pum_get_all_themes();

		foreach ( $themes as $theme ) {
			$theme_styles = pum_get_rendered_theme_styles( $theme->ID );

			if ( '' !== $theme_styles ) {
				$styles .= '/* Popup Theme ' . esc_attr( $theme->ID ) . ': ' . esc_html( $theme->post_title ) . " */\r\n";
				$styles .= $theme_styles . "\r\n";
			}
		}

		$styles = apply_filters( 'popmake_theme_styles', $styles );

		$styles = apply_filters( 'pum_generate_popup_theme_styles', $styles );

		return $styles;
	}


	/**
	 * Reset the cache to force regeneration.
	 */
	public static function reset_cache() {
		update_option( 'pum-has-cached-css', false );
		update_option( 'pum-has-cached-js', false );
	}

	/**
	 * Adds admin notice if the files are not writeable.
	 *
	 * @param array $alerts The alerts currently in the alert system.
	 * @return array Alerts for the alert system.
	 * @since 1.9.0
	 */
	public static function cache_alert( $alerts ) {
		if ( self::should_not_show_alert() ) {
			return $alerts;
		}

		$nonce = wp_create_nonce( 'pum-write-notice-action' );

		$undo_url = add_query_arg( [
			'pum_writeable_notice_check' => 'undo',
			'_wpnonce'                   => $nonce,
		] );

		$dismiss_url = add_query_arg([
			'pum_writeable_notice_check' => 'dismiss',
			'_wpnonce'                   => $nonce,
		] );

		ob_start();
		?>
		<ul>
			<li><a href="<?php echo esc_attr( $undo_url ); ?>"><strong><?php esc_html_e( 'Try to create cache again', 'popup-maker' ); ?></strong></a></li>
			<li><a href="<?php echo esc_attr( $dismiss_url ); ?>" class="pum-dismiss"><?php esc_html_e( 'Keep current method', 'popup-maker' ); ?></a></li>
			<li><a href="https://wppopupmaker.com/docs/problem-solving/popup-maker-detected-an-issue-with-your-file-system/?utm_source=filesystem-error-alert&utm_medium=inline-doclink&utm_campaign=filesystem-error" target="_blank" rel="noreferrer noopener"><?php esc_html_e( 'Learn more', 'popup-maker' ); ?></a></li>
		</ul>
		<?php
		$html     = ob_get_clean();
		$alerts[] = [
			'code'        => 'pum_writeable_notice',
			'type'        => 'warning',
			'message'     => esc_html__( "Popup Maker detected an issue with your file system's ability and is unable to create & save cached assets for your popup styling and settings. This may lead to suboptimal performance. Please check your filesystem and contact your hosting provide to ensure Popup Maker can create and write to cache files.", 'popup-maker' ),
			'html'        => $html,
			'priority'    => 1000,
			'dismissible' => '2 weeks',
			'global'      => true,
		];
		return $alerts;
	}

	/**
	 * Checks if any options have been clicked from admin notices.
	 *
	 * @since 1.9.0
	 */
	public static function admin_notice_check() {
		if ( isset( $_GET['pum_writeable_notice_check'] ) ) {
			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
			check_admin_referer( 'pum-write-notice-action' );

			// If either dismiss or try again button is clicked, hide the admin notice.
			update_option( '_pum_writeable_notice_dismissed', true );
			if ( 'undo' === $_GET['pum_writeable_notice_check'] ) {
				// If try again is clicked, remove flag.
				update_option( 'pum_files_writeable', true );
			} else {
				pum_update_option( 'disable_asset_caching', true );
			}
		}
	}

	/**
	 * Whether or not we should show admin notice
	 *
	 * @since 1.9.0
	 * @return bool True if notice should not be shown
	 */
	public static function should_not_show_alert() {
		return true === (bool) get_option( 'pum_files_writeable', true ) || true === (bool) get_option( '_pum_writeable_notice_dismissed', true );
	}

	/**
	 * Tests whether the file is accessible and returns 200 status code
	 *
	 * @param string $filename Filename of cache file to test.
	 *
	 * @return bool True if file exists and is accessible
	 */
	private static function is_file_accessible( $filename ) {
		if ( ! $filename || ! is_string( $filename ) ) {
			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
			pum_log_message( 'Cannot check if file is accessible. Filename passed: ' . print_r( $filename, true ) );
			return false;
		}
		$cache_url = PUM_Helpers::get_cache_dir_url();
		if ( false === $cache_url ) {
			pum_log_message( 'Cannot access cache file when tested. Cache URL returned false.' );
		}
		$protocol = is_ssl() ? 'https:' : 'http:';
		$file     = $protocol . $cache_url . '/' . $filename;
		$results  = wp_remote_request(
			$file,
			[
				'method'    => 'HEAD',
				'sslverify' => false,
			]
		);

		// If it returned a WP_Error, let's log its error message.
		if ( is_wp_error( $results ) ) {
			$error = $results->get_error_message();
			pum_log_message( sprintf( 'Cannot access cache file when tested. Tested file: %s Error given: %s', esc_html( $file ), esc_html( $error ) ) );
		}

		// If it returned valid array...
		if ( is_array( $results ) && isset( $results['response'] ) ) {
			$status_code = $results['response']['code'];

			// ... then, check if it's a valid status code. Only if it is a valid 2XX code, will this method return true.
			if ( false !== $status_code && ( 200 <= $status_code && 300 > $status_code ) ) {
				return true;
			} else {
				pum_log_message( sprintf( 'Cannot access cache file when tested. Status code received was: %s', esc_html( $status_code ) ) );
			}
		}
		return false;
	}

	/**
	 * Process a change to options conditionally if the user came from a bugged version.
	 *
	 * @param string $upgraded_from The version the user upgraded from.
	 */
	public static function maybe_reset_asset_cache_notices( $upgraded_from ) {
		// If version compoare upgraded from was v1.18.0 exactly delete the notice.
		if ( '1.18.0' === $upgraded_from ) {
			update_option( 'pum_files_writeable', true );
			update_option( '_pum_writeable_notice_dismissed', true );
			pum_update_option( 'disable_asset_caching', false );
		}
	}


	/**
	 * Retrieve asset contents while ignoring SSL verification errors.
	 *
	 * @param string $src URL or path to the asset.
	 *
	 * @return string|false Asset contents on success, false on failure.
	 *
	 * @since 1.21.0
	 */
	private static function get_asset_contents( $src ) {
		$scheme = wp_parse_url( $src, PHP_URL_SCHEME );

		if ( in_array( $scheme, [ 'http', 'https' ], true ) ) {
				$response = wp_remote_get( $src, [ 'sslverify' => false ] );

			if ( ! is_wp_error( $response ) ) {
					return wp_remote_retrieve_body( $response );
			}
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		return file_get_contents( $src );
	}

	/**
	 * Stores registered scripts.
	 *
	 * @var array{src:string,deps:array,version:string,in_footer:bool}[]
	 *
	 * @since 1.21.0
	 */
	public static $registered_scripts = [];

	/**
	 * Stores registered styles.
	 *
	 * @var array{src:string,deps:array,version:string,media:string}[]
	 *
	 * @since 1.21.0
	 */
	public static $registered_styles = [];

	/**
	 * Stores enqueued scripts.
	 *
	 * @var string[]
	 *
	 * @since 1.21.0
	 */
	public static $enqueued_scripts = [];

	/**
	 * Stores dependencies from bundled scripts.
	 *
	 * @var string[]
	 *
	 * @since 1.21.0
	 */
	public static $bundled_script_deps = [];

	/**
	 * Stores dependencies from bundled styles.
	 *
	 * @var string[]
	 *
	 * @since 1.21.0
	 */
	public static $bundled_style_deps = [];

	/**
	 * Stores enqueued styles.
	 *
	 * @var string[]
	 *
	 * @since 1.21.0
	 */
	public static $enqueued_styles = [];

	/**
	 * Stores localized scripts. Multiple for the same handle.
	 *
	 * @var array<string,array{object_name:string,value:mixed}[]>
	 *
	 * @since 1.21.0
	 */
	public static $localized_scripts = [];

	/**
	 * Register a script for possible caching.
	 *
	 * @param string $handle The script handle.
	 * @param string $src The script src.
	 * @param array  $deps The script dependencies.
	 * @param string $version The script version.
	 * @param bool   $in_footer Whether to enqueue the script in the footer.
	 *
	 * @return bool
	 *
	 * @since 1.21.0
	 */
	public static function register_script( $handle, $src, $deps = [], $version = null, $in_footer = false, $merge_priority = 5 ) {
		// Implement internal store of scripts. AssetCache will be built using this if enabled, otherwised passed to wp_register_* directly.
		self::$registered_scripts[ $handle ] = [
			'src'       => $src,
			'deps'      => $deps,
			'version'   => $version,
			'in_footer' => $in_footer,
		];

		// Temporary until we handle this in a global way.
		if ( ! self::enabled() ) {
			wp_register_script( $handle, $src, $deps, $version, $in_footer );
		} else {
			// Collect all dependencies for bundled scripts (filter later when bundle is registered).
			if ( ! empty( $deps ) ) {
				self::$bundled_script_deps = array_merge( self::$bundled_script_deps, $deps );
			}

			add_filter( 'pum_generated_js', function ( $js = [] ) use ( $handle, $src, $merge_priority ): array {
				$js[ $handle ] = [
					'content'  => self::get_asset_contents( $src ),
					'priority' => $merge_priority,
				];

				return $js;
			} );
		}

		return true;
	}

	/**
	 * Register a style for possible caching.
	 *
	 * @param string $handle The style handle.
	 * @param string $src The style src.
	 * @param array  $deps The style dependencies.
	 * @param string $version The style version.
	 * @param string $media The style media.
	 *
	 * @return bool
	 *
	 * @since 1.21.0
	 */
	public static function register_style( $handle, $src, $deps = [], $version = null, $media = 'all', $merge_priority = 5 ) {
		// Implement internal store of styles. AssetCache will be built using this if enabled, otherwised passed to wp_register_* directly.
		self::$registered_styles[ $handle ] = [
			'src'     => $src,
			'deps'    => $deps,
			'version' => $version,
			'media'   => $media,
		];

		// Temporary until we handle this in a global way.
		if ( ! self::enabled() ) {
			wp_register_style( $handle, $src, $deps, $version, $media );
		} else {
			// Collect all dependencies for bundled styles (filter later when bundle is registered).
			if ( ! empty( $deps ) ) {
				self::$bundled_style_deps = array_merge( self::$bundled_style_deps, $deps );
			}

			add_filter( 'pum_generated_css', function ( $css = [] ) use ( $handle, $src, $merge_priority ): array {
				$css[ $handle ] = [
					'content'  => self::get_asset_contents( $src ),
					'priority' => $merge_priority,
				];

				return $css;
			} );
		}

		return true;
	}

	/**
	 * Enqueue a script for possible caching.
	 *
	 * @param string $handle The script handle.
	 * @param string $src The script src.
	 * @param array  $deps The script dependencies.
	 * @param string $version The script version.
	 * @param bool   $in_footer Whether to enqueue the script in the footer.
	 *
	 * @return bool
	 *
	 * @since 1.21.0
	 */
	public static function enqueue_script( $handle, $src = '', $deps = [], $version = null, $in_footer = false ) {
		// Implement internal store of scripts. AssetCache will be built using this if enabled, otherwised passed to wp_register_* directly.
		if ( ! empty( $src ) ) {
			self::$registered_scripts[ $handle ] = [
				'src'       => $src,
				'deps'      => $deps,
				'version'   => $version,
				'in_footer' => $in_footer,
			];
		}

		self::$enqueued_scripts[] = $handle;

		// Temporary until we handle this in a global way.
		if ( ! self::enabled() ) {
			wp_enqueue_script( $handle );
		}

		return true;
	}

	/**
	 * Check if a script is enqueued.
	 *
	 * @param string $handle The script handle.
	 * @param string $status The script status.
	 *
	 * @return bool
	 *
	 * @since 1.21.0
	 */
	public static function script_is( $handle, $status = 'enqueued' ) {
		if ( in_array( $handle, self::$enqueued_scripts, true ) ) {
			return true;
		}

		return wp_script_is( $handle, $status );
	}

	/**
	 * Enqueue a style for possible caching.
	 *
	 * @param string $handle The style handle.
	 * @param string $src The style src.
	 * @param array  $deps The style dependencies.
	 * @param string $version The style version.
	 * @param string $media The style media.
	 *
	 * @return bool
	 *
	 * @since 1.21.0
	 */
	public static function enqueue_style( $handle, $src = '', $deps = [], $version = null, $media = 'all' ) {
		// Implement internal store of styles. AssetCache will be built using this if enabled, otherwised passed to wp_register_* directly.
		if ( ! empty( $src ) ) {
			self::$registered_styles[ $handle ] = [
				'src'     => $src,
				'deps'    => $deps,
				'version' => $version,
				'media'   => $media,
			];
		}

		self::$enqueued_styles[] = $handle;

		// Temporary until we handle this in a global way.
		if ( ! self::enabled() ) {
			wp_enqueue_style( $handle );
		}

		return true;
	}

	/**
	 * Dequeue a script.
	 *
	 * @param string $handle The script handle.
	 *
	 * @return bool
	 *
	 * @since 1.21.0
	 */
	public static function dequeue_script( $handle ) {
		if ( in_array( $handle, self::$enqueued_scripts, true ) ) {
			self::$enqueued_scripts = array_diff( self::$enqueued_scripts, [ $handle ] );

			// Temporary until we handle this in a global way.
			if ( ! self::enabled() ) {
				wp_dequeue_script( $handle );
			}

			return true;
		}

		return false;
	}

	/**
	 * Dequeue a style.
	 *
	 * @param string $handle The style handle.
	 *
	 * @return bool
	 *
	 * @since 1.21.0
	 */
	public static function dequeue_style( $handle ) {
		if ( in_array( $handle, self::$enqueued_styles, true ) ) {
			self::$enqueued_styles = array_diff( self::$enqueued_styles, [ $handle ] );

			// Temporary until we handle this in a global way.
			if ( ! self::enabled() ) {
				wp_dequeue_style( $handle );
			}

			return true;
		}

		return false;
	}

	/**
	 * Localize a script.
	 *
	 * @param string $handle      Script handle the data will be attached to.
	 * @param string $object_name Name for the JavaScript object. Passed directly, so it should be qualified JS variable.
	 *                            Example: '/[a-zA-Z0-9_]+/'.
	 * @param array  $value       The data itself. The data can be either a single or multi-dimensional array.
	 *
	 * @return bool
	 *
	 * @since 1.21.0
	 */
	public static function localize_script( $handle, $object_name, $value ) {
		if ( ! isset( self::$localized_scripts[ $handle ] ) ) {
			self::$localized_scripts[ $handle ] = [];
		}

		self::$localized_scripts[ $handle ][] = [
			'object' => $object_name,
			'value'  => $value,
		];

		// Temporary until we handle this in a global way.
		if ( ! self::enabled() ) {
			wp_localize_script( $handle, $object_name, $value );
		}

		return true;
	}

	public static function localize_bundled_scripts() {
		if ( ! self::enabled() ) {
			return;
		}

		// Localize all registered scripts that have vars under the main plugin handle.
		foreach ( self::$localized_scripts as $handle => $var_sets ) {
			foreach ( $var_sets as $vars ) {
				wp_localize_script( 'popup-maker-site', $vars['object'], $vars['value'] );
			}
		}
	}

	/**
	 * Get all dependencies from bundled scripts.
	 *
	 * @return string[] Array of script dependencies.
	 *
	 * @since 1.21.0
	 */
	public static function get_bundled_script_dependencies() {
		// Filter out self-references, main bundle, and other bundled scripts to prevent circular dependencies.
		$filtered_deps = array_filter( self::$bundled_script_deps, function ( $dep ) {
			return 'popup-maker-site' !== $dep &&
					! isset( self::$registered_scripts[ $dep ] );
		});

		return array_unique( $filtered_deps );
	}

	/**
	 * Get all dependencies from bundled styles.
	 *
	 * @return string[] Array of style dependencies.
	 *
	 * @since 1.21.0
	 */
	public static function get_bundled_style_dependencies() {
		// Filter out self-references, main bundle, and other bundled styles to prevent circular dependencies.
		$filtered_deps = array_filter( self::$bundled_style_deps, function ( $dep ) {
			return 'popup-maker-site' !== $dep &&
					! isset( self::$registered_styles[ $dep ] );
		});

		return array_unique( $filtered_deps );
	}

	/**
	 * Generate obfuscated handle name for script/style IDs.
	 *
	 * Uses same logic as cache filenames but incorporates handle for uniqueness.
	 *
	 * @since 1.21.0
	 *
	 * @param string $handle The original script/style handle.
	 * @return string Obfuscated handle name.
	 */
	public static function generate_obfuscated_handle( $handle ) {
		$site_url = get_site_url();
		$method   = pum_get_option( 'adblock_bypass_url_method', 'custom' );

		// Strip popup-maker-/pum- prefix for cleaner custom names.
		$suffix = preg_replace( '/^(popup-maker-|pum-)/', '', $handle );

		switch ( $method ) {
			case 'random':
				return md5( $site_url . $handle );

			case 'custom':
			default:
				$prefix = pum_get_option( 'adblock_bypass_custom_filename', 'pm' );
				$prefix = ! empty( $prefix ) ? $prefix : 'pm';
				return sanitize_html_class( $prefix . '-' . $suffix );
		}
	}

	/**
	 * Obfuscate script tag IDs for ad-blocker bypass.
	 *
	 * Uses script_loader_tag filter to catch ALL script output including
	 * inline scripts (-js-before, -js-after, -js-translations).
	 *
	 * @since 1.21.0
	 *
	 * @param string $tag    The complete <script> tag HTML.
	 * @param string $handle The script handle.
	 * @param string $src    The script source URL.
	 * @return string Modified tag with obfuscated ID.
	 */
	public static function obfuscate_script_tag( $tag, $handle, $src ) {
		// Only run on frontend when bypass is enabled.
		if ( is_admin() || ! pum_get_option( 'bypass_adblockers', false ) ) {
			return $tag;
		}

		// Only obfuscate Popup Maker handles.
		if ( ! self::should_obfuscate_handle( $handle ) ) {
			return $tag;
		}

		// Generate obfuscated handle name.
		$obfuscated = self::generate_obfuscated_handle( $handle );

		// Replace all ID variants in the tag.
		$tag = preg_replace(
			'/id=["\']' . preg_quote( $handle, '/' ) . '-js(-[a-z]+)?["\']/',
			'id="' . $obfuscated . '-js$1"',
			$tag
		);

		return $tag;
	}

	/**
	 * Obfuscate style tag IDs for ad-blocker bypass.
	 *
	 * @since 1.21.0
	 *
	 * @param string $tag    The complete <link> tag HTML.
	 * @param string $handle The style handle.
	 * @param string $href   The style source URL.
	 * @param string $media  The media attribute.
	 * @return string Modified tag with obfuscated ID.
	 */
	public static function obfuscate_style_tag( $tag, $handle, $href, $media ) {
		// Only run on frontend when bypass is enabled.
		if ( is_admin() || ! pum_get_option( 'bypass_adblockers', false ) ) {
			return $tag;
		}

		// Only obfuscate Popup Maker handles.
		if ( ! self::should_obfuscate_handle( $handle ) ) {
			return $tag;
		}

		// Generate obfuscated handle name.
		$obfuscated = self::generate_obfuscated_handle( $handle );

		// Replace all ID variants in the tag.
		$tag = preg_replace(
			'/id=["\']' . preg_quote( $handle, '/' ) . '(-rtl)?(-inline)?-css["\']/',
			'id="' . $obfuscated . '$1$2-css"',
			$tag
		);

		return $tag;
	}

	/**
	 * Obfuscate inline script IDs (for wp_localize_script -js-extra tags).
	 *
	 * WordPress prints -js-extra tags separately before the main script tag,
	 * so they don't go through script_loader_tag. This filter catches them.
	 *
	 * @since 1.21.0
	 *
	 * @param array  $attributes Script tag attributes including 'id'.
	 * @param string $data       The inline script content.
	 * @return array Modified attributes with obfuscated ID.
	 */
	public static function obfuscate_inline_script_id( $attributes, $data ) {
		// Only run on frontend when bypass is enabled.
		if ( is_admin() || ! pum_get_option( 'bypass_adblockers', false ) ) {
			return $attributes;
		}

		if ( empty( $attributes['id'] ) ) {
			return $attributes;
		}

		// Check if this is a Popup Maker script (e.g., "popup-maker-site-js-extra").
		$id = $attributes['id'];

		// Extract handle from ID (remove -js-extra suffix).
		if ( preg_match( '/^(.+)-js-extra$/', $id, $matches ) ) {
			$handle = $matches[1];

			if ( self::should_obfuscate_handle( $handle ) ) {
				$obfuscated       = self::generate_obfuscated_handle( $handle );
				$attributes['id'] = $obfuscated . '-js-extra';
			}
		}

		return $attributes;
	}

	/**
	 * Check if a script/style handle should be obfuscated.
	 *
	 * Determines if the given handle matches Popup Maker patterns and should
	 * have its ID attribute obfuscated. Excludes admin scripts from obfuscation.
	 *
	 * @since 1.21.0
	 *
	 * @param string $handle Script or style handle.
	 * @return bool True if handle should be obfuscated, false otherwise.
	 */
	public static function should_obfuscate_handle( $handle ) {
		// Obfuscate frontend Popup Maker handles only.
		$patterns = [
			'popup-maker-',  // Modern packages.
			'pum-',          // Legacy handles (but not admin).
		];

		foreach ( $patterns as $pattern ) {
			if ( 0 === strpos( $handle, $pattern ) ) {
				// Exclude admin scripts.
				if ( false !== strpos( $handle, 'admin' ) ) {
					return false;
				}
				return true;
			}
		}

		return false;
	}
}
