<?php
/**
 * BaseCloud Boost — Activator
 *
 * @package BaseCloud\Boost
 */

declare( strict_types=1 );

namespace BaseCloud\Boost;

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

use BaseCloud\Boost\Admin\Settings_API;

/**
 * Handles plugin activation tasks.
 */
class Activator {

	/**
	 * Run activation tasks.
	 *
	 * @return void
	 */
	public static function activate(): void {
		self::create_cache_directories();
		self::install_advanced_cache();
		self::set_default_options();
		self::write_options_cache();
		self::schedule_cron();
		self::write_htaccess_rules();

		update_option( 'bcboost_version', BCBOOST_VERSION, false );

		flush_rewrite_rules();
	}

	/**
	 * Self-healing upgrade routine.
	 *
	 * Plugin updates do NOT re-run activation, so the advanced-cache.php drop-in
	 * (a copy in wp-content/) and the .htaccess rules can become stale. When the
	 * stored version differs from the running version we refresh both and clear
	 * the page cache so header/logic fixes take effect without a manual
	 * deactivate/reactivate. Hooked on admin_init.
	 *
	 * @return void
	 */
	public static function maybe_upgrade(): void {
		if ( get_option( 'bcboost_version', '' ) === BCBOOST_VERSION ) {
			return;
		}

		self::install_advanced_cache();
		self::ensure_options_not_autoloaded();
		self::write_options_cache();
		self::write_htaccess_rules();
		\BaseCloud\Boost\Database\DB_Optimizer::reschedule();

		// Old cached pages were written under the previous caching policy — clear
		// them so the corrected headers (and any markup changes) apply at once.
		if ( class_exists( \BaseCloud\Boost\Core\Cache_Purge::class ) ) {
			( new \BaseCloud\Boost\Core\Cache_Purge() )->purge_all();
		}

		update_option( 'bcboost_version', BCBOOST_VERSION, false );
	}

	/**
	 * Create cache directories.
	 *
	 * @return void
	 */
	private static function create_cache_directories(): void {
		foreach ( [ BCBOOST_CACHE_DIR, BCBOOST_MINIFY_DIR ] as $dir ) {
			if ( ! is_dir( $dir ) ) {
				wp_mkdir_p( $dir );
			}

			// Prevent directory listing.
			$index = $dir . 'index.php';
			if ( ! file_exists( $index ) ) {
				file_put_contents( $index, '<?php // Silence is golden.' );
			}
		}
	}

	/**
	 * Install the advanced-cache.php drop-in.
	 *
	 * @return void
	 */
	private static function install_advanced_cache(): void {
		$destination = WP_CONTENT_DIR . '/advanced-cache.php';

		// Managed hosts (WP Engine, Kinsta, Pagely, Pantheon, WordPress.com VIP) run
		// their own full-page cache and do not allow third-party page-cache drop-ins.
		// Installing ours there can clash with the host's caching layer and, in the
		// worst case, fatal every request (advanced-cache.php loads on every request
		// before WordPress finishes booting, so a failure 500s the whole site,
		// admin included). On those hosts we never install the drop-in — and we
		// remove ours if an earlier version left one behind — so Boost is safe to run
		// for its asset-optimisation features while the host handles page caching.
		if ( self::host_runs_own_page_cache() ) {
			if (
				file_exists( $destination )
				&& false !== strpos( (string) @file_get_contents( $destination ), 'BaseCloud Boost' )
			) {
				@unlink( $destination );
			}
			return; // Do not copy the drop-in or touch the WP_CACHE constant.
		}

		$source = BCBOOST_DIR . 'advanced-cache.php';

		// Only install if ours isn't already there (don't overwrite a different plugin's drop-in).
		if ( ! file_exists( $destination ) || false !== strpos( (string) file_get_contents( $destination ), 'BaseCloud Boost' ) ) {
			copy( $source, $destination );
		}

		self::enable_wp_cache_constant();
	}

	/**
	 * Whether the current host runs its own full-page cache (so Boost must not
	 * install its advanced-cache.php drop-in).
	 *
	 * @return bool
	 */
	private static function host_runs_own_page_cache(): bool {
		if ( ! class_exists( \BaseCloud\Boost\Compatibility\Host_Detector::class ) ) {
			return false;
		}
		return ( new \BaseCloud\Boost\Compatibility\Host_Detector() )->detect_has_page_cache();
	}

