<?php
/**
 * Critical CSS module.
 *
 * @package Hummingbird\Core\Modules
 * @since 3.6.0
 */

namespace Hummingbird\Core\Modules;

use Hummingbird\Core\Filesystem;
use Hummingbird\Core\Module;
use Hummingbird\Core\Modules\Minify\Fonts;
use Hummingbird\Core\Modules\Caching\Fast_CGI;
use Hummingbird\Core\Traits\Module as ModuleContract;
use Hummingbird\Core\Utils;
use Hummingbird\Core\Settings;

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

/**
 * Class Critical CSS
 */
class Critical_Css extends Module {
	use ModuleContract;

	/**
	 * Key to store critical status for a single post.
	 *
	 * @var string
	 */
	const CRITICAL_POST_META_KEY = '_wphb_singular_css_status';

	/**
	 * Key to store critical css process queue.
	 *
	 * @var string
	 */
	const QUEUE_OPTION_ID = 'wphb_cs_process_queue';

	/**
	 * Transient expiration timeout.
	 *
	 * @var string
	 */
	const TRANSIENT_EXPIRATION = 30;

	/**
	 * Transient name.
	 *
	 * @var string
	 */
	const TRANSIENT_NAME = 'wphb-cs-processing';

	/**
	 * Store all the items.
	 *
	 * @var array
	 */
	private $items = array();

	/**
	 * Fonts class.
	 *
	 * @var Font\Fonts
	 */
	private $fonts;

	/**
	 * Holds excluded stylesheet IDs.
	 *
	 * @var array
	 */
	public $excluded_stylesheet_ids = array();

	/**
	 * Holds all combined exclusions.
	 *
	 * @var array
	 */
	public $combined_exclusions = null;

	/**
	 * Initialize module.
	 *
	 * @since 3.6.0
	 */
	public function init() {
		$this->fonts = new Fonts();
		add_filter( 'wp_hummingbird_is_active_module_critical_css', array( $this, 'module_status' ) );
		add_filter( 'wp_hummingbird_default_options', array( $this, 'wp_hummingbird_default_options' ) );
	}

	/**
	 * Execute module actions.
	 *
	 * @since 3.6.0
	 */
	public function run() {
		add_filter( 'wphb_dont_combine_handles', array( $this, 'wphb_dont_combine_handles' ), 10, 3 );
		add_filter( 'wphb_should_cache_exit', array( $this, 'should_cache_exit' ) );
		add_filter( 'wphb_buffer', array( $this, 'add_critical_css' ) );
		// Process the cs queue through WP Cron.
		add_action( 'wphb_cs_process_queue_cron', array( $this, 'generate_critical_for_queue' ) );
		add_action( 'wphb_cs_ping_queue_cron', array( $this, 'get_critical_for_queue' ) );
		add_action( 'admin_init', array( $this, 'schedule_get_critical_cron' ), 20000 );
		add_action( 'wp_footer', array( $this, 'schedule_cron' ), 20000 );
		add_action( 'wp_head', array( $this, 'insert_load_css_script' ) );
		add_action( 'after_switch_theme', array( $this, 'regenerate_critical_css' ) );
		add_filter( 'wphb_minify_resource', array( $this, 'filter_resource_minify' ), 10, 4 );
	}

	/**
	 * Add page types to minify settings.
	 *
	 * @param array $defaults An array of default settings.
	 *
	 * @return array
	 */
	public function wp_hummingbird_default_options( $defaults ) {
		$defaults['minify']['critical_page_types'] = Page_Cache::get_page_types( true );

		return $defaults;
	}

