<?php

namespace EasyCriticalCSS;

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

class Critical_CSS_Injector {
	public static function init() {
		add_filter( 'style_loader_tag', [ __CLASS__, 'add_async_styles' ], 9999, 3 );

		add_action( 'wp_head', [ __CLASS__, 'inject_critical_css' ], 1 );
		add_action( 'wp_print_styles', [ __CLASS__, 'handle_styles' ], 9999 );
		add_action( 'wp_footer', [ __CLASS__, 'enqueue_secondary_styles' ] );
	}

	public static function add_async_styles( $tag, $handle, $href ) {
		// Only apply async loading to the secondary CSS.
		if ( 'easy-secondary-css' !== $handle ) {
			return $tag;
		}

		// Add async loading: build the preload tag with onload swap.
		$async_tag = sprintf(
			'<link rel="preload" as="style" href="%s" onload="this.onload=null;this.rel=\'stylesheet\'">',
			esc_url( $href )
		);

		// Fallback for users with JavaScript disabled.
		$async_tag .= "<noscript>$tag</noscript>";

		return $async_tag;
	}

	public static function inject_critical_css() {
		if ( Critical_CSS::should_skip_critical() ) {
			return;
		}

		$identifier   = Critical_CSS::get_page_identifier(); // Get post ID or URL.
		$critical_css = Critical_CSS::get_critical_css( $identifier );

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

		// Adjust output if expired and set to use fallback.
		$status = Critical_CSS_Status::get_status( $identifier );
		if ( in_array( $status, [ 'expired', 'pending' ], true ) && Settings::get_global_expired_css_behavior() === 'use_fallback' ) {
			$critical_css = Settings::get_individual_default_critical_css( $identifier );
		}

		// Add Critical CSS through use of wp_add_inline_style. We register a blank style to make this work. No need for a query param because this is never loaded.
		wp_register_style( 'easy-critical-css', false, [], null ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
		wp_enqueue_style( 'easy-critical-css' );
		wp_add_inline_style( 'easy-critical-css', Helpers::esc_css( $critical_css ) );
	}

	public static function handle_styles() {
		if ( Critical_CSS::should_skip_critical() ) {
			return;
		}

		// Don't proceed if Critical CSS was never enqueued.
		if ( ! wp_style_is( 'easy-critical-css', 'enqueued' ) ) {
			return;
		}

		$identifier     = Critical_CSS::get_page_identifier(); // Get post ID or URL.
		$generated_css  = Critical_CSS::get_generated_css( $identifier );
		$is_manual_mode = Settings::get_individual_mode( $identifier ) === 'manual';
		$url_hash       = $generated_css['url_hash'];
		if ( empty( $url_hash ) ) {
			return;
		}

		// Check for critical CSS.
		$serve_from_files  = Settings::get_global_serve_css_from_files();
		$has_db_critical   = ! empty( $generated_css['critical_css'] );
		$has_file_critical = $serve_from_files && is_readable( trailingslashit( wp_upload_dir()['basedir'] ) . 'easy-critical-css/critical/' . sanitize_file_name( $url_hash ) . '.css' );

		// Check for critical CSS.
		if ( ! $has_db_critical && ! $has_file_critical ) {
			// We only care about the generated to determine secondary output on auto mode.
			if ( ! $is_manual_mode ) {
				return;
			}
			// We care about the Manual CSS settings on manual mode.
			if ( empty( Settings::get_individual_default_critical_css( $url_hash ) ) ) {
				return;
			}
		}

		// Bounce if expired and not set to use expired.
		$status = Critical_CSS_Status::get_status( $url_hash );
		if ( ! $is_manual_mode && in_array( $status, [ 'expired', 'pending' ], true ) && Settings::get_global_expired_css_behavior() !== 'use_expired' ) {
			return;
		}

		// Don't output anything if we are manual and have no secondary.
		// Use Critical_CSS::get_secondary_css so any URL-detection/acknowledge logic applies.
		if ( $is_manual_mode && empty( Critical_CSS::get_secondary_css( $url_hash ) ) ) {
			return;
		}

		global $wp_styles;
		$site_host = wp_parse_url( home_url(), PHP_URL_HOST );

		$excluded_handles = [
			'easy-critical-css-admin',
			'admin-bar',
			'query-monitor',
			'rank-math-analytics-stats',
			'yoast-seo-adminbar',
		];

		/**
		 * Filters the list of excluded CSS handles that should not be dequeued.
		 *
		 * @since 1.0.0
		 *
		 * @param array $safelist List of CSS handles that should not be dequeued.
		 */
		$excluded_handles = apply_filters( 'easy_cc_excluded_handles', $excluded_handles );

		$allowed_cdn_suffixes = [
			'.b-cdn.net', // BunnyCDN
			'.cloudfront.net', // Amazon CloudFront
			'.kxcdn.com', // KeyCDN
			'.pantheonsite.io', // Pantheon
			'.sg-cdn.com', // SiteGround CDN
			'.wpenginepowered.com', // WP Engine CDN
		];

		$inline_styles = [];

		// Dequeue all styles except excluded.
		foreach ( $wp_styles->registered as $handle => $style ) {
			$src = isset( $style->src ) ? $style->src : '';

			// Move on if we aren't enqueued.
			if ( ! in_array( $handle, $wp_styles->queue, true ) ) {
				continue;
			}

			// We've already handled Easy Critical CSS styles, so move on.
			if ( $handle === 'easy-critical-css' ) {
				continue;
			}

			// Capture all inline styles.
			if ( ! empty( $style->extra['after'] ) ) {
				$inline_styles[ $handle ] = implode( "\n", $style->extra['after'] );
			}

			// Only dequeue if not excluded.
			if ( in_array( $handle, $excluded_handles, true ) || Helpers::is_excluded_css_file( $src ) ) {
				continue;
			}

			// Exclude Cross-Domain CSS if setting exists.
			if (
				Settings::get_global_ignore_cross_domain_css()
				&& ( preg_match( '~^https?://~', $src ) || strpos( $src, '//' ) === 0 )
			) {
				$style_host = wp_parse_url( $src, PHP_URL_HOST );
				if ( $style_host && strcasecmp( $style_host, $site_host ) !== 0 ) {
					$allowed_cdn = false;

					// Check if this is an allowed CDN.
					foreach ( $allowed_cdn_suffixes as $suffix ) {
						if ( str_ends_with( $style_host, $suffix ) ) {
							$allowed_cdn = true;
							break;
						}
					}

					// If it's not from an allowed CDN, it's likely coming from fonts.googleapis.com (or any other 3rd-party), so leave it alone.
					if ( ! $allowed_cdn ) {
						continue;
					}
				}
			}

			wp_dequeue_style( $handle );
		}

		// Timestamp for cache busting
		$timestamp = ! empty( $generated_css['generated_time'] ) ? gmdate( 'YmdHis', strtotime( $generated_css['generated_time'] ) ) : time();
		if ( $is_manual_mode ) {
			$timestamp = ! empty( $generated_css['updated_time'] ) ? strtotime( $generated_css['updated_time'] ) : time();
		}

		// Now enqueue secondary/remaining CSS.
		$type    = Settings::get_individual_secondary_behavior( $url_hash );
		$css_url = home_url( "/easy-$type-css/$url_hash-$timestamp.css" );

		// Enqueue proper CSS URL based on setting.
		if ( ! $is_manual_mode && Settings::get_global_serve_css_from_files() ) {
			$css_url = content_url( 'uploads/easy-critical-css' ) . "/$type/$url_hash-$timestamp.css";

		}

		// Timestamps are added to the URLs, so there's no need to add a query param
		wp_register_style( 'easy-secondary-css', esc_url( $css_url ), [ 'easy-critical-css' ], null ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion

		// Attach inline styles to the secondary CSS
		if ( ! empty( $inline_styles ) ) {
			$merged_styles = implode( "\n", $inline_styles );
			wp_add_inline_style( 'easy-secondary-css', Helpers::esc_css( $merged_styles ) );
		}
	}

	public static function enqueue_secondary_styles() {
		if ( ! wp_style_is( 'easy-secondary-css', 'registered' ) ) {
			return;
		}

		wp_enqueue_style( 'easy-secondary-css' );
	}
}
