<?php

declare(strict_types=1);

namespace SiteFort\Modules\Cloud\Services;

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

use SiteFort\Includes\ServerConfigWriter;
use SiteFort\Modules\Cloud\SecureApi;
use SiteFort\Modules\Cloud\Service\CloudSettings;
use SiteFort\Modules\Firewall\FirewallConfigService;
use SiteFort\Modules\Firewall\LockoutManager;
use SiteFort\Modules\Hardening\LockoutRegistry;

class StatePush {

	private const DIRTY_KEY = 'sitefort_state_dirty';

	public const DISPATCH_HOOK = 'sitefort_state_dispatch';

	public const LOW_PRIORITY_DISPATCH_HOOK = 'sitefort_state_dispatch_low';

	public const NOTIFICATION_DISPATCH_HOOK = 'sitefort_state_notification_dispatch';

	public const AUDIT_DISPATCH_HOOK = 'sitefort_state_audit_dispatch';

	private const DISPATCH_DELAY = 10;

	private const LOW_PRIORITY_DISPATCH_DELAY = 1800;

	private const AUDIT_REPORTING_DISPATCH_DELAY = 1800;

	private const AUDIT_REPORTING_BUFFER_OPTION = 'sitefort_state_audit_event_buffer';

	private const AUDIT_REPORTING_BUFFER_LOCK_OPTION = 'sitefort_state_audit_event_buffer_lock';

	private const AUDIT_REPORTING_BUFFER_MAX_EVENTS = 500;

	private const AUDIT_REPORTING_BUFFER_LOCK_TTL = 10;

	private const AUDIT_REPORTING_BUFFER_LOCK_WAIT_US = 20000;

	private const AUDIT_REPORTING_BUFFER_LOCK_ATTEMPTS = 10;

	private const ASYNC_RETRY_DELAYS = [ 60, 300, 900 ];

	private static array $audit_buffer = [];
	private static bool $audit_shutdown_registered = false;
	private static array $notification_buffer = [];
	private static bool $notification_shutdown_registered = false;
	private const SNAPSHOT_SCHEMA_VERSION = 11;
	private const SCHEMA_VERSION_OPTION = 'sitefort_state_schema_version';
	private const CONSOLE_FINDING_LIMIT = 50;

	private const ALL_DOMAINS = [
		'environment', 'hardening', 'firewall', 'scan_results',
		'two_factor', 'notification',
		'cloudflare_status', 'security_metrics',
		'site_health',
	];


	/**
	 * Register WordPress hooks for event-driven state pushing.
	 * Called during module initialization.
	 */
	public function register_hooks(): void {
		add_action( 'sitefort_push_notification_event', [ $this, 'handle_notification_event' ], 10, 2 );

		// Deferred dispatch handler.
		add_action( self::DISPATCH_HOOK, [ __CLASS__, 'process_deferred' ] );
		add_action( self::LOW_PRIORITY_DISPATCH_HOOK, [ __CLASS__, 'process_deferred' ] );
		add_action( self::NOTIFICATION_DISPATCH_HOOK, [ __CLASS__, 'process_notification_event_batch' ], 10, 2 );
		add_action( self::AUDIT_DISPATCH_HOOK, [ __CLASS__, 'process_audit_event_batch' ], 10, 2 );

		// Environment changes: plugins, themes, core updates.
		add_action( 'activated_plugin', [ __CLASS__, 'mark_environment_dirty' ] );
		add_action( 'deactivated_plugin', [ __CLASS__, 'mark_environment_dirty' ] );
		add_action( 'switch_theme', [ __CLASS__, 'mark_environment_dirty' ] );
		add_action( 'upgrader_process_complete', [ __CLASS__, 'mark_environment_dirty' ] );
		add_action( 'upgrader_process_complete', [ __CLASS__, 'mark_site_health_dirty' ] );

		// Hardening settings changed.
		add_action( 'sitefort_settings_saved_hardening', [ __CLASS__, 'mark_hardening_dirty' ] );
		add_action( 'sitefort_settings_saved_hardening', [ __CLASS__, 'mark_site_health_dirty' ] );
		add_action( 'sitefort_server_rule_state_changed', [ __CLASS__, 'mark_hardening_dirty' ] );
		add_action( 'sitefort_server_rule_state_changed', [ __CLASS__, 'mark_site_health_dirty' ] );

		// Firewall settings changed.
		add_action( 'sitefort_settings_saved_firewall', [ __CLASS__, 'mark_firewall_dirty' ] );
		add_action( 'sitefort_settings_saved_firewall', [ __CLASS__, 'mark_site_health_dirty' ] );

		// Scan lifecycle updates.
		add_action( 'sitefort_scan_started', [ __CLASS__, 'mark_scan_results_dirty' ] );
		add_action( 'sitefort_scan_results_processed', [ __CLASS__, 'mark_scan_results_dirty' ] );
		add_action( 'sitefort_scan_cancelled', [ __CLASS__, 'mark_scan_results_dirty' ] );
		add_action( 'sitefort_scan_completed', [ __CLASS__, 'mark_scan_results_dirty' ] );
		add_action( 'sitefort_scan_stopped', [ __CLASS__, 'mark_scan_results_dirty' ] );
		add_action( 'sitefort_scan_failed', [ __CLASS__, 'mark_scan_results_dirty' ] );
		add_action( 'sitefort_scan_cancelled', [ __CLASS__, 'push_terminal_scan_state' ], 20 );
		add_action( 'sitefort_scan_completed', [ __CLASS__, 'push_terminal_scan_state' ], 20 );
		add_action( 'sitefort_scan_stopped', [ __CLASS__, 'push_terminal_scan_state' ], 20 );
		add_action( 'sitefort_scan_failed', [ __CLASS__, 'push_terminal_scan_state' ], 20 );
		add_action( 'sitefort_scan_started', [ __CLASS__, 'mark_site_health_dirty' ] );
		add_action( 'sitefort_scan_results_processed', [ __CLASS__, 'mark_site_health_dirty' ] );
		add_action( 'sitefort_scan_cancelled', [ __CLASS__, 'mark_site_health_dirty' ] );
		add_action( 'sitefort_scan_completed', [ __CLASS__, 'mark_site_health_dirty' ] );
		add_action( 'sitefort_scan_stopped', [ __CLASS__, 'mark_site_health_dirty' ] );
		add_action( 'sitefort_scan_failed', [ __CLASS__, 'mark_site_health_dirty' ] );
		add_action( 'sitefort_scan_results_changed', [ __CLASS__, 'mark_scan_results_dirty' ] );
		add_action( 'sitefort_scan_results_changed', [ __CLASS__, 'mark_site_health_dirty' ] );

		// TwoFactor state changes.
		add_action( 'sitefort_settings_saved_twofa', [ __CLASS__, 'mark_two_factor_dirty' ] );
		add_action( 'sitefort_2fa_enrolled', [ __CLASS__, 'mark_two_factor_dirty' ] );
		add_action( 'sitefort_2fa_disabled', [ __CLASS__, 'mark_two_factor_dirty' ] );
		add_action( 'sitefort_settings_saved_twofa', [ __CLASS__, 'mark_site_health_dirty' ] );
		add_action( 'sitefort_2fa_enrolled', [ __CLASS__, 'mark_site_health_dirty' ] );
		add_action( 'sitefort_2fa_disabled', [ __CLASS__, 'mark_site_health_dirty' ] );

		add_action( 'sitefort_settings_saved_notifications', [ __CLASS__, 'mark_notification_dirty' ] );

		// AuditLog event batching.
		add_action( 'sitefort_audit_log_entry', [ __CLASS__, 'buffer_audit_event' ], 10, 2 );

		// Cloudflare action.
		add_action( 'sitefort_cloudflare_action', [ __CLASS__, 'mark_cloudflare_dirty' ] );
		add_action( 'sitefort_settings_saved_cloudflare', [ __CLASS__, 'mark_cloudflare_dirty' ] );

		// Runtime lockout counters are low-priority Console state.
		add_action( 'sitefort_lockout_activated', [ __CLASS__, 'mark_runtime_lockout_dirty' ], 10, 2 );
		add_action( 'sitefort_lockout_cleared', [ __CLASS__, 'mark_runtime_lockout_dirty' ], 10, 2 );

		// Firewall ban/allow mutations update Console reporting on a low-priority interval.
		add_action( 'sitefort_firewall_lockout_changed', [ __CLASS__, 'mark_firewall_lockout_dirty' ], 10, 1 );

		// Hourly metrics push (firewall stats).
		add_action( 'sitefort_metrics_push', [ __CLASS__, 'push_hourly_metrics' ] );

		$this->ensure_snapshot_schema_alignment();
	}


	/**
	 * Mark a state domain dirty and schedule a deferred push.
	 */
	private static function mark_dirty( string $domain ): void {
		self::mark_domains_dirty( [ $domain ], true );
	}

	/**
	 * Mark one or more domains dirty.
	 *
	 * @param string[] $domains
	 */
	private static function mark_domains_dirty( array $domains, bool $schedule_dispatch, bool $low_priority = false ): void {
		$dirty = self::get_dirty_domains();
		$next  = array_values(
			array_intersect(
				array_unique( array_merge( $dirty, array_map( 'strval', $domains ) ) ),
				self::ALL_DOMAINS
			)
		);

		if ( $next !== $dirty ) {
			set_transient( self::DIRTY_KEY, wp_json_encode( $next ), HOUR_IN_SECONDS );
		}

		if ( $schedule_dispatch ) {
			self::ensure_dispatch_scheduled( $low_priority );
		}
	}