	/**
	 * Get module status.
	 *
	 * @return bool
	 */
	public function module_status() {
		if ( ! Utils::is_member() ) {
			return false;
		}

		$options           = Settings::get_settings( 'minify' );
		$critical_css      = $options['critical_css'];
		$critical_css_mode = $options['critical_css_mode'];

		if ( ! $options['enabled'] || ! $critical_css || ( 'critical_css' !== $critical_css_mode ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Function to ignore minify, if critical css is enabled.
	 *
	 * @param boolean $result Result.
	 * @param string  $handles Asset Handles.
	 * @param string  $type Asset type.
	 *
	 * @return bool
	 */
	public function wphb_dont_combine_handles( $result, $handles, $type ) {
		// Only process styles.
		if ( 'styles' !== $type ) {
			return $result;
		}

		$options           = Utils::get_module( 'minify' )->get_options();
		$critical_css_type = $options['critical_css_type'];

		// Don't combine any styles when critical CSS type is 'remove'.
		if ( 'remove' === $critical_css_type ) {
			return true;
		}

		// Check if any handle matches the combined exclusions list.
		$combined_exclusions = $this->get_combined_exclusions();
		if ( empty( $combined_exclusions ) ) {
			return $result;
		}

		// Check each exclusion pattern against the handles string.
		foreach ( $combined_exclusions as $exclusion ) {
			if ( stripos( $handles, $exclusion ) !== false ) {
				return true; // Don't combine - handle found in exclusions.
			}
		}

		return $result;
	}

	/**
	 * Add into Item.
	 *
	 * @param string $url URL.
	 * @param string $type Type.
	 * @param string $singular Is singular.
	 */
	private function add_item( $url, $type, $singular = '' ) {
		// Nothing to be added.
		if ( empty( $url ) || empty( $type ) || ! empty( $_GET ) ) {
			return;
		}

		$url = add_query_arg(
			array(
				'avoid-minify'      => true,
				'hb_doing_critical' => 1,
			),
			$url
		);

		$queue = array(
			'url'            => $url,
			'type'           => $type,
			'singular'       => $singular, // For the individual pages.
			'status'         => 'pending', // pending - we have not sent the data for processing, processing - we have sent the data for processing, complete - response has been received.
			'result'         => false,  // False for no css generated , True for css generated .
			'error_message'  => '',
			'error_code'     => '',
			'last_updated'   => '',
			'display_notice' => 0, // 0 - Notice not displayed yet, 1 - Notice has been displayed.
			'id'             => '',
			'hash'           => $this->hash( $type ),
		);

		$this->items[ $queue['hash'] ] = (object) $queue;
	}

	/**
	 * Sets the items for critical CSS generation.
	 */
	private function set_items() {
		// Add frontpage page url to queue.
		if ( ! $this->skip_page_type( 'frontpage' ) ) {
			$this->add_item( home_url( '/' ), 'frontpage' );
		}

		$page_for_posts = get_option( 'page_for_posts' );

		if ( 'page' === get_option( 'show_on_front' ) && ! empty( $page_for_posts ) ) {
			if ( ! $this->skip_page_type( 'home' ) ) {
				$this->add_item( get_permalink( get_option( 'page_for_posts' ) ), 'home' );
			}
		}

		$post_types      = $this->get_public_post_types();
		$site_url_length = strlen( get_site_url() );

		foreach ( $post_types as $post_type ) {
			$get_post_type_url = get_permalink( $post_type->ID );
			if ( ! empty( $get_post_type_url ) && substr( $get_post_type_url, 0, $site_url_length ) === get_site_url() ) {
				if ( ! $this->skip_page_type( $post_type->post_type ) ) {
					$this->add_item( $get_post_type_url, $post_type->post_type );
				}
			}
		}

		$taxonomies = $this->get_public_taxonomies();
		foreach ( $taxonomies as $taxonomy ) {
			$get_term_url = get_term_link( (int) $taxonomy->ID );
			if ( ! empty( $get_term_url ) && substr( $get_term_url, 0, $site_url_length ) === get_site_url() ) {
				if ( ! $this->skip_page_type( 'category' ) ) {
					$this->add_item( $get_term_url, $taxonomy->taxonomy );
				}
			}
		}

		$this->items = (array) apply_filters( 'wphb_css_items', $this->items );
	}

	/**
	 * Push items into queue.
	 *
	 * @param bool $overwrite Overwrite.
	 *
	 * @return boolean
	 */
	private function persist_queue_to_db( $overwrite = false ) {
		if ( empty( $this->items ) ) {
			return false;
		}

		$current_queue = $this->get_persistent_queue();

		if ( empty( $current_queue ) ) {
			update_option( self::QUEUE_OPTION_ID, $this->items );
			return true;
		}

		$updated = false;

		foreach ( $this->items as $hash => $item ) {
			if ( empty( $current_queue[ $hash ] ) || $overwrite ) {
				$updated                = true;
				$current_queue[ $hash ] = $item;
			}
		}

		if ( $updated ) {
			update_option( self::QUEUE_OPTION_ID, $current_queue );
		}

		return $updated;
	}

	/**
	 * Toggle critical CSS function.
	 *
	 * @param bool $value Critical CSS to set.
	 */
	public function toggle_critical_css( $value ) {
		$minify_options               = Utils::get_module( 'minify' );
		$options                      = $minify_options->get_options();
		$options['critical_css']      = $value;
		$options['critical_css_mode'] = 'critical_css';

		$minify_options->update_options( $options );

		// Clear queue and generate critical css.
		if ( ! empty( $value ) ) {
			$this->regenerate_critical_css();
		}
	}

	/**
	 * General purpose function. Returns an array hashed.
	 *
	 * @param array|string $list Array of strings or single string.
	 *
	 * @return string
	 */
	private function hash( $list ) {
		return wp_hash( maybe_serialize( $list ) );
	}

	/**
	 * Should Cache Exit.
	 *
	 * @param bool $should_exit Should Cache Exit. Default: false.
	 */
	public function should_cache_exit( $should_exit ) {
		$queue = $this->get_persistent_queue( array( 'pending', 'processing' ) ); // Fetch only Pending queue.

		if ( empty( $queue ) ) {
			return $should_exit;
		}

		$type = $this->get_url_type();

		if ( empty( $type ) ) {
			return $should_exit;
		}

		$all_pages = array();

		foreach ( $queue as $item ) {
			$all_pages[] = $item->type;
		}

		$singular_css_id                 = $this->get_singular_page_id();
		$single_post_critical_css_status = $this->get_single_post_critical_css_status( $singular_css_id );

		if ( in_array( $type, $all_pages, true ) || 'processing' === $single_post_critical_css_status ) {
			return true;
		}

		return $should_exit;
	}

	/**
	 * Determines if we can add critical css.
	 *
	 * @param string $html HTML page buffer.
	 *
	 * @return boolean
	 */
	public function should_add_critical_css( $html ) {
		if ( isset( $_GET['hb_doing_critical'] ) && 1 === absint( $_GET['hb_doing_critical'] ) ) {
			return false;
		}

		if ( is_user_logged_in() ) {
			return false;
		}

		if ( ! apply_filters( 'wphb_should_add_critical_css', true ) ) {
			return false;
		}

		if ( ! $this->is_html( $html ) ) {
			return false;
		}

		if ( ( defined( 'WPHBDONOTOPTIMIZE' ) && WPHBDONOTOPTIMIZE ) || Utils::is_amp() || Utils::wphb_is_page_builder() || is_preview() || is_customize_preview() ) {
			return false;
		}

		if ( Utils::get_module( 'exclusions' )->is_current_page_excluded( 'critical_css' ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Function to apply critical css in buffer.
	 *
	 * @param string $html HTML page buffer.
	 *
	 * @return string
	 */
	public function add_critical_css( $html ) {
		if ( ! $this->should_add_critical_css( $html ) ) {
			return $html;
		}
		// Only known url types.
		$type = $this->get_url_type();
		if ( empty( $type ) || $this->ignore_types( $type ) || $this->skip_page_type( $type ) ) {
			return $html;
		}

		// Check if singular type exists.
		$singular_type = $this->maybe_get_singular_page_type();

		if ( false === $singular_type ) {
			return $html;
		}

		$type = $singular_type ?: $type;

		// Setup file variables.
		$used_css_path   = $this->used_css_path( $this->maybe_get_critical_css_path_for_mobile( $type ) );
		$used_css_exists = file_exists( $used_css_path );

		// If Used CSS File exists.
		if ( $used_css_exists ) {
			$options = Utils::get_module( 'minify' )->get_options();

			// Delay stylesheets.
			if ( empty( $options['critical_css_type'] ) || 'remove' === $options['critical_css_type'] ) {
				if ( 'user_interaction_with_remove' === $options['critical_css_remove_type'] ) {
					$html = $this->load_stylesheet_on_user_interaction( $html );
				} else {
					$html = $this->remove_used_css_from_html( $html );
				}
			} elseif ( 'asynchronously' === $options['critical_css_type'] ) {
				if ( 'load_stylesheet_on_user_interaction' === $options['above_fold_load_stylesheet_method'] ) {
					$html = $this->load_stylesheet_on_user_interaction( $html );
				} else {
					$html = $this->make_css_async( $html );
				}
			}

			// Print used css inline after first title tag.
			$pos = strpos( $html, '</title>' );

			if ( false !== $pos ) {
				// IF critical css is generated.
				$generated_critical = apply_filters( 'wphb_generated_used_css', file_get_contents( $used_css_path ) );
				$used_css_output    = $this->get_used_css_markup( $this->maybe_get_critical_css_path_for_mobile( $type ), $generated_critical );
				$html               = substr_replace( $html, '</title>' . $used_css_output, $pos, 8 );
				$html               = $this->fonts->add_preload_to_fonts_in_used_css( $html, $generated_critical );
			}
		}

		// If generated critical css is not available, generate.
		if ( ! $used_css_exists ) {
			$req_url  = is_single() ? get_permalink() : home_url( $_SERVER['REQUEST_URI'] );
			$singular = '';

			// For singular pages.
			if ( 'frontpage' === $type || strpos( $type, 'page-' ) !== false ) {
				$singular = get_the_ID();
			}

			$this->add_item( $req_url, $type, $singular );

			// If new Item is pushed.
			if ( $this->persist_queue_to_db() ) {
				// Fire the cron.
				$this->schedule_cron();
			}
		}

		return $html;
	}

	/**
	 * Get all stylesheets in the HTML.
	 *
	 * @param string $html HTML code.
	 *
	 * @return array
	 */
	public function get_stylesheets( $html ) {
		/**
		 * Filters the pattern used to get all stylesheets in the HTML.
		 *
		 * @since 3.6.0
		 */
		$stylesheet_pattern = apply_filters(
			'wphb_css_stylesheet_pattern',
			'/(?=<link[^>]*\s(rel\s*=\s*[\'"]stylesheet[\'"]))(?:<link[^>]*\shref\s*=\s*[\'"]([^\'"]+)[\'"](.*)>)/iU'
		);

		preg_match_all( $stylesheet_pattern, $html, $stylesheets, PREG_SET_ORDER );

		return $stylesheets;
	}

	/**
	 * Load stylesheet on user interaction.
	 *
	 * @param string $html HTML code.
	 *
	 * @return string
	 */
	public function load_stylesheet_on_user_interaction( $html ) {
		$stylesheets = $this->get_stylesheets( $html );

		if ( ! empty( $stylesheets ) ) {
			foreach ( $stylesheets as $stylesheet ) {
				if ( ! apply_filters( 'wphb_should_load_stylesheet_on_user_interaction', true, $stylesheet ) ) {
					continue;
				}

				$stylesheet_attr = $stylesheet[0];

				if ( $this->is_stylesheet_excluded( $stylesheet_attr ) ) {
					continue;
				}

				$style_href = trim( $stylesheet[2] );
				$new_link   = preg_replace( '#href=([\'"]).+?\1#', 'data-wphbdelayedstyle="' . $style_href . '"', $stylesheet_attr );
				$html       = str_replace( $stylesheet_attr, $new_link, $html );
			}

			$script = '<script type="text/javascript" id="wphb-delayed-styles-js">
			(function () {
				const events = ["keydown", "mousemove", "wheel", "touchmove", "touchstart", "touchend"];
				function wphb_load_delayed_stylesheets() {
					document.querySelectorAll("link[data-wphbdelayedstyle]").forEach(function (element) {
						element.setAttribute("href", element.getAttribute("data-wphbdelayedstyle"));
					}),
						 events.forEach(function (event) {
						  window.removeEventListener(event, wphb_load_delayed_stylesheets, { passive: true });
						});
				}
			   events.forEach(function (event) {
				window.addEventListener(event, wphb_load_delayed_stylesheets, { passive: true });
			  });
			})();
		</script>';

			$html = str_replace( '</body>', $script . '</body>', $html );
		}

		return $html;
	}

	/**
	 * Check if the stylesheet is excluded from critical css.
	 *
	 * @param string $stylesheet_attr Stylesheet attribute.
	 *
	 * @return array
	 */
	public function is_stylesheet_excluded( $stylesheet_attr ) {
		$combined_exclusions = $this->get_combined_exclusions();

		return array_reduce(
			$combined_exclusions,
			fn( $carry, $item ) => $carry || stripos( $stylesheet_attr, $item ) !== false,
			false
		);
	}

	/**
	 * Get all combined exclusions.
	 *
	 * @return array
	 */
	private function get_combined_exclusions() {
		if ( is_null( $this->combined_exclusions ) ) {
			$this->combined_exclusions = array_merge(
				Utils::get_module( 'exclusions' )->get_combined_asset_path_exclusion_list_for_critical_css(),
				$this->excluded_stylesheet_ids
			);
		}

		return $this->combined_exclusions;
	}

	/**
	 * Remove all CSS which was used on the current page.
	 *
	 * @param string $html HTML content.
	 *
	 * @return string
	 */
	public function remove_used_css_from_html( $html ) {
		$stylesheets = $this->get_stylesheets( $html );

		if ( ! empty( $stylesheets ) ) {
			foreach ( $stylesheets as $stylesheet ) {
				$stylesheet_attr = $stylesheet[0];
				if ( $this->is_stylesheet_excluded( $stylesheet_attr ) ) {
					continue;
				}

				$html = str_replace( $stylesheet_attr, '', $html );
			}
		}

		return $html;
	}

	/**
	 * Convert CSS stylesheet to load asynchronously.
	 *
	 * @param string $html HTML code.
	 *
	 * @return string
	 */
	public function make_css_async( $html ) {
		$stylesheets = $this->get_stylesheets( $html );

		if ( ! empty( $stylesheets ) ) {
			$noscripts = '<noscript>';

			foreach ( $stylesheets as $stylesheet ) {
				// Skip stylesheets with media="print".
				if ( stripos( $stylesheet[0], 'media="print"' ) !== false || stripos( $stylesheet[0], "media='print'" ) !== false ) {
					continue;
				}

				// Prepare the replacements.
				$preload        = str_replace( 'stylesheet', 'preload', $stylesheet[1] );
				$onload         = str_replace( $stylesheet[3] . '>', ' as="style" onload="" ' . $stylesheet[3] . '>', $stylesheet[0] );
				$onload         = str_replace( 'onload=""', 'onload="this.onload=null;this.rel=\'stylesheet\'"', $onload );
				$stylesheet_tag = str_replace( $stylesheet[1], $preload, $onload );
				$stylesheet_tag = preg_replace( '/\s*id\s*=\s*[\'"][^\'"]*[\'"]/', '', $stylesheet_tag );

				// Replace the original stylesheet tag in the HTML.
				$html       = str_replace( $stylesheet[0], $stylesheet_tag, $html );
				$noscripts .= $stylesheet[0];
			}

			$noscripts .= '</noscript>';
			$html      = str_replace( '</body>', $noscripts . '</body>', $html );
		}

		return $html;
	}

	/**
	 * Return Markup for used_css into the page.
	 *
	 * @param string $type Current page type.
	 * @param string $used_css_contents Used CSS content.
	 *
	 * @return string
	 */
	public function get_used_css_markup( $type, $used_css_contents ) {
		$manual_critical = Minify::get_css( 'manual-critical' );

		return sprintf( /* translators: %1$s - page type, %2$s - Used css content, %3$s - manual critical css */
			'<style id="wphb-used-css-%1$s">%2$s%3$s</style>',
			$type,
			$used_css_contents,
			$manual_critical
		);
	}

	/**
	 * Get path to store used css.
	 *
	 * @return string
	 */
	public function get_critical_css_path() {

		if ( is_multisite() ) {
			$blog = get_blog_details();

			if ( '/' === $blog->path ) {
				$site = trailingslashit( $blog->domain );
			} else {
				$site = $blog->path;
			}
		} else {
			$http_host = get_option( 'siteurl' );
			if ( ! empty( $http_host ) ) {
				$http_host = preg_replace( '/^https?:\/\/|\/$/', '', $http_host );
			} elseif ( ! empty( $_SERVER['HTTP_HOST'] ) ) {
				$http_host = htmlentities( wp_unslash( $_SERVER['HTTP_HOST'] ) ); // Input var ok.
			}

			$site = $http_host . '/';
		}

		// Remove starting www.
		if ( strpos( $site, 'www.' ) !== false ) {
			$used_css_dir    = WP_CONTENT_DIR . '/wphb-cache/critical-css/' . $site;
			$is_used_css_dir = is_dir( $used_css_dir );

			// If Used CSS directory exists.
			if ( ! $is_used_css_dir ) {
				$site = preg_replace( '/^(www\.)/', '', $site );
			}
		}

		return WP_CONTENT_DIR . '/wphb-cache/critical-css/' . $site;
	}

	/**
	 * Set url to store used css
	 *
	 * @return string
	 */
	public function get_critical_css_url() {
		$site = '';
		if ( is_multisite() ) {
			$blog = get_blog_details();

			if ( '/' === $blog->path ) {
				$site = trailingslashit( $blog->domain );
			} else {
				$site = $blog->path;
			}
		}

		return trailingslashit( content_url() ) . '/wphb-cache/critical-css/' . $site;
	}

	/**
	 * Get path to store used css.
	 *
	 * @param string $type Script or style.
	 *
	 * @return string
	 */
	public function used_css_path( $type ) {
		return $this->get_critical_css_path() . '/' . $type . '-used.css';
	}

	/**
	 * Check if critical css for singular post is exist.
	 *
	 * @return string|boolean
	 */
	private function maybe_get_singular_page_type() {
		// Check if singular type exists.
		$singular_css_id                 = $this->get_singular_page_id();
		$single_post_critical_css_status = $this->get_single_post_critical_css_status( $singular_css_id );

		if ( $single_post_critical_css_status ) {
			// If there was an error in generating singular file or File is being generated.
			if ( 'error' === $single_post_critical_css_status || 'processing' === $single_post_critical_css_status ) {
				return false;
			}

			$singular_type            = $this->get_singular_page_type();
			$singular_used_css_path   = $this->used_css_path( $singular_type );
			$singular_used_css_exists = file_exists( $singular_used_css_path );

			// Update the type.
			if ( $singular_used_css_exists ) {
				return $singular_type;
			}
		}

		return '';
	}

	/**
	 * Get the url type.
	 *
	 * @return string
	 */
	public function get_url_type() {
		global $wp_query;

		$type = '';

		if ( $wp_query->is_page || is_front_page() ) {
			$type = is_front_page() ? 'frontpage' : 'page-' . $wp_query->post->ID;
		} elseif ( $wp_query->is_home ) {
			$type = 'home';
		} elseif ( $wp_query->is_single ) {
			$type = get_post_type() !== false ? get_post_type() : 'single';
		} elseif ( $wp_query->is_category ) {
			$type = 'category';
		} elseif ( $wp_query->is_tax ) {
			$term = get_queried_object();
			$type = $term->taxonomy;
		} elseif ( $wp_query->is_tag ) {
			$type = 'post_tag';
		} elseif ( $wp_query->is_archive ) {
			$type = $wp_query->is_day ? 'day' : ( $wp_query->is_month ? 'month' : ( $wp_query->is_year ? 'year' : ( $wp_query->is_author ? 'author' : 'archive' ) ) );
		}

		return $type;
	}

	/**
	 * Returns the correct type so that we can correctly check whether to exclude it or not.
	 *
	 * @param string $type type.
	 *
	 * @return string
	 */
	public function get_mapped_type_name_for_skipping( $type ) {
		if ( 'post' === $type ) {
			return 'single';
		} elseif ( 'post_tag' === $type ) {
			return 'tag';
		} elseif ( strpos( $type, 'page-' ) !== false ) {
			return 'page';
		} elseif ( in_array( $type, array( 'day', 'month', 'year', 'author' ), true ) ) {
			return 'archive';
		}

		return $type;
	}

	/**
	 * Process the queue.
	 *
	 * @return boolean|void
	 */
	public function generate_critical_for_queue() {
		// Process the queue.
		if ( get_transient( self::TRANSIENT_NAME ) ) {
			// Still processing. Try again.
			if ( ! $this->is_cron_disabled() ) {
				$this->maybe_schedule_generate_critical_cron();
			}

			return;
		}

		$queue = $this->get_persistent_queue( array( 'pending' ) ); // Get only pending queue to proceed.
		if ( empty( $queue ) ) {
			return;
		}

		set_transient( self::TRANSIENT_NAME, true, self::TRANSIENT_EXPIRATION );
		$status = $this->send_generate_critical_api_request();
		delete_transient( self::TRANSIENT_NAME );

		return $status;
	}

	/**
	 * Ping the critical api.
	 */
	public function get_critical_for_queue() {
		// Process the queue.
		if ( get_transient( self::TRANSIENT_NAME ) ) {
			// Still processing. Try again.
			if ( ! $this->is_cron_disabled() ) {
				$this->maybe_schedule_get_critical_cron();
			}

			return;
		}

		$queue = $this->get_persistent_queue( array( 'processing' ) ); // Get only processed queue.

		if ( empty( $queue ) ) {
			return;
		}

		set_transient( self::TRANSIENT_NAME, true, self::TRANSIENT_EXPIRATION );
		$this->fetch_generated_css_from_api();

		$updated_queue = $this->get_persistent_queue( array( 'processing' ) ); // Get only processed queue.

		if ( ! $this->is_cron_disabled() ) {
			if ( ! empty( $updated_queue ) ) {
				// Still needs processing.
				$this->maybe_schedule_get_critical_cron();
			}
		}

		if ( empty( $updated_queue ) ) {
			// Finish processing.
			delete_transient( self::TRANSIENT_NAME );
		}
	}

	/**
	 * Schedule queue process through WP Cron.
	 */
	public function maybe_schedule_generate_critical_cron() {
		if ( ! wp_next_scheduled( 'wphb_cs_process_queue_cron' ) ) {
			wp_schedule_single_event( time(), 'wphb_cs_process_queue_cron' );
		}
	}

	/**
	 * Schedule queue process through WP Cron.
	 */
	public function maybe_schedule_get_critical_cron() {
		if ( ! wp_next_scheduled( 'wphb_cs_ping_queue_cron' ) ) {
			wp_schedule_single_event( time(), 'wphb_cs_ping_queue_cron' );
		}
	}

	/**
	 * Update queue item with respective statuses.
	 *
	 * @param string $hash          Item hash.
	 * @param bool   $status        Process status.
	 * @param int    $id Queue      ID.
	 * @param string $result        Result.
	 * @param string $error_message Error message.
	 * @param string $error_code    Error code.
	 */
	private function update_item_in_persistent_queue( $hash, $status = false, $id = '', $result = '', $error_message = '', $error_code = '' ) {
		$queue = $this->get_persistent_queue();
		if ( ! empty( $queue[ $hash ] ) ) {
			if ( false !== $status ) {
				$queue[ $hash ]->status = $status;
			}

			if ( $id ) {
				$queue[ $hash ]->id = $id;
			}

			$get_date_time = date_i18n( get_option( 'date_format' ) ) . ' @ ' . date_i18n( get_option( 'time_format' ) );

			if ( '' !== $result ) {
				$queue[ $hash ]->result        = $result;
				$queue[ $hash ]->error_message = $error_message;
				$queue[ $hash ]->error_code    = $error_code;
				$queue[ $hash ]->last_updated  = $get_date_time;
			}

			$log_data = array(
				'status'        => $status,
				'result'        => $result,
				'error_message' => $error_message,
				'error_code'    => $error_code,
				'last_updated'  => $get_date_time,
			);

			update_option( 'wphb_critical_css_log', $log_data );
			update_option( self::QUEUE_OPTION_ID, $queue );

			return true;
		}

		return false;
	}

	/**
	 * Get the list of groups that are yet pending to be processed.
	 *
	 * @param array $status An array of status.
	 */
	public function get_persistent_queue( $status = array() ) {
		$wphb_cs_process_queue = $this->get_queue_option();
		if ( empty( $status ) || empty( $wphb_cs_process_queue ) ) {
			return $wphb_cs_process_queue;
		}

		$process_array = array();

		foreach ( $wphb_cs_process_queue as $hash => $item ) {
			if ( isset( $item->status ) && in_array( $item->status, $status, true ) ) {
				$process_array[ $hash ] = $item;
			}
		}

		return $process_array;
	}

	/**
	 * Get the critical queue by hash.
	 *
	 * @param bool $hash Hash.
	 */
	public function get_queue_item_by_hash( $hash ) {
		$wphb_cs_process_queue = $this->get_queue_option();
		if ( false === $hash || empty( $wphb_cs_process_queue ) || empty( $wphb_cs_process_queue[ $hash ] ) ) {
			return false;
		}

		return $wphb_cs_process_queue[ $hash ];
	}

	/**
	 * Fetch the first id.
	 */
	public function fetch_id_from_processing_queue() {
		$wphb_cs_process_queue = $this->get_queue_option();
		if ( empty( $wphb_cs_process_queue ) ) {
			return false;
		}

		$id = false;

		foreach ( $wphb_cs_process_queue as $hash => $item ) {
			if ( isset( $item->status ) && 'processing' === $item->status ) {
				$id = $item->id;
				break;
			}
		}

		return $id;
	}

	/**
	 * Fetch all the queue by id.
	 *
	 * @param int $id ID.
	 */
	public function get_queue_items_by_job_id( $id ) {
		$process_array         = array();
		$wphb_cs_process_queue = $this->get_queue_option();

		if ( empty( $wphb_cs_process_queue ) && false === $id ) {
			return $process_array;
		}

		foreach ( $wphb_cs_process_queue as $hash => $item ) {
			if ( isset( $item->id ) && $item->id === $id ) {
				$process_array[ $hash ] = $item;
			}
		}

		return $process_array;
	}

	/**
	 * Deletes the persistent queue completely
	 */
	public function delete_pending_persistent_queue() {
		delete_option( self::QUEUE_OPTION_ID );
		wp_cache_delete( self::QUEUE_OPTION_ID, 'options' );
	}

	/**
	 * Clear pending queue.
	 */
	public function clear_pending_process_queue() {
		$this->delete_pending_persistent_queue();
		delete_transient( self::TRANSIENT_NAME );

		// Clear cron events.
		if ( wp_next_scheduled( 'wphb_cs_process_queue_cron' ) ) {
			wp_clear_scheduled_hook( 'wphb_cs_process_queue_cron' );
		}
		// Clear cron events.
		if ( wp_next_scheduled( 'wphb_cs_ping_queue_cron' ) ) {
			wp_clear_scheduled_hook( 'wphb_cs_ping_queue_cron' );
		}
	}

	/**
	 * CLear critical css data and files.
	 */
	public function regenerate_critical_css() {
		if ( ! $this->is_active() ) {
			return;
		}

		// Clear cache before generating the critical css.
		Utils::get_module( 'page_cache' )->clear_cache();

		$this->clear_pending_process_queue();
		$this->delete_all_post_meta_for_critical();
		$this->delete_css_cache_files();

		$this->set_items();
		$this->persist_queue_to_db();
		$this->generate_critical_for_queue();
	}

	/**
	 * Trigger the action to process the ping queue.
	 */
	public function schedule_get_critical_cron() {
		$queue_processing = $this->get_persistent_queue( array( 'processing' ) ); // Get all the pending Queue.

		if ( ! empty( $queue_processing ) ) {
			if ( $this->is_cron_disabled() ) {
				$this->get_critical_for_queue();
			} else {
				$this->maybe_schedule_get_critical_cron();
			}
		}
	}

	/**
	 * Schedule the first cron if we need to process the queue for generation or schedule the second cron if we need to fetch the generated critical css.
	 */
	public function schedule_cron() {
		$this->schedule_generate_critical_cron();
		$this->schedule_get_critical_cron();
	}

	/**
	 * Process the critical css.
	 *
	 * @return boolean|void
	 */
	public function send_generate_critical_api_request() {
		$queue = $this->get_persistent_queue( array( 'pending' ) ); // Get only pending queue to proceed.

		if ( empty( $queue ) ) {
			$this->log( 'Critical CSS API: No pending queue items found' );
			return;
		}

		$urls    = null;
		$api     = Utils::get_api();
		$options = Utils::get_module( 'minify' )->get_options();

		if ( empty( $options['critical_css_type'] ) || 'remove' === $options['critical_css_type'] ) {
			$api_call_type = 'PURGE';
		} else {
			$api_call_type = 'CRITICAL';
		}

		foreach ( $queue as $hash => $item ) {
			$urls[ $hash ] = $this->get_item_data_for_critical_calculate_api( $item );

			if ( $this->is_mobile_critical_css_allowed() ) {
				$urls[ $hash . '-mobile' ] = $this->get_item_data_for_critical_calculate_api( $item, true );
			}
		}

		$this->log( 'Critical CSS API: Prepared ' . count( $urls ) . ' URLs for processing (including mobile variants)' );

		$ignored_files = Utils::get_module( 'exclusions' )->get_ignored_files_for_critical_api();
		$response      = $api->critical_css->generate_critical_css( $urls, $api_call_type, $ignored_files );
		$is_type_error = true;

		if ( ! is_wp_error( $response ) && ! empty( $response ) ) {
			$response = json_decode( wp_remote_retrieve_body( (array) $response ) );
			// Fetch the id.
			$success = ! empty( $response ) && 1 === absint( $response->success );

			if ( $success ) {
				// Fetch the id.
				$response_id = ! empty( $response->id ) ? $response->id : false;

				$this->log( 'Critical CSS API: Received successful response with ID: ' . $response_id );

				// Update Queue.
				$is_type_error = false;

				// If id is received from the response, update in the queue.
				if ( $response_id ) {
					foreach ( $urls as $hash => $url ) {
						$this->update_item_in_persistent_queue( $hash, 'processing', $response_id );
					}
				}
			}
		}

		// Log the error.
		if ( $is_type_error ) {
			$api_error = is_wp_error( $response ) ? $response->get_error_message() : esc_html__( 'HB critical unknown error', 'wphb' );
			$this->log( 'Critical CSS API: Request failed with error: ' . $api_error );

			// Update the pending queues.
			foreach ( $urls as $hash => $url ) {
				$this->update_item_in_persistent_queue( $hash, 'complete', '', 'ERROR', $api_error );
				$hash_object = $this->get_queue_item_by_hash( $hash );
				$singular    = ! empty( $hash_object->singular ) ? $hash_object->singular : '';

				if ( ! empty( $singular ) ) {
					$this->delete_post_meta_for_single_post( $singular );
					update_post_meta( $singular, self::CRITICAL_POST_META_KEY, 'error' );
				}
			}
		}

		return ! $is_type_error;
	}

	/**
	 * Ping the critical api to fetch the css.
	 *
	 * @return boolean|void
	 */
	public function fetch_generated_css_from_api() {
		$fs = Filesystem::instance();
		$id = $this->fetch_id_from_processing_queue();

		if ( ! $id ) {
			$this->log( 'Critical CSS Fetch: No processing ID found in queue' );
			return;
		}

		$this->log( "Critical CSS Fetch: Fetching generated CSS for job ID: $id" );

		$api      = Utils::get_api();
		$response = $api->critical_css->get_generated_critical_css( $id );

		if ( ! is_wp_error( $response ) ) {
			$response      = json_decode( wp_remote_retrieve_body( (array) $response ) );
			$status        = isset( $response->status ) ? $response->status : '';
			$urls          = isset( $response->items ) ? $response->items : '';
			$error_message = ! empty( $response->errorMessage ) ? $response->errorMessage : '';
			$error_code    = ! empty( $response->errorCode ) ? $response->errorCode : '';

			$this->log( "Critical CSS Fetch: API response status: $status" );

			if ( 'COMPLETE' === $status ) {
				$this->log( 'Critical CSS Fetch: Processing completed CSS files' );
				$critical_css = isset( $response->criticalCss ) ? $response->criticalCss : array();
				foreach ( $critical_css as $hash => $css_value ) {
					$hash_object = $this->get_queue_item_by_hash( $hash );
					if ( ! empty( $css_value ) && ! empty( $hash_object ) ) {
						$type     = ! empty( $hash_object->type ) ? $hash_object->type : '';
						$singular = ! empty( $hash_object->singular ) ? $hash_object->singular : '';

						$used_css_path = $this->used_css_path( $type );
						// Create the css file.
						$status_file = $fs->write( $used_css_path, apply_filters( 'wphb_used_css', $css_value ) );

						// Create the css file for mobile.
						if ( 'asynchronously' === $this->get_critical_css_type() ) {
							$mobile_css_key = $hash . '-mobile';
							$mobile_css     = isset( $critical_css->$mobile_css_key ) ? $critical_css->$mobile_css_key : '';
							if ( ! empty( $mobile_css ) ) {
								$used_css_path_mobile = $this->used_css_path( $type . '-mobile' );
								$fs->write( $used_css_path_mobile, apply_filters( 'wphb_mobile_used_css', $mobile_css ) );
							}
						}

						// If singular page.
						if ( ! empty( $singular ) ) {
							update_post_meta( $singular, self::CRITICAL_POST_META_KEY, 'complete' );
						}
					}

					$this->update_item_in_persistent_queue( $hash, 'complete', '', $status, $error_message, $error_code );
				}
			} elseif ( 'ERROR' === $status && ! empty( $urls ) ) {
				$this->log( "Critical CSS Fetch: API returned error status with message: $error_message" );
				foreach ( $urls as $hash => $url_value ) {
					$hash_object = $this->get_queue_item_by_hash( $hash );

					if ( ! empty( $hash_object ) ) {
						$singular = ! empty( $hash_object->singular ) ? $hash_object->singular : '';
						// If singular page.
						if ( ! empty( $singular ) ) {
							$this->delete_post_meta_for_single_post( $singular );
							update_post_meta( $singular, self::CRITICAL_POST_META_KEY, 'error' );
						}
					}

					$this->update_item_in_persistent_queue( $hash, 'complete', '', $status, $error_message, $error_code );
				}
			} elseif ( 'ERROR' === $status ) {
				$this->log( "Critical CSS Fetch: General API error - $error_message" );
				$this->update_item_for_general_api_error( $id, $error_message, $error_code );
			}
		} else {
			// If any API error occurs.
			$api_error = $response->get_error_message();
			$this->log( "Critical CSS Fetch: API request error - $api_error" );
			$this->update_item_for_general_api_error( $id, $api_error, $api_error );
		}
	}

	/**
	 * Function to update the item if there is a general API error, e.g. job not found.
	 *
	 * @param int    $id            Job ID.
	 * @param string $error_message Error message.
	 * @param string $error_code    Error code.
	 */
	public function update_item_for_general_api_error( $id, $error_message, $error_code ) {
		$queue_by_ids = $this->get_queue_items_by_job_id( $id );

		foreach ( $queue_by_ids as $hash => $id_value ) {
			$singular = ! empty( $id_value->singular ) ? $id_value->singular : '';
			// If singular page.
			if ( ! empty( $singular ) ) {
				$this->delete_post_meta_for_single_post( $singular );
				update_post_meta( $singular, self::CRITICAL_POST_META_KEY, 'error' );
			}

			$this->update_item_in_persistent_queue( $hash, 'complete', '', 'ERROR', $error_message, $error_code );
		}
	}

	/**
	 * Function to delete all the critical css files.
	 */
	public function delete_css_cache_files() {
		global $wphb_fs;
		if ( ! $wphb_fs ) {
			$wphb_fs = Filesystem::instance();
		}

		$directory = 'critical-css';

		$is_network_admin = false;
		if ( is_multisite() && isset( $_SERVER['HTTP_REFERER'] ) ) {
			$is_network_admin = preg_match( '#^' . network_admin_url() . '#i', $_SERVER['HTTP_REFERER'] );
		}

		// For multisite we need to set this to null.
		if ( is_multisite() && ! $is_network_admin ) {
			$current_blog = get_site( get_current_blog_id() );
			$directory    .= $current_blog->path;
		}

		return $wphb_fs->purge( $directory );
	}

	/**
	 * Function to get the singular css.
	 *
	 * @param int $post_id Post ID.
	 */
	public function get_single_post_critical_css_status( $post_id ) {
		return get_post_meta( $post_id, self::CRITICAL_POST_META_KEY, true );
	}

	/**
	 * Function to create critical css file for post.
	 *
	 * @param int $id Post ID.
	 */
	public function create_post_css_file( $id ) {
		$type    = $this->make_post_type_key( $id );
		$req_url = get_permalink( $id );
		update_post_meta( $id, self::CRITICAL_POST_META_KEY, '' );

		$this->add_item( $req_url, $type, $id );
		// If new Item is pushed.
		if ( $this->persist_queue_to_db( true ) ) {
			// Delete Existing CSS File.
			$this->unlink_generated_critical_css_file( $type, $id );
			update_post_meta( $id, self::CRITICAL_POST_META_KEY, 'processing' );
			// Fire the cron.
			$this->generate_critical_for_queue();
		}

		return true;
	}

	/**
	 * Function to create critical css file for post.
	 *
	 * @param int $id Post ID.
	 */
	public function recreate_post_css_file( $id ) {
		return $this->create_post_css_file( $id );
	}

	/**
	 * Delete a generated critical css file.
	 *
	 * @param string $type Post type.
	 * @param int    $post_id Post ID.
	 */
	public function unlink_generated_critical_css_file( $type, $post_id ) {
		$css_paths = array(
			$this->used_css_path( $type ),
			$this->used_css_path( $type . '-mobile' ),
		);

		$clear_cache = false;

		foreach ( $css_paths as $css_path ) {
			if ( $this->is_hb_critical_css_path( $css_path ) && file_exists( $css_path ) ) {
				unlink( $css_path );
				$clear_cache = true;
			}
		}

		if ( $clear_cache && $post_id ) {
			do_action( 'wphb_clear_page_cache', $post_id ); // Clear page cache for the supplied post.
		}
	}

	/**
	 * Check if the path is a critical css path.
	 *
	 * @param string $path Path.
	 *
	 * @return bool
	 */
	public function is_hb_critical_css_path( $path ) {
		return false !== strpos( $path, 'wphb-cache' ) && false !== strpos( $path, 'critical-css' );
	}

	/**
	 * Function to revert critical css file for post.
	 *
	 * @param int $id Post ID.
	 */
	public function revert_post_css_file( $id ) {
		// Delete Existing CSS FIle.
		$type = $this->make_post_type_key( $id );
		$this->unlink_generated_critical_css_file( $type, $id );

		$this->delete_post_meta_for_single_post( $id );

		return true;
	}

	/**
	 * Delete post meta key for single post.
	 *
	 * @param int $id Post ID.
	 */
	public function delete_post_meta_for_single_post( $id ) {
		delete_post_meta( $id, self::CRITICAL_POST_META_KEY );

		return true;
	}

	/**
	 * Deletes all the post meta critical key.
	 */
	public function delete_all_post_meta_for_critical() {
		global $wpdb;
		$post_table = $wpdb->prefix . 'postmeta';

		$wpdb->delete( $post_table, array( 'meta_key' => self::CRITICAL_POST_META_KEY ) );
	}

	/**
	 * Skip page type selected in settings.
	 *
	 * @since   3.6.0
	 * @access  public
	 * @param bool $type Post Type.
	 *
	 * @return bool
	 */
	public function skip_page_type( $type ) {
		if ( apply_filters( 'wphb_should_skip_page_type', false, $type ) ) {
			return true;
		}

		$minify_options                     = Settings::get_settings( 'minify' );
		$critical_page_types                = $minify_options['critical_page_types'];
		$critical_skipped_custom_post_types = $minify_options['critical_skipped_custom_post_types'];
		$all_pages_type                     = Page_Cache::get_page_types( true );

		if ( $type ) {
			$type = $this->get_mapped_type_name_for_skipping( $type );
			if ( ! empty( $critical_skipped_custom_post_types ) && in_array( $type, $critical_skipped_custom_post_types, true ) ) {
				return true;
			}

			if ( in_array( $type, $all_pages_type, true ) && ! in_array( $type, $critical_page_types, true ) ) {
				return true;
			}

			if ( taxonomy_exists( $type ) && ! in_array( 'category', $critical_page_types, true ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Fetch the queue by type.
	 *
	 * @param int $type Type.
	 */
	public function get_queue_item_by_type( $type ) {
		if ( empty( $type ) ) {
			return false;
		}

		$get_hash = $this->hash( $type );

		return $this->get_queue_item_by_hash( $get_hash );
	}

	/**
	 * Gets all public post types.
	 */
	private function get_public_post_types() {
		global $wpdb;

		$post_types = get_post_types(
			array(
				'public'             => true,
				'publicly_queryable' => true,
			)
		);

		$excluded_post_types = $this->get_excluded_post_types();

		// Remove excluded post types from the retrieved ones.
		$post_types = array_diff( $post_types, $excluded_post_types );

		// Sanitize the post types for safe SQL use.
		$post_types = esc_sql( $post_types );
		$post_types = "'" . implode( "','", $post_types ) . "'";

		// Retrieve the most recent post ID per post type.
		$query = "SELECT MAX(ID) as ID, post_type
			FROM (
				SELECT ID, post_type
				FROM $wpdb->posts
				WHERE post_type IN ( $post_types )
				AND post_status = 'publish'
				ORDER BY post_date DESC
			) AS posts
			GROUP BY post_type";

		$results = $wpdb->get_results( $query );

		// Return results or an empty array if there's an error.
		return ! is_wp_error( $results ) ? $results : array();
	}

	/**
	 * Gets all public post types that needs to be excluded.
	 */
	public function get_excluded_post_types() {
		/**
		 * List of post types to be excluded.
		 */
		$excluded_post_types = array(
			'blocks',
			'cms_block',
			'elementor_library',
			'fl-builder-template',
			'fusion_template',
			'jet-woo-builder',
			'karma-slider',
			'oceanwp_library',
			'slider',
			'tbuilder_layout',
			'tbuilder_layout_part',
			'tt-gallery',
			'web-story',
			'xlwcty_thankyou',
		);

		/**
		 * Filter the excluded post types.
		 *
		 * @param array $excluded_post_types Excluded post types.
		 */
		return (array) apply_filters( 'wphb_css_excluded_post_types', $excluded_post_types );
	}

	/**
	 * Gets all public taxonomies.
	 */
	private function get_public_taxonomies() {
		global $wpdb;

		$available_taxonomies = get_taxonomies(
			array(
				'public'             => true,
				'publicly_queryable' => true,
			)
		);

		$excluded_taxonomies = $this->get_excluded_taxonomies();

		// Remove excluded taxonomies from the available list.
		$filtered_taxonomies = array_diff( $available_taxonomies, $excluded_taxonomies );

		// Sanitize and format the remaining taxonomies for SQL query.
		$sanitized_taxonomies = esc_sql( $filtered_taxonomies );
		$taxonomy_list        = "'" . implode( "','", $sanitized_taxonomies ) . "'";

		// Build the query to fetch the term with the highest term_id per taxonomy.
		$query = "SELECT MAX( term_id ) AS ID, taxonomy
			FROM (
				SELECT term_id, taxonomy
				FROM $wpdb->term_taxonomy
				WHERE taxonomy IN ( $taxonomy_list )
				AND count > 0
			) AS taxonomies
			GROUP BY taxonomy";

		// Execute the query and return the results.
		$results = $wpdb->get_results( $query );
		$results = ! is_wp_error( $results ) ? $results : array();

		return apply_filters( 'wphb_public_taxonomies_results', $results );
	}

	/**
	 * Gets all public taxonomies that needs to be excluded.
	 */
	public function get_excluded_taxonomies() {
		$excluded_taxonomies = array(
			'attachment_category',
			'coupon_campaign',
			'element_category',
			'karma-slider-category',
			'mediamatic_wpfolder',
			'post_format',
			'product_shipping_class',
			'truethemes-gallery-category',
		);

		/**
		 * Filter the excluded taxonomies.
		 *
		 * @param array $excluded_taxonomies Excluded taxonomies.
		 */
		return (array) apply_filters( 'wphb_css_excluded_taxonomies', $excluded_taxonomies );
	}

	/**
	 * Get the page id.
	 *
	 * @return string
	 */
	public function get_singular_page_type() {
		global $wp_query;
		$page_type = false;
		if ( $wp_query->is_page ) {
			$page_type = 'page-' . $wp_query->post->ID;
		} elseif ( $wp_query->is_single ) {
			$page_type = $this->make_post_type_key( $wp_query->post->ID );
		}

		return $page_type;
	}

	/**
	 * Get the page id.
	 *
	 * @return string
	 */
	private function get_singular_page_id() {
		global $wp_query;
		$page_id = false;

		if ( $wp_query->is_page || $wp_query->is_single ) {
			$page_id = $wp_query->post->ID;
		}

		return $page_id;
	}

	/**
	 * Function to ignore types.
	 *
	 * @param string $type Page type.
	 */
	public function ignore_types( $type ) {
		$ignore_types = array( '404' );

		if ( in_array( $type, $ignore_types, true ) ) {
			return true;
		}

		return false;
	}

	/**
	 * This notice is displayed when the critical CSS generation is complete.
	 *
	 * @since 3.6.0
	 */
	public function critical_css_generation_complete_notice() {
		$wphb_cs_process_queue          = $this->get_queue_option();
		$wphb_cs_created_css_log_update = array();
		$update_css_log                 = false;
		$message                        = '';
		$group_id                       = null;

		if ( $wphb_cs_process_queue && is_array( $wphb_cs_process_queue ) ) {
			foreach ( $wphb_cs_process_queue as $hash => $queue_array ) {
				$keep_queue_key = true;

				if ( 'complete' === $queue_array->status && 0 === $queue_array->display_notice ) {
					if ( 'COMPLETE' !== $queue_array->result ) {

						if ( $group_id !== $queue_array->id ) {
							$group_id = $queue_array->id;
							$error    = ! empty( $queue_array->error_message ) ? __( '<em> Error: ', 'wphb' ) . $queue_array->error_message . ' </em>' : $queue_array->result;
							$message .= sprintf(
							// translators: %1$s = error message.
								__( '<br />%1$s', 'wphb' ),
								'<strong>' . $error . '</strong>'
							);
						}
					} else {
						$keep_queue_key = false;
					}

					$update_css_log = true;
				}

				if ( $keep_queue_key ) {
					$wphb_cs_created_css_log_update[ $hash ] = $queue_array;
				}
			}

			if ( $update_css_log ) {
				update_option( self::QUEUE_OPTION_ID, $wphb_cs_created_css_log_update );
				if ( Utils::is_maintenance_mode() ) {
					$message = sprintf(
					// translators: %1$s = error message.
						__( '<br />%1$s', 'wphb' ),
						'<strong>' . __( 'Critical CSS doesn’t run while your site is in maintenance mode, so a 503 error is expected. It will work normally again once maintenance mode is disabled.', 'wphb' ) . '</strong>'
					);
				}

				// If all the statuses are completed.
				if ( ! empty( $message ) ) {
					$message = sprintf(
					// translators: %1$s = message.
						__( 'Critical CSS generation failed. Please review the below error to troubleshoot.%1$s', 'wphb' ),
						$message
					);
				}
			}
		}

		return $message;
	}

	/**
	 * Adds load css script.
	 */
	public function insert_load_css_script() {
		$options  = Utils::get_module( 'minify' )->get_options();
		$is_async = 'asynchronously' === $options['critical_css_type'] || ( 'remove' === $options['critical_css_type'] && 'async_with_remove' === $options['critical_css_remove_type'] );

		if ( ! $is_async ) {
			return;
		}

		// Don't load on search page.
		if ( is_search() ) {
			return;
		}

		// Don't load on 404 page.
		if ( is_404() ) {
			return;
		}

		echo '<script>
		/*! loadCSS rel=preload polyfill. [c]2017 Filament Group, Inc. MIT License */
		(function(w){"use strict";if(!w.loadCSS){w.loadCSS=function(){}}
		var rp=loadCSS.relpreload={};rp.support=(function(){var ret;try{ret=w.document.createElement("link").relList.supports("preload")}catch(e){ret=!1}
		return function(){return ret}})();rp.bindMediaToggle=function(link){var finalMedia=link.media||"all";function enableStylesheet(){link.media=finalMedia}
		if(link.addEventListener){link.addEventListener("load",enableStylesheet)}else if(link.attachEvent){link.attachEvent("onload",enableStylesheet)}
		setTimeout(function(){link.rel="stylesheet";link.media="only x"});setTimeout(enableStylesheet,3000)};rp.poly=function(){if(rp.support()){return}
		var links=w.document.getElementsByTagName("link");for(var i=0;i<links.length;i++){var link=links[i];if(link.rel==="preload"&&link.getAttribute("as")==="style"&&!link.getAttribute("data-loadcss")){link.setAttribute("data-loadcss",!0);rp.bindMediaToggle(link)}}};if(!rp.support()){rp.poly();var run=w.setInterval(rp.poly,500);if(w.addEventListener){w.addEventListener("load",function(){rp.poly();w.clearInterval(run)})}else if(w.attachEvent){w.attachEvent("onload",function(){rp.poly();w.clearInterval(run)})}}
		if(typeof exports!=="undefined"){exports.loadCSS=loadCSS}
		else{w.loadCSS=loadCSS}}(typeof global!=="undefined"?global:this))
		</script>';
	}

	/**
	 * This notice is displayed when the critical CSS generation is complete.
	 *
	 * @since 3.6.0
	 */
	public function critical_css_status_for_queue() {
		if ( ! $this->is_active() ) {
			return false;
		}

		$critical_css_log = $this->get_log_option();

		if ( empty( $critical_css_log ) ) {
			return false;
		}

		return $critical_css_log;
	}

	/**
	 * Get html data for status tag.
	 *
	 * @return bool|string
	 * @since 3.6.0
	 */
	public function get_html_for_status_tag() {
		if ( ! Utils::is_member() || ! $this->is_active() ) {
			return '<span id="critical_progress_tag"></span>';
		}

		$critical_css_log = $this->get_log_option();

		$status = isset( $critical_css_log['status'] ) ? $critical_css_log['status'] : '';
		if ( $this->is_active() && ( 'processing' === $status || 'pending' === $status ) ) {
			$tag_display_value = esc_html__( 'Optimizing ', 'wphb' );
			$sui_tag           = 'sui-tag sui-tag-blue sui-tooltip sui-tooltip-constrained';
			$sui_icon          = 'sui-icon-loader sui-loading';
			$tooltip_text      = esc_html__( 'Generating Critical CSS. This could take a few minutes depending on the number of site assets that need to be checked.', 'wphb' );
		} elseif ( $this->is_active() && 'complete' === $status ) {
			$is_result_complete = 'COMPLETE' === $critical_css_log['result'];
			$tag_display_value  = $is_result_complete ? esc_html__( 'Optimized', 'wphb' ) : esc_html__( 'Error', 'wphb' );
			$sui_tag            = $is_result_complete ? 'sui-tag sui-tag-green sui-tooltip sui-tooltip-constrained' : 'sui-tag sui-tag-yellow';
			$sui_icon           = $is_result_complete ? 'sui-icon-info' : 'sui-icon-info sui-icon-error';
			$tooltip_text       = $is_result_complete ? esc_html__( 'Last Generated: ', 'wphb' ) . $critical_css_log['last_updated'] : '';
		} else {
			$tag_display_value = esc_html__( 'Unoptimized', 'wphb' );
			$sui_tag           = 'sui-tag sui-tag-grey sui-tooltip sui-tooltip-constrained';
			$sui_icon          = 'sui-icon-info';
			$tooltip_text      = esc_html__( 'Select settings and saves changes to generate Critical CSS', 'wphb' );
		}

		return sprintf(
		// translators: %1$s = tooltip text, %2$s = sui tag, %3$s = info icon, %4$s = text for tag.
			__( '<span id="critical_progress_tag" data-tooltip="%1$s" class="wphb_progress_tag %2$s"><span class="%3$s" aria-hidden="true"></span>%4$s</span>', 'wphb' ),
			$tooltip_text,
			$sui_tag,
			$sui_icon,
			$tag_display_value
		);
	}

	/**
	 * Get processing queue data.
	 */
	private function get_queue_option() {
		return get_option( self::QUEUE_OPTION_ID, array() );
	}

	/**
	 * Get status tag data.
	 */
	private function get_log_option() {
		return get_option( 'wphb_critical_css_log', array() );
	}

	/**
	 * Get key for single post type.
	 *
	 * @param int $id Post ID.
	 */
	private function make_post_type_key( $id ) {
		return get_post_type( $id ) . '-' . $id;
	}

	/**
	 * Checks if cron is disabled or not. Returns true if disabled false otherwise.
	 */
	private function is_cron_disabled() {
		return defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON;
	}

	/**
	 * Schedule the cron to fetch the generated critical css.
	 *
	 * @return void
	 */
	private function schedule_generate_critical_cron() {
		$queue_pending = $this->get_persistent_queue( array( 'pending' ) ); // Get all the pending Queue.
		if ( ! empty( $queue_pending ) ) {
			if ( $this->is_cron_disabled() ) {
				$this->generate_critical_for_queue();
			} else {
				$this->maybe_schedule_generate_critical_cron();
			}
		}
	}

	/**
	 * Returns the formatted error message for MP.
	 *
	 * @param array $item_detail Item detail.
	 */
	public function get_error_code_from_log( $item_detail = array() ) {
		$critical_css_log = ! empty( $item_detail ) ? $item_detail : $this->get_log_option();
		$error_code       = '';

		if ( isset( $critical_css_log['result'] ) && 'ERROR' === $critical_css_log['result'] ) {
			$error_code = isset( $critical_css_log['error_code'] ) ? $critical_css_log['error_code'] : 'unknown';
		}

		return $error_code;
	}

	/**
	 * Get the critical css type.
	 */
	public function get_critical_css_type() {
		$options = Utils::get_module( 'minify' )->get_options();

		return ! empty( $options['critical_css_type'] ) ? $options['critical_css_type'] : 'remove';
	}

	/**
	 * Check if critical css for mobile is exist for current page.
	 *
	 * @param string $type Type.
	 */
	public function maybe_get_critical_css_path_for_mobile( $type ) {
		if ( ! $this->is_mobile_critical_css_allowed() ) {
			return $type;
		}

		// Check if the request is coming from a mobile device.
		if ( Page_Cache::is_mobile_agent() ) {
			$file_name_for_mobile = $type . '-mobile';
			$critical_css_path    = $this->used_css_path( $file_name_for_mobile );

			// If a mobile-specific CSS file exists, just return the name.
			$critical_css_content = file_exists( $critical_css_path );
			if ( $critical_css_content ) {
				return $file_name_for_mobile;
			}
		}

		return $type;
	}

	/**
	 * Check if mobile critical CSS is allowed.
	 *
	 * @return bool
	 */
	public function is_mobile_critical_css_allowed() {
		return 'asynchronously' === $this->get_critical_css_type();
	}

	/**
	 * Get item for critical calculate api.
	 *
	 * @param object $item      Item.
	 * @param bool   $is_mobile Is mobile.
	 */
	public function get_item_data_for_critical_calculate_api( $item, $is_mobile = false ) {
		return array(
			'url'    => $item->url,
			'width'  => $is_mobile ? 414 : 1920,
			'height' => $is_mobile ? 915 : 1080,
		);
	}

	/**
	 * Get the critical mode for MP.
	 */
	public function get_critical_mode_for_mp() {
		$options = Utils::get_module( 'minify' )->get_options();

		if ( 'asynchronously' === $options['critical_css_type'] ) {
			return 'load_stylesheet_on_user_interaction' === $options['above_fold_load_stylesheet_method'] ? 'abovefold_delay' : 'abovefold_async';
		}

		return 'remove_unused' === $options['critical_css_remove_type'] ? 'fullpage_remove' : 'fullpage_delay';
	}

	/**
	 * Filter minified resources.
	 *
	 * @param bool   $value   Current value.
	 * @param string $handle  Resource handle.
	 * @param string $type    Script or style.
	 * @param string $url     Script URL.
	 *
	 * @return bool
	 */
	public function filter_resource_minify( $value, $handle, $type, $url ) {
		if ( 'styles' === $type ) {
			$patterns = Utils::get_module( 'exclusions' )->get_combined_asset_path_exclusion_list_for_critical_css();
			if ( empty( $patterns ) ) {
				return $value;
			}

			$combined_pattern = '#(' . implode( '|', array_map( 'preg_quote', $patterns ) ) . ')#i';
			if ( preg_match( $combined_pattern, $url ) ) {
				$this->excluded_stylesheet_ids[] = $handle;
			}
		}

		return $value;
	}

	/**
	 * Checks if a given string contains a complete HTML document.
	 *
	 * @param string $buffer The string content to check.
	 * @return bool Returns `true` if the string contains both `<html>` and `</html>`, otherwise `false`.
	 */
	public function is_html( $buffer ) {
		return stripos( $buffer, '<html' ) !== false && stripos( $buffer, '</html>' ) !== false;
	}
}
