<?php

declare(strict_types=1);

namespace SiteFort\Modules\Cloud\Services;

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

use SiteFort\Includes\SettingsStore;
use SiteFort\Modules\Firewall\FirewallConfigService;
use SiteFort\Modules\Firewall\FirewallSettings;
use SiteFort\Modules\Firewall\LockoutManager;

/**
 * CommandExecutor - Executes allowlisted Console commands.
 *
 * Shared by the durable local command queue worker.
 */
class CommandExecutor {

	/**
	 * Current executor version reported back to Console.
	 */
	public const VERSION = '3.0.0';

	/**
	 * Commands accepted from Console.
	 *
	 * @return array<int,string>
	 */
	public static function supported_commands(): array {
		return [
			'scan.start',
			'scan.stop',
			'finding.ignore',
			'finding.unignore',
			'plugin.update',
			'plugin.toggle',
			'theme.update',
			'theme.switch',
			'core.update',
			'hardening.apply',
			'firewall.update_rules',
			'config.apply_template',
			'notification.update',
			'branding.sync',
			'two_factor.configure',
			'tools.regenerate_salts',
			'tools.change_prefix',
			'login.configure',
			'cloudflare.policy_update',
			'cloudflare.verify',
			'integration_secret.apply',
			'multisite.propagate',
			'settings.export',
			'settings.import',
			'audit.query',
			'console.enable',
			'console.disable',
			'license.sync',
			'firewall.request_state_push',
		];
	}

	/**
	 * @param string $command
	 */
	public static function is_supported_command( string $command ): bool {
		return in_array( strtolower( sanitize_text_field( $command ) ), self::supported_commands(), true );
	}

	/**
	 * Commands that run inline during the delivery request. Long-running or
	 * network-bound commands stay on the background queue.
	 *
	 * @return array<int,string>
	 */
	public static function inline_commands(): array {
		return [
			'scan.stop',
			'finding.ignore',
			'finding.unignore',
			'plugin.toggle',
			'theme.switch',
			'hardening.apply',
			'firewall.update_rules',
			'config.apply_template',
			'notification.update',
			'two_factor.configure',
			'login.configure',
			'integration_secret.apply',
			'settings.export',
			'settings.import',
			'audit.query',
			'console.enable',
			'console.disable',
			'firewall.request_state_push',
		];
	}

	/**
	 * @param string $command
	 */
	public static function runs_inline( string $command ): bool {
		return in_array( strtolower( sanitize_text_field( $command ) ), self::inline_commands(), true );
	}

	/**
	 * Validate command parameters that must fail before local queue persistence.
	 *
	 * @param string              $command Command name.
	 * @param array<string,mixed> $params  Command parameters.
	 * @return true|\WP_Error
	 */
	public function validate_for_acceptance( string $command, array $params ) {
		$command = strtolower( sanitize_text_field( $command ) );
		if ( ! self::is_supported_command( $command ) ) {
			return new \WP_Error( 'unsupported_command', 'Unsupported command.' );
		}

		if ( 'branding.sync' === $command ) {
			$branding = $this->validate_branding_sync_params( $params );
			if ( is_wp_error( $branding ) ) {
				return $branding;
			}
		}

		return true;
	}

	/**
	 * Execute a command.
	 *
	 * @param string $command
	 * @param array<string,mixed> $params
	 * @return array<string,mixed>
	 */
	public function execute( string $command, array $params = [] ): array {
		$command = sanitize_text_field( $command );
		$command = strtolower( $command );

		switch ( $command ) {
			case 'scan.start':
				return $this->scan_start( $params );
			case 'scan.stop':
				return $this->scan_stop();
			case 'finding.ignore':
				return $this->finding_ignore( $params );
			case 'finding.unignore':
				return $this->finding_unignore( $params );
			case 'plugin.update':
				return $this->plugin_update( $params );
			case 'plugin.toggle':
				return $this->plugin_toggle( $params );
			case 'theme.update':
				return $this->theme_update( $params );
			case 'theme.switch':
				return $this->theme_switch( $params );
			case 'core.update':
				return $this->core_update( $params );
			case 'hardening.apply':
				return $this->hardening_apply( $params );
			case 'firewall.update_rules':
				return $this->firewall_update_rules( $params );
			case 'config.apply_template':
				return $this->config_apply_template( $params );
			case 'notification.update':
				return $this->notification_update( $params );
			case 'branding.sync':
				return $this->branding_sync( $params );
			case 'two_factor.configure':
				return $this->two_factor_configure( $params );
			case 'tools.regenerate_salts':
				return $this->tools_regenerate_salts();
			case 'tools.change_prefix':
				return $this->tools_change_prefix( $params );
			case 'login.configure':
				return $this->login_configure( $params );
			case 'cloudflare.policy_update':
				return $this->cloudflare_policy_update( $params );
			case 'cloudflare.verify':
				return $this->cloudflare_verify();
			case 'integration_secret.apply':
				return $this->integration_secret_apply( $params );
			case 'multisite.propagate':
				return $this->multisite_propagate( $params );
			case 'settings.export':
				return $this->settings_export();
			case 'settings.import':
				return $this->settings_import( $params );
			case 'audit.query':
				return $this->audit_query( $params );
			case 'console.enable':
				return $this->console_set_enabled( true );
			case 'console.disable':
				return $this->console_set_enabled( false );
			case 'license.sync':
				return $this->license_sync();
			case 'firewall.request_state_push':
				return $this->firewall_request_state_push();

			default:
				return [ 'ok' => false, 'error' => 'unsupported_command', 'error_code' => 'CMD_UNSUPPORTED' ];
		}
	}


	/**
	 * @param array<string,mixed> $params
	 * @return array<string,mixed>
	 */
	private function scan_start( array $params ): array {
		if ( ! class_exists( '\\SiteFort\\Modules\\Scanner\\Queue\\ScanQueue' ) ) {
			return [ 'ok' => false, 'error' => 'scanner_unavailable', 'error_code' => 'CMD_PRECONDITION' ];
		}

		if ( \SiteFort\Modules\Scanner\Queue\ScanQueue::is_active() ) {
			$queue = \SiteFort\Modules\Scanner\Queue\ScanQueue::get();
			return [ 'ok' => false, 'error' => 'scan_already_running', 'error_code' => 'CMD_PRECONDITION', 'scan_id' => $queue['id'] ?? '' ];
		}

		// Scan type: 'standard' (fast, targeted) or 'deep' (all files incl. images).
		$raw_type = '';
		if ( isset( $params['scan_type'] ) ) {
			$raw_type = sanitize_text_field( (string) $params['scan_type'] );
		} elseif ( isset( $params['type'] ) ) {
			$raw_type = sanitize_text_field( (string) $params['type'] );
		}
		$scan_type = ( 'deep' === $raw_type ) ? 'deep' : 'standard';

		$scan_id = 'scan_' . wp_generate_uuid4();

		// Skip preflight here - ScanWorker runs its own preflight on first iteration.
		// Doing it here blocks the HTTP response for 5-10s causing push delivery timeouts.

		$queue = \SiteFort\Modules\Scanner\Queue\ScanQueue::create( $scan_type, 'cloud', $scan_id );
		if ( class_exists( '\\SiteFort\\Modules\\Scanner\\Queue\\ScanWorker' ) ) {
			\SiteFort\Modules\Scanner\Queue\ScanWorker::dispatch_next();
		}

		return [
			'ok' => true,
			'scan_id' => $queue['id'] ?? $scan_id,
			'scan_type' => $scan_type,
		];
	}

