<?php
/**
 * BaseCloud Boost — Divi Compatibility
 *
 * Cache lifecycle coordination for the Divi theme, Divi child themes, Extra,
 * and the standalone Divi Builder plugin.
 *
 * Scope (deliberate): purge coordination + settings-overlap detection ONLY.
 * The builder-editor request gate (?et_fb=1) and the /et-cache/ query-string
 * exemption are owned elsewhere — do not add them here.
 *
 * Verified hook sources (2026-07):
 * - `et_save_post` (post ID) + `wp_ajax_et_theme_builder_api_save`:
 *   WP Rocket inc/ThirdParty/Themes/Divi.php (develop branch).
 * - `et_epanel_changing_options` (fires inside the Theme Options save handler,
 *   AJAX `wp_ajax_save_epanel`, BEFORE options are written — hence the
 *   deferred-to-shutdown purge): Divi epanel/core_functions.php.
 * - `et_core_page_resource_auto_clear`: Divi's own WP-Cron event that clears
 *   the static-CSS (et-cache) files; Divi's handler runs at default priority,
 *   we purge after it at priority 20.
 * - Theme Options storage is the `et_divi` option array (Divi Builder plugin:
 *   `et_pb_builder_options`), so `update_option_et_divi` is a version-proof
 *   backstop that fires only when a value actually changed (incl. Divi 5,
 *   which keeps `et_divi` storage).
 *
 * NOTE: `customize_save_after` is already wired in Core\Cache_Engine — it is
 * intentionally NOT repeated here (no double-purge on Customizer saves).
 *
 * @package BaseCloud\Boost\Compatibility
 */

declare( strict_types=1 );

namespace BaseCloud\Boost\Compatibility;

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

use BaseCloud\Boost\Core\Cache_Purge;

/**
 * Keeps Boost's page cache in lock-step with Divi's own save/cache lifecycle,
 * and warns when Divi's performance options double-process what Boost owns.
 */
class Divi_Compat {

	/** @var bool One purge-all per request, no matter how many triggers fire. */
	private static bool $purged_all = false;

	/**
	 * Boot. Divi is a THEME: none of its constants (ET_CORE_VERSION,
	 * ET_BUILDER_VERSION…) exist yet at `plugins_loaded` when Boost boots, so
	 * detection must wait until the theme has loaded.
	 *
	 * @return void
	 */
	public function boot(): void {
		add_action( 'after_setup_theme', [ $this, 'maybe_init' ], 20 );
	}

	/**
	 * Wire the Divi hooks once we can positively detect Divi.
	 *
	 * @return void
	 */
	public function maybe_init(): void {
		if ( ! self::is_divi_active() ) {
			return;
		}

		$this->init_purge_hooks();

		if ( is_admin() ) {
			add_action( 'admin_notices', [ $this, 'maybe_show_conflict_notice' ] );
		}
	}

	/**
	 * Is Divi (theme, child theme, Extra, or the Divi Builder plugin) active?
	 *
	 * Only reliable at/after `after_setup_theme` — theme constants are not
	 * defined earlier. Everything is guarded so nothing fatals when absent.
	 *
	 * @return bool
	 */
	public static function is_divi_active(): bool {
		// Divi/Extra theme, including child themes (get_template() resolves the parent).
		if ( function_exists( 'wp_get_theme' ) ) {
			$template = (string) wp_get_theme()->get_template();
			if ( in_array( $template, [ 'Divi', 'Extra' ], true ) ) {
				return true;
			}
		}

		// Divi Builder plugin / any Elegant Themes builder runtime.
		return defined( 'ET_BUILDER_PLUGIN_VERSION' )
			|| defined( 'ET_BUILDER_VERSION' )
			|| defined( 'ET_CORE_VERSION' );
	}

	/**
	 * Purge coordination: every Divi lifecycle event that changes rendered
	 * output must invalidate Boost's page cache.
	 *
	 * @return void
	 */
	private function init_purge_hooks(): void {
		// 1. Builder save (Visual Builder / classic builder) → purge that post.
		//    Fires with the saved post ID; also fires for Theme Builder template
		//    posts (harmless — the AJAX hook below handles the site-wide fan-out).
		add_action( 'et_save_post', [ $this, 'on_builder_save' ] );

		// 1b. Theme Builder "Save Changes" — global header/footer/body templates
		//     affect every page that uses them → purge all. Priority 0 runs
		//     before Divi's own AJAX handler (which dies via wp_send_json), and
		//     the actual purge is deferred to shutdown so it happens AFTER the
		//     templates are persisted. WP's shutdown action still fires after
		//     wp_die()/wp_send_json().
		add_action( 'wp_ajax_et_theme_builder_api_save', [ $this, 'schedule_purge_all' ], 0 );

		// 2. Theme Options (ePanel) save → global CSS/markup can change → purge
		//    all. This action fires BEFORE the options are written, so defer to
		//    shutdown (purging inline would let a visitor re-cache the OLD page
		//    between purge and write).
		add_action( 'et_epanel_changing_options', [ $this, 'schedule_purge_all' ] );

		// 2b. Version-proof backstop: any actual change to Divi's stored options
		//     (ePanel, Divi 5 settings, programmatic et_update_option()). Only
		//     fires when a value really changed; debounced to one purge/request.
		add_action( 'update_option_et_divi', [ $this, 'schedule_purge_all' ] );
		add_action( 'update_option_et_pb_builder_options', [ $this, 'schedule_purge_all' ] );

		// (Customizer saves: already purged via customize_save_after in Cache_Engine.)

		// 3. Divi's scheduled static-CSS clear (et-cache regeneration). After
		//    Divi's own handler has removed the files (default priority 10), our
		//    cached HTML may still reference the deleted et-cache URLs → purge
		//    immediately at priority 20.
		add_action( 'et_core_page_resource_auto_clear', [ $this, 'purge_all_once' ], 20 );
	}