	/**
	 * Get the list of dirty domain names.
	 *
	 * @return string[]
	 */
	private static function get_dirty_domains(): array {
		$raw = get_transient( self::DIRTY_KEY );
		if ( ! is_string( $raw ) || $raw === '' ) {
			return [];
		}

		$decoded = json_decode( $raw, true );
		if ( ! is_array( $decoded ) ) {
			return [];
		}

		$valid = array_values(
			array_intersect(
				array_map( 'strval', $decoded ),
				self::ALL_DOMAINS
			)
		);

		if ( count( $valid ) !== count( $decoded ) ) {
			if ( empty( $valid ) ) {
				delete_transient( self::DIRTY_KEY );
			} else {
				set_transient( self::DIRTY_KEY, wp_json_encode( $valid ), HOUR_IN_SECONDS );
			}
		}

		return $valid;
	}

	/**
	 * Clear dirty flags for the given domains (or all).
	 *
	 * @param string[] $domains Domains to clear.  Empty = clear all.
	 */
	private static function clear_dirty( array $domains = [] ): void {
		if ( empty( $domains ) ) {
			delete_transient( self::DIRTY_KEY );
			return;
		}

		$remaining = array_values( array_diff( self::get_dirty_domains(), $domains ) );
		if ( empty( $remaining ) ) {
			delete_transient( self::DIRTY_KEY );
		} else {
			set_transient( self::DIRTY_KEY, wp_json_encode( $remaining ), HOUR_IN_SECONDS );
		}
	}

	/**
	 * Schedule the deferred dispatch if not already pending.
	 */
	private static function ensure_dispatch_scheduled( bool $low_priority = false ): void {
		$hook  = $low_priority ? self::LOW_PRIORITY_DISPATCH_HOOK : self::DISPATCH_HOOK;
		$delay = $low_priority ? self::LOW_PRIORITY_DISPATCH_DELAY : self::DISPATCH_DELAY;

		if ( function_exists( '\\as_has_scheduled_action' ) && function_exists( '\\as_schedule_single_action' ) ) {
			if ( ! \as_has_scheduled_action( $hook, [], 'sitefort_cloud' ) ) {
				\as_schedule_single_action( time() + $delay, $hook, [], 'sitefort_cloud' );
			}
			return;
		}

		// WP-Cron fallback.
		if ( ! wp_next_scheduled( $hook ) ) {
			wp_schedule_single_event( time() + $delay, $hook );
		}
	}


	public static function mark_environment_dirty(): void {
		self::mark_dirty( 'environment' );
	}

	public static function mark_hardening_dirty(): void {
		self::mark_dirty( 'hardening' );
	}

	public static function mark_firewall_dirty(): void {
		self::mark_dirty( 'firewall' );
	}

	public static function mark_scan_results_dirty(): void {
		self::mark_dirty( 'scan_results' );
	}

	public static function mark_site_health_dirty(): void {
		self::mark_dirty( 'site_health' );
	}

	public static function mark_two_factor_dirty(): void {
		self::mark_dirty( 'two_factor' );
	}

	public static function mark_notification_dirty(): void {
		self::mark_dirty( 'notification' );
	}

	public static function mark_cloudflare_dirty(): void {
		self::mark_dirty( 'cloudflare_status' );
	}

	public static function mark_security_metrics_dirty(): void {
		self::mark_dirty( 'security_metrics' );
	}

	public static function mark_runtime_lockout_dirty( string $identifier = '', string $type = '' ): void {
		unset( $identifier, $type );

		self::mark_domains_dirty( [ 'hardening', 'security_metrics' ], true, true );
	}

	/**
	 * Firewall list changes can be high-volume and should ride the reporting queue.
	 *
	 * @param array<string,mixed> $payload
	 */
	public static function mark_firewall_lockout_dirty( array $payload ): void {
		if ( ! self::should_push_firewall_lockout_change( $payload ) ) {
			return;
		}

		self::mark_domains_dirty( [ 'firewall', 'security_metrics' ], true, true );
	}

	/**
	 * @param array<string,mixed> $payload
	 */
	private static function should_push_firewall_lockout_change( array $payload ): bool {
		$event = sanitize_key( (string) ( $payload['event'] ?? '' ) );
		if ( ! in_array( $event, [ 'added', 'removed', 'purged', 'expired' ], true ) ) {
			return false;
		}

		$type = (string) ( $payload['type'] ?? 'ban' );
		return in_array( $type, [ 'ban', 'allow' ], true );
	}

	/**
	 * Mark ALL domains dirty and schedule a deferred push.
	 *
	 * Non-blocking alternative to run() - returns immediately, push
	 * happens ~10 s later via Action Scheduler / WP-Cron.
	 */
	public static function schedule_full_push(): void {
		self::mark_domains_dirty( self::ALL_DOMAINS, true );
	}


	/**
	 * Process deferred state push.  Runs in an Action Scheduler async action.
	 * Pushes only dirty domains, then clears their flags on success.
	 */
	public static function process_deferred(): void {
		$dirty = self::get_dirty_domains();
		if ( empty( $dirty ) ) {
			return;
		}

		$instance = new self();
		$api = $instance->get_api();
		if ( ! $api ) {
			return; // Not connected - flags stay dirty for daily catch-all.
		}

		// Atomic batch: any failure leaves all flags dirty for the daily catch-all.
		if ( $instance->push_domains_batch( $api, $dirty ) ) {
			self::clear_dirty( $dirty );
		}
	}


	/**
	 * Buffer a notification event for asynchronous batched delivery.
	 */
	public function handle_notification_event( string $event_type, array $data ): void {
		self::$notification_buffer[] = [
			'event_id'   => wp_generate_uuid4(),
			'event_type' => 'notification.' . $event_type,
			'data'       => $data,
		];

		if ( ! self::$notification_shutdown_registered ) {
			register_shutdown_function( [ __CLASS__, 'flush_notification_buffer' ] );
			self::$notification_shutdown_registered = true;
		}
	}

	/**
	 * Queue buffered notification events for asynchronous delivery.
	 */
	public static function flush_notification_buffer(): void {
		if ( empty( self::$notification_buffer ) ) {
			return;
		}

		if ( self::get_api_settings() === null ) {
			self::$notification_buffer = [];
			return;
		}

		$buffer = self::$notification_buffer;
		self::$notification_buffer = [];

		self::schedule_async_payload( self::NOTIFICATION_DISPATCH_HOOK, [ $buffer, 0 ], 1 );
	}

	/**
	 * Send a queued notification event batch from Action Scheduler/WP-Cron.
	 *
	 * @param array<int,array{event_id:string,event_type:string,data:array<string,mixed>}> $events
	 */
	public static function process_notification_event_batch( array $events, int $attempt = 0 ): void {
		$events = self::sanitize_event_batch( $events );
		if ( empty( $events ) ) {
			return;
		}

		$instance = new self();
		$api = $instance->get_api();
		if ( ! $api ) {
			return;
		}

		if ( ! $instance->send_event_batch( $api, $events ) ) {
			self::retry_async_payload( self::NOTIFICATION_DISPATCH_HOOK, [ $events ], $attempt );
		}
	}


	/**
	 * Run a full state push across all domains.
	 * Called from the daily sync.  Acts as the catch-all safety net -
	 * only clears dirty flags for domains that succeeded.
	 */
	public function run(): void {
		$api = $this->get_api();
		if ( ! $api ) {
			return;
		}

		if ( $this->push_domains_batch( $api, self::ALL_DOMAINS ) ) {
			self::clear_dirty( self::ALL_DOMAINS );
		}
	}


	/**
	 * Push a single state domain.
	 *
	 * @return bool True if the push was accepted (2xx).
	 */
	private function push_domain( SecureApi $api, string $domain ): bool {
		switch ( $domain ) {
			case 'environment':
				return $this->push_environment( $api );
			case 'hardening':
				return $this->push_hardening( $api );
			case 'firewall':
				return $this->push_firewall( $api );
			case 'scan_results':
				return $this->push_scan_results( $api );
			case 'two_factor':
				return $this->push_two_factor( $api );
			case 'notification':
				return $this->push_notification( $api );
			case 'cloudflare_status':
				return $this->push_cloudflare_status( $api );
			case 'security_metrics':
				return $this->push_security_metrics( $api );
			case 'site_health':
				return $this->push_site_health( $api );
			default:
				return false;
		}
	}

	private function push_environment( SecureApi $api ): bool {
		return $this->send_event( $api, 'state.environment', $this->build_environment_payload() );
	}