	/**
	 * @return array<string,mixed>
	 */
	private function scan_stop(): array {
		if ( ! class_exists( '\\SiteFort\\Modules\\Scanner\\Queue\\ScanQueue' ) ) {
			return [ 'ok' => false, 'error' => 'scanner_unavailable', 'error_code' => 'CMD_PRECONDITION' ];
		}
		if ( ! \SiteFort\Modules\Scanner\Queue\ScanQueue::is_active() ) {
			return [ 'ok' => false, 'error' => 'no_active_scan', 'error_code' => 'CMD_PRECONDITION' ];
		}
		\SiteFort\Modules\Scanner\Queue\ScanQueue::fail(
			'Stopped by Console command',
			[
				'error_code' => 'console_stopped',
				'stage'      => 'remote_command',
				'retryable'  => false,
				'status_code'=> 409,
			]
		);
		return [ 'ok' => true ];
	}

	/**
	 * Ignore one finding by immutable issue hash.
	 *
	 * @param array<string,mixed> $params
	 * @return array<string,mixed>
	 */
	private function finding_ignore( array $params ): array {
		$issue_hash = $this->resolve_issue_hash( $params );
		if ( $issue_hash === '' ) {
			return [ 'ok' => false, 'error' => 'issue_hash_required', 'error_code' => 'CMD_PARAM_INVALID' ];
		}

		if ( ! class_exists( '\\SiteFort\\Modules\\Scanner\\Repository\\IssueRepository' ) || ! class_exists( '\\SiteFort\\Modules\\Scanner\\ScanController' ) ) {
			return [ 'ok' => false, 'error' => 'scanner_unavailable', 'error_code' => 'CMD_PRECONDITION' ];
		}

		$issue = \SiteFort\Modules\Scanner\Repository\IssueRepository::getByHash( $issue_hash );
		if ( ! is_array( $issue ) ) {
			return [ 'ok' => false, 'error' => 'finding_not_found', 'error_code' => 'CMD_PRECONDITION' ];
		}

		if ( (int) ( $issue['resolved_at'] ?? 0 ) > 0 ) {
			return [ 'ok' => false, 'error' => 'finding_resolved', 'error_code' => 'CMD_PRECONDITION' ];
		}

		$is_ignored = ! empty( $issue['is_ignored'] ) || \SiteFort\Modules\Scanner\Repository\IssueRepository::isIgnored( $issue_hash );
		if ( $is_ignored ) {
			return [ 'ok' => true, 'issue_hash' => $issue_hash, 'finding_state' => 'ignored', 'noop' => true ];
		}

		$controller = new \SiteFort\Modules\Scanner\ScanController();
		if ( ! $controller->ignore_item( $issue_hash ) ) {
			return [ 'ok' => false, 'error' => 'finding_ignore_failed', 'error_code' => 'CMD_EXECUTION_FAILED' ];
		}

		\SiteFort\Modules\Scanner\ScanController::emit_issue_state_change( 'ignore', $issue_hash );
		if ( class_exists( '\\SiteFort\\Modules\\Cloud\\Services\\StatePush' ) ) {
			\SiteFort\Modules\Cloud\Services\StatePush::push_scan_state_now();
		}

		return [ 'ok' => true, 'issue_hash' => $issue_hash, 'finding_state' => 'ignored' ];
	}

	/**
	 * Remove the ignore state for one finding by immutable issue hash.
	 *
	 * @param array<string,mixed> $params
	 * @return array<string,mixed>
	 */
	private function finding_unignore( array $params ): array {
		$issue_hash = $this->resolve_issue_hash( $params );
		if ( $issue_hash === '' ) {
			return [ 'ok' => false, 'error' => 'issue_hash_required', 'error_code' => 'CMD_PARAM_INVALID' ];
		}

		if ( ! class_exists( '\\SiteFort\\Modules\\Scanner\\Repository\\IssueRepository' ) || ! class_exists( '\\SiteFort\\Modules\\Scanner\\ScanController' ) ) {
			return [ 'ok' => false, 'error' => 'scanner_unavailable', 'error_code' => 'CMD_PRECONDITION' ];
		}

		$issue = \SiteFort\Modules\Scanner\Repository\IssueRepository::getByHash( $issue_hash );
		if ( ! is_array( $issue ) ) {
			return [ 'ok' => false, 'error' => 'finding_not_found', 'error_code' => 'CMD_PRECONDITION' ];
		}

		if ( (int) ( $issue['resolved_at'] ?? 0 ) > 0 ) {
			return [ 'ok' => false, 'error' => 'finding_resolved', 'error_code' => 'CMD_PRECONDITION' ];
		}

		$is_ignored = ! empty( $issue['is_ignored'] ) || \SiteFort\Modules\Scanner\Repository\IssueRepository::isIgnored( $issue_hash );
		if ( ! $is_ignored ) {
			return [ 'ok' => true, 'issue_hash' => $issue_hash, 'finding_state' => 'active', 'noop' => true ];
		}

		$controller = new \SiteFort\Modules\Scanner\ScanController();
		if ( ! $controller->unignore_item( $issue_hash ) ) {
			return [ 'ok' => false, 'error' => 'finding_unignore_failed', 'error_code' => 'CMD_EXECUTION_FAILED' ];
		}

		\SiteFort\Modules\Scanner\ScanController::emit_issue_state_change( 'unignore', $issue_hash );
		if ( class_exists( '\\SiteFort\\Modules\\Cloud\\Services\\StatePush' ) ) {
			\SiteFort\Modules\Cloud\Services\StatePush::push_scan_state_now();
		}

		return [ 'ok' => true, 'issue_hash' => $issue_hash, 'finding_state' => 'active' ];
	}


