<?php

declare(strict_types=1);

namespace SiteFort\Core;

use SiteFort\Core\Database\SchemaInspector;
use SiteFort\Includes\DataDirectoryProtection;
use SiteFort\Includes\SettingsStore;
use SiteFort\Modules\Hardening\Hardening;
use WP_Site;

// If this file is called directly, abort.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Provision plugin-owned storage and cleanup tasks during lifecycle events.
 */
class Installer {
	private const SCHEDULED_HOOKS = [
		'sitefort_daily_scan_cron',
		'sitefort_weekly_report_cron',
		'sitefort_telemetry_heartbeat',
		'sitefort_daily_event',
		'sitefort_entitlement_refresh',
		'sitefort_state_push',
		'sitefort_keepalive',
		'sitefort_state_dispatch',
		'sitefort_state_dispatch_low',
		'sitefort_state_notification_dispatch',
		'sitefort_state_audit_dispatch',
		'sitefort_scheduled_scan_job',
		'sitefort_scan_cron_check',
		'sitefort_scan_worker_fallback',
		'sitefort_scanner_watchdog',
		'sitefort_blocklist_sync',
		'sitefort_geoip_update',
		'sitefort_safe_network_ranges_update',
		'sitefort_verified_bot_ranges_update',
		'sitefort_login_attempt_cleanup',
		'sitefort_rate_limit_cleanup',
		'sitefort_rest_rate_limit_cleanup',
		'sitefort_firewall_lockout_cleanup',
		'sitefort_audit_log_cleanup',
		'sitefort_db_migration_runner',
		'sitefort_quarantine_purge',
		'sitefort_firewall_log_cleanup',
		'sitefort_firewall_waf_log_ingest',
		'sitefort_email_digest',
		'sitefort_fw_block_digest_send',
		'sitefort_webhook_retry',
		'sitefort_metrics_push',
		'sitefort_cf_reconcile',
		'sitefort_vulnerability_refresh',
		'sitefort_command_execute',
		'sitefort_command_queue_maintenance',
		'sitefort_server_rule_reconcile',
		'sitefort_server_rule_maintenance',
		'sitefort_server_rule_verify',
		'sitefort_user_nicename_remediation',
	];

	public static function ensure_runtime_directories(): void {
		self::create_data_directory();
	}

	public static function ensure_file_states_table_schema(): void {
		self::create_file_states_table();
	}

	public static function ensure_two_factor_table_schema(): void {
		self::create_two_factor_table();
	}

	public static function ensure_two_factor_recovery_codes_table_schema(): void {
		self::create_two_factor_recovery_codes_table();
	}

	public static function ensure_two_factor_sessions_table_schema(): void {
		self::create_two_factor_sessions_table();
	}

	public static function ensure_audit_table_schema(): void {
		self::create_audit_table();
	}

	public static function ensure_login_attempts_table_schema(): void {
		self::create_login_attempts_table();
	}

	public static function ensure_firewall_events_table_schema(): void {
		self::create_firewall_events_table();
	}

	public static function ensure_waf_runtime_table_schema(): void {
		self::create_waf_runtime_table();
	}

	public static function ensure_blocklist_ranges_table_schema(): void {
		self::create_blocklist_ranges_table();
	}

	public static function ensure_command_queue_table_schema(): void {
		\SiteFort\Modules\Cloud\Services\CommandQueue::ensure_table();
	}

	/**
	 * Return every plugin-owned scheduled hook that may exist in WP-Cron or Action Scheduler.
	 *
	 * @return array<int,string>
	 */
	public static function scheduled_hooks(): array {
		return self::SCHEDULED_HOOKS;
	}

	/**
	 * Remove a scheduled hook from both WP-Cron and Action Scheduler, including argument variants.
	 */
	public static function clear_scheduled_hook_everywhere( string $hook ): void {
		wp_clear_scheduled_hook( $hook );

		if ( function_exists( '_get_cron_array' ) ) {
			$crons = _get_cron_array();
			if ( is_array( $crons ) ) {
				foreach ( $crons as $timestamp => $cron_hooks ) {
					if ( ! isset( $cron_hooks[ $hook ] ) || ! is_array( $cron_hooks[ $hook ] ) ) {
						continue;
					}

					foreach ( $cron_hooks[ $hook ] as $event ) {
						$args = [];
						if ( is_array( $event ) && isset( $event['args'] ) && is_array( $event['args'] ) ) {
							$args = $event['args'];
						}

						wp_unschedule_event( (int) $timestamp, $hook, $args );
					}
				}
			}
		}

		if ( function_exists( 'as_unschedule_all_actions' ) ) {
			as_unschedule_all_actions( $hook );
		}
	}

	/**
	 * Activation hook.
	 *
	 * Code to run on plugin activation.
	 * This is typically used to set up default options, create database tables,
	 * schedule cron jobs, etc.
	 *
	 * @since    1.0.0
	 */
	public static function activate( bool $network_wide = false ): void {
		if ( is_multisite() && $network_wide ) {
			self::provision_network_sites();
			return;
		}

		self::provision_current_site( 'activation' );
		self::schedule_cloudflare_reconcile_if_enabled();
	}