	/**
	 * Try to add define('WP_CACHE', true) to wp-config.php.
	 *
	 * @return void
	 */
	private static function enable_wp_cache_constant(): void {
		$config_file = ABSPATH . 'wp-config.php';

		if ( ! file_exists( $config_file ) || ! is_writable( $config_file ) ) {
			return;
		}

		$config = (string) file_get_contents( $config_file );

		if ( strpos( $config, 'WP_CACHE' ) !== false ) {
			// Already defined — replace false with true if needed.
			$config = preg_replace(
				"/define\s*\(\s*['\"]WP_CACHE['\"]\s*,\s*false\s*\)/",
				"define('WP_CACHE', true)",
				$config
			) ?? $config;
		} else {
			// Insert after the opening <?php tag.
			$config = preg_replace(
				'/<\?php/',
				"<?php\ndefine( 'WP_CACHE', true ); // Added by BaseCloud Boost",
				$config,
				1
			) ?? $config;
		}

		file_put_contents( $config_file, $config, LOCK_EX );
	}

	/**
	 * Set default plugin options if not already set.
	 *
	 * @return void
	 */
	private static function set_default_options(): void {
		if ( get_option( BCBOOST_OPTIONS_KEY ) !== false ) {
			self::ensure_options_not_autoloaded();
			return;
		}

		$defaults = require BCBOOST_DIR . 'bcboost-config.php';
		// autoload=no: Boost settings must NEVER live in the shared `alloptions`
		// blob — concurrent autoloaded-option writes (preload chain watchdog cron,
		// third-party front-end writes) would race stale copies of that blob back
		// into the object cache, silently masking settings saves (trac #31245).
		update_option( BCBOOST_OPTIONS_KEY, $defaults, false );
	}

	/**
	 * One-shot repair for existing installs: move bcboost_options OUT of the
	 * autoloaded `alloptions` blob so no alloptions cache race can ever mask a
	 * settings save again, and evict any already-poisoned cached copy so the
	 * next read comes from the database. Runs on activation and on every
	 * version change (idempotent, one indexed UPDATE).
	 *
	 * @return void
	 */
	private static function ensure_options_not_autoloaded(): void {
		try {
			if ( function_exists( 'wp_set_option_autoload' ) ) {
				// WP 6.4+ — handles the DB column and cache bookkeeping.
				wp_set_option_autoload( BCBOOST_OPTIONS_KEY, false );
			} else {
				global $wpdb;
				$wpdb->update(
					$wpdb->options,
					[ 'autoload' => 'no' ],
					[ 'option_name' => BCBOOST_OPTIONS_KEY ]
				);
			}

			// Evict both cache locations regardless of path taken: the individual
			// key AND the alloptions blob (which may still carry a stale copy).
			wp_cache_delete( BCBOOST_OPTIONS_KEY, 'options' );
			wp_cache_delete( 'alloptions', 'options' );
		} catch ( \Throwable $e ) {
			// Fail open — never let a repair step break activation/upgrade.
			if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
				error_log( '[BaseCloud Boost] ensure_options_not_autoloaded failed: ' . $e->getMessage() );
			}
		}
	}

	/**
	 * Write the JSON options cache so the advanced-cache.php drop-in
	 * can read settings without a database query.
	 *
	 * @return void
	 */
	private static function write_options_cache(): void {
		Settings_API::write_options_cache();
	}

	/**
	 * Write .htaccess rules on activation.
	 *
	 * @return void
	 */
	private static function write_htaccess_rules(): void {
		$manager = new \BaseCloud\Boost\Compatibility\Htaccess_Manager();
		$manager->add_rules();
	}

	/**
	 * Schedule plugin cron events.
	 *
	 * @return void
	 */
	private static function schedule_cron(): void {
		if ( ! wp_next_scheduled( 'BCBOOST_expire_cache' ) ) {
			wp_schedule_event( time(), 'hourly', 'BCBOOST_expire_cache' );
		}

		if ( ! wp_next_scheduled( 'BCBOOST_minify_cache_cleanup' ) ) {
			wp_schedule_event( time(), 'weekly', 'BCBOOST_minify_cache_cleanup' );
		}

		if ( ! wp_next_scheduled( 'BCBOOST_webhook_daily' ) ) {
			wp_schedule_event( time(), 'daily', 'BCBOOST_webhook_daily' );
		}

		if ( ! wp_next_scheduled( 'BCBOOST_lighthouse_daily' ) ) {
			wp_schedule_event( time(), 'daily', 'BCBOOST_lighthouse_daily' );
		}

		// Automatic DB cleanup follows its own (off by default) setting.
		\BaseCloud\Boost\Database\DB_Optimizer::reschedule();
	}
}