	private function build_environment_payload(): array {
		global $wp_version;
		$cloud_settings = CloudSettings::get_all();
		$connector_plugin_file = defined( 'SITEFORT_PLUGIN_FILE' ) ? plugin_basename( SITEFORT_PLUGIN_FILE ) : '';

		$plugin_updates = get_site_transient( 'update_plugins' );
		$update_responses = ( is_object( $plugin_updates ) && isset( $plugin_updates->response ) && is_array( $plugin_updates->response ) )
			? $plugin_updates->response
			: [];
		$plugins_data = [];
		if ( function_exists( 'get_plugins' ) ) {
			$all_plugins = get_plugins();
			$active = (array) get_option( 'active_plugins', [] );
			foreach ( $all_plugins as $file => $meta ) {
				$update_info    = $update_responses[ $file ] ?? null;
				$new_version    = is_object( $update_info ) ? ( $update_info->new_version ?? null ) : null;
				$plugins_data[] = [
					'file'             => $file,
					'name'             => $meta['Name'] ?? '',
					'version'          => $meta['Version'] ?? '',
					'active'           => in_array( $file, $active, true ),
					'is_connector'     => ( '' !== $connector_plugin_file && $file === $connector_plugin_file ),
					'update_available' => $new_version !== null,
					'new_version'      => $new_version,
				];
			}
		}

		$theme_updates = get_site_transient( 'update_themes' );
		$theme_update_responses = ( is_object( $theme_updates ) && isset( $theme_updates->response ) && is_array( $theme_updates->response ) )
			? $theme_updates->response
			: [];
		$themes_data = [];
		$themes = wp_get_themes();
		$active_theme = get_stylesheet();
		foreach ( $themes as $slug => $theme ) {
			$update_info   = $theme_update_responses[ $slug ] ?? null;
			$new_version   = is_array( $update_info ) ? ( $update_info['new_version'] ?? null ) : null;
			$themes_data[] = [
				'slug'             => $slug,
				'name'             => $theme->get( 'Name' ),
				'version'          => $theme->get( 'Version' ),
				'active'           => ( $slug === $active_theme ),
				'update_available' => $new_version !== null,
				'new_version'      => $new_version,
			];
		}

		return [
			'wp_version'      => $wp_version ?? '',
			'php_version'     => PHP_VERSION,
			'shield_version'  => defined( 'SITEFORT_VERSION' ) ? SITEFORT_VERSION : '1.0.0',
			'site_name'       => (string) get_option( 'blogname', '' ),
			'console_enabled' => ! empty( $cloud_settings['console_enabled'] ),
			'connector_plugin_file' => $connector_plugin_file,
			'multisite'       => is_multisite(),
			'plugins'         => $plugins_data,
			'themes'          => $themes_data,
			'active_theme'    => $active_theme,
		];
	}

	private function push_hardening( SecureApi $api ): bool {
		$payload = $this->build_hardening_payload();
		if ( $payload === null ) {
			return true;
		}
		return $this->send_event( $api, 'state.hardening', $payload );
	}

	private function build_hardening_payload(): ?array {
		if ( ! class_exists( '\\SiteFort\\Modules\\Hardening\\Hardening' ) ) {
			return null;
		}

		$defaults = \SiteFort\Modules\Hardening\Hardening::get_default_settings();
		$current = \SiteFort\Includes\SettingsStore::get( \SiteFort\Includes\SettingsStore::COL_HARDENING, $defaults );
		if ( ! is_array( $current ) ) {
			$current = $defaults;
		}

		$active_provider = \SiteFort\Modules\Hardening\Hardening::normalize_captcha_provider_name( (string) ( $current['captcha_provider'] ?? $defaults['captcha_provider'] ) );
		$safe = [];
		foreach ( $defaults as $key => $default ) {
			// Never push secret material.
			if ( 'captcha_secret_key' === $key ) {
				continue;
			}
			if ( 'captcha_provider_profiles' === $key ) {
				$safe[ $key ] = \SiteFort\Modules\Hardening\Hardening::get_captcha_provider_profiles_response( $current );
				continue;
			}
			$value = $current[ $key ] ?? $default;
			if ( is_bool( $default ) ) {
				$safe[ $key ] = (bool) $value;
			} elseif ( is_int( $default ) ) {
				$safe[ $key ] = (int) $value;
			} elseif ( is_float( $default ) ) {
				$safe[ $key ] = (float) $value;
			} elseif ( is_array( $default ) ) {
				$safe[ $key ] = is_array( $value ) ? array_values( $value ) : [];
			} else {
				$safe[ $key ] = (string) $value;
			}
		}
		$safe['captcha_secret_configured'] = \SiteFort\Modules\Hardening\Hardening::has_captcha_secret( $active_provider );
		$safe['_server_rule_state'] = ServerConfigWriter::get_state();

		return $safe;
	}

	private function push_firewall( SecureApi $api ): bool {
		return $this->send_event( $api, 'state.firewall', $this->build_firewall_payload() );
	}

	private function build_firewall_payload(): array {
		$defaults = \SiteFort\Modules\Firewall\FirewallSettings::defaults();
		$current = \SiteFort\Modules\Firewall\FirewallSettings::redact_sensitive_keys(
			FirewallConfigService::load_effective_settings(),
			false
		);

		// Push non-secret configuration fields only; exclude API keys/secrets.
		$safe = [];
		foreach ( $defaults as $key => $default ) {
			if ( ! array_key_exists( $key, $current ) ) {
				continue;
			}
			$value = $current[ $key ] ?? $default;
			if ( is_bool( $default ) ) {
				$safe[ $key ] = (bool) $value;
			} elseif ( is_int( $default ) ) {
				$safe[ $key ] = (int) $value;
			} elseif ( is_float( $default ) ) {
				$safe[ $key ] = (float) $value;
			} else {
				$safe[ $key ] = (string) $value;
			}
		}

		// Include cached readiness only when it matches the current proxy
		// configuration and the effective mode chosen for this request context.
		$effective_readiness = FirewallConfigService::get_effective_readiness_snapshot( $safe );
		if ( is_array( $effective_readiness ) ) {
			$safe['ip_readiness'] = $effective_readiness;
		} elseif ( FirewallConfigService::get_origin_enforcement_mode( $safe ) === FirewallConfigService::MODE_PENDING_VALIDATION ) {
			$safe['ip_readiness'] = FirewallConfigService::build_pending_validation_readiness();
		}

		return $safe;
	}

	private function push_scan_results( SecureApi $api, bool $critical = false ): bool {
		return $this->send_event( $api, 'state.scan_results', $this->build_scan_results_payload(), $critical );
	}

	private function build_scan_results_payload(): array {
		self::ensure_scan_queue_loaded();
		$has_issue_repo = self::ensure_issue_repository_loaded();
		$scan_status    = $this->get_scan_status();
		$published_id   = \SiteFort\Includes\SettingsStore::get_field( \SiteFort\Includes\SettingsStore::COL_STATE, 'published_scan_id', '' );
		$last_scan_time = (int) \SiteFort\Includes\SettingsStore::get_field( \SiteFort\Includes\SettingsStore::COL_STATE, 'last_scan_time', 0 );
		$issues_count   = (int) \SiteFort\Includes\SettingsStore::get_field( \SiteFort\Includes\SettingsStore::COL_STATE, 'scan_issues_count', 0 );
		$ignored_issues_count = 0;
		$failure_context = [];

		if ( $has_issue_repo ) {
			$issues_count = \SiteFort\Modules\Scanner\Repository\IssueRepository::countCurrentNonSystem( false );
			$ignored_issues_count = \SiteFort\Modules\Scanner\Repository\IssueRepository::countCurrentNonSystem( true );
		}

		// Build findings summary from active, non-ignored issues.
		$findings       = [];
		$ignored_findings = [];

		if ( $has_issue_repo ) {
			if ( $issues_count > 0 ) {
				$raw_issues = self::get_console_snapshot_issues( false );

				foreach ( $raw_issues as $issue ) {
					$findings[] = self::build_console_finding( $issue );
				}
			}

			if ( $ignored_issues_count > 0 ) {
				$raw_ignored_issues = self::get_console_snapshot_issues( true );

				foreach ( $raw_ignored_issues as $issue ) {
					$ignored_findings[] = self::build_console_finding( $issue );
				}
			}
		}

		if ( in_array( $scan_status, [ 'failed', 'stopped' ], true ) ) {
			$failure_context = self::get_scan_failure_context();
		}

		$data = [
			'scan_status'       => $scan_status,
			'issues_count'      => $issues_count,
			'ignored_issues_count' => $ignored_issues_count,
			'published_scan_id' => $published_id,
			'last_scan_at'      => $last_scan_time,
			'findings'          => $findings,
			'ignored_findings'  => $ignored_findings,
		];
		if ( ! empty( $failure_context ) ) {
			$data['failure_context'] = $failure_context;
		}

		return $data;
	}

	private static function get_console_snapshot_issues( bool $ignored ): array {
		$user_issues = $ignored
			? \SiteFort\Modules\Scanner\Repository\IssueRepository::getIgnoredPrioritized( self::CONSOLE_FINDING_LIMIT, 0, 'user' )
			: \SiteFort\Modules\Scanner\Repository\IssueRepository::getActivePrioritized( self::CONSOLE_FINDING_LIMIT, 0, 'user' );
		$general_issues = $ignored
			? \SiteFort\Modules\Scanner\Repository\IssueRepository::getIgnoredPrioritized( self::CONSOLE_FINDING_LIMIT )
			: \SiteFort\Modules\Scanner\Repository\IssueRepository::getActivePrioritized( self::CONSOLE_FINDING_LIMIT );

		$merged = [];
		$seen = [];
		foreach ( array_merge( $user_issues, $general_issues ) as $index => $issue ) {
			if ( ! is_array( $issue ) || ( $issue['type'] ?? '' ) === 'system_error' ) {
				continue;
			}

			$key = (string) ( $issue['hash'] ?? $issue['issue_hash'] ?? $issue['id'] ?? '' );
			if ( $key === '' ) {
				$key = 'row:' . $index . ':' . hash( 'sha256', wp_json_encode( $issue ) ?: '' );
			}
			if ( isset( $seen[ $key ] ) ) {
				continue;
			}

			$seen[ $key ] = true;
			$merged[] = $issue;
			if ( count( $merged ) >= self::CONSOLE_FINDING_LIMIT ) {
				break;
			}
		}

		return $merged;
	}