	/**
	 * Deactivation clears the Cloudflare edge rule; reactivation restores it.
	 */
	private static function schedule_cloudflare_reconcile_if_enabled(): void {
		if ( ! class_exists( '\SiteFort\Includes\SettingsStore' ) ) {
			return;
		}

		try {
			$cloudflare = \SiteFort\Includes\SettingsStore::get( \SiteFort\Includes\SettingsStore::COL_CLOUDFLARE, [] );
		} catch ( \Throwable $e ) {
			return;
		}

		if ( ! is_array( $cloudflare ) || empty( $cloudflare['enabled'] ) ) {
			return;
		}

		if ( ! wp_next_scheduled( 'sitefort_cf_reconcile' ) ) {
			wp_schedule_single_event( time() + 30, 'sitefort_cf_reconcile' );
		}
	}

	/**
	 * Provision plugin storage for a newly created multisite site.
	 *
	 * @param WP_Site             $new_site New site instance.
	 * @param array<string,mixed> $args     Site initialization arguments.
	 */
	public static function initialize_new_site( WP_Site $new_site, array $args = [] ): void {
		unset( $args );

		if ( ! is_multisite() ) {
			return;
		}

		$blog_id = isset( $new_site->blog_id ) ? (int) $new_site->blog_id : 0;
		if ( $blog_id <= 0 ) {
			return;
		}

		self::provision_site( $blog_id, 'site_init' );
	}

	/**
	 * Persist the default hardening baseline before the first manual save.
	 */
	private static function seed_hardening_defaults(): void {
		$defaults = Hardening::get_default_settings();
		$stored   = SettingsStore::get( SettingsStore::COL_HARDENING );
		$stored   = is_array( $stored ) ? $stored : [];
		$merged   = wp_parse_args( $stored, $defaults );

		if ( $merged !== $stored ) {
			SettingsStore::save( SettingsStore::COL_HARDENING, $merged );
		}
	}

	/**
	 * Deactivation hook.
	 *
	 * Code to run on plugin deactivation.
	 * This is typically used to clean up options, remove cron jobs, etc.
	 * Data removal should ideally be handled in an uninstall script.
	 *
	 * @since    1.0.0
	 */
	public static function deactivate() {
		// Fire notification before cleanup (modules are loaded via plugins_loaded).
		do_action( 'sitefort_plugin_deactivating' );

		// Deactivated SiteFort must not keep visitors blocked at the edge.
		if ( class_exists( '\SiteFort\Modules\Cloudflare\Manager' ) ) {
			( new \SiteFort\Modules\Cloudflare\Manager() )->clear_edge_enforcement();
		}

		foreach ( self::scheduled_hooks() as $hook ) {
			self::clear_scheduled_hook_everywhere( $hook );
		}

		// Remove the auto_prepend_file WAF hook before the plugin is deleted.
		if ( class_exists( '\SiteFort\Modules\Firewall\ExtendedProtection\Manager' ) ) {
			\SiteFort\Modules\Firewall\ExtendedProtection\Manager::uninstall( true );
		}

		// Remove server config rules (.htaccess markers / nginx conf).
		\SiteFort\Includes\ServerConfigWriter::remove();

		// Remove wp-config.php rules that are explicitly managed by Hardening.
		if ( class_exists( '\SiteFort\Modules\Hardening\Hardening' ) ) {
			\SiteFort\Modules\Hardening\Hardening::remove_wp_config_rules_on_deactivation();
		}

		// Flush rewrite rules
		flush_rewrite_rules();
	}

	/**
	 * Provision the current site with plugin-owned schema and default posture.
	 */
	private static function provision_current_site( string $runner ): void {
		self::ensure_runtime_directories();
		\SiteFort\Modules\Firewall\Firewall::invalidate_runtime_assets_reconcile_cache();

		self::create_queue_table();
		self::create_task_queue_table();
		self::create_audit_table();
		self::create_scan_status_table();
		self::create_settings_table();
		self::create_request_nonces_table();

		self::create_file_states_table();
		self::create_cloud_jobs_table();
		self::create_hash_cache_table();
		self::create_file_index_table();

		self::create_issues_table();
		self::create_ignored_issues_table();
		self::create_scan_runs_table();
		self::create_issue_instances_table();
		\SiteFort\Modules\Scanner\Repository\QuarantineRepository::ensure_table();

		self::create_two_factor_table();
		self::create_two_factor_recovery_codes_table();
		self::create_two_factor_sessions_table();
		self::create_login_attempts_table();
		self::create_rate_limits_table();
		self::create_blocklist_table();
		self::ensure_command_queue_table_schema();
		self::ensure_firewall_events_table_schema();

		\SiteFort\Includes\SettingsStore::maybe_migrate();
		self::seed_hardening_defaults();
		\SiteFort\Modules\Firewall\FirewallLogger::reset_table_state();

		\SiteFort\Core\Database\MigrationManager::sync_registry_state( $runner );

		$hardening = Hardening::get_saved_settings();
		if ( ! empty( $hardening['rename_login_url_enabled'] ) ) {
			flush_rewrite_rules();
		}
	}