	/**
	 * Update one or more plugins.
	 * params: { "plugins": ["slug/slug.php", ...] } or { "plugin": "slug/slug.php" }
	 *
	 * @param array<string,mixed> $params
	 * @return array<string,mixed>
	 */
	private function plugin_update( array $params ): array {
		$plugins = $this->resolve_plugin_list( $params );
		if ( empty( $plugins ) ) {
			return [ 'ok' => false, 'error' => 'no_plugins_specified', 'error_code' => 'CMD_PARAM_INVALID' ];
		}

		if ( ! function_exists( 'get_plugins' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
		if ( ! class_exists( '\\Plugin_Upgrader' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
		}

		$installed = array_keys( get_plugins() );

		$results = [];
		foreach ( $plugins as $plugin_file ) {
			$plugin_file = sanitize_text_field( $plugin_file );

			// Validate against installed plugin list to prevent path manipulation.
			if ( ! in_array( $plugin_file, $installed, true ) ) {
				$results[ $plugin_file ] = [ 'ok' => false, 'error' => 'plugin_not_installed' ];
				continue;
			}

			ob_start();
			$skin = new \Automatic_Upgrader_Skin();
			$upgrader = new \Plugin_Upgrader( $skin );
			$result = $upgrader->upgrade( $plugin_file );
			ob_end_clean();

			if ( is_wp_error( $result ) ) {
				$results[ $plugin_file ] = [ 'ok' => false, 'error' => $result->get_error_message() ];
			} elseif ( $result === true ) {
				$results[ $plugin_file ] = [ 'ok' => true ];
			} else {
				$results[ $plugin_file ] = [ 'ok' => false, 'error' => 'update_failed' ];
			}
		}

		$all_ok = ! in_array( false, array_column( $results, 'ok' ), true );
		return [ 'ok' => $all_ok, 'results' => $results ];
	}

	/**
	 * Activate or deactivate a plugin.
	 * params: { "plugin": "slug/slug.php", "action": "activate"|"deactivate" }
	 *
	 * @param array<string,mixed> $params
	 * @return array<string,mixed>
	 */
	private function plugin_toggle( array $params ): array {
		$plugin_file = sanitize_text_field( (string) ( $params['plugin'] ?? '' ) );
		$action = sanitize_text_field( (string) ( $params['action'] ?? '' ) );

		if ( $plugin_file === '' || ! in_array( $action, [ 'activate', 'deactivate' ], true ) ) {
			return [ 'ok' => false, 'error' => 'missing_plugin_or_action', 'error_code' => 'CMD_PARAM_INVALID' ];
		}

		if ( ! function_exists( 'activate_plugin' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}

		// Validate against installed plugin list to prevent path manipulation.
		if ( ! function_exists( 'get_plugins' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
		$installed = array_keys( get_plugins() );
		if ( ! in_array( $plugin_file, $installed, true ) ) {
			return [ 'ok' => false, 'error' => 'plugin_not_installed', 'error_code' => 'CMD_PARAM_INVALID' ];
		}

		if ( 'deactivate' === $action && defined( 'SITEFORT_PLUGIN_FILE' ) && $plugin_file === plugin_basename( SITEFORT_PLUGIN_FILE ) ) {
			return [ 'ok' => false, 'error' => 'connector_plugin_protected', 'error_code' => 'CMD_FORBIDDEN' ];
		}

		if ( $action === 'activate' ) {
			$result = activate_plugin( $plugin_file );
			if ( is_wp_error( $result ) ) {
				return [ 'ok' => false, 'error' => $result->get_error_message() ];
			}
			return [ 'ok' => true, 'status' => 'activated' ];
		}

		deactivate_plugins( $plugin_file );
		return [ 'ok' => true, 'status' => 'deactivated' ];
	}


	/**
	 * Update one or more themes.
	 * params: { "themes": ["theme-slug", ...] } or { "theme": "theme-slug" }
	 *
	 * @param array<string,mixed> $params
	 * @return array<string,mixed>
	 */
	private function theme_update( array $params ): array {
		$themes = [];
		if ( ! empty( $params['themes'] ) && is_array( $params['themes'] ) ) {
			$themes = array_map( 'sanitize_text_field', $params['themes'] );
		} elseif ( ! empty( $params['theme'] ) ) {
			$themes = [ sanitize_text_field( (string) $params['theme'] ) ];
		}

		if ( empty( $themes ) ) {
			return [ 'ok' => false, 'error' => 'no_themes_specified', 'error_code' => 'CMD_PARAM_INVALID' ];
		}

		if ( ! class_exists( '\\Theme_Upgrader' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
		}

		$results = [];
		foreach ( $themes as $theme_slug ) {
			ob_start();
			$skin = new \Automatic_Upgrader_Skin();
			$upgrader = new \Theme_Upgrader( $skin );
			$result = $upgrader->upgrade( $theme_slug );
			ob_end_clean();

			if ( is_wp_error( $result ) ) {
				$results[ $theme_slug ] = [ 'ok' => false, 'error' => $result->get_error_message() ];
			} elseif ( $result === true ) {
				$results[ $theme_slug ] = [ 'ok' => true ];
			} else {
				$results[ $theme_slug ] = [ 'ok' => false, 'error' => 'update_failed' ];
			}
		}

		$all_ok = ! in_array( false, array_column( $results, 'ok' ), true );
		return [ 'ok' => $all_ok, 'results' => $results ];
	}

	/**
	 * Switch the active theme.
	 * params: { "theme": "theme-slug" }
	 *
	 * @param array<string,mixed> $params
	 * @return array<string,mixed>
	 */
	private function theme_switch( array $params ): array {
		$theme_slug = sanitize_text_field( (string) ( $params['theme'] ?? '' ) );
		if ( $theme_slug === '' ) {
			return [ 'ok' => false, 'error' => 'no_theme_specified', 'error_code' => 'CMD_PARAM_INVALID' ];
		}

		$theme = wp_get_theme( $theme_slug );
		if ( ! $theme->exists() ) {
			return [ 'ok' => false, 'error' => 'theme_not_found', 'error_code' => 'CMD_PRECONDITION' ];
		}

		switch_theme( $theme_slug );

		return [ 'ok' => true, 'active_theme' => $theme_slug ];
	}


	/**
	 * Update WordPress core.
	 * params: { "version": "auto" } (auto = latest stable)
	 *
	 * @param array<string,mixed> $params
	 * @return array<string,mixed>
	 */
	private function core_update( array $params ): array {
		if ( ! function_exists( 'wp_version_check' ) ) {
			require_once ABSPATH . 'wp-includes/update.php';
		}
		if ( ! class_exists( '\\Core_Upgrader' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
		}

		// Force WP to check for the latest version.
		wp_version_check();

		$updates = get_core_updates();
		if ( ! is_array( $updates ) || empty( $updates ) ) {
			return [ 'ok' => true, 'message' => 'already_up_to_date' ];
		}

		$update = $updates[0];
		if ( isset( $update->response ) && $update->response === 'latest' ) {
			return [ 'ok' => true, 'message' => 'already_up_to_date' ];
		}

		ob_start();
		$skin = new \Automatic_Upgrader_Skin();
		$upgrader = new \Core_Upgrader( $skin );
		$result = $upgrader->upgrade( $update );
		ob_end_clean();

		if ( is_wp_error( $result ) ) {
			return [ 'ok' => false, 'error' => $result->get_error_message() ];
		}

		global $wp_version;
		return [ 'ok' => true, 'version' => $wp_version ];
	}


	/**
	 * Apply hardening settings from Console.
	 * params: { "settings": { "hide_wp_version": true, ... } }
	 *
	 * @param array<string,mixed> $params
	 * @return array<string,mixed>
	 */
	private function hardening_apply( array $params ): array {
		$new_settings = $params['settings'] ?? [];
		if ( ! is_array( $new_settings ) || empty( $new_settings ) ) {
			return [ 'ok' => false, 'error' => 'no_settings_provided', 'error_code' => 'CMD_PARAM_INVALID' ];
		}

		if ( ! class_exists( '\\SiteFort\\Modules\\Hardening\\Hardening' ) ) {
			return [ 'ok' => false, 'error' => 'hardening_module_unavailable', 'error_code' => 'CMD_PRECONDITION' ];
		}

		$sanitized = \SiteFort\Modules\Hardening\Hardening::sanitize_settings_patch( $new_settings );
		if ( empty( $sanitized ) ) {
			return [ 'ok' => false, 'error' => 'no_valid_settings_provided', 'error_code' => 'CMD_PARAM_INVALID' ];
		}

		// Same hard guard as REST path.
		if ( ! empty( $sanitized['rename_login_url_enabled'] ) && '' === get_option( 'permalink_structure' ) ) {
			return [ 'ok' => false, 'error' => 'rename_login_requires_pretty_permalinks', 'error_code' => 'CMD_PRECONDITION' ];
		}

		$current = \SiteFort\Includes\SettingsStore::get(
			\SiteFort\Includes\SettingsStore::COL_HARDENING,
			\SiteFort\Modules\Hardening\Hardening::get_default_settings()
		);
		if ( ! is_array( $current ) ) {
			$current = \SiteFort\Modules\Hardening\Hardening::get_default_settings();
		}
		$candidate = array_merge( $current, $sanitized );
		$login_error_code = \SiteFort\Modules\Hardening\Hardening::validate_login_slug_configuration( $candidate );
		if ( $login_error_code !== null ) {
			return [
				'ok'         => false,
				'error'      => \SiteFort\Modules\Hardening\Hardening::get_login_slug_validation_message( $login_error_code ),
				'error_code' => 'CMD_PARAM_INVALID',
			];
		}

		$module = new \SiteFort\Modules\Hardening\Hardening( 'sitefort', defined( 'SITEFORT_VERSION' ) ? SITEFORT_VERSION : '1.0.0' );
		$module->update_settings( $sanitized );

		return [ 'ok' => true, 'applied' => array_keys( $sanitized ) ];
	}


	/**
	 * Update firewall rules from Console.
	 * params: { "settings": { "enabled": true, "banned_ips": "...", ... } }
	 *
	 * @param array<string,mixed> $params
	 * @return array<string,mixed>
	 */
	private function firewall_update_rules( array $params ): array {
		$new_settings = $params['settings'] ?? [];
		if ( ! is_array( $new_settings ) || empty( $new_settings ) ) {
			return [ 'ok' => false, 'error' => 'no_settings_provided', 'error_code' => 'CMD_PARAM_INVALID' ];
		}

		$current = FirewallSettings::get();
		$previous_settings = $current;
		$requested_allowlist = array_key_exists( 'allowlist', $new_settings ) ? $new_settings['allowlist'] : null;
		$lockout_managed_keys = [ 'banned_ips', 'allowlist' ];
		$previous_blocklist_enabled = ! empty( $current['blocklist_enabled'] );
		try {
			$result = FirewallConfigService::apply_patch(
				$current,
				$new_settings,
				$lockout_managed_keys,
				FirewallConfigService::CONTEXT_NON_INTERACTIVE
			);
			$merged = $result['settings'];
		} catch ( \InvalidArgumentException $exception ) {
			return [
				'ok' => false,
				'error' => sanitize_text_field( $exception->getMessage() ),
				'error_code' => 'CMD_PARAM_INVALID',
			];
		}

		if ( ! FirewallConfigService::persist_non_interactive_settings( $merged ) ) {
			return [ 'ok' => false, 'error' => 'firewall_settings_save_failed', 'error_code' => 'CMD_EXECUTION_FAILED' ];
		}

		$allowlist_result = null;
		if ( $requested_allowlist !== null ) {
			$lockout_manager = new LockoutManager();
			$allowlist_result = $lockout_manager->replace_allowlist( $requested_allowlist );

			if ( empty( $allowlist_result['success'] ) ) {
				$rollback_ok = FirewallConfigService::persist_non_interactive_settings( $previous_settings );
				$code = sanitize_key( (string) ( $allowlist_result['code'] ?? 'allowlist_update_failed' ) );

				return [
					'ok'           => false,
					'error'        => sanitize_text_field( (string) ( $allowlist_result['message'] ?? 'allowlist_update_failed' ) ),
					'error_code'   => in_array( $code, [ 'overlap_conflict', 'settings_conflict' ], true )
						? 'CMD_PARAM_INVALID'
						: 'CMD_EXECUTION_FAILED',
					'lockout_code' => $code,
					'rollback_ok'  => $rollback_ok,
				];
			}
		}

		$blocklist_sync = [
			'triggered' => false,
			'success'   => true,
		];
		if ( ! $previous_blocklist_enabled && ! empty( $merged['blocklist_enabled'] ) ) {
			try {
				$service = new \SiteFort\Modules\Firewall\Blocklist\BlocklistService( $merged );
				$stats = $service->sync();
				$blocklist_sync = [
					'triggered' => true,
					'success'   => true,
					'stats'     => is_array( $stats ) ? $stats : [],
				];
			} catch ( \Throwable $throwable ) {
				$blocklist_sync = [
					'triggered' => true,
					'success'   => false,
					'error'     => sanitize_text_field( $throwable->getMessage() ),
				];
			}
		}

		$applied = [];
		foreach ( array_keys( FirewallSettings::defaults() ) as $key ) {
			if ( in_array( $key, $lockout_managed_keys, true ) ) {
				continue;
			}
			if ( array_key_exists( $key, $new_settings ) ) {
				$applied[] = $key;
			}
		}
		if ( $allowlist_result !== null ) {
			$applied[] = 'allowlist';
		}

		return [
			'ok'            => true,
			'applied'       => $applied,
			'blocklist_sync'=> $blocklist_sync,
			'lockouts'      => [
				'allowlist' => $allowlist_result,
			],
		];
	}

	/**
	 * Apply a config template that sets hardening + firewall settings in one shot.
	 * params: { "hardening": {...}, "firewall": {...} }
	 *
	 * @param array<string,mixed> $params
	 * @return array<string,mixed>
	 */
	private function config_apply_template( array $params ): array {
		$applied = [];

		if ( ! empty( $params['hardening'] ) && is_array( $params['hardening'] ) ) {
			$hr = $this->hardening_apply( [ 'settings' => $params['hardening'] ] );
			$applied['hardening'] = $hr;
		}

		if ( ! empty( $params['firewall'] ) && is_array( $params['firewall'] ) ) {
			$fr = $this->firewall_update_rules( [ 'settings' => $params['firewall'] ] );
			$applied['firewall'] = $fr;
		}

		if ( ! empty( $params['notification'] ) && is_array( $params['notification'] ) ) {
			$nr = $this->notification_update( [ 'settings' => $params['notification'] ] );
			$applied['notification'] = $nr;
		}

		if ( ! empty( $params['two_factor'] ) && is_array( $params['two_factor'] ) ) {
			$result = $this->two_factor_configure( [ 'settings' => $params['two_factor'] ] );
			$applied['two_factor'] = $result;
		}

		if ( ! empty( $params['login'] ) && is_array( $params['login'] ) ) {
			$result = $this->login_configure( [ 'settings' => $params['login'] ] );
			$applied['login'] = $result;
		}

		if ( ! empty( $params['scanner_settings'] ) && is_array( $params['scanner_settings'] ) ) {
			$result = $this->scanner_settings_update( [ 'settings' => $params['scanner_settings'] ] );
			$applied['scanner_settings'] = $result;
		}

		if ( empty( $applied ) ) {
			return [ 'ok' => false, 'error' => 'no_config_sections', 'error_code' => 'CMD_PARAM_INVALID' ];
		}

		$all_ok = true;
		foreach ( $applied as $section ) {
			if ( empty( $section['ok'] ) ) {
				$all_ok = false;
			}
		}

		if ( $all_ok ) {
			return [ 'ok' => true, 'sections' => $applied ];
		}

		$failed_section    = '';
		$failed_error      = 'command_failed';
		$failed_error_code = 'CMD_EXECUTION_FAILED';
		foreach ( $applied as $section_name => $section_result ) {
			if ( ! empty( $section_result['ok'] ) ) {
				continue;
			}

			$failed_section = sanitize_key( (string) $section_name );
			$failed_error = sanitize_text_field( (string) ( $section_result['error'] ?? $failed_error ) );
			$failed_error_code = sanitize_text_field( (string) ( $section_result['error_code'] ?? $failed_error_code ) );
			break;
		}

		return [
			'ok'            => false,
			'error'         => $failed_error,
			'error_code'    => $failed_error_code,
			'failed_section'=> $failed_section,
			'sections'      => $applied,
		];
	}


	/**
	 * Update plugin notification settings from Console.
	 * params: { "settings": { "email_enabled": true, "events": {...}, ... } }
	 *
	 * @param array<string,mixed> $params
	 * @return array<string,mixed>
	 */
	private function notification_update( array $params ): array {
		$settings = $params['settings'] ?? $params;
		if ( ! is_array( $settings ) || empty( $settings ) ) {
			return [ 'ok' => false, 'error' => 'no_settings_provided', 'error_code' => 'CMD_PARAM_INVALID' ];
		}

		if ( ! class_exists( '\\SiteFort\\Modules\\Notifications\\NotificationSettings' ) ) {
			return [ 'ok' => false, 'error' => 'notifications_module_unavailable', 'error_code' => 'CMD_PRECONDITION' ];
		}

		$ns = new \SiteFort\Modules\Notifications\NotificationSettings();
		try {
			$ns->save( $settings );
		} catch ( \InvalidArgumentException $exception ) {
			return [
				'ok'         => false,
				'error'      => sanitize_text_field( $exception->getMessage() ),
				'error_code' => 'CMD_PARAM_INVALID',
			];
		}

		StatePush::push_notification_state_now( true );

		return [ 'ok' => true, 'applied' => array_keys( $settings ) ];
	}


	/**
	 * Update onsite scanner settings (schedule, intensity, exclusions, quarantine
	 * retention) from Console.
	 * params: { "settings": { "schedule": "daily", "scanIntensity": "standard", ... } }
	 *
	 * @param array<string,mixed> $params
	 * @return array<string,mixed>
	 */
	private function scanner_settings_update( array $params ): array {
		$settings = $params['settings'] ?? [];
		if ( ! is_array( $settings ) || empty( $settings ) ) {
			return [ 'ok' => false, 'error' => 'no_settings_provided', 'error_code' => 'CMD_PARAM_INVALID' ];
		}

		if ( ! class_exists( '\\SiteFort\\Modules\\Scanner\\Service\\ScannerSettings' ) ) {
			return [ 'ok' => false, 'error' => 'scanner_module_unavailable', 'error_code' => 'CMD_PRECONDITION' ];
		}

		if ( array_key_exists( 'scanExclusions', $settings ) ) {
			$settings['scanExclusions'] = \SiteFort\Modules\Scanner\Service\ScannerSettings::normalize_scan_exclusions( $settings['scanExclusions'] );
		}

		// Re-check entitlement server-side; a stored schedule must never outrun the plan.
		$allows_scheduled_scans = class_exists( '\\SiteFort\\Modules\\Cloud\\Service\\Entitlements' )
			&& \SiteFort\Modules\Cloud\Service\Entitlements::allowsScheduledScans();

		$requested_schedule = isset( $settings['schedule'] ) ? sanitize_key( (string) $settings['schedule'] ) : '';
		if ( ! $allows_scheduled_scans && $requested_schedule !== '' && $requested_schedule !== 'none' ) {
			$settings['schedule'] = 'none';
		}

		if ( ! \SiteFort\Modules\Scanner\Service\ScannerSettings::save( $settings ) ) {
			return [ 'ok' => false, 'error' => 'scanner_settings_save_failed', 'error_code' => 'CMD_EXECUTION_FAILED' ];
		}

		// Schedule changes need WP-Cron rescheduled, not just the setting written.
		if ( class_exists( '\\SiteFort\\Modules\\Scanner\\Queue\\ScanCron' ) ) {
			$current = \SiteFort\Modules\Scanner\Service\ScannerSettings::get_all();
			\SiteFort\Modules\Scanner\Queue\ScanCron::reconcile_scheduled_scan( $current, $allows_scheduled_scans, true );
		}

		$applied = [];
		foreach ( [ 'schedule', 'scanIntensity', 'scanExclusions', 'quarantine_retention_days' ] as $key ) {
			if ( array_key_exists( $key, $settings ) ) {
				$applied[] = $key;
			}
		}

		return [ 'ok' => true, 'applied' => $applied ];
	}


	/**
	 * Sync branding settings from Console.
	 * params: { "branding": { "plugin_name": "...", ... } }
	 *
	 * @param array<string,mixed> $params
	 * @return array<string,mixed>
	 */
	private function branding_sync( array $params ): array {
		if ( ! class_exists( '\\SiteFort\\Modules\\WhiteLabel\\WhiteLabel' ) ) {
			return [ 'ok' => false, 'error' => 'whitelabel_unavailable', 'error_code' => 'CMD_PRECONDITION' ];
		}

		$branding = $this->validate_branding_sync_params( $params );
		if ( is_wp_error( $branding ) ) {
			return [
				'ok'         => false,
				'error'      => sanitize_text_field( $branding->get_error_message() ),
				'error_code' => 'CMD_PARAM_INVALID',
			];
		}

		$cloud_settings = class_exists( '\\SiteFort\\Modules\\Cloud\\Service\\CloudSettings' )
			? \SiteFort\Modules\Cloud\Service\CloudSettings::get_all()
			: [];

		\SiteFort\Modules\WhiteLabel\WhiteLabel::sync_for_license_state( $cloud_settings, $branding, true );
		if ( ! \SiteFort\Modules\WhiteLabel\WhiteLabel::license_allows_branding( $cloud_settings ) ) {
			return [
				'ok'      => true,
				'applied' => [],
				'cleared' => true,
				'reason'  => 'branding_not_licensed',
			];
		}

		return [ 'ok' => true, 'applied' => array_keys( $branding ) ];
	}

	/**
	 * Validate branding.sync payload before local persistence.
	 *
	 * This mirrors the Console command schema so a malformed signed command fails
	 * closed instead of clearing or partially applying local white-label settings.
	 *
	 * @param array<string,mixed> $params
	 * @return array<string,mixed>|\WP_Error
	 */
	private function validate_branding_sync_params( array $params ) {
		$branding = $params['branding'] ?? null;
		if ( ! is_array( $branding ) ) {
			return new \WP_Error( 'invalid_branding_payload', 'Branding payload must be an object.' );
		}
		if ( [] === $branding ) {
			return [];
		}

		$allowed_keys = [
			'plugin_name',
			'company_name',
			'hide_attribution',
			'plugin_description',
			'author_name',
			'author_url',
			'plugin_url',
			'hide_plugin',
			'support_url',
			'admin_footer_text',
			'logo_url',
			'email_from_name',
		];
		foreach ( array_keys( $branding ) as $key ) {
			if ( ! is_string( $key ) || ! in_array( $key, $allowed_keys, true ) ) {
				return new \WP_Error( 'invalid_branding_field', 'Branding payload contains an unsupported field.' );
			}
		}

		foreach ( [ 'plugin_name' => 80, 'company_name' => 120 ] as $field => $max_length ) {
			if ( ! array_key_exists( $field, $branding ) || ! $this->is_valid_branding_text( $branding[ $field ], $max_length, true ) ) {
				return new \WP_Error( 'invalid_branding_field', sprintf( 'Invalid branding field: %s.', $field ) );
			}
		}

		foreach ( [ 'hide_attribution', 'hide_plugin' ] as $field ) {
			if ( ! array_key_exists( $field, $branding ) || ! is_bool( $branding[ $field ] ) ) {
				return new \WP_Error( 'invalid_branding_field', sprintf( 'Invalid branding field: %s.', $field ) );
			}
		}

		foreach (
			[
				'plugin_description' => 300,
				'author_name'        => 120,
				'admin_footer_text'  => 240,
				'email_from_name'    => 120,
			] as $field => $max_length
		) {
			if ( array_key_exists( $field, $branding ) && ! $this->is_valid_branding_text( $branding[ $field ], $max_length, false ) ) {
				return new \WP_Error( 'invalid_branding_field', sprintf( 'Invalid branding field: %s.', $field ) );
			}
		}

		foreach ( [ 'author_url', 'plugin_url', 'support_url', 'logo_url' ] as $field ) {
			if ( array_key_exists( $field, $branding ) && ! $this->is_valid_branding_url( $branding[ $field ] ) ) {
				return new \WP_Error( 'invalid_branding_field', sprintf( 'Invalid branding field: %s.', $field ) );
			}
		}

		return \SiteFort\Modules\WhiteLabel\WhiteLabel::normalize_branding( $branding );
	}

	/**
	 * @param mixed $value
	 */
	private function is_valid_branding_text( $value, int $max_length, bool $required ): bool {
		if ( null === $value ) {
			return ! $required;
		}
		if ( ! is_string( $value ) ) {
			return false;
		}
		$value = trim( $value );
		if ( '' === $value ) {
			return ! $required;
		}
		if ( preg_match( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $value ) ) {
			return false;
		}
		$length = function_exists( 'mb_strlen' ) ? mb_strlen( $value ) : strlen( $value );
		return $length <= $max_length;
	}

	/**
	 * @param mixed $value
	 */
	private function is_valid_branding_url( $value ): bool {
		if ( null === $value ) {
			return true;
		}
		if ( ! is_string( $value ) ) {
			return false;
		}
		if ( '' === trim( $value ) ) {
			return true;
		}
		return \SiteFort\Modules\WhiteLabel\WhiteLabel::sanitize_brand_url( $value ) !== '';
	}


	/**
	 * Pull the latest plan and entitlements from Console and store locally.
	 * Called after a plan switch so the plugin reflects the change immediately.
	 *
	 * @return array<string,mixed>
	 */
	private function license_sync(): array {
		if ( ! class_exists( '\\SiteFort\\Modules\\Cloud\\License\\LicenseManager' ) ) {
			return [ 'ok' => false, 'error' => 'license_manager_unavailable', 'error_code' => 'CMD_PRECONDITION' ];
		}
		$settings = \SiteFort\Modules\Cloud\Service\CloudSettings::get_all();
		$manager  = new \SiteFort\Modules\Cloud\License\LicenseManager( $settings );
		$result   = $manager->revalidate();
		if ( is_wp_error( $result ) ) {
			return [ 'ok' => false, 'error' => $result->get_error_message(), 'error_code' => 'LICENSE_SYNC_FAILED' ];
		}
		// Converge local state in this same request. This command executes inline in a REST
		// push, where ScanCron::init() skips its own reconciliation, so a suspended site would
		// otherwise keep its scheduled-scan event until the next admin page view.
		\SiteFort\Modules\Cloud\Service\Entitlements::reconcile();
		$settings = \SiteFort\Modules\Cloud\Service\CloudSettings::get_all();
		return [
			'ok'   => true,
			'plan' => (string) ( $settings['license_plan'] ?? 'free' ),
		];
	}

	/**
	 * Configure 2FA settings from Console.
	 * params: { "settings": { "enabled": true, "enforce_for_roles": ["administrator"], ... } }
	 */
	private function two_factor_configure( array $params ): array {
		$settings = $params['settings'] ?? [];
		if ( ! is_array( $settings ) || empty( $settings ) ) {
			return [ 'ok' => false, 'error' => 'no_settings_provided', 'error_code' => 'CMD_PARAM_INVALID' ];
		}

		$defaults = class_exists( '\\SiteFort\\Modules\\TwoFactor\\TwoFactor' )
			? \SiteFort\Modules\TwoFactor\TwoFactor::get_default_settings()
			: [
				'enabled'              => false,
				'enforce_for_roles'    => [ 'administrator' ],
				'remember_device_days' => 30,
				'grace_period_days'    => 3,
				'allowed_methods'      => [ 'totp', 'email' ],
			];

		$current = \SiteFort\Includes\SettingsStore::get( \SiteFort\Includes\SettingsStore::COL_TWOFA, $defaults );
		if ( ! is_array( $current ) ) {
			$current = $defaults;
		}

		$sanitized = $current;
		if ( array_key_exists( 'enabled', $settings ) ) {
			$sanitized['enabled'] = (bool) $settings['enabled'];
		}
		if ( array_key_exists( 'enforce_for_roles', $settings ) && is_array( $settings['enforce_for_roles'] ) ) {
			$raw_roles = array_map( 'sanitize_key', $settings['enforce_for_roles'] );
			$wp_roles  = function_exists( 'wp_roles' ) ? wp_roles() : null;
			$valid_map = ( $wp_roles && is_array( $wp_roles->roles ) ) ? $wp_roles->roles : [];
			$sanitized['enforce_for_roles'] = array_values(
				array_unique(
					array_filter(
						$raw_roles,
						static function ( $role ) use ( $valid_map ) {
							return is_string( $role ) && $role !== '' && isset( $valid_map[ $role ] );
						}
					)
				)
			);
		}
		if ( array_key_exists( 'remember_device_days', $settings ) ) {
			$sanitized['remember_device_days'] = max( 1, min( 365, absint( $settings['remember_device_days'] ) ) );
		}
		if ( array_key_exists( 'grace_period_days', $settings ) ) {
			$sanitized['grace_period_days'] = max( 0, min( 30, absint( $settings['grace_period_days'] ) ) );
		}
		if ( array_key_exists( 'allowed_methods', $settings ) && is_array( $settings['allowed_methods'] ) ) {
			$allowed_methods = class_exists( '\\SiteFort\\Modules\\TwoFactor\\TwoFactor' )
				? \SiteFort\Modules\TwoFactor\TwoFactor::sanitize_allowed_methods( $settings['allowed_methods'] )
				: [];
			$sanitized['allowed_methods'] = ! empty( $allowed_methods ) ? $allowed_methods : ( $defaults['allowed_methods'] ?? [ 'totp', 'email' ] );
		}

		if ( ! \SiteFort\Includes\SettingsStore::save( \SiteFort\Includes\SettingsStore::COL_TWOFA, $sanitized ) ) {
			return [ 'ok' => false, 'error' => 'two_factor_settings_save_failed', 'error_code' => 'CMD_EXECUTION_FAILED' ];
		}

		if ( class_exists( '\\SiteFort\\Modules\\TwoFactor\\TwoFactor' ) ) {
			\SiteFort\Modules\TwoFactor\TwoFactor::apply_settings_side_effects( $current, $sanitized );
		}

		return [ 'ok' => true, 'applied' => array_keys( $settings ) ];
	}


	/**
	 * Regenerate WordPress security salts remotely.
	 */
	private function tools_regenerate_salts(): array {
		if ( ! class_exists( '\\SiteFort\\Modules\\Tools\\SaltsRegeneration' ) ) {
			return [ 'ok' => false, 'error' => 'tools_module_unavailable', 'error_code' => 'CMD_PRECONDITION' ];
		}

		$salts  = new \SiteFort\Modules\Tools\SaltsRegeneration();
		$result = $salts->regenerate();

		if ( is_wp_error( $result ) ) {
			return [ 'ok' => false, 'error' => $result->get_error_message() ];
		}

		do_action( 'sitefort_tools_action_completed', 'regenerate_salts' );
		return [ 'ok' => true, 'message' => 'salts_regenerated' ];
	}

	/**
	 * Change the database table prefix remotely.
	 * params: { "new_prefix": "wp_abc_" }
	 */
	private function tools_change_prefix( array $params ): array {
		$new_prefix = sanitize_text_field( $params['new_prefix'] ?? '' );
		if ( empty( $new_prefix ) ) {
			return [ 'ok' => false, 'error' => 'new_prefix_required', 'error_code' => 'CMD_PARAM_INVALID' ];
		}

		if ( ! class_exists( '\\SiteFort\\Modules\\Tools\\DatabasePrefixChanger' ) ) {
			return [ 'ok' => false, 'error' => 'tools_module_unavailable', 'error_code' => 'CMD_PRECONDITION' ];
		}

		$changer = new \SiteFort\Modules\Tools\DatabasePrefixChanger();
		$result  = $changer->change( $new_prefix );

		if ( is_wp_error( $result ) ) {
			return [ 'ok' => false, 'error' => $result->get_error_message(), 'code' => $result->get_error_code() ];
		}

		do_action( 'sitefort_tools_action_completed', 'change_prefix' );
		return [
			'ok'        => true,
			'new_prefix' => $new_prefix,
			'warnings'  => $changer->get_last_warnings(),
		];
	}


	/**
	 * Configure login protection settings from Console.
	 * params: { "settings": { "rename_login_url_enabled": true, "rename_login_url_slug": "my-login" } }
	 */
	private function login_configure( array $params ): array {
		$settings = $params['settings'] ?? [];
		if ( ! is_array( $settings ) || empty( $settings ) ) {
			return [ 'ok' => false, 'error' => 'no_settings_provided', 'error_code' => 'CMD_PARAM_INVALID' ];
		}

		if ( ! class_exists( '\\SiteFort\\Modules\\Hardening\\Hardening' ) ) {
			return [ 'ok' => false, 'error' => 'hardening_module_unavailable', 'error_code' => 'CMD_PRECONDITION' ];
		}

		$sanitized = \SiteFort\Modules\Hardening\Hardening::sanitize_settings_patch(
			$settings,
			\SiteFort\Modules\Hardening\Hardening::get_login_setting_keys()
		);
		if ( empty( $sanitized ) ) {
			return [ 'ok' => false, 'error' => 'no_valid_settings_provided', 'error_code' => 'CMD_PARAM_INVALID' ];
		}

		// Same hard guard as REST path.
		if ( ! empty( $sanitized['rename_login_url_enabled'] ) && '' === get_option( 'permalink_structure' ) ) {
			return [ 'ok' => false, 'error' => 'rename_login_requires_pretty_permalinks', 'error_code' => 'CMD_PRECONDITION' ];
		}

		$current = \SiteFort\Includes\SettingsStore::get(
			\SiteFort\Includes\SettingsStore::COL_HARDENING,
			\SiteFort\Modules\Hardening\Hardening::get_default_settings()
		);
		if ( ! is_array( $current ) ) {
			$current = \SiteFort\Modules\Hardening\Hardening::get_default_settings();
		}
		$candidate = array_merge( $current, $sanitized );
		$login_error_code = \SiteFort\Modules\Hardening\Hardening::validate_login_slug_configuration( $candidate );
		if ( $login_error_code !== null ) {
			return [
				'ok'         => false,
				'error'      => \SiteFort\Modules\Hardening\Hardening::get_login_slug_validation_message( $login_error_code ),
				'error_code' => 'CMD_PARAM_INVALID',
			];
		}

		$module = new \SiteFort\Modules\Hardening\Hardening( 'sitefort', defined( 'SITEFORT_VERSION' ) ? SITEFORT_VERSION : '1.0.0' );
		$module->update_settings( $sanitized );

		return [ 'ok' => true, 'applied' => array_keys( $sanitized ) ];
	}

	/**
	 * Verify Cloudflare API token validity.
	 */
	private function cloudflare_verify(): array {
		if ( ! class_exists( '\\SiteFort\\Modules\\Cloudflare\\Manager' ) ) {
			return [ 'ok' => false, 'error' => 'cloudflare_module_unavailable', 'error_code' => 'CMD_PRECONDITION' ];
		}

		$manager = new \SiteFort\Modules\Cloudflare\Manager();
		$result = $manager->verify_connection();

		if ( is_wp_error( $result ) ) {
			return [ 'ok' => false, 'error' => $result->get_error_message() ];
		}

		do_action( 'sitefort_cloudflare_action', 'verify', '' );
		$success = isset( $result['success'] ) ? (bool) $result['success'] : true;
		return [ 'ok' => $success, 'result' => $result ];
	}

	/**
	 * Update Cloudflare Sync policy from Console.
	 *
	 * params: {
	 *   "enabled": true|false,
	 *   "auto_block_firewall_events": true|false,
	 *   "event_threshold": 3,
	 *   "event_window_minutes": 10,
	 *   "event_ban_minutes": 120,
	 *   "max_block_entries": 500
	 * }
	 */
	private function cloudflare_policy_update( array $params ): array {
		if ( ! class_exists( '\\SiteFort\\Modules\\Cloudflare\\Manager' ) ) {
			return [ 'ok' => false, 'error' => 'cloudflare_module_unavailable', 'error_code' => 'CMD_PRECONDITION' ];
		}

		$manager = new \SiteFort\Modules\Cloudflare\Manager();
		$policy = [];
		$allowed_keys = [
			'auto_block_firewall_events',
			'event_threshold',
			'event_window_minutes',
			'event_ban_minutes',
			'max_block_entries',
		];

		if ( array_key_exists( 'enabled', $params ) ) {
			$policy['enabled'] = ! empty( $params['enabled'] );
		}

		foreach ( $allowed_keys as $key ) {
			if ( array_key_exists( $key, $params ) ) {
				$policy[ $key ] = $params[ $key ];
			}
		}

		$result = $manager->save_policy( $policy );

		return [ 'ok' => true, 'result' => $result ];
	}

	/**
	 * Apply a one-time console-delivered integration credential to local secure storage.
	 *
	 * params: {
	 *   "integration": "cloudflare",
	 *   "secret_name": "api_token",
	 *   "delivery_token": "uuid"
	 * }
	 *
	 * @param array<string,mixed> $params
	 * @return array<string,mixed>
	 */
	private function integration_secret_apply( array $params ): array {
		$service = new IntegrationSecretApplyService();
		$result = $service->apply( $params );
		if ( is_wp_error( $result ) ) {
			return [
				'ok'         => false,
				'error'      => $result->get_error_message(),
				'error_code' => 'CMD_EXTERNAL',
			];
		}

		return is_array( $result ) ? $result : [ 'ok' => true ];
	}


	/**
	 * Propagate network settings to subsites.
	 * params: { "module": "firewall"|"hardening"|"all" }
	 */
	private function multisite_propagate( array $params ): array {
		$result = \SiteFort\Modules\Multisite\NetworkSettingsService::propagate( (string) ( $params['module'] ?? 'all' ) );
		if ( is_wp_error( $result ) ) {
			$error_code = (string) $result->get_error_code();
			return [
				'ok'         => false,
				'error'      => $error_code,
				'error_code' => ( 'invalid_module' === $error_code ) ? 'CMD_PARAM_INVALID' : 'CMD_PRECONDITION',
				'message'    => $result->get_error_message(),
			];
		}

		return $result;
	}


	/**
	 * Export all plugin settings as JSON.
	 */
	private function settings_export(): array {
		$notification = [];
		if ( class_exists( '\\SiteFort\\Modules\\Notifications\\NotificationSettings' ) ) {
			$notification = ( new \SiteFort\Modules\Notifications\NotificationSettings() )->get_redacted();
		}

		$export = [
			'hardening'    => \SiteFort\Includes\SettingsStore::get( \SiteFort\Includes\SettingsStore::COL_HARDENING ),
			'firewall'     => \SiteFort\Modules\Firewall\FirewallSettings::redact_sensitive_keys(
				\SiteFort\Modules\Firewall\FirewallSettings::get(),
				false
			),
			'two_factor'   => \SiteFort\Includes\SettingsStore::get( \SiteFort\Includes\SettingsStore::COL_TWOFA ),
			'notification' => $notification,
		];

		return [ 'ok' => true, 'data' => $export, 'exported_at' => time() ];
	}

	/**
	 * Import settings from a Console template.
	 *
	 * Returns a truthful batch result contract with per-module outcomes and an
	 * aggregate status of success, partial, or failed.
	 *
	 * params: { "data": { "hardening": {...}, "firewall": {...}, ... } }
	 */
	private function settings_import( array $params ): array {
		$data = $params['data'] ?? [];
		if ( ! is_array( $data ) || empty( $data ) ) {
			return [ 'ok' => false, 'error' => 'no_data_provided', 'error_code' => 'CMD_PARAM_INVALID' ];
		}

		$module_results = [];

		if ( ! empty( $data['hardening'] ) && is_array( $data['hardening'] ) ) {
			$result = $this->hardening_apply( [ 'settings' => $data['hardening'] ] );
			$module_results['hardening'] = [
				'status' => ! empty( $result['ok'] ) ? 'success' : 'failed',
				'result' => $result,
			];
		}

		if ( ! empty( $data['firewall'] ) && is_array( $data['firewall'] ) ) {
			$result = $this->firewall_update_rules( [ 'settings' => $data['firewall'] ] );
			$module_results['firewall'] = [
				'status' => ! empty( $result['ok'] ) ? 'success' : 'failed',
				'result' => $result,
			];
		}

		if ( ! empty( $data['two_factor'] ) && is_array( $data['two_factor'] ) ) {
			$result = $this->two_factor_configure( [ 'settings' => $data['two_factor'] ] );
			$module_results['two_factor'] = [
				'status' => ! empty( $result['ok'] ) ? 'success' : 'failed',
				'result' => $result,
			];
		}

		if ( ! empty( $data['notification'] ) && is_array( $data['notification'] ) ) {
			$result = $this->notification_update( [ 'settings' => $data['notification'] ] );
			$module_results['notification'] = [
				'status' => ! empty( $result['ok'] ) ? 'success' : 'failed',
				'result' => $result,
			];
		}

		if ( empty( $module_results ) ) {
			return [
				'ok' => false,
				'error' => 'no_supported_modules',
				'error_code' => 'CMD_PARAM_INVALID',
				'message' => 'No supported settings modules were provided for import.',
			];
		}

		$imported = [];
		$failed_modules = [];
		foreach ( $module_results as $module_key => $module_result ) {
			$module_status = (string) ( $module_result['status'] ?? 'failed' );
			$imported[ $module_key ] = $module_status === 'success';
			if ( $module_status !== 'success' ) {
				$failed_modules[] = $module_key;
			}
		}

		$status = 'success';
		if ( ! empty( $failed_modules ) && count( $failed_modules ) === count( $module_results ) ) {
			$status = 'failed';
		} elseif ( ! empty( $failed_modules ) ) {
			$status = 'partial';
		}

		return [
			'ok' => $status === 'success',
			'status' => $status,
			'imported' => $imported,
			'failed_modules' => $failed_modules,
			'results' => $module_results,
		];
	}


	/**
	 * Query audit log entries from Console.
	 * params: { "limit": 50, "offset": 0, "event_filter": "Login Failed", "date_from": 1700000000 }
	 */
	private function audit_query( array $params ): array {
		global $wpdb;
		$table = self::table_identifier( $wpdb->prefix . 'sitefort_audit_log' );
		$table_name = trim( $table, '`' );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Checks existence of a plugin-owned audit table before querying it.
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) !== $table_name ) {
			return [ 'ok' => false, 'error' => 'audit_table_not_found', 'error_code' => 'CMD_PRECONDITION' ];
		}

		$limit  = max( 1, min( 500, absint( $params['limit'] ?? 50 ) ) );
		$offset = max( 0, absint( $params['offset'] ?? 0 ) );

		$conditions = [ '1=1' ];
		$binds      = [];

		if ( ! empty( $params['event_filter'] ) ) {
			$conditions[] = 'event = %s';
			$binds[]      = sanitize_text_field( $params['event_filter'] );
		}

		if ( ! empty( $params['date_from'] ) ) {
			$conditions[] = 'created_at >= %d';
			$binds[]      = absint( $params['date_from'] );
		}

		if ( ! empty( $params['date_to'] ) ) {
			$conditions[] = 'created_at <= %d';
			$binds[]      = absint( $params['date_to'] );
		}

		$where = implode( ' AND ', $conditions );
		$binds[] = $limit;
		$binds[] = $offset;
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,PluginCheck.Security.DirectDB.UnescapedDBParameter
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT id, event, message, user_id, ip_address, level, created_at FROM {$table} WHERE {$where} ORDER BY created_at DESC LIMIT %d OFFSET %d",
				...$binds
			),
			ARRAY_A
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,PluginCheck.Security.DirectDB.UnescapedDBParameter

		return [ 'ok' => true, 'entries' => $rows ?: [], 'count' => count( $rows ?: [] ) ];
	}

	/**
	 * Escape a plugin-owned table identifier for direct interpolation.
	 */
	private static function table_identifier( string $table ): string {
		if ( $table === '' || strlen( $table ) > 64 || ! preg_match( '/^[A-Za-z0-9_]+$/', $table ) ) {
			throw new \InvalidArgumentException( 'Invalid table identifier.' );
		}

		return '`' . str_replace( '`', '``', $table ) . '`';
	}


	/**
	 * Resolve a list of plugin files from params.
	 *
	 * @param array<string,mixed> $params
	 * @return string[]
	 */
	private function resolve_plugin_list( array $params ): array {
		if ( ! empty( $params['plugins'] ) && is_array( $params['plugins'] ) ) {
			return array_map( 'sanitize_text_field', $params['plugins'] );
		}
		if ( ! empty( $params['plugin'] ) ) {
			return [ sanitize_text_field( (string) $params['plugin'] ) ];
		}
		return [];
	}

	/**
	 * Resolve and validate one immutable issue hash from command params.
	 *
	 * @param array<string,mixed> $params
	 * @return string
	 */
	private function resolve_issue_hash( array $params ): string {
		$issue_hash = strtolower( sanitize_text_field( (string) ( $params['issue_hash'] ?? '' ) ) );
		if ( $issue_hash === '' ) {
			return '';
		}

		return preg_match( '/^[a-f0-9]{64}$/', $issue_hash ) ? $issue_hash : '';
	}



	/**
	 * Handle a Console request to enable or disable the connection.
	 *
	 * Enforced entirely by SiteFort\Modules\Cloud\Service\ConsoleGate: when the local gate is off,
	 * an "enable" request is held as a pending approval instead of connecting. Console cannot open
	 * its own gate through this or any other command payload.
	 *
	 * @param bool $enabled
	 * @return array<string,mixed>
	 */
	private function console_set_enabled( bool $enabled ): array {
		return \SiteFort\Modules\Cloud\Service\ConsoleGate::handle_console_command( $enabled );
	}

	/**
	 * firewall.request_state_push
	 *
	 * Marks the firewall domain dirty so the cached readiness (from the last
	 * real admin HTTP context) is pushed to Console within ~10 seconds.
	 * Does not recompute readiness -- that only happens in real HTTP contexts.
	 *
	 * @return array<string,mixed>
	 */
	private function firewall_request_state_push(): array {
		StatePush::mark_firewall_dirty();
		return [ 'ok' => true, 'message' => 'firewall_state_push_scheduled' ];
	}
}
