<?php
/**
 * Plugin Name:       BaseCloud Boost
 * Plugin URI:        https://basecloud.co/boost
 * Description:       World-class page caching, asset optimization, and performance acceleration for WordPress — by BaseCloud.
 * Version:           1.3.8
 * Author:            BaseCloud
 * Author URI:        https://basecloud.co
 * License:           GPL-2.0+
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       basecloud-boost
 * Domain Path:       /languages
 * Requires PHP:      7.4
 * Requires at least: 5.5
 *
 * @package BaseCloud\Boost
 */

declare( strict_types=1 );

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

// ─── Constants ────────────────────────────────────────────────────────────────

define( 'BCBOOST_VERSION',     '1.3.8' );
define( 'BCBOOST_DIR',         plugin_dir_path( __FILE__ ) );
define( 'BCBOOST_URL',         plugin_dir_url( __FILE__ ) );
define( 'BCBOOST_CACHE_DIR',   WP_CONTENT_DIR . '/cache/basecloud-boost/' );
define( 'BCBOOST_MINIFY_DIR',  WP_CONTENT_DIR . '/cache/basecloud-boost-min/' );
define( 'BCBOOST_OPTIONS_KEY', 'bcboost_options' );
define( 'BCBOOST_MIN_PHP',     '7.4' );
define( 'BCBOOST_MIN_WP',      '5.5' );

// ─── Environment Checks ───────────────────────────────────────────────────────

if ( version_compare( PHP_VERSION, BCBOOST_MIN_PHP, '<' ) ) {
	add_action( 'admin_notices', static function (): void {
		printf(
			'<div class="notice notice-error"><p>%s</p></div>',
			esc_html( sprintf(
				/* translators: 1: required version 2: current version */
				__( 'BaseCloud Boost requires PHP %1$s or higher. You are running PHP %2$s.', 'basecloud-boost' ),
				BCBOOST_MIN_PHP,
				PHP_VERSION
			) )
		);
	} );
	return;
}

// ─── PSR-4 Autoloader ─────────────────────────────────────────────────────────

spl_autoload_register( static function ( string $class ): void {
	$prefix   = 'BaseCloud\\Boost\\';
	$base_dir = BCBOOST_DIR . 'includes/';
	$len      = strlen( $prefix );

	if ( strncmp( $prefix, $class, $len ) !== 0 ) {
		return;
	}

	$file = $base_dir . str_replace( '\\', '/', substr( $class, $len ) ) . '.php';
	if ( file_exists( $file ) ) {
		require_once $file;
	}
} );

// ─── Boot ─────────────────────────────────────────────────────────────────────

add_action( 'plugins_loaded', static function (): void {
	BaseCloud\Boost\Plugin::instance()->boot();
}, 1 );

// ─── Lifecycle Hooks ──────────────────────────────────────────────────────────

register_activation_hook( __FILE__,   [ 'BaseCloud\\Boost\\Activator',   'activate'   ] );
register_deactivation_hook( __FILE__, [ 'BaseCloud\\Boost\\Deactivator', 'deactivate' ] );

// ─── Global Helpers ───────────────────────────────────────────────────────────

/**
 * Clear all cached pages.
 *
 * @return bool
 */
function BCBOOST_clear_all(): bool {
	return ( new BaseCloud\Boost\Core\Page_Cache() )->delete_all_cache();
}

/**
 * Clear the cache for a specific URL.
 *
 * @param string $url Full URL to purge.
 * @return bool
 */
function BCBOOST_clear_url( string $url ): bool {
	return ( new BaseCloud\Boost\Core\Page_Cache() )->delete_cache_file( $url );
}

/**
 * Get a BaseCloud Boost option.
 *
 * @param string $key     Option key.
 * @param mixed  $default Fallback value.
 * @return mixed
 */