	private static function apply_table_schema( string $table_name, string $sql ): bool {
		global $wpdb;

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		dbDelta( $sql );
		SchemaInspector::flush_cache( $table_name );

		if ( SchemaInspector::table_exists( $table_name ) ) {
			return SchemaInspector::ensure_table_uses_innodb( $table_name );
		}

		sitefort_log(
			'[SiteFort] Table provisioning failed for '
			. $table_name
			. ( $wpdb->last_error !== '' ? ': ' . sanitize_text_field( $wpdb->last_error ) : '' )
		);

		return false;
	}

	/**
	 * Provision every site on the network during network activation.
	 */
	private static function provision_network_sites(): void {
		$page = 1;

		do {
			$site_ids = get_sites(
				[
					'fields'                 => 'ids',
					'number'                 => 100,
					'paged'                  => $page,
					'update_site_cache'      => false,
					'update_site_meta_cache' => false,
				]
			);

			if ( empty( $site_ids ) ) {
				break;
			}

			foreach ( $site_ids as $site_id ) {
				self::provision_site( (int) $site_id, 'network_activation' );
			}

			$page++;
		} while ( count( $site_ids ) === 100 );
	}

	/**
	 * Provision a specific multisite blog in isolation.
	 */
	private static function provision_site( int $blog_id, string $runner ): void {
		if ( $blog_id <= 0 ) {
			return;
		}

		switch_to_blog( $blog_id );
		try {
			SettingsStore::reset_cache();
			\SiteFort\Modules\Firewall\FirewallLogger::reset_table_state();
			self::provision_current_site( $runner );
		} finally {
			SettingsStore::reset_cache();
			\SiteFort\Modules\Firewall\FirewallLogger::reset_table_state();
			restore_current_blog();
		}
	}

	/**
	 * Create the scan queue table.
	 */
	private static function create_queue_table() {
		global $wpdb;

		$table_name = $wpdb->prefix . 'sitefort_scan_queue';
		$charset_collate = SchemaInspector::validated_charset_collate( $wpdb );

		$sql = "CREATE TABLE $table_name (
			id bigint(20) NOT NULL AUTO_INCREMENT,
			job_id varchar(64) NOT NULL,
			scan_type varchar(50) NOT NULL,
			source varchar(20) DEFAULT 'local',
			status varchar(20) NOT NULL,
			processed_count int(11) DEFAULT 0,
			total_count int(11) DEFAULT 0,
			cloud_stats longtext DEFAULT NULL,
			component_status longtext DEFAULT NULL,
			cloud_error_count int(10) UNSIGNED DEFAULT 0,
			malware_signature_version varchar(20) DEFAULT NULL COMMENT 'Current signature version for this scan',
			next_run_at int(11) DEFAULT 0,
			last_dispatch_at int(11) DEFAULT 0,
			task_runner_lease_token varchar(64) DEFAULT '',
			task_runner_lease_until int(11) NOT NULL DEFAULT 0,
			last_ping int(11) DEFAULT 0,
			created_at int(11) NOT NULL,
			updated_at int(11) NOT NULL,
			PRIMARY KEY  (id),
			UNIQUE KEY job_id (job_id),
			KEY status (status),
			KEY created_at (created_at)
		) $charset_collate;";

