<?php
/**
 * Request Firewall — DECOMMISSIONED in bw-dev 1.18.0.
 *
 * The former "adaptive load-shedding" firewall generated a self-managed
 * mu-plugin (wp-content/mu-plugins/bw-request-firewall.php) whose file-backed
 * pressure store did a BLOCKING flock() on every non-allowlisted request. On a
 * managed host with a networked/overlay filesystem (Flywheel), that flock
 * serializes PHP workers under real concurrency — the pool fills with workers
 * blocked in flock(), the site goes down, and only a PHP-pool restart clears it
 * (the database is idle the whole time). This caused a production outage on
 * aismartventures.com on 2026-06-11.
 *
 * It "passed validation" only because a local-filesystem dev container makes
 * flock() instant; the failure is a dev-vs-prod filesystem gap, invisible
 * except under real load on the real host. These sites also have no fast
 * shared-memory store (the memcached extension is loaded but no server runs;
 * APCu is absent), so the adaptive engine has no safe backend here at all.
 *
 * The feature is removed. THIS MODULE NO LONGER REGISTERS ANY HOOKS, generates
 * no files, and performs ZERO per-request filesystem I/O. Its only remaining
 * job is a one-time, idempotent, fail-safe DECOMMISSION that deletes the stub
 * mu-plugin and its data directory and disables the module — automatically, on
 * every site, even when the update arrives via SFTP/rsync (no WP upgrader hook).
 *
 * Driven from the plugin bootstrap (plugins_loaded, before the module registry
 * is built) so it runs regardless of the module-enabled setting — the stub is a
 * standalone mu-plugin that loads independently of any toggle, so the PHYSICAL
 * file must be removed.
 *
 * @package BW_Dev
 */

defined( 'ABSPATH' ) || exit;

class BW_Dev_Module_Request_Firewall implements BW_Dev_Module_Interface {

	const STUB_FILENAME = 'bw-request-firewall.php';
	const DATA_DIRNAME  = 'bw-request-firewall-data';

	// One-time decommission bookkeeping. DONE_OPTION is autoloaded so the
	// steady-state check is a single in-memory read that no-ops.
	const DONE_OPTION   = 'bw_dev_request_firewall_decommissioned';
	const DONE_VERSION  = '1'; // bump only if the routine itself must ever re-run
	const TRY_TRANSIENT = 'bw_dev_request_firewall_decommission_try';
	const ERROR_OPTION  = 'bw_dev_request_firewall_decommission_error';

	// ===================================================================
	// Module interface — intentionally inert (no hooks, no I/O)
	// ===================================================================

	public function slug(): string {
		return 'request_firewall';
	}

	public function label(): string {
		return __( 'Request Firewall (removed)', 'bw-dev' );
	}

	public function group(): string {
		return 'core';
	}

	public function default_settings(): array {
		return array();
	}

	public function sanitize( array $data ): array {
		return array();
	}

	/**
	 * The feature is gone — register NO hooks. (Decommission is driven from the
	 * plugin bootstrap, independent of module-enabled state.)
	 */
	public function register(): void {}

	public function uninstall(): void {
		self::do_decommission();
	}

	public function render_tab(): void {
		$stub      = self::mu_dir() . '/' . self::STUB_FILENAME;
		$stub_gone = ! file_exists( $stub );
		?>
		<div class="notice notice-info inline" style="margin:0 0 12px;padding:12px 16px;">
			<p style="margin:0 0 6px;"><strong><?php esc_html_e( 'This feature has been removed.', 'bw-dev' ); ?></strong></p>
			<p style="margin:0;"><?php esc_html_e( 'The Request Firewall was decommissioned in bw-dev 1.18.0. Its adaptive load-shedding relied on a per-request filesystem lock that can exhaust the PHP worker pool on managed hosts with a networked filesystem. The self-managed mu-plugin and its data are removed automatically on this site.', 'bw-dev' ); ?></p>
			<?php if ( $stub_gone ) : ?>
				<p style="margin:8px 0 0;color:#1a7f37;font-weight:600;">&#9679; <?php esc_html_e( 'Removed — the firewall mu-plugin is no longer present.', 'bw-dev' ); ?></p>
			<?php else : ?>
				<p style="margin:8px 0 0;color:#b32d2e;font-weight:600;">&#9679; <?php
					/* translators: %s: file path */
					printf( esc_html__( 'Could not auto-remove %s — its directory is not writable. Please delete it (and its bw-request-firewall-data folder) manually.', 'bw-dev' ), '<code>' . esc_html( $stub ) . '</code>' );
				?></p>
			<?php endif; ?>
		</div>
		<?php
	}