	private static function build_console_finding( array $issue ): array {
		$type     = sanitize_key( (string) ( $issue['type'] ?? 'unknown' ) );
		$severity = strtolower( (string) ( $issue['severity'] ?? 'low' ) );
		$data     = is_array( $issue['data'] ?? null ) ? $issue['data'] : [];
		$display  = is_array( $issue['display'] ?? null ) ? $issue['display'] : [];
		$file     = trim( (string) ( $issue['file'] ?? $issue['file_path'] ?? '' ) );
		$component = self::resolve_console_component( $issue, $data );
		$issue_hash = trim( (string) ( $issue['hash'] ?? $issue['issue_hash'] ?? '' ) );
		$is_ignored = ! empty( $issue['is_ignored'] );

		if ( ! in_array( $severity, [ 'critical', 'high', 'medium', 'low', 'info' ], true ) ) {
			$severity = 'low';
		}

		$finding = [
			'type'        => $type !== '' ? $type : 'unknown',
			'severity'    => $severity,
			'disposition' => $is_ignored ? 'ignored' : 'active',
			'is_ignored'  => $is_ignored,
		];

		if ( $issue_hash !== '' ) {
			$finding['issue_hash'] = $issue_hash;
		}

		$name = trim( (string) ( $issue['name'] ?? '' ) );
		if ( $name === '' && ! empty( $display['title'] ) ) {
			$name = trim( (string) $display['title'] );
		}
		if ( $name !== '' ) {
			$finding['name'] = $name;
		}

		$title = trim( (string) ( $issue['title'] ?? '' ) );
		if ( $title !== '' ) {
			$finding['title'] = $title;
		}

		$description = self::resolve_console_description( $issue, $data, $display );
		if ( $description !== '' ) {
			$finding['description'] = $description;
		}

		if ( $file !== '' ) {
			$finding['file'] = $file;
		}
		if ( $component !== '' ) {
			$finding['component'] = $component;
		}
		if ( ! empty( $display ) ) {
			$finding['display'] = $display;
		}

		$component_type = sanitize_key( (string) ( $data['component_type'] ?? '' ) );
		if ( $component_type !== '' ) {
			$finding['component_type'] = $component_type;
		}

		$component_slug = sanitize_key( (string) ( $data['slug'] ?? ( $issue['slug'] ?? '' ) ) );
		if ( $component_slug !== '' ) {
			$finding['component_slug'] = $component_slug;
		}

		switch ( $type ) {
			case 'vulnerability':
				$asset_name = trim( (string) ( $data['component_name'] ?? $data['asset_name'] ?? '' ) );
				if ( $asset_name !== '' ) {
					$finding['asset_name'] = $asset_name;
				}

				$affected_versions = trim( (string) ( $issue['vnv'] ?? $data['vnv'] ?? '' ) );
				if ( $affected_versions !== '' ) {
					$finding['affected_versions'] = $affected_versions;
				}

				$cve = trim( (string) ( $issue['cve'] ?? $data['cve'] ?? '' ) );
				if ( $cve !== '' ) {
					$finding['cve'] = $cve;
				}

				$fixed_in = trim( (string) ( $issue['fixed_in'] ?? $data['fixed_in'] ?? '' ) );
				if ( $fixed_in !== '' ) {
					$finding['fixed_in'] = $fixed_in;
				}

				$cvss = (float) ( $issue['cvss'] ?? $data['cvss'] ?? 0 );
				if ( $cvss > 0 ) {
					$finding['cvss'] = $cvss;
				}

				if ( isset( $issue['can_update'] ) || isset( $data['can_update'] ) ) {
					$finding['can_update'] = ! empty( $issue['can_update'] ) || ! empty( $data['can_update'] );
				}
				break;

			case 'malware':
				$threat_type = trim( (string) ( $issue['threat_type'] ?? $data['threat_type'] ?? '' ) );
				if ( $threat_type !== '' ) {
					$finding['threat_type'] = $threat_type;
				}

				$detection_type = sanitize_key( (string) ( $data['detection_type'] ?? '' ) );
				if ( $detection_type !== '' ) {
					$finding['detection_type'] = $detection_type;
				}
				break;

			case 'fake_image':
				foreach ( [ 'detection_type', 'validation_reason', 'expected_extension', 'detected_type', 'detected_mime', 'parser_type' ] as $key ) {
					$value = trim( sanitize_text_field( (string) ( $data[ $key ] ?? '' ) ) );
					if ( $value !== '' ) {
						$finding[ $key ] = $value;
					}
				}

				$expected_mime = self::sanitize_console_string_list( $data['expected_mime'] ?? [] );
				if ( ! empty( $expected_mime ) ) {
					$finding['expected_mime'] = $expected_mime;
				}

				foreach ( [ 'bytes_checked', 'file_size' ] as $key ) {
					if ( array_key_exists( $key, $data ) ) {
						$finding[ $key ] = max( 0, (int) $data[ $key ] );
					}
				}
				break;

			case 'user':
				$user_login = trim( (string) ( $data['user_login'] ?? '' ) );
				if ( $user_login !== '' ) {
					$finding['user_login'] = $user_login;
				}

				$user_id = max( 0, (int) ( $data['user_id'] ?? 0 ) );
				if ( $user_id > 0 ) {
					$finding['user_id'] = $user_id;
				}

				$roles = self::sanitize_console_string_list( $data['roles'] ?? [] );
				if ( ! empty( $roles ) ) {
					$finding['roles'] = $roles;
				}

				$detection_reason = sanitize_key( (string) ( $data['reason'] ?? '' ) );
				if ( $detection_reason !== '' ) {
					$finding['detection_reason'] = $detection_reason;
				}

				$blog_id = max( 0, (int) ( $data['blog_id'] ?? 0 ) );
				if ( $blog_id > 0 || array_key_exists( 'blog_id', $data ) ) {
					$finding['blog_id'] = $blog_id;
				}

				foreach ( [ 'cap_key', 'level_key', 'raw_sha256' ] as $key ) {
					$value = trim( sanitize_text_field( (string) ( $data[ $key ] ?? '' ) ) );
					if ( $value !== '' ) {
						$finding[ $key ] = $value;
					}
				}

				foreach ( [ 'umeta_id', 'row_count', 'first_umeta_id', 'last_umeta_id', 'raw_length' ] as $key ) {
					if ( array_key_exists( $key, $data ) ) {
						$finding[ $key ] = max( 0, (int) $data[ $key ] );
					}
				}

				foreach ( [ 'expected_user_level', 'actual_user_level' ] as $key ) {
					if ( array_key_exists( $key, $data ) ) {
						$finding[ $key ] = max( -1, (int) $data[ $key ] );
					}
				}

				$privilege_sources = self::sanitize_console_string_list( $data['privilege_sources'] ?? [] );
				if ( ! empty( $privilege_sources ) ) {
					$finding['privilege_sources'] = $privilege_sources;
				}

				$direct_caps = self::sanitize_console_string_list( $data['direct_caps'] ?? [] );
				if ( ! empty( $direct_caps ) ) {
					$finding['direct_caps'] = $direct_caps;
				}

				$evidence = self::sanitize_console_user_evidence( $data['evidence'] ?? [] );
				if ( ! empty( $evidence ) ) {
					$finding['evidence'] = $evidence;
				}

				$event = self::sanitize_console_user_event( $data['event'] ?? [] );
				if ( ! empty( $event ) ) {
					$finding['event'] = $event;
				}
				break;
			}

		return $finding;
	}

	private static function sanitize_console_string_list( $values ): array {
		if ( ! is_array( $values ) ) {
			return [];
		}

		$out = [];
		foreach ( $values as $value ) {
			$item = trim( sanitize_text_field( (string) $value ) );
			if ( $item !== '' ) {
				$out[] = $item;
			}
		}

		return array_values( array_unique( $out ) );
	}

	private static function sanitize_console_user_evidence( $value ): array {
		if ( ! is_array( $value ) ) {
			return [];
		}

		$evidence = [];

		foreach ( [ 'wp_can_manage_options', 'visible_in_admin_user_query' ] as $key ) {
			if ( array_key_exists( $key, $value ) ) {
				$evidence[ $key ] = ! empty( $value[ $key ] );
			}
		}

		if ( array_key_exists( 'user_level', $value ) ) {
			$evidence['user_level'] = max( -1, (int) $value['user_level'] );
		}

		$wp_roles = self::sanitize_console_string_list( $value['wp_roles'] ?? [] );
		if ( ! empty( $wp_roles ) ) {
			$evidence['wp_roles'] = $wp_roles;
		}

		$missing_profile_meta = self::sanitize_console_string_list( $value['missing_profile_meta'] ?? [] );
		if ( ! empty( $missing_profile_meta ) ) {
			$evidence['missing_profile_meta'] = $missing_profile_meta;
		}

		return $evidence;
	}

	private static function sanitize_console_user_event( $value ): array {
		if ( ! is_array( $value ) ) {
			return [];
		}

		$event = [];

		foreach ( [ 'event_id', 'request_path' ] as $key ) {
			$text = trim( sanitize_text_field( (string) ( $value[ $key ] ?? '' ) ) );
			if ( $text !== '' ) {
				$event[ $key ] = $text;
			}
		}

		foreach ( [ 'event_type', 'channel', 'method' ] as $key ) {
			$text = sanitize_key( (string) ( $value[ $key ] ?? '' ) );
			if ( $text !== '' ) {
				$event[ $key ] = $text;
			}
		}

		foreach ( [ 'captured_at', 'actor_user_id' ] as $key ) {
			if ( array_key_exists( $key, $value ) ) {
				$event[ $key ] = max( 0, (int) $value[ $key ] );
			}
		}

		$query_keys = self::sanitize_console_string_list( $value['query_keys'] ?? [] );
		if ( ! empty( $query_keys ) ) {
			$event['query_keys'] = $query_keys;
		}

		$context_flags = self::sanitize_console_string_list( $value['context_flags'] ?? [] );
		if ( ! empty( $context_flags ) ) {
			$event['context_flags'] = $context_flags;
		}

		return $event;
	}