		self::apply_table_schema( $table_name, $sql );
	}

	/**
	 * Create the durable scanner task queue table.
	 */
	private static function create_task_queue_table() {
		global $wpdb;

		$table_name = $wpdb->prefix . 'sitefort_task_queue';
		$charset_collate = SchemaInspector::validated_charset_collate( $wpdb );

		$sql = "CREATE TABLE $table_name (
			id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
			scan_id varchar(64) NOT NULL,
			task_type varchar(40) NOT NULL,
			status varchar(20) NOT NULL DEFAULT 'pending',
			payload longtext DEFAULT NULL,
			result longtext DEFAULT NULL,
			available_at int(11) NOT NULL DEFAULT 0,
			lease_until int(11) NOT NULL DEFAULT 0,
			attempts tinyint unsigned NOT NULL DEFAULT 0,
			max_attempts tinyint unsigned NOT NULL DEFAULT 5,
			priority tinyint unsigned NOT NULL DEFAULT 100,
			created_at int(11) NOT NULL,
			updated_at int(11) NOT NULL,
			dedupe_key varchar(64) DEFAULT NULL,
			PRIMARY KEY  (id),
			KEY idx_claim (scan_id, status, available_at, priority),
			KEY idx_scan_type (scan_id, task_type, status),
			KEY idx_lease (status, lease_until),
			UNIQUE KEY idx_dedupe (scan_id, dedupe_key)
		) $charset_collate;";

		self::apply_table_schema( $table_name, $sql );
	}

	/**
	 * Create the audit log table.
	 */
	private static function create_audit_table() {
		global $wpdb;

		$table_name       = $wpdb->prefix . 'sitefort_audit_log';
		$charset_collate  = SchemaInspector::validated_charset_collate( $wpdb );

		$sql = "CREATE TABLE $table_name (
			id bigint(20) NOT NULL AUTO_INCREMENT,
			event varchar(50) NOT NULL,
			message text NOT NULL,
			user_id bigint(20) NOT NULL DEFAULT 0,
			ip_address varchar(45) NOT NULL DEFAULT '',
			level varchar(20) NOT NULL DEFAULT 'info',
			category varchar(32) NOT NULL DEFAULT 'general',
			meta longtext NOT NULL,
			created_at int(11) DEFAULT 0 NOT NULL,
			PRIMARY KEY  (id),
			KEY event (event),
			KEY user_id (user_id),
			KEY created_at (created_at),
			KEY category (category)
		) $charset_collate;";

		self::apply_table_schema( $table_name, $sql );
	}

	/**
	 * Create the transient file-state table used during a scan run.
	 */
	private static function create_file_states_table() {
		global $wpdb;

		$table_name      = $wpdb->prefix . 'sitefort_file_states';
		$charset_collate = SchemaInspector::validated_charset_collate( $wpdb );
		$scan_file_unique_key = SchemaInspector::table_exists( $table_name )
			? ''
			: ",
			UNIQUE KEY uk_scan_file (scan_id, file_path_hash)";

		$sql = "CREATE TABLE $table_name (
			id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
			scan_id varchar(64) NOT NULL COMMENT 'scan_xxx UUID from scan_queue',
			file_path varchar(512) NOT NULL COMMENT 'Relative path from ABSPATH',
			file_path_hash char(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL DEFAULT '' COMMENT 'SHA-256 of relative path for portable uniqueness',
			file_hash varchar(64) DEFAULT NULL COMMENT 'SHA-256 hash',
			file_size bigint(20) UNSIGNED DEFAULT NULL,
			state varchar(32) NOT NULL DEFAULT 'discovered',
			file_type varchar(32) NOT NULL,
			component_slug varchar(191) DEFAULT NULL COMMENT 'Plugin/theme/core slug captured with the file hash',
			component_version varchar(100) DEFAULT NULL COMMENT 'Component version captured with the file hash',
			component_locale varchar(20) DEFAULT NULL COMMENT 'Core locale captured with the file hash',
			file_mtime_at_hash int(10) UNSIGNED DEFAULT NULL COMMENT 'File modification time captured with the file hash',
			cloud_job_id varchar(64) DEFAULT NULL COMMENT 'Intel Engine job ID',
			upload_attempt int(10) UNSIGNED DEFAULT 0,
			hash_status varchar(32) DEFAULT 'unknown' COMMENT 'Cloud hash check result',
			scanned_with_signature varchar(20) DEFAULT NULL COMMENT 'Malware signature version',
			error_code varchar(32) DEFAULT NULL,
			error_message text DEFAULT NULL,
			scan_result longtext DEFAULT NULL COMMENT 'Serialized scan result payload (optional)',
			skip_reason varchar(50) DEFAULT NULL COMMENT 'Reason file was skipped (e.g. credit_exhausted)',
			created_at int(10) UNSIGNED NOT NULL,
			updated_at int(10) UNSIGNED NOT NULL,
			available_at int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Earliest time a deferred file-state row may be processed',
			PRIMARY KEY  (id),
			KEY idx_scan_id (scan_id),
			KEY idx_state (state),
			KEY idx_scan_state_created (scan_id, state, created_at),
			KEY idx_scan_state_available (scan_id, state, available_at, created_at),
			KEY idx_scan_state_manifest_wait (scan_id, state, error_code, available_at),
			KEY idx_scan_created (scan_id, created_at),
			KEY idx_file_path (file_path(191)),
			KEY idx_file_hash (file_hash),
			KEY idx_cloud_job (cloud_job_id),
			KEY idx_hash_status (hash_status),
			KEY idx_signature (scanned_with_signature)$scan_file_unique_key
		) $charset_collate;";

		self::apply_table_schema( $table_name, $sql );
	}

	/**
	 * Create scan settings table for scanner configuration.
	 */
	private static function create_settings_table() {
		global $wpdb;

		$table_name = $wpdb->prefix . 'sitefort_settings';
		$charset_collate = SchemaInspector::validated_charset_collate( $wpdb );

		// Single source of truth for the column list lives in SettingsStore so the
		// activation schema can never drift from the runtime provisioning path.
		$sql = \SiteFort\Includes\SettingsStore::settings_table_schema_sql( $table_name, $charset_collate );

		if ( self::apply_table_schema( $table_name, $sql ) ) {
			// Mark the schema current so the first post-activation request does not
			// re-run dbDelta on the table we just provisioned.
			\SiteFort\Includes\SettingsStore::mark_schema_current();
		}
	}

	/**
	 * Create targeted WAF runtime key-value storage.
	 */
	private static function create_waf_runtime_table(): void {
		global $wpdb;

		$table_name      = $wpdb->prefix . 'sitefort_waf_runtime';
		$charset_collate = SchemaInspector::validated_charset_collate( $wpdb );

		$sql = "CREATE TABLE $table_name (
			runtime_key varchar(64) NOT NULL,
			payload longtext NULL,
			payload_hash char(64) NOT NULL DEFAULT '',
			updated_at int(10) unsigned NOT NULL,
			expires_at int(10) unsigned NOT NULL DEFAULT 0,
			PRIMARY KEY  (runtime_key),
			KEY idx_expires_at (expires_at)
		) $charset_collate;";

		self::apply_table_schema( $table_name, $sql );
	}

	/**
	 * Create request nonce table for inbound C2 replay protection.
	 */
	private static function create_request_nonces_table() {
		global $wpdb;

		$table_name = $wpdb->prefix . 'sitefort_request_nonces';
		$charset_collate = SchemaInspector::validated_charset_collate( $wpdb );

		$sql = "CREATE TABLE $table_name (
			id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
			site_token varchar(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
			nonce varchar(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
			request_id varchar(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
			created_at int(10) unsigned NOT NULL,
			PRIMARY KEY  (id),
			UNIQUE KEY uk_site_nonce (site_token, nonce),
			UNIQUE KEY uk_site_request (site_token, request_id),
			KEY idx_created (created_at)
		) $charset_collate;";

		self::apply_table_schema( $table_name, $sql );
	}


    /**
     * Create cloud_jobs table for tracking Intel Engine scan jobs (v2.1).
     */
    private static function create_cloud_jobs_table() {
        global $wpdb;

        $table_name = $wpdb->prefix . 'sitefort_cloud_jobs';
        $charset_collate = SchemaInspector::validated_charset_collate( $wpdb );

        $sql = "CREATE TABLE $table_name (
            id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
            job_id varchar(64) NOT NULL,
            scan_id varchar(64) NOT NULL,
            status varchar(32) NOT NULL DEFAULT 'queued',
            error_message text DEFAULT NULL,
            retry_count int(10) UNSIGNED DEFAULT 0,
            max_retries int(10) UNSIGNED DEFAULT 3,
            created_at int(10) UNSIGNED NOT NULL,
            submitted_at int(10) UNSIGNED DEFAULT NULL,
            completed_at int(10) UNSIGNED DEFAULT NULL,
            timeout_at int(10) UNSIGNED DEFAULT NULL,
            last_poll_at int(10) UNSIGNED DEFAULT NULL,
            PRIMARY KEY  (id),
            UNIQUE KEY uk_job_id (job_id),
            KEY idx_scan_id (scan_id),
            KEY idx_scan_status (scan_id, status),
            KEY idx_status (status),
            KEY idx_timeout (timeout_at)
        ) $charset_collate;";

        self::apply_table_schema( $table_name, $sql );
    }


    /**
     * Create scan_status table for logging scan activity.
     */
    private static function create_scan_status_table() {
        global $wpdb;

        $table_name = $wpdb->prefix . 'sitefort_scan_status';
        $charset_collate = SchemaInspector::validated_charset_collate( $wpdb );

        $sql = "CREATE TABLE $table_name (
            id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
            scan_id varchar(64) NOT NULL,
            ctime varchar(20) NOT NULL COMMENT 'Microtime with decimal precision',
            level int(10) UNSIGNED DEFAULT 1 COMMENT '1=info, 2=warning, 3=error',
            type varchar(32) NOT NULL DEFAULT 'info',
            message text NOT NULL,
            created_at int(10) UNSIGNED DEFAULT 0 NOT NULL,
            PRIMARY KEY  (id),
            KEY idx_scan_id (scan_id),
            KEY idx_ctime (ctime),
            KEY idx_level (level)
        ) $charset_collate;";

        self::apply_table_schema( $table_name, $sql );
    }

    /**
     * Create minimal signed hash cache table (v2.2).
     *
     * Verdicts:
     * - verified: repo-verified content (not tied to signature version)
     * - safe: signature-verified content (invalidated on signature updates)
     */
    private static function create_hash_cache_table() {
        global $wpdb;

        $table_name = $wpdb->prefix . 'sitefort_hash_cache';
        $charset_collate = SchemaInspector::validated_charset_collate( $wpdb );

        $sql = "CREATE TABLE $table_name (
            file_hash varchar(64) NOT NULL COMMENT 'SHA-256 hash (hex)',
            verdict varchar(16) NOT NULL COMMENT 'verified|safe',
            last_seen_at int(10) UNSIGNED NOT NULL COMMENT 'Unix timestamp',
            proof varbinary(128) NOT NULL COMMENT 'Signed proof blob (v1)',
            PRIMARY KEY (file_hash),
            KEY idx_verdict (verdict),
            KEY idx_last_seen (last_seen_at)
        ) $charset_collate;";

        self::apply_table_schema( $table_name, $sql );
    }

    /**
     * Create the two-factor authentication table.
     */
    private static function create_two_factor_table() {
        global $wpdb;

        $table_name = $wpdb->prefix . 'sitefort_two_factor';
        $charset_collate = SchemaInspector::validated_charset_collate( $wpdb );

        $sql = "CREATE TABLE $table_name (
            user_id bigint(20) UNSIGNED NOT NULL,
            totp_secret text DEFAULT NULL,
            setup_email_code_hash varchar(255) DEFAULT NULL,
            setup_email_attempts int(10) UNSIGNED NOT NULL DEFAULT 0,
            setup_email_sent_at int(10) UNSIGNED DEFAULT NULL,
            setup_email_delivered tinyint(1) NOT NULL DEFAULT 0,
            method varchar(10) NOT NULL DEFAULT 'totp',
            is_enabled tinyint(1) NOT NULL DEFAULT 0,
            confirmed_at int(10) UNSIGNED DEFAULT NULL,
            last_used_at int(10) UNSIGNED DEFAULT NULL,
            remember_token varchar(64) DEFAULT NULL,
            remember_expires int(10) UNSIGNED DEFAULT NULL,
            created_at int(10) UNSIGNED NOT NULL,
            updated_at int(10) UNSIGNED NOT NULL,
            PRIMARY KEY (user_id)
        ) $charset_collate;";

        self::apply_table_schema( $table_name, $sql );
    }

    /**
     * Create the hashed recovery-code table for 2FA.
     */
    private static function create_two_factor_recovery_codes_table() {
        global $wpdb;

        $table_name = $wpdb->prefix . 'sitefort_two_factor_recovery_codes';
        $charset_collate = SchemaInspector::validated_charset_collate( $wpdb );

        $sql = "CREATE TABLE $table_name (
            id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
            user_id bigint(20) UNSIGNED NOT NULL,
            code_hash varchar(255) NOT NULL,
            batch_id varchar(36) NOT NULL,
            created_at int(10) UNSIGNED NOT NULL,
            used_at int(10) UNSIGNED DEFAULT NULL,
            PRIMARY KEY (id),
            KEY idx_user_active (user_id, used_at),
            KEY idx_batch_id (batch_id)
        ) $charset_collate;";

        self::apply_table_schema( $table_name, $sql );
    }

    /**
     * Create the DB-backed pre-auth 2FA session table.
     */
    private static function create_two_factor_sessions_table() {
        global $wpdb;

        $table_name = $wpdb->prefix . 'sitefort_two_factor_sessions';
        $charset_collate = SchemaInspector::validated_charset_collate( $wpdb );

        $sql = "CREATE TABLE $table_name (
            id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
            token_hash varchar(64) NOT NULL,
            user_id bigint(20) UNSIGNED NOT NULL,
            method varchar(12) NOT NULL,
            notice_code varchar(40) NOT NULL DEFAULT '',
            redirect_to text DEFAULT NULL,
            remember tinyint(1) NOT NULL DEFAULT 0,
            fail_count int(10) UNSIGNED NOT NULL DEFAULT 0,
            email_code_hash varchar(255) DEFAULT NULL,
            email_attempts int(10) UNSIGNED NOT NULL DEFAULT 0,
            email_sent_at int(10) UNSIGNED DEFAULT NULL,
            email_delivered tinyint(1) NOT NULL DEFAULT 0,
            expires_at int(10) UNSIGNED NOT NULL,
            created_at int(10) UNSIGNED NOT NULL,
            updated_at int(10) UNSIGNED NOT NULL,
            PRIMARY KEY (id),
            UNIQUE KEY uk_token_hash (token_hash),
            KEY idx_expires_at (expires_at),
            KEY idx_user_id (user_id)
        ) $charset_collate;";

        self::apply_table_schema( $table_name, $sql );
    }

    /**
     * Create the login attempt counter table.
     *
     * Keeps failed-login counters in a plugin-owned table so lockout logic
     * remains correct on hosts that use persistent object caches.
     */
    private static function create_login_attempts_table() {
        global $wpdb;

        $table_name = $wpdb->prefix . 'sitefort_login_attempts';
        $charset_collate = SchemaInspector::validated_charset_collate( $wpdb );

        $sql = "CREATE TABLE $table_name (
            id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
            subject_type varchar(10) NOT NULL,
            subject_hash varchar(64) NOT NULL,
            first_attempt_at int(10) UNSIGNED NOT NULL,
            last_attempt_at int(10) UNSIGNED NOT NULL,
            attempt_count int(10) UNSIGNED NOT NULL DEFAULT 1,
            expires_at int(10) UNSIGNED NOT NULL,
            PRIMARY KEY (id),
            UNIQUE KEY uk_subject (subject_type, subject_hash),
            KEY idx_expires_at (expires_at),
            KEY idx_last_attempt_at (last_attempt_at)
        ) $charset_collate;";

        self::apply_table_schema( $table_name, $sql );
    }

    /**
     * Create the rate limits table for sliding window counters.
     */
    private static function create_rate_limits_table() {
        global $wpdb;

        $table_name = $wpdb->prefix . 'sitefort_rate_limits';
        $charset_collate = SchemaInspector::validated_charset_collate( $wpdb );

        $sql = "CREATE TABLE $table_name (
            id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
            ip_hash varchar(64) NOT NULL,
            hit_type varchar(20) NOT NULL,
            minute_bucket int(10) UNSIGNED NOT NULL,
            hits int(10) UNSIGNED NOT NULL DEFAULT 1,
            PRIMARY KEY (id),
            UNIQUE KEY uk_ip_type_minute (ip_hash, hit_type, minute_bucket),
            KEY idx_bucket (minute_bucket)
        ) $charset_collate;";

        self::apply_table_schema( $table_name, $sql );
    }

    /**
     * Create the community blocklist table for indexed IP lookups.
     */
    private static function create_blocklist_table() {
        global $wpdb;

        $table_name      = $wpdb->prefix . 'sitefort_blocklist';
        $charset_collate = SchemaInspector::validated_charset_collate( $wpdb );

        $sql = "CREATE TABLE $table_name (
            id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
            host varchar(45) NOT NULL,
            type varchar(10) NOT NULL DEFAULT 'ip',
            source varchar(20) NOT NULL DEFAULT 'cloud',
            created_at int(10) UNSIGNED NOT NULL,
            PRIMARY KEY (id),
            UNIQUE KEY uk_host (host),
            KEY idx_type (type),
            KEY idx_source (source)
        ) $charset_collate;";

        self::apply_table_schema( $table_name, $sql );
    }

    /**
     * Create the indexed CIDR range table used by managed-host WAF bootstrap.
     */
    private static function create_blocklist_ranges_table(): void {
        global $wpdb;

        $table_name      = $wpdb->prefix . 'sitefort_blocklist_ranges';
        $charset_collate = SchemaInspector::validated_charset_collate( $wpdb );

        $sql = "CREATE TABLE $table_name (
            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            family tinyint(1) unsigned NOT NULL,
            range_start varbinary(16) NOT NULL,
            range_end varbinary(16) NOT NULL,
            host varchar(45) NOT NULL,
            created_at int(10) unsigned NOT NULL,
            PRIMARY KEY  (id),
            UNIQUE KEY uk_host (host),
            KEY idx_family_start (family, range_start),
            KEY idx_family_end (family, range_end)
        ) $charset_collate;";

        self::apply_table_schema( $table_name, $sql );
    }

    /**
     * Create the firewall events log table.
     *
     * Records every blocked request with full context for dashboard analytics,
     * live traffic feed, and security auditing. Provisioned during activation,
     * site initialization, and database migrations.
     */
    public static function create_firewall_events_table(): void {
        global $wpdb;

        $table           = $wpdb->prefix . 'sitefort_firewall_events';
        $charset_collate = SchemaInspector::validated_charset_collate( $wpdb );

        $sql = "CREATE TABLE $table (
            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            event_time int(11) unsigned NOT NULL,
            ip_address varchar(45) NOT NULL DEFAULT '',
            request_uri varchar(500) NOT NULL DEFAULT '',
            http_method varchar(10) NOT NULL DEFAULT '',
            http_status smallint(5) unsigned NOT NULL DEFAULT 403,
            attack_type varchar(100) NOT NULL DEFAULT '',
            user_agent varchar(500) NOT NULL DEFAULT '',
            user_id bigint(20) unsigned NOT NULL DEFAULT 0,
            blog_id bigint(20) unsigned NOT NULL DEFAULT 1,
            PRIMARY KEY  (id),
            KEY idx_event_time (event_time),
            KEY idx_ip_address (ip_address(20)),
            KEY idx_attack_type (attack_type(20)),
            KEY idx_ip_time (ip_address(20), event_time),
            KEY idx_blog_time (blog_id, event_time)
        ) $charset_collate;";

        self::apply_table_schema( $table, $sql );
    }

    /**
     * Create the data directory for WAF config, GeoIP database, etc.
     */
    private static function create_data_directory() {
        $data_dir = DataDirectoryProtection::get_data_dir_path();
        DataDirectoryProtection::ensure_local_protection( $data_dir );

        $geoip_dir = $data_dir . '/geoip';
        DataDirectoryProtection::ensure_local_protection( $geoip_dir );

        $logs_dir = $data_dir . '/logs';
        DataDirectoryProtection::ensure_local_protection( $logs_dir );

        $scan_logs_dir = $data_dir . '/scan-logs';
        DataDirectoryProtection::ensure_local_protection( $scan_logs_dir );

        $quarantine_dir = $data_dir . '/quarantine';
        DataDirectoryProtection::ensure_local_protection( $quarantine_dir );

        self::cleanup_retired_firewall_directory( $data_dir . '/firewall' );
    }

    /**
     * Remove the deprecated cloud firewall rules directory when it only
     * contains boilerplate protection files from older versions.
     */
    private static function cleanup_retired_firewall_directory( string $firewall_dir ): void {
        if ( ! is_dir( $firewall_dir ) ) {
            return;
        }

        $entries = @scandir( $firewall_dir );
        if ( ! is_array( $entries ) ) {
            return;
        }

        $entries = array_values(
            array_diff(
                $entries,
                [ '.', '..', '.htaccess', 'index.php' ]
            )
        );

        if ( ! empty( $entries ) ) {
            return;
        }

        $htaccess = trailingslashit( $firewall_dir ) . '.htaccess';
        $index    = trailingslashit( $firewall_dir ) . 'index.php';

        if ( is_file( $htaccess ) ) {
            wp_delete_file( $htaccess );
        }

        if ( is_file( $index ) ) {
            wp_delete_file( $index );
        }

        @rmdir( $firewall_dir ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir
    }

    /**
     * Create file index table for persistent enumeration (v2.5).
     */
    private static function create_file_index_table() {
        global $wpdb;

        $table_name = $wpdb->prefix . 'sitefort_file_index';
        $charset_collate = SchemaInspector::validated_charset_collate( $wpdb );

        $sql = "CREATE TABLE $table_name (
            scan_id varchar(64) NOT NULL,
            stack longtext DEFAULT NULL,
            total_files int(10) UNSIGNED NOT NULL DEFAULT 0,
            is_complete tinyint(1) NOT NULL DEFAULT 0,
            created_at int(10) UNSIGNED NOT NULL,
            updated_at int(10) UNSIGNED NOT NULL,
            PRIMARY KEY  (scan_id),
            KEY idx_complete (is_complete),
            KEY idx_updated (updated_at)
        ) $charset_collate;";

        self::apply_table_schema( $table_name, $sql );
    }

    /**
     * Create issues table for scanner findings (v2.3).
     */
    private static function create_issues_table() {
        global $wpdb;

        $table_name = $wpdb->prefix . 'sitefort_issues';
        $charset_collate = SchemaInspector::validated_charset_collate( $wpdb );

        $sql = "CREATE TABLE $table_name (
            id bigint(20) NOT NULL AUTO_INCREMENT,
            issue_hash varchar(64) NOT NULL,
            scan_id varchar(64) DEFAULT NULL,
            type varchar(50) NOT NULL,
            severity varchar(20) DEFAULT 'medium',
            status varchar(50) NOT NULL,
            file_path text DEFAULT NULL,
            message text DEFAULT NULL,
            data longtext DEFAULT NULL,
            is_ignored tinyint(1) DEFAULT 0,
            resolved_at int(11) NOT NULL DEFAULT 0,
            first_seen_at int(11) NOT NULL DEFAULT 0,
            last_seen_at int(11) NOT NULL DEFAULT 0,
            created_at int(11) NOT NULL,
            updated_at int(11) NOT NULL,
            PRIMARY KEY  (id),
            UNIQUE KEY issue_hash (issue_hash),
            KEY scan_id (scan_id),
            KEY type (type),
            KEY is_ignored (is_ignored),
            KEY idx_active (is_ignored, resolved_at)
        ) $charset_collate;";

        self::apply_table_schema( $table_name, $sql );
    }

    /**
     * Create ignored issues table (v2.3).
     */
    private static function create_ignored_issues_table() {
        global $wpdb;

        $table_name = $wpdb->prefix . 'sitefort_ignored_issues';
        $charset_collate = SchemaInspector::validated_charset_collate( $wpdb );

        $sql = "CREATE TABLE $table_name (
            id bigint(20) NOT NULL AUTO_INCREMENT,
            issue_hash varchar(64) NOT NULL,
            ignored_at int(11) NOT NULL,
            PRIMARY KEY  (id),
            UNIQUE KEY issue_hash (issue_hash)
        ) $charset_collate;";

        self::apply_table_schema( $table_name, $sql );
    }

    /**
     * Create scan runs table for immutable execution history (v2.3).
     */
    private static function create_scan_runs_table() {
        global $wpdb;

        $table_name = $wpdb->prefix . 'sitefort_scan_runs';
        $charset_collate = SchemaInspector::validated_charset_collate( $wpdb );

        $sql = "CREATE TABLE $table_name (
            id bigint(20) NOT NULL AUTO_INCREMENT,
            scan_id varchar(64) NOT NULL,
            config longtext DEFAULT NULL,
            component_status longtext DEFAULT NULL,
            started_at int(11) NOT NULL,
            finished_at int(11) DEFAULT 0,
            PRIMARY KEY  (id),
            UNIQUE KEY scan_id (scan_id)
        ) $charset_collate;";

        self::apply_table_schema( $table_name, $sql );
    }

    /**
     * Create issue instances table for staging and occurrence history (v2.3).
     */
    private static function create_issue_instances_table() {
        global $wpdb;

        $table_name = $wpdb->prefix . 'sitefort_issue_instances';
        $charset_collate = SchemaInspector::validated_charset_collate( $wpdb );

        $sql = "CREATE TABLE $table_name (
            id bigint(20) NOT NULL AUTO_INCREMENT,
            issue_hash varchar(64) NOT NULL,
            scan_run_id varchar(64) NOT NULL,
            type varchar(50) DEFAULT NULL,
            severity varchar(20) DEFAULT NULL,
            status varchar(50) DEFAULT NULL,
            file_path text DEFAULT NULL,
            message text DEFAULT NULL,
            evidence longtext DEFAULT NULL,
            created_at int(11) NOT NULL,
            PRIMARY KEY  (id),
            KEY issue_ref (issue_hash, scan_run_id)
        ) $charset_collate;";

        self::apply_table_schema( $table_name, $sql );
    }

}