function BCBOOST_get_option( string $key, $default = null ) {
	$options = get_option( BCBOOST_OPTIONS_KEY, [] );
	if ( ! is_array( $options ) || ! array_key_exists( $key, $options ) ) {
		return $default;
	}

	$value = $options[ $key ];

	// Transparently decrypt any value stored with the encryption marker so every
	// consumer (CDN integrations, webhooks, Lighthouse) receives plaintext.
	if ( is_string( $value ) && BaseCloud\Boost\Core\Crypto::is_encrypted( $value ) ) {
		$value = BaseCloud\Boost\Core\Crypto::decrypt( $value );
	}

	return $value;
}

/**
 * Whether the current request may be optimized at all (buffered, minified,
 * lazy-loaded, script-delayed, page-cached).
 *
 * Returns false inside ANY page-builder editor/preview context, the Customizer
 * preview, or a post preview. Builders load the front end inside their editor
 * iframe: collapsing whitespace, stripping comments, rewriting images or
 * neutralising inline scripts there breaks the editor outright (historically
 * Boost could not be installed on Divi sites because the ?et_fb=1 Visual
 * Builder DOM was minified). This is the SINGLE source of truth — every
 * front-end buffer/transform must consult it; never add per-builder checks
 * elsewhere.
 *
 * Detected contexts:
 *   elementor-preview, action=elementor* ... Elementor (preview/editor/AJAX)
 *   et_fb, et_pb_preview, et_bfb .......... Divi (Visual Builder, layout preview, backend builder)
 *   fl_builder ............................ Beaver Builder
 *   vc_editable, vc_action ................ WPBakery front-end editor
 *   bricks=run ............................ Bricks
 *   ct_builder ............................ Oxygen
 *   op3editor ............................. OptimizePress 3
 *   tve=true .............................. Thrive Architect
 *   is_customize_preview() / is_preview() . Customizer + post previews
 *
 * NOTE: this can be called very early (plugins_loaded, or from the drop-in
 * context), where the main query does not exist yet — is_preview()/
 * is_customize_preview() are therefore guarded and only consulted once
 * available. Buffer-level callers run at template_redirect where both work,
 * so previews are still caught even though the boot-time call cannot see them.
 *
 * @return bool True when optimization/caching may run on this request.
 */
function BCBOOST_is_optimizable_request(): bool {
	$optimizable = true;

	// phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only builder detection, no state change.
	// Presence-based builder flags (the parameter existing at all means "editor").
	static $presence_params = [
		'elementor-preview', // Elementor preview iframe
		'et_fb',             // Divi Visual Builder
		'et_pb_preview',     // Divi layout/module preview
		'et_bfb',            // Divi Backend Visual Builder iframe
		'fl_builder',        // Beaver Builder editor (usually valueless)
		'vc_editable',       // WPBakery front-end editor iframe
		'vc_action',         // WPBakery editor AJAX action
		'ct_builder',        // Oxygen builder
		'op3editor',         // OptimizePress 3 editor
	];
	foreach ( $presence_params as $param ) {
		if ( isset( $_GET[ $param ] ) ) {
			$optimizable = false;
			break;
		}
	}

	// Value-based flags.
	if ( $optimizable && isset( $_GET['action'] )
		&& strpos( (string) $_GET['action'], 'elementor' ) === 0 ) {
		$optimizable = false; // action=elementor / elementor_ajax / elementor_* (keeps prior behaviour).
	}
	if ( $optimizable && isset( $_GET['bricks'] ) && 'run' === $_GET['bricks'] ) {
		$optimizable = false; // Bricks builder.
	}
	if ( $optimizable && isset( $_GET['tve'] ) && 'true' === $_GET['tve'] ) {
		$optimizable = false; // Thrive Architect.
	}
	// phpcs:enable

	// Customizer preview / post preview. Guarded: this helper can run before the
	// query layer is loaded or parsed (is_preview() on a missing main query would
	// _doing_it_wrong and return false anyway — skip it cleanly instead).
	if ( $optimizable && function_exists( 'is_customize_preview' ) && is_customize_preview() ) {
		$optimizable = false;
	}
	if ( $optimizable && function_exists( 'is_preview' )
		&& isset( $GLOBALS['wp_query'] ) && $GLOBALS['wp_query'] instanceof WP_Query
		&& is_preview() ) {
		$optimizable = false;
	}

	/**
	 * Filter whether the current request may be optimized/cached.
	 *
	 * @param bool $optimizable False inside builder editors, Customizer, previews.
	 */
	return (bool) apply_filters( 'BCBOOST_is_optimizable_request', $optimizable );
}