	/**
	 * Resolve the finding description without injecting generic filler copy.
	 *
	 * @param array $issue
	 * @param array $data
	 * @param array $display
	 * @return string
	 */
	private static function resolve_console_description( array $issue, array $data, array $display ): string {
		$type = sanitize_key( (string) ( $issue['type'] ?? '' ) );

		if ( $type === 'content' && ! empty( $data['signature_desc'] ) ) {
			return trim( (string) $data['signature_desc'] );
		}

		if ( $type === 'user' ) {
			$display_description = trim( (string) ( $display['description'] ?? '' ) );
			if ( $display_description !== '' && $display_description !== 'Review this finding and apply remediation if needed.' ) {
				return $display_description;
			}
		}

		foreach ( [ 'details', 'description', 'message' ] as $field ) {
			if ( ! empty( $issue[ $field ] ) ) {
				return trim( (string) $issue[ $field ] );
			}
		}

		if ( ! empty( $data['description'] ) ) {
			return trim( (string) $data['description'] );
		}

		$display_description = trim( (string) ( $display['description'] ?? '' ) );
		if ( $display_description === 'Review this finding and apply remediation if needed.' ) {
			return '';
		}

		return $display_description;
	}

	/**
	 * Resolve the actionable component identifier for console commands.
	 *
	 * @param array $issue
	 * @param array $data
	 * @return string
	 */
	private static function resolve_console_component( array $issue, array $data ): string {
		$plugin_file = trim( (string) ( $data['plugin_file'] ?? '' ) );
		if ( $plugin_file !== '' ) {
			return $plugin_file;
		}

		foreach ( [ 'slug', 'component' ] as $field ) {
			if ( ! empty( $issue[ $field ] ) ) {
				return trim( (string) $issue[ $field ] );
			}
		}

		return '';
	}


	private function push_two_factor( SecureApi $api ): bool {
		return $this->send_event( $api, 'state.two_factor', $this->build_two_factor_payload() );
	}

	private function build_two_factor_payload(): array {
		$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' ],
			];
		$settings = \SiteFort\Includes\SettingsStore::get( \SiteFort\Includes\SettingsStore::COL_TWOFA, $defaults );
		if ( ! is_array( $settings ) ) {
			$settings = $defaults;
		}
		$sanitized_methods = class_exists( '\\SiteFort\\Modules\\TwoFactor\\TwoFactor' )
			? \SiteFort\Modules\TwoFactor\TwoFactor::sanitize_allowed_methods( $settings['allowed_methods'] ?? $defaults['allowed_methods'] )
			: [];
		$settings['allowed_methods'] = ! empty( $sanitized_methods ) ? $sanitized_methods : ( $defaults['allowed_methods'] ?? [ 'totp', 'email' ] );

		$role_enrollment = [];
		$required_scope  = [
			'roles'    => [],
			'total'    => 0,
			'enrolled' => 0,
		];

		$wp_roles   = function_exists( 'wp_roles' ) ? wp_roles() : null;
		$valid_map  = ( $wp_roles && is_array( $wp_roles->roles ) ) ? $wp_roles->roles : [];
		$role_scope = array_values(
			array_unique(
				array_filter(
					array_map(
						'sanitize_key',
						is_array( $settings['enforce_for_roles'] ?? null ) ? $settings['enforce_for_roles'] : []
					),
					static function ( string $role ) use ( $valid_map ): bool {
						return '' !== $role && isset( $valid_map[ $role ] );
					}
				)
			)
		);

		// Count enrollment stats per role.
		$enrollment = [];
		global $wpdb;
		$table = self::table_identifier( $wpdb->prefix . 'sitefort_two_factor' );
		$table_name = trim( $table, '`' );