	// ===================================================================
	// Lifecycle hooks (referenced from bw-dev.php)
	// ===================================================================

	public static function on_plugin_activate(): void {
		self::do_decommission();
		update_option( self::DONE_OPTION, self::DONE_VERSION, true );
	}

	public static function on_plugin_deactivate(): void {
		// The feature is dead — always remove the stub on deactivate.
		self::do_decommission();
	}

	// ===================================================================
	// Decommission — the only live behavior
	// ===================================================================

	/**
	 * Bootstrap entry, hooked early on `plugins_loaded`. One-time, throttled,
	 * fail-safe. After a successful run it's a single autoloaded-option read
	 * that returns immediately. A failed delete (mu-plugins not writable) backs
	 * off behind a 5-minute transient so it can never storm or loop, and never
	 * fatals.
	 */
	public static function maybe_decommission(): void {
		if ( get_option( self::DONE_OPTION ) === self::DONE_VERSION ) {
			return; // already decommissioned — cheap no-op
		}
		if ( get_transient( self::TRY_TRANSIENT ) ) {
			return; // backed off after a recent failed attempt
		}
		set_transient( self::TRY_TRANSIENT, 1, 5 * MINUTE_IN_SECONDS );

		$removed = self::do_decommission();

		if ( $removed ) {
			update_option( self::DONE_OPTION, self::DONE_VERSION, true ); // autoloaded
			delete_option( self::ERROR_OPTION );
			delete_transient( self::TRY_TRANSIENT );
		} else {
			// Stub still on disk (mu-plugins not writable). Flag the admin notice;
			// the transient throttles retries to once / 5 min.
			update_option( self::ERROR_OPTION, 1, false );
		}
	}