/**
 * Whether it is safe to deliver CSS asynchronously (non-render-blocking).
 *
 * Async CSS (`lazy_css`) only paints correctly when Critical CSS is inlined to
 * style the above-the-fold while the full stylesheet loads. Without it, ALL CSS —
 * including each page-builder template's per-element rules (Elementor/Divi/etc.
 * `post-XXXX.css`) — is deferred past first paint, so builder pages render with
 * default styling until the bundle swaps in: broken headings, missing section
 * background overlays, mis-sized widgets. To prevent that silent breakage, async
 * delivery is enabled only when Critical CSS is actually present.
 *
 * Since the per-template store (Optimizer\Critical_CSS) this gate resolves PER
 * REQUEST: per-URL override → template CSS → 'default' template → the global
 * `critical_css` textarea. A page whose template has stored critical CSS gets
 * async delivery; a page with no coverage keeps render-blocking CSS. Callers
 * therefore must consult this at REQUEST time (buffer filters, style_loader_tag),
 * never once at boot — before the main query exists only the per-URL and global
 * levels are visible, which is deliberately conservative.
 *
 * @return bool
 */
function BCBOOST_async_css_active(): bool {
	if ( ! (bool) BCBOOST_get_option( 'lazy_css', false ) ) {
		return false;
	}
	if ( ! (bool) BCBOOST_get_option( 'critical_css_enabled', false ) ) {
		return false;
	}
	try {
		return BaseCloud\Boost\Optimizer\Critical_CSS::css_exists_for_current_request();
	} catch ( Throwable $e ) {
		// Fail SAFE: if coverage cannot be proven, CSS stays render-blocking.
		return false;
	}
}

/**
 * Whether the BunnyCDN Optimizer is converting images at the edge.
 *
 * When the Optimizer add-on is active on the pull zone, Bunny negotiates WebP/AVIF
 * (with JPEG/PNG fallback) per browser for every CDN-served image. Boost then stands
 * down its own format conversion (rewriting <img>/srcset/CSS-bg to .webp and
 * generating .webp siblings) so the two never overlap and the edge keeps full
 * per-browser control. Requires CDN rewriting to be in effect (hostname set).
 *
 * @return bool
 */
function BCBOOST_bunny_optimizer_active(): bool {
	return (bool) BCBOOST_get_option( 'bunnycdn_enabled', false )
		&& (bool) BCBOOST_get_option( 'bunnycdn_optimizer', false )
		&& '' !== trim( (string) BCBOOST_get_option( 'bunnycdn_hostname', '' ) );
}

/**
 * Update a BaseCloud Boost option.
 *
 * @param string $key   Option key.
 * @param mixed  $value Value to store.
 * @return bool
 */
function BCBOOST_update_option( string $key, $value ): bool {
	// Read-modify-write of the FULL options array: both the read and the write
	// must bypass/evict possibly-stale object-cache copies, or this helper would
	// silently persist stale values for every other key (see
	// Settings_API::persist() for the alloptions race this guards against).
	wp_cache_delete( BCBOOST_OPTIONS_KEY, 'options' );
	wp_cache_delete( 'alloptions', 'options' );

	$options         = (array) get_option( BCBOOST_OPTIONS_KEY, [] );
	$options[ $key ] = $value;

	$updated = update_option( BCBOOST_OPTIONS_KEY, $options, false );

	wp_cache_delete( BCBOOST_OPTIONS_KEY, 'options' );
	wp_cache_delete( 'alloptions', 'options' );

	return $updated;
}