		// Check table exists before querying.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Checks existence of a plugin-owned 2FA table before reading enrollment metrics.
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name ) {
			// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
			$enrolled_ids = array_map( 'intval', $wpdb->get_col( "SELECT user_id FROM {$table} WHERE is_enabled = 1 AND confirmed_at IS NOT NULL" ) );
			// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
			$enrolled_lookup = array_fill_keys( $enrolled_ids, true );

			$admin_users      = array_map( 'intval', get_users( [ 'role' => 'administrator', 'fields' => 'ID' ] ) );
			$total_admins     = count( $admin_users );
			$enrolled_admins  = count(
				array_filter(
					$admin_users,
					static function ( int $user_id ) use ( $enrolled_lookup ): bool {
						return isset( $enrolled_lookup[ $user_id ] );
					}
				)
			);

			$required_user_ids = [];
			foreach ( $role_scope as $role ) {
				$role_user_ids = array_values( array_unique( array_map( 'intval', get_users( [ 'role' => $role, 'fields' => 'ID' ] ) ) ) );
				$role_enrollment[ $role ] = [
					'total'    => count( $role_user_ids ),
					'enrolled' => count(
						array_filter(
							$role_user_ids,
							static function ( int $user_id ) use ( $enrolled_lookup ): bool {
								return isset( $enrolled_lookup[ $user_id ] );
							}
						)
					),
				];
				$required_user_ids = array_merge( $required_user_ids, $role_user_ids );
			}

			$required_user_ids = array_values( array_unique( $required_user_ids ) );
			$required_scope    = [
				'roles'    => $role_scope,
				'total'    => count( $required_user_ids ),
				'enrolled' => count(
					array_filter(
						$required_user_ids,
						static function ( int $user_id ) use ( $enrolled_lookup ): bool {
							return isset( $enrolled_lookup[ $user_id ] );
						}
					)
				),
			];

			$enrollment = $role_enrollment + [
				'administrator' => [
					'total'    => $total_admins,
					'enrolled' => $enrolled_admins,
				],
				'required_scope' => $required_scope,
				'total_enrolled' => count( $enrolled_ids ),
			];
		}

		$payload = $settings;
		$payload['enrollment'] = $enrollment;

		return $payload;
	}

	private function push_notification( SecureApi $api, bool $critical = false ): bool {
		$payload = $this->build_notification_payload();
		if ( $payload === null ) {
			return true;
		}
		return $this->send_event( $api, 'state.notification', $payload, $critical );
	}

	private function build_notification_payload(): ?array {
		if ( ! class_exists( '\\SiteFort\\Modules\\Notifications\\NotificationSettings' ) ) {
			return null;
		}

		$notification_settings = new \SiteFort\Modules\Notifications\NotificationSettings();
		$settings              = $notification_settings->get_redacted( true );
		if ( ! is_array( $settings ) || empty( $settings ) ) {
			return null;
		}

		$settings['available_admin_contacts'] = $notification_settings->get_available_admin_contacts();

		return $settings;
	}

	private function push_cloudflare_status( SecureApi $api ): bool {
		return $this->send_event( $api, 'state.cloudflare_status', $this->build_cloudflare_status_payload() );
	}

	private function build_cloudflare_status_payload(): array {
		$configured = false;
		$connected  = false;
		$enabled    = false;
		$zone_id    = '';
		$auth_mode  = '';
		$auth_email = '';
		$plan       = [];
		$limits     = [];
		$policy     = [];
		$sync       = [];

		if ( class_exists( '\\SiteFort\\Modules\\Cloudflare\\Manager' ) ) {
			$settings = ( new \SiteFort\Modules\Cloudflare\Manager() )->get_admin_settings();
			if ( is_array( $settings ) ) {
				$configured = ! empty( $settings['configured'] );
				$connected  = ! empty( $settings['connected'] );
				$enabled    = ! empty( $settings['enabled'] );
				$zone_id    = sanitize_text_field( (string) ( $settings['zone_id'] ?? '' ) );
				$auth_mode  = sanitize_key( (string) ( $settings['auth_mode'] ?? '' ) );
				$auth_email = sanitize_email( (string) ( $settings['auth_email'] ?? '' ) );
				$plan_source = is_array( $settings['plan'] ?? null ) ? $settings['plan'] : [];
				$limits_source = is_array( $settings['limits'] ?? null ) ? $settings['limits'] : [];
				$policy_source = is_array( $settings['policy'] ?? null ) ? $settings['policy'] : [];
				$sync_source = is_array( $settings['sync'] ?? null ) ? $settings['sync'] : [];

				$plan = [
					'key'             => sanitize_key( (string) ( $plan_source['key'] ?? '' ) ),
					'name'            => sanitize_text_field( (string) ( $plan_source['name'] ?? '' ) ),
					'last_checked_at' => (int) ( $plan_source['last_checked_at'] ?? 0 ),
				];
				$limits = [
					'max_block_entries' => (int) ( $limits_source['max_block_entries'] ?? 0 ),
					'max_allow_entries' => (int) ( $limits_source['max_allow_entries'] ?? 0 ),
				];
				$policy = [
					'auto_block_firewall_events' => ! empty( $policy_source['auto_block_firewall_events'] ),
					'event_threshold'            => (int) ( $policy_source['event_threshold'] ?? 0 ),
					'event_window_minutes'       => (int) ( $policy_source['event_window_minutes'] ?? 0 ),
					'event_ban_minutes'          => (int) ( $policy_source['event_ban_minutes'] ?? 0 ),
					'max_block_entries'          => (int) ( $policy_source['max_block_entries'] ?? 0 ),
				];
				$sync = [
					'pending'       => ! empty( $sync_source['pending'] ),
					'last_sync_at'  => (int) ( $sync_source['last_sync_at'] ?? 0 ),
					'last_error'    => sanitize_text_field( (string) ( $sync_source['last_error'] ?? '' ) ),
					'last_warning'  => sanitize_text_field( (string) ( $sync_source['last_warning'] ?? '' ) ),
					'block_entries' => (int) ( $sync_source['block_entries'] ?? 0 ),
					'allow_entries' => (int) ( $sync_source['allow_entries'] ?? 0 ),
				];
			}
		}

		return [
			'configured' => $configured,
			'connected'  => $connected,
			'enabled'    => $enabled,
			'zone_id'    => $zone_id,
			'auth_mode'  => $auth_mode,
			'auth_email' => $auth_email,
			'plan'       => $plan,
			'limits'     => $limits,
			'policy'     => $policy,
			'sync'       => $sync,
		];
	}

	private function push_security_metrics( SecureApi $api ): bool {
		return $this->send_event( $api, 'state.security_metrics', $this->build_security_metrics_payload() );
	}

	private function build_security_metrics_payload(): array {
		global $wpdb;

		$data = [
			'active_bans'                => 0,
			'rate_limit_hits_1h'         => 0,
			'waf_blocks_1h'              => 0, // populated below from firewall_events table
			'extended_protection_active' => false,
			'waf_enabled'                => false,
			'active_lockouts_ip'         => 0,
			'active_lockouts_user'       => 0,
		];

		$ban_entries = ( new LockoutManager() )->get_all( 'ban' );
		$data['active_bans'] = count(
			array_filter(
				$ban_entries,
				static function ( array $entry ): bool {
					return ( $entry['status'] ?? '' ) === 'active';
				}
			)
		);

		// Rate limit hits in last hour.
		$rate_table = self::table_identifier( $wpdb->prefix . 'sitefort_rate_limits' );
		$rate_table_name = trim( $rate_table, '`' );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Checks existence of a plugin-owned rate-limit table before reading aggregates.
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $rate_table_name ) ) === $rate_table_name ) {
			$one_hour_ago_bucket = (int) ( ( time() - HOUR_IN_SECONDS ) / 60 );
			// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
			$data['rate_limit_hits_1h'] = (int) $wpdb->get_var( $wpdb->prepare(
				"SELECT COALESCE(SUM(hits), 0) FROM {$rate_table} WHERE minute_bucket >= %d",
				$one_hour_ago_bucket
			) );
			// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
		}

		// WAF blocks in last hour (from firewall_events table).
		$fw_table = self::table_identifier( $wpdb->prefix . 'sitefort_firewall_events' );
		$fw_table_name = trim( $fw_table, '`' );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Checks existence of a plugin-owned firewall events table before reading aggregates.
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $fw_table_name ) ) ) === $fw_table_name ) {
			$one_hour_ago = time() - HOUR_IN_SECONDS;
			// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
			$data['waf_blocks_1h'] = (int) $wpdb->get_var( $wpdb->prepare(
				"SELECT COUNT(*) FROM {$fw_table} WHERE event_time >= %d",
				$one_hour_ago
			) );
			// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
		}

		// ExtendedProtection status.
		$ep_config = WP_CONTENT_DIR . '/sitefort-data/waf-config.php';
		$data['extended_protection_active'] = file_exists( $ep_config );

		// WAF origin enforcement state.
		$fw_settings = FirewallConfigService::load_effective_settings();
		$data['waf_enabled'] = FirewallConfigService::is_origin_enforcement_active( $fw_settings );
		$data['waf_mode']    = FirewallConfigService::get_origin_enforcement_mode( $fw_settings );

		$active_lockouts = LockoutRegistry::count_active_lockouts();
		$data['active_lockouts_ip'] = (int) ( $active_lockouts['ip'] ?? 0 );
		$data['active_lockouts_user'] = (int) ( $active_lockouts['user'] ?? 0 );

		return $data;
	}

	private function push_site_health( SecureApi $api, bool $critical = false ): bool {
		$payload = $this->build_site_health_payload();
		if ( $payload === null ) {
			return false;
		}
		return $this->send_event( $api, 'state.site_health', $payload, $critical );
	}

	private function build_site_health_payload(): ?array {
		if ( ! self::ensure_site_health_score_loaded() ) {
			return null;
		}

		$cloud_settings = CloudSettings::get_all();
		$result = SiteHealthScore::calculate();
		$issues = self::sanitize_site_health_issues( $result['issues'] ?? [] );

		// Only push label-based fields; the numeric score stays local.
		return [
			'health_label'      => $result['health_label'] ?? 'Fair',
			'posture_label'     => $result['posture_label'] ?? ( $result['health_label'] ?? 'Fair' ),
			'scanner_label'     => $result['scanner_label'] ?? 'Secure',
			'console_enabled'   => ! empty( $cloud_settings['console_enabled'] ),
			'has_scan_baseline' => ! empty( $result['has_scan_baseline'] ),
			'scanner_summary'   => is_array( $result['scanner_summary'] ?? null )
				? $result['scanner_summary']
				: [
					'total_active' => 0,
					'by_severity'  => [ 'critical' => 0, 'high' => 0, 'medium' => 0, 'low' => 0 ],
					'max_severity' => 'none',
					'by_type'      => [],
				],
			'issue_count'       => count( $issues ),
			'primary_issue'     => $issues[0] ?? '',
			'issues'            => array_slice( $issues, 0, 12 ),
		];
	}

	/**
	 * Remove advisory security-header findings from the shared site-health feed.
	 *
	 * Header analysis remains available in the dedicated analyzer, but it must
	 * never leak into site health, scoring, or console health summaries.
	 *
	 * @param mixed $raw_issues
	 * @return array<int,string>
	 */
	private static function sanitize_site_health_issues( $raw_issues ): array {
		if ( ! is_array( $raw_issues ) ) {
			return [];
		}

		$issues = [];
		foreach ( $raw_issues as $issue ) {
			if ( ! is_string( $issue ) ) {
				continue;
			}
			$issue = trim( $issue );
			if ( $issue === '' || self::is_header_health_issue( $issue ) ) {
				continue;
			}
			$issues[] = $issue;
		}

		return array_values( $issues );
	}

	/**
	 * Determine whether a site-health issue string refers to header analysis.
	 *
	 * @param string $issue
	 * @return bool
	 */
	private static function is_header_health_issue( string $issue ): bool {
		$normalized = strtolower( trim( $issue ) );
		if ( $normalized === '' ) {
			return false;
		}

		foreach ( [
			'security headers',
			'recommended headers',
			'content security policy',
			'csp header',
			'hsts header',
			'strict transport security',
		] as $needle ) {
			if ( strpos( $normalized, $needle ) !== false ) {
				return true;
			}
		}

		return false;
	}


	/**
	 * Buffer a critical audit event for asynchronous batched delivery.
	 *
	 * Only buffers events of interest to Console - not every DB insert.
	 *
	 * @param string $event The event name.
	 * @param array  $data  Event metadata.
	 */
	public static function buffer_audit_event( string $event, array $data ): void {
		$critical_events = [
			// Authentication
			'User Login', 'User Logout', 'Login Failed',
			// User management
			'User Registered', 'User Deleted', 'User Role Changed',
			// Password
			'Password Changed', 'Password Reset Requested', 'Password Reset Completed',
			'Application Password Created', 'Application Password Deleted',
			// Plugins / themes / core
			'Plugin Activated', 'Plugin Deactivated', 'Plugin Updated',
			'Theme Switched', 'Theme Updated', 'WordPress Core Updated',
			// Hardening / settings
			'Hardening Settings Changed', 'Site Setting Changed',
			// Lockouts
			'IP Lockout', 'User Lockout',
			// Firewall
			'Firewall Block',
			// Two-factor authentication
			'Two-Factor Enabled', 'Two-Factor Disabled', 'Recovery Codes Regenerated',
			'Two-Factor Verified', 'Two-Factor Verification Failed',
			'Two-Factor Challenge Locked', 'Two-Factor Fallback Activated',
			'Two-Factor Unavailable', 'Two-Factor Remembered Device Accepted',
		];
		if ( ! in_array( $event, $critical_events, true ) ) {
			return;
		}

		self::$audit_buffer[] = array_merge( $data, [
			'event' => $event,
			'ts'    => time(),
		] );

		if ( ! self::$audit_shutdown_registered ) {
			register_shutdown_function( [ __CLASS__, 'flush_audit_buffer' ] );
			self::$audit_shutdown_registered = true;
		}
	}

	/**
	 * Queue buffered audit events for asynchronous delivery.
	 */
	public static function flush_audit_buffer(): void {
		if ( empty( self::$audit_buffer ) ) {
			return;
		}

		if ( self::get_api_settings() === null ) {
			self::$audit_buffer = [];
			return;
		}

		$buffer = self::$audit_buffer;
		self::$audit_buffer = [];

		self::queue_audit_reporting_events( $buffer );
		self::ensure_audit_reporting_dispatch_scheduled();
	}

	/**
	 * Send a queued audit event batch from Action Scheduler/WP-Cron.
	 *
	 * @param array{event_id:string,event_type:string,data:array<string,mixed>} $event
	 */
	public static function process_audit_event_batch( array $event = [], int $attempt = 0 ): void {
		$events = [];
		if ( ! empty( $event ) ) {
			$events = self::sanitize_event_batch( [ $event ] );
		}

		$instance = new self();
		$api = $instance->get_api();
		if ( ! $api ) {
			return;
		}

		if ( empty( $events ) ) {
			$queued = self::drain_audit_reporting_events();
			if ( empty( $queued ) ) {
				return;
			}

			$events = self::sanitize_event_batch( [
				self::build_audit_reporting_event( $queued ),
			] );
		}

		if ( empty( $events ) ) {
			return;
		}

		if ( ! $instance->send_event_batch( $api, $events ) ) {
			self::retry_async_payload( self::AUDIT_DISPATCH_HOOK, [ $events[0] ], $attempt );
		}
	}

	/**
	 * @param array<int,array<string,mixed>> $events
	 */
	private static function queue_audit_reporting_events( array $events ): void {
		$events = array_values( array_filter( $events, 'is_array' ) );
		if ( empty( $events ) ) {
			return;
		}

		$lock_token = self::acquire_audit_reporting_buffer_lock();
		if ( $lock_token === null ) {
			self::schedule_async_payload(
				self::AUDIT_DISPATCH_HOOK,
				[ self::build_audit_reporting_event( $events ), 0 ],
				self::AUDIT_REPORTING_DISPATCH_DELAY
			);
			return;
		}

		try {
			$queued = get_option( self::AUDIT_REPORTING_BUFFER_OPTION, [] );
			if ( ! is_array( $queued ) ) {
				$queued = [];
			}

			$queued = array_values( array_filter( $queued, 'is_array' ) );
			$queued = array_merge( $queued, $events );

			if ( count( $queued ) > self::AUDIT_REPORTING_BUFFER_MAX_EVENTS ) {
				$queued = array_slice( $queued, -self::AUDIT_REPORTING_BUFFER_MAX_EVENTS );
			}

			update_option( self::AUDIT_REPORTING_BUFFER_OPTION, $queued, false );
		} finally {
			self::release_audit_reporting_buffer_lock( $lock_token );
		}
	}

	/**
	 * @return array<int,array<string,mixed>>
	 */
	private static function drain_audit_reporting_events(): array {
		$lock_token = self::acquire_audit_reporting_buffer_lock();
		if ( $lock_token === null ) {
			self::schedule_async_payload( self::AUDIT_DISPATCH_HOOK, [], 60 );
			return [];
		}

		try {
			$queued = get_option( self::AUDIT_REPORTING_BUFFER_OPTION, [] );
			delete_option( self::AUDIT_REPORTING_BUFFER_OPTION );
		} finally {
			self::release_audit_reporting_buffer_lock( $lock_token );
		}

		if ( ! is_array( $queued ) ) {
			return [];
		}

		return array_values( array_filter( $queued, 'is_array' ) );
	}

	/**
	 * @param array<int,array<string,mixed>> $events
	 * @return array{event_id:string,event_type:string,data:array<string,mixed>}
	 */
	private static function build_audit_reporting_event( array $events ): array {
		return [
			'event_id'   => wp_generate_uuid4(),
			'event_type' => 'state.audit_events',
			'data'       => [
				'events' => $events,
				'count'  => count( $events ),
			],
		];
	}

	private static function ensure_audit_reporting_dispatch_scheduled(): void {
		if ( function_exists( '\\as_has_scheduled_action' ) && function_exists( '\\as_schedule_single_action' ) ) {
			if ( ! \as_has_scheduled_action( self::AUDIT_DISPATCH_HOOK, [], 'sitefort_cloud' ) ) {
				\as_schedule_single_action(
					time() + self::AUDIT_REPORTING_DISPATCH_DELAY,
					self::AUDIT_DISPATCH_HOOK,
					[],
					'sitefort_cloud'
				);
			}
			return;
		}

		if ( ! wp_next_scheduled( self::AUDIT_DISPATCH_HOOK ) ) {
			wp_schedule_single_event( time() + self::AUDIT_REPORTING_DISPATCH_DELAY, self::AUDIT_DISPATCH_HOOK );
		}
	}

	private static function acquire_audit_reporting_buffer_lock(): ?string {
		$token = wp_generate_uuid4();

		for ( $attempt = 0; $attempt < self::AUDIT_REPORTING_BUFFER_LOCK_ATTEMPTS; $attempt++ ) {
			$expires_at = time() + self::AUDIT_REPORTING_BUFFER_LOCK_TTL;
			if ( add_option(
				self::AUDIT_REPORTING_BUFFER_LOCK_OPTION,
				[
					'token'      => $token,
					'expires_at' => $expires_at,
				],
				'',
				false
			) ) {
				return $token;
			}

			$lock = get_option( self::AUDIT_REPORTING_BUFFER_LOCK_OPTION, [] );
			$locked_until = is_array( $lock ) ? (int) ( $lock['expires_at'] ?? 0 ) : 0;
			if ( $locked_until <= time() ) {
				delete_option( self::AUDIT_REPORTING_BUFFER_LOCK_OPTION );
				continue;
			}

			usleep( self::AUDIT_REPORTING_BUFFER_LOCK_WAIT_US );
		}

		return null;
	}

	private static function release_audit_reporting_buffer_lock( string $token ): void {
		$lock = get_option( self::AUDIT_REPORTING_BUFFER_LOCK_OPTION, [] );
		if ( is_array( $lock ) && hash_equals( $token, (string) ( $lock['token'] ?? '' ) ) ) {
			delete_option( self::AUDIT_REPORTING_BUFFER_LOCK_OPTION );
		}
	}

	/**
	 * @param array<int,array<string,mixed>> $events
	 * @return array<int,array{event_id:string,event_type:string,data:array<string,mixed>}>
	 */
	private static function sanitize_event_batch( array $events ): array {
		$sanitized = [];
		foreach ( $events as $event ) {
			if ( ! is_array( $event ) ) {
				continue;
			}

			$event_id = trim( (string) ( $event['event_id'] ?? '' ) );
			$event_type = trim( (string) ( $event['event_type'] ?? '' ) );
			$data = $event['data'] ?? null;

			if ( $event_id === '' || $event_type === '' || ! is_array( $data ) ) {
				continue;
			}

			$sanitized[] = [
				'event_id'   => $event_id,
				'event_type' => $event_type,
				'data'       => $data,
			];
		}

		return $sanitized;
	}

	/**
	 * @param array<int,mixed> $args
	 */
	private static function retry_async_payload( string $hook, array $args, int $attempt ): void {
		if ( $attempt >= count( self::ASYNC_RETRY_DELAYS ) ) {
			return;
		}

		$args[] = $attempt + 1;
		self::schedule_async_payload( $hook, $args, self::ASYNC_RETRY_DELAYS[ $attempt ] );
	}

	/**
	 * @param array<int,mixed> $args
	 */
	private static function schedule_async_payload( string $hook, array $args, int $delay_seconds ): void {
		$timestamp = time() + max( 1, $delay_seconds );
		if ( function_exists( '\\as_schedule_single_action' ) ) {
			\as_schedule_single_action( $timestamp, $hook, $args, 'sitefort_cloud' );
			return;
		}

		wp_schedule_single_event( $timestamp, $hook, $args );
	}

	/**
	 * Push security metrics hourly via WP-Cron.
	 */
	public static function push_hourly_metrics(): void {
		$instance = new self();
		$api = $instance->get_api();
		if ( ! $api ) {
			return;
		}

		$instance->push_domain( $api, 'security_metrics' );
	}

	/**
	 * Push notification state immediately so Console settings converge without
	 * waiting for the deferred dirty-flag flush.
	 */
	public static function push_notification_state_now( bool $critical = false ): void {
		self::do_notification_state_push( $critical );
	}

	/**
	 * Push terminal scan state immediately so Console status converges with
	 * completion notifications instead of waiting for the deferred dirty flush.
	 *
	 * This runs inside the scanner worker/background path, not on a normal
	 * browser request, so the short background HTTP timeout is acceptable.
	 */
	public static function push_terminal_scan_state(): void {
		self::push_scan_state_now();
	}

	/**
	 * Push scan_results + site_health immediately.
	 *
	 * Used by remote command paths that need Console to observe finding
	 * state transitions without waiting for the deferred dirty-flag flush.
	 */
	public static function push_scan_state_now(): void {
		self::do_scan_state_push( true );
	}

	/**
	 * Ensure the terminal scan state has been pushed to Console.
	 *
	 * Safe to call multiple times; skips the push if the dirty flags for
	 * scan_results and site_health have already been cleared (i.e. a
	 * previous push succeeded).
	 */
	public static function ensure_terminal_scan_pushed(): void {
		$dirty = self::get_dirty_domains();
		if ( ! in_array( 'scan_results', $dirty, true ) && ! in_array( 'site_health', $dirty, true ) ) {
			return; // Already pushed successfully.
		}
		self::do_scan_state_push( true );
	}

	/**
	 * Push scan_results + site_health synchronously.
	 */
	private static function do_scan_state_push( bool $critical = false ): void {
		$instance = new self();
		$api = $instance->get_api();
		if ( ! $api ) {
			return;
		}

		$domains = [ 'scan_results', 'site_health' ];
		if ( $instance->push_domains_batch( $api, $domains, $critical ) ) {
			self::clear_dirty( $domains );
		} else {
			sitefort_log( '[SiteFort] scan state push: batch failed' );
		}
	}

	/**
	 * Push notification state synchronously.
	 */
	private static function do_notification_state_push( bool $critical = false ): void {
		$instance = new self();
		$api = $instance->get_api();
		if ( ! $api ) {
			return;
		}

		if ( $instance->push_notification( $api, $critical ) ) {
			self::clear_dirty( [ 'notification' ] );
			return;
		}

		sitefort_log( '[SiteFort] notification state push: state.notification failed' );
	}


	/**
	 * Build a SecureApi instance from stored credentials, or null if not connected.
	 */
	private function get_api(): ?SecureApi {
		$settings = self::get_api_settings();
		if ( $settings === null ) {
			return null;
		}

		return new SecureApi(
			$settings['license_key'] ?? '',
			$settings['site_token'] ?? '',
			$settings['site_secret'] ?? ''
		);
	}

	/**
	 * Return Console API settings when state pushes are allowed for this site.
	 *
	 * @return array<string,mixed>|null
	 */
	private static function get_api_settings(): ?array {
		$settings = CloudSettings::get_all();
		if ( empty( $settings['console_enabled'] ) ) {
			return null; // Console not active - skip push.
		}
		$migration_status = strtolower( trim( (string) ( $settings['migration_status'] ?? 'none' ) ) );
		if ( $migration_status !== '' && $migration_status !== 'none' ) {
			return null; // Migrated copy is paused until Console resolves the move.
		}
		// State push + command ACK are Console-scoped and HMAC-authenticated with
		// site token/secret. license_key is not required for these endpoints.
		if ( empty( $settings['site_token'] ) || empty( $settings['site_secret'] ) ) {
			return null;
		}

		return $settings;
	}

	/**
	 * 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 ) . '`';
	}

	/**
	 * Send a state event to Console.
	 *
	 * Uses post_background() (5 s timeout, 1 retry) - designed to run
	 * inside Action Scheduler or WP-Cron workers, never on a user request.
	 *
	 * @return bool True if Console accepted the event (2xx).
	 */
	private function send_event( SecureApi $api, string $event_type, array $data, bool $critical = false ): bool {
		$event_id = wp_generate_uuid4();

		$payload = [
			'event_id'       => $event_id,
			'event_type'     => $event_type,
			'data'           => $data,
			'plugin_version' => defined( 'SITEFORT_VERSION' ) ? SITEFORT_VERSION : '1.0.0',
		];

		if ( $critical ) {
			return $api->post_control_plane( 'services/state/push', $payload );
		}

		return $api->post_background( 'services/state/push', $payload );
	}

	/**
	 * @param array<int,array{event_id:string,event_type:string,data:array<string,mixed>}> $events
	 */
	private function send_event_batch( SecureApi $api, array $events, bool $critical = false ): bool {
		if ( empty( $events ) ) {
			return true;
		}

		$payload = [
			'events'         => $events,
			'plugin_version' => defined( 'SITEFORT_VERSION' ) ? SITEFORT_VERSION : '1.0.0',
		];

		if ( $critical ) {
			return $api->post_control_plane( 'services/state/push', $payload );
		}

		return $api->post_background( 'services/state/push', $payload );
	}

	/**
	 * Returns null when the domain has nothing to push (treated as success).
	 *
	 * @return array{event_id:string,event_type:string,data:array<string,mixed>}|null
	 */
	private function build_domain_event( string $domain ): ?array {
		$data = null;
		switch ( $domain ) {
			case 'environment':
				$data = $this->build_environment_payload();
				break;
			case 'hardening':
				$data = $this->build_hardening_payload();
				break;
			case 'firewall':
				$data = $this->build_firewall_payload();
				break;
			case 'scan_results':
				$data = $this->build_scan_results_payload();
				break;
			case 'two_factor':
				$data = $this->build_two_factor_payload();
				break;
			case 'notification':
				$data = $this->build_notification_payload();
				break;
			case 'cloudflare_status':
				$data = $this->build_cloudflare_status_payload();
				break;
			case 'security_metrics':
				$data = $this->build_security_metrics_payload();
				break;
			case 'site_health':
				$data = $this->build_site_health_payload();
				break;
		}

		if ( $data === null ) {
			return null;
		}

		return [
			'event_id'   => wp_generate_uuid4(),
			'event_type' => 'state.' . $domain,
			'data'       => $data,
		];
	}

	/**
	 * Push the named state domains in one batched POST. All-or-nothing.
	 *
	 * @param array<int,string> $domains
	 */
	private function push_domains_batch( SecureApi $api, array $domains, bool $critical = false ): bool {
		$events = [];
		foreach ( $domains as $domain ) {
			$event = $this->build_domain_event( $domain );
			if ( $event !== null ) {
				$events[] = $event;
			}
		}

		if ( empty( $events ) ) {
			return true;
		}

		return $this->send_event_batch( $api, $events, $critical );
	}

	/**
	 * Get current scan status string.
	 *
	 * @return string idle|running|failed|stopped
	 */
	private function get_scan_status(): string {
		$last_status = strtolower( trim( (string) \SiteFort\Includes\SettingsStore::get_field( \SiteFort\Includes\SettingsStore::COL_STATE, 'last_scan_status', '' ) ) );

		if ( ! self::ensure_scan_queue_loaded() ) {
			if ( in_array( $last_status, [ 'failed', 'error' ], true ) ) {
				return 'failed';
			}
			if ( in_array( $last_status, [ 'stopped', 'cancelled' ], true ) ) {
				return 'stopped';
			}
			return 'idle';
		}

		$queue = \SiteFort\Modules\Scanner\Queue\ScanQueue::get();
		if ( ! $queue ) {
			if ( in_array( $last_status, [ 'failed', 'error' ], true ) ) {
				return 'failed';
			}
			if ( in_array( $last_status, [ 'stopped', 'cancelled' ], true ) ) {
				return 'stopped';
			}
			return 'idle';
		}

		$status = (string) ( $queue['status'] ?? '' );
		if ( in_array( $status, [ 'pending', 'running' ], true ) ) {
			return 'running';
		}

		if ( in_array( $last_status, [ 'failed', 'error' ], true ) ) {
			return 'failed';
		}
		if ( in_array( $last_status, [ 'stopped', 'cancelled' ], true ) ) {
			return 'stopped';
		}

		return 'idle';
	}

	/**
	 * Return a sanitized scan failure payload for console consumers.
	 *
	 * @return array<string, mixed>
	 */
	private static function get_scan_failure_context(): array {
		$raw = \SiteFort\Includes\SettingsStore::get_field(
			\SiteFort\Includes\SettingsStore::COL_STATE,
			'last_scan_failure_context',
			[]
		);
		if ( ! is_array( $raw ) || empty( $raw ) ) {
			return [];
		}

		$context = [
			'scan_id'           => sanitize_text_field( (string) ( $raw['scan_id'] ?? '' ) ),
			'message'           => sanitize_text_field( (string) ( $raw['message'] ?? '' ) ),
			'error_code'        => sanitize_key( (string) ( $raw['error_code'] ?? 'scan_failed' ) ),
			'stage'             => sanitize_key( (string) ( $raw['stage'] ?? 'scanner' ) ),
			'status_code'       => isset( $raw['status_code'] ) ? (int) $raw['status_code'] : 0,
			'request_id'        => sanitize_text_field( (string) ( $raw['request_id'] ?? '' ) ),
			'host_id'           => sanitize_text_field( (string) ( $raw['host_id'] ?? '' ) ),
			'retryable'         => array_key_exists( 'retryable', $raw ) ? (bool) $raw['retryable'] : true,
			'occurred_at'       => isset( $raw['occurred_at'] ) ? (int) $raw['occurred_at'] : 0,
			'incident_ref'      => sanitize_text_field( (string) ( $raw['incident_ref'] ?? '' ) ),
			'cloud_error_count' => isset( $raw['cloud_error_count'] ) ? (int) $raw['cloud_error_count'] : 0,
			'technical_message' => sanitize_text_field( (string) ( $raw['technical_message'] ?? '' ) ),
		];

		return array_filter(
			$context,
			static function ( $value, $key ) {
				if ( in_array( $key, [ 'retryable', 'status_code', 'occurred_at', 'cloud_error_count' ], true ) ) {
					return true;
				}

				return $value !== '';
			},
			ARRAY_FILTER_USE_BOTH
		);
	}

	private static function ensure_scan_queue_loaded(): bool {
		if ( class_exists( '\\SiteFort\\Modules\\Scanner\\Queue\\ScanQueue' ) ) {
			return true;
		}

		$path = dirname( __DIR__, 2 ) . '/Scanner/Queue/ScanQueue.php';
		if ( is_readable( $path ) ) {
			require_once $path;
		}

		return class_exists( '\\SiteFort\\Modules\\Scanner\\Queue\\ScanQueue' );
	}

	private static function ensure_issue_repository_loaded(): bool {
		if ( class_exists( '\\SiteFort\\Modules\\Scanner\\Repository\\IssueRepository' ) ) {
			return true;
		}

		$path = dirname( __DIR__, 2 ) . '/Scanner/Repository/IssueRepository.php';
		if ( is_readable( $path ) ) {
			require_once $path;
		}

		return class_exists( '\\SiteFort\\Modules\\Scanner\\Repository\\IssueRepository' );
	}

	private static function ensure_site_health_score_loaded(): bool {
		if ( class_exists( '\\SiteFort\\Modules\\Cloud\\Services\\SiteHealthScore' ) ) {
			return true;
		}

		$path = __DIR__ . '/SiteHealthScore.php';
		if ( is_readable( $path ) ) {
			require_once $path;
		}

		return class_exists( '\\SiteFort\\Modules\\Cloud\\Services\\SiteHealthScore' );
	}

	private function ensure_snapshot_schema_alignment(): void {
		$stored = (int) get_option( self::SCHEMA_VERSION_OPTION, 0 );
		if ( $stored >= self::SNAPSHOT_SCHEMA_VERSION ) {
			return;
		}

		update_option( self::SCHEMA_VERSION_OPTION, self::SNAPSHOT_SCHEMA_VERSION, false );
		self::schedule_full_push();
	}
}