	/**
	 * Delete the stub mu-plugin + its data dir, disable the module, and clear
	 * the old engine's options/cron. Pure deletes via @unlink/@rmdir — no
	 * WP_Filesystem, no writes to the firewall files, never fatal. Returns true
	 * once the stub file is gone.
	 */
	public static function do_decommission(): bool {
		$mu   = self::mu_dir();
		$stub = $mu . '/' . self::STUB_FILENAME;
		$data = $mu . '/' . self::DATA_DIRNAME;

		// 1. The lethal file: the mu-plugin that does the flock. It loads
		//    independently of any setting, so the physical file MUST go.
		if ( is_file( $stub ) ) {
			@unlink( $stub ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
		}

		// 2. The data dir (store.json, allowlist.json, README).
		if ( is_dir( $data ) ) {
			foreach ( (array) glob( $data . '/*' ) as $f ) {
				if ( is_file( $f ) ) {
					@unlink( $f ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
				}
			}
			@rmdir( $data ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
		}

		// 3. Disable the module so nothing ever initializes it again. Write only
		//    if it isn't already false (avoid needless option churn).
		$settings = get_option( BW_Dev_Settings::OPTION, array() );
		if ( ! is_array( $settings ) ) {
			$settings = array();
		}
		if ( ! isset( $settings['modules'] ) || ! is_array( $settings['modules'] ) ) {
			$settings['modules'] = array();
		}
		if ( ( $settings['modules']['request_firewall'] ?? null ) !== false ) {
			$settings['modules']['request_firewall'] = false;
			update_option( BW_Dev_Settings::OPTION, $settings );
		}

		// 4. Clear leftover scheduled events + stale options from the old engine.
		wp_clear_scheduled_hook( 'bw_dev_request_firewall_rebuild' );
		wp_clear_scheduled_hook( 'bw_dev_request_firewall_rebuild_daily' );
		foreach ( array(
			'bw_dev_request_firewall_fingerprint',
			'bw_dev_request_firewall_allowlist_meta',
			'bw_dev_request_firewall_activations',
			'bw_dev_request_firewall_deployed_version',
			'bw_dev_request_firewall_error',
		) as $opt ) {
			delete_option( $opt );
		}
		delete_transient( 'bw_dev_request_firewall_checked' );
		delete_transient( 'bw_dev_request_firewall_selfheal' );
		delete_transient( 'bw_dev_request_firewall_backend' );

		return ! file_exists( $stub );
	}

	/**
	 * Admin notice shown when the stub could not be auto-removed (its directory
	 * is not writable). Self-clears if the file is gone by other means.
	 */
	public static function decommission_notice(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}
		if ( ! get_option( self::ERROR_OPTION ) ) {
			return;
		}
		$stub = self::mu_dir() . '/' . self::STUB_FILENAME;
		if ( ! file_exists( $stub ) ) {
			delete_option( self::ERROR_OPTION ); // resolved out-of-band
			return;
		}
		echo '<div class="notice notice-error"><p><strong>' . esc_html__( 'BW Dev: the Request Firewall mu-plugin must be removed manually.', 'bw-dev' ) . '</strong> ';
		printf(
			/* translators: %s: file path */
			esc_html__( 'It was decommissioned for stability but could not be auto-deleted because its directory is not writable. Please delete %s and the bw-request-firewall-data folder next to it.', 'bw-dev' ),
			'<code>' . esc_html( $stub ) . '</code>'
		);
		echo '</p></div>';
	}

	private static function mu_dir(): string {
		return defined( 'WPMU_PLUGIN_DIR' ) ? WPMU_PLUGIN_DIR : ( WP_CONTENT_DIR . '/mu-plugins' );
	}
}

// ===================================================================
// WP-CLI: wp bw-dev firewall decommission | status   (deletion-only)
// ===================================================================
// There is intentionally NO deploy command — the feature must never regenerate.
if ( defined( 'WP_CLI' ) && WP_CLI ) {

	/**
	 * Decommission the removed bw-dev Request Firewall.
	 */
	class BW_Dev_Request_Firewall_CLI {

		/**
		 * Force the one-time decommission now: delete the stub mu-plugin + data
		 * dir and disable the module. Use this to verify the fix headlessly
		 * across a fleet (no browser needed).
		 *
		 * ## EXAMPLES
		 *
		 *     wp bw-dev firewall decommission
		 *
		 * @when after_wp_load
		 */
		public function decommission( $args, $assoc_args ) {
			$ok = BW_Dev_Module_Request_Firewall::do_decommission();
			update_option( BW_Dev_Module_Request_Firewall::DONE_OPTION, BW_Dev_Module_Request_Firewall::DONE_VERSION, true );
			if ( $ok ) {
				WP_CLI::success( 'Request Firewall decommissioned: stub mu-plugin + data dir removed, module disabled.' );
			} else {
				WP_CLI::error( 'Stub still present — wp-content/mu-plugins is not writable. Remove bw-request-firewall.php manually.' );
			}
		}

		/**
		 * Report whether the firewall stub has been removed.
		 *
		 * ## EXAMPLES
		 *
		 *     wp bw-dev firewall status
		 *
		 * @when after_wp_load
		 */
		public function status( $args, $assoc_args ) {
			$mu           = defined( 'WPMU_PLUGIN_DIR' ) ? WPMU_PLUGIN_DIR : ( WP_CONTENT_DIR . '/mu-plugins' );
			$stub_present = file_exists( $mu . '/' . BW_Dev_Module_Request_Firewall::STUB_FILENAME );
			$data_present = is_dir( $mu . '/' . BW_Dev_Module_Request_Firewall::DATA_DIRNAME );
			$done         = get_option( BW_Dev_Module_Request_Firewall::DONE_OPTION ) === BW_Dev_Module_Request_Firewall::DONE_VERSION;
			WP_CLI::line( 'stub mu-plugin : ' . ( $stub_present ? 'PRESENT — run: wp bw-dev firewall decommission' : 'removed' ) );
			WP_CLI::line( 'data dir       : ' . ( $data_present ? 'PRESENT' : 'removed' ) );
			WP_CLI::line( 'decommissioned : ' . ( $done ? 'yes' : 'no' ) );
			if ( $stub_present ) {
				WP_CLI::warning( 'Firewall stub still on disk.' );
			}
		}
	}

	WP_CLI::add_command( 'bw-dev firewall', 'BW_Dev_Request_Firewall_CLI' );
}