	/**
	 * Divi builder saved a post → purge its page cache.
	 *
	 * @param int|string $post_id Saved post ID (Divi passes an int).
	 * @return void
	 */
	public function on_builder_save( $post_id ): void {
		if ( ! is_numeric( $post_id ) || (int) $post_id <= 0 ) {
			return;
		}

		try {
			( new Cache_Purge() )->purge_post( (int) $post_id );

			if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
				error_log( sprintf( '[BaseCloud Boost] Divi builder save (et_save_post) — purged post %d', (int) $post_id ) );
			}
		} catch ( \Throwable $e ) {
			// Fail open: never let a purge failure break the builder save.
			if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
				error_log( '[BaseCloud Boost] Divi builder-save purge failed: ' . $e->getMessage() );
			}
		}
	}

	/**
	 * Defer a site-wide purge to shutdown, after the triggering change has been
	 * persisted. Safe to call multiple times — purge_all_once() debounces.
	 *
	 * @return void
	 */
	public function schedule_purge_all(): void {
		if ( self::$purged_all ) {
			return;
		}

		if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
			error_log( '[BaseCloud Boost] Divi change detected (' . current_filter() . ') — site-wide purge scheduled for shutdown' );
		}

		add_action( 'shutdown', [ $this, 'purge_all_once' ] );
	}

	/**
	 * Run purge_all() exactly once per request.
	 *
	 * @return void
	 */
	public function purge_all_once(): void {
		if ( self::$purged_all ) {
			return;
		}
		self::$purged_all = true;

		try {
			( new Cache_Purge() )->purge_all();

			if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
				error_log( '[BaseCloud Boost] Divi lifecycle purge-all completed (' . current_filter() . ')' );
			}
		} catch ( \Throwable $e ) {
			// Fail open: a failed purge must never take down the request.
			if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
				error_log( '[BaseCloud Boost] Divi lifecycle purge-all failed: ' . $e->getMessage() );
			}
		}
	}

	/**
	 * Detect Divi performance options that double-process what Boost already
	 * owns. Conservative by design: only warns on an explicitly stored 'on'
	 * (Divi checkboxes persist as 'on'/'false' in the `et_divi` array once
	 * Theme Options has been saved) — never on assumed defaults, so a site
	 * that has never saved Theme Options produces no false positive.
	 *
	 * @return string[] Human-readable overlap descriptions (empty = no conflict).
	 */
	public function detect_divi_overlaps(): array {
		$overlaps = [];

		$divi_options = get_option( 'et_divi', [] );
		if ( ! is_array( $divi_options ) ) {
			$divi_options = [];
		}

		// Mirror the Optimizer's actual runtime reads (JS_Minifier.php / CSS_Minifier.php).
		$boost_js  = (bool) BCBOOST_get_option( 'minify_js', true ) || (bool) BCBOOST_get_option( 'combine_js', false );
		$boost_css = (bool) BCBOOST_get_option( 'minify_css', true ) || (bool) BCBOOST_get_option( 'combine_css', false );

		if ( $boost_js && 'on' === ( $divi_options['divi_minify_combine_scripts'] ?? '' ) ) {
			$overlaps[] = __( 'Divi "Minify And Combine Javascript Files" is ON while Boost is also minifying/combining JavaScript', 'basecloud-boost' );
		}

		// Removed from the Divi UI in 4.10+ but still stored (and honoured by
		// older Divi) in et_divi on sites that saved Theme Options before that.
		if ( $boost_css && 'on' === ( $divi_options['divi_minify_combine_styles'] ?? '' ) ) {
			$overlaps[] = __( 'Divi "Minify And Combine CSS Files" is ON while Boost is also minifying/combining CSS', 'basecloud-boost' );
		}

		return $overlaps;
	}

	/**
	 * Same style as Admin_Controller::show_admin_notices(): a dismissible
	 * warning recommending one owner per job — Boost owns minify/combine,
	 * Divi keeps Static CSS File Generation.
	 *
	 * @return void
	 */
	public function maybe_show_conflict_notice(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		$overlaps = $this->detect_divi_overlaps();
		if ( empty( $overlaps ) ) {
			return;
		}

		echo '<div class="notice notice-warning is-dismissible"><p>';
		printf(
			/* translators: %s: semicolon-separated list of overlapping optimizations */
			esc_html__( 'BaseCloud Boost and Divi are both optimizing the same assets: %s. Two optimizers double-processing the same files can break styling and scripts. Recommended: turn OFF Divi\'s "Minify And Combine" options (Divi → Theme Options → Builder → Advanced) and let Boost own minification. Leave Divi\'s "Static CSS File Generation" ON — it does a different job and works fine alongside Boost.', 'basecloud-boost' ),
			'<strong>' . esc_html( implode( '; ', $overlaps ) ) . '</strong>'
		);
		echo '</p></div>';
	}
}
