<?php
/**
 * ServerCommunication — Central hub for all plugin↔server communications.
 *
 * ARCHITECTURE
 * ─────────────────────────────────────────────────────────────────────────────
 * PUSH (real-time, both tiers)
 *   Every security event that matters is immediately queued and dispatched to
 *   the UltraGuard server via a non-blocking background request using
 *   wp_remote_post with `blocking => false`.  Events are also written to a
 *   persistent queue (wp_options) so they are retried on failure.
 *
 *   Pushed events:
 *     • hack_attempt       — firewall blocks, WAF rule matches
 *     • brute_force        — login-limiter lockouts
 *     • malware_detected   — antivirus scan threats
 *     • vulnerability      — CVE / plugin vulnerability found
 *     • file_modified      — file-integrity suspicious/modified file
 *
 * PULL (tiered, scheduled)
 *   Pro tier  → immediate (next cron tick, typically ≤ 1 hr)
 *   Free tier → 24-hour delay (server stamps `available_at`; client skips
 *               packages where now < available_at + FREE_DELAY_DAYS*86400)
 *
 *   Pulled resources:
 *     • Global threat IP blacklist (crowd-sourced across all UltraGuard sites)
 *     • Malware / file-scanner signature patterns
 *     • Vulnerability database entries (CVE-mapped, plugin/theme/core)
 *     • Firewall & WAF rule-set updates
 *     • .htaccess security rule templates
 *
 * SECURITY
 *   Every request is signed with an HMAC-SHA256 signature using the site's
 *   license key as the secret.  The server validates this before processing
 *   any report.  Payloads are JSON-encoded.
 *
 * @package UltraguardPro\support
 * @since   6.6.0
 */

namespace UltraguardPro\support;

use UltraguardPro\Plugin;

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

class ServerCommunication {

    // ── Constants ──────────────────────────────────────────────────────────────

    /** Default base URL of the UltraGuard intelligence API. */
    public const API_BASE = 'https://ultraguard.net/wp-json/ultraguard/v2';

    // Push endpoints (realtime, non-blocking)
    public const EP_PUSH_EVENT       = '/events/push';
    public const EP_PUSH_BATCH       = '/events/batch';

    // Pull endpoints (scheduled)
    public const EP_PULL_BLACKLIST   = '/intelligence/blacklist';
    public const EP_PULL_SIGNATURES  = '/intelligence/signatures';
    public const EP_PULL_VULNDB      = '/intelligence/vulndb';
    public const EP_PULL_FW_RULES    = '/intelligence/firewall-rules';
    public const EP_PULL_HTACCESS    = '/intelligence/htaccess-rules';
    public const EP_PULL_MANIFEST    = '/intelligence/manifest';

    /** Push queue WP option key. */
    private const PUSH_QUEUE_OPTION  = 'ultraguard_push_queue';

    /** Max items held in queue before forcing a batch flush. */
    private const QUEUE_FLUSH_SIZE   = 50;

    /** Max age of a queued item before it is discarded (seconds). */
    private const QUEUE_TTL          = 3 * DAY_IN_SECONDS;

    /** Option keys for storing pull state. */
    private const OPT_BL_HASH        = 'ultraguard_si_blacklist_hash';
    private const OPT_SIG_VERSION    = 'ultraguard_si_sig_version';
    private const OPT_VULN_VERSION   = 'ultraguard_si_vuln_version';
    private const OPT_FW_HASH        = 'ultraguard_si_fw_hash';
    private const OPT_HTA_HASH       = 'ultraguard_si_hta_hash';
    private const OPT_LAST_PULL      = 'ultraguard_si_last_pull';
    private const OPT_FILECHECKER_PATTERNS = 'ultraguard_cloud_filechecker_patterns';
    private const OPT_SYNC_PREFIX    = 'ultraguard_si_';

    /**
     * Syncable intelligence resources and their cron metadata.
     *
     * @var array<string,array<string,mixed>>
     */
    private const SYNC_RESOURCES = [
        'blacklist'  => [
            'hook'         => 'ultraguard_si_pull_blacklist',
            'default_pro'  => 'hourly',
            'default_free' => 'twicedaily',
            'offset'       => 60,
        ],
        'signatures' => [
            'hook'         => 'ultraguard_si_pull_signatures',
            'default_pro'  => 'daily',
            'default_free' => 'daily',
            'offset'       => 120,
        ],
        'vulndb'     => [
            'hook'         => 'ultraguard_si_pull_vulndb',
            'default_pro'  => 'daily',
            'default_free' => 'daily',
            'offset'       => 180,
        ],
        'fw_rules'   => [
            'hook'         => 'ultraguard_si_pull_fw_rules',
            'default_pro'  => 'twicedaily',
            'default_free' => 'twicedaily',
            'offset'       => 240,
        ],
        'htaccess'   => [
            'hook'         => 'ultraguard_si_pull_htaccess',
            'default_pro'  => 'weekly',
            'default_free' => 'weekly',
            'offset'       => 300,
        ],
    ];

    // ── Instance ───────────────────────────────────────────────────────────────

    private Plugin $plugin;

    /** In-memory dedup set for this request. */
    private array $pushed_this_request = [];

    public function __construct( Plugin $plugin ) {
        $this->plugin = $plugin;
    }

    // ── Registration ──────────────────────────────────────────────────────────

    /**
     * Register all WordPress hooks.
     * Called once by Plugin::init() after modules are booted.
     */
    public function register(): void {
        // ── PUSH: hook into security events ───────────────────────────────────
        add_action( 'ultraguard_request_blocked',                [ $this, 'on_request_blocked'   ], 10, 3 );
        add_action( 'ultraguard_ip_locked_out',                  [ $this, 'on_ip_locked_out'     ], 10, 3 );
        add_action( 'ultraguard_antivirus_scan_completed',       [ $this, 'on_scan_completed'    ], 10, 2 );
        add_action( 'ultraguard_antivirus_file_quarantined',     [ $this, 'on_file_quarantined'  ], 10, 2 );
        add_action( 'ultraguard_antivirus_critical_vuln',        [ $this, 'on_vulnerability'     ], 10, 2 );
        add_action( 'ultraguard_pro/filechecker/suspicious_file',[ $this, 'on_suspicious_file'   ], 10, 2 );
        add_action( 'ultraguard_pro/filechecker/file_modified',  [ $this, 'on_file_modified'     ], 10, 2 );
        add_action( 'ultraguard_pro/integration/security_event', [ $this, 'on_integration_security_event' ], 10, 1 );
        add_action( 'ultraguard_utils/security_event',           [ $this, 'on_logged_security_event' ], 10, 4 );

        // ── PUSH: flush queue on shutdown (catches batched items) ──────────────
        add_action( 'shutdown',     [ $this, 'flush_push_queue' ], 99, 0 );  // 0 = accept no args from WP hook

        // ── PULL: cron hooks ───────────────────────────────────────────────────
        add_action( 'ultraguard_si_pull_blacklist',   [ $this, 'pull_blacklist'   ] );
        add_action( 'ultraguard_si_pull_signatures',  [ $this, 'pull_signatures'  ] );
        add_action( 'ultraguard_si_pull_vulndb',      [ $this, 'pull_vulndb'      ] );
        add_action( 'ultraguard_si_pull_fw_rules',    [ $this, 'pull_fw_rules'    ] );
        add_action( 'ultraguard_si_pull_htaccess',    [ $this, 'pull_htaccess'    ] );

        // ── PULL: schedule cron events if not already registered ──────────────
        add_action( 'wp_loaded', [ $this, 'maybe_schedule_crons' ] );
    }

    /**
     * Return the supported sync resources.
     *
     * @return array<string,array<string,mixed>>
     */
    public static function get_sync_resources(): array {
        return self::SYNC_RESOURCES;
    }

    public static function apiBaseUrl(): string {
        if ( class_exists( LicenseInfo::class ) && method_exists( LicenseInfo::class, 'serverApiBaseUrl' ) ) {
            return LicenseInfo::serverApiBaseUrl();
        }

        return self::API_BASE;
    }

    /**
     * @return string[]
     */
    public static function endpointUrlCandidates( string $endpoint ): array {
        if ( class_exists( LicenseInfo::class ) && method_exists( LicenseInfo::class, 'endpointUrlCandidates' ) ) {
            return LicenseInfo::endpointUrlCandidates( $endpoint );
        }

        return [
            self::apiBaseUrl() . '/' . ltrim( $endpoint, '/' ),
        ];
    }

    public static function shouldVerifySslForUrl( string $url ): bool {
        $host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) );

        if (
            '' === $host
            || 'localhost' === $host
            || '127.0.0.1' === $host
            || '::1' === $host
            || str_ends_with( $host, '.test' )
            || str_ends_with( $host, '.local' )
            || str_ends_with( $host, '.localhost' )
        ) {
            return false;
        }

        return true;
    }

    /**
     * Return supported recurrence labels for the Server Intelligence UI.
     *
     * @return array<string,string>
     */
    public static function get_interval_labels(): array {
        return [
            'every_30_minutes' => __( 'Every 30 minutes', 'ultraguard-security-suite' ),
            'hourly'           => __( 'Hourly', 'ultraguard-security-suite' ),
            'twicedaily'       => __( 'Twice daily', 'ultraguard-security-suite' ),
            'daily'            => __( 'Daily', 'ultraguard-security-suite' ),
            'weekly'           => __( 'Weekly', 'ultraguard-security-suite' ),
        ];
    }

    /**
     * Return the current automatic-fetch settings for all resources.
     *
     * @return array<string,array<string,mixed>>
     */
    public function get_schedule_snapshot(): array {
        $snapshot = [];

        foreach ( self::SYNC_RESOURCES as $resource => $config ) {
            $hook     = (string) $config['hook'];
            $interval = $this->get_resource_interval( $resource );

            $snapshot[ $resource ] = [
                'hook'      => $hook,
                'enabled'   => $this->is_resource_enabled( $resource ),
                'interval'  => $interval,
                'next_run'  => wp_next_scheduled( $hook ),
                'label'     => self::get_interval_labels()[ $interval ] ?? ucfirst( $interval ),
            ];
        }

        return $snapshot;
    }

    /**
     * Persist automatic-fetch settings and reschedule the intelligence crons.
     *
     * @param array<string,array<string,mixed>> $settings
     * @return array<string,array<string,mixed>>
     */
    public function save_schedule_settings( array $settings ): array {
        $allowed_intervals = $this->get_allowed_intervals();

        foreach ( self::SYNC_RESOURCES as $resource => $config ) {
            $resource_settings = isset( $settings[ $resource ] ) && is_array( $settings[ $resource ] )
                ? $settings[ $resource ]
                : [];

            $enabled = ! empty( $resource_settings['enabled'] );
            $interval = sanitize_key( (string) ( $resource_settings['interval'] ?? '' ) );

            if ( ! isset( $allowed_intervals[ $interval ] ) ) {
                $interval = $this->get_default_interval( $resource );
            }

            update_option( $this->get_enabled_option_key( $resource ), $enabled ? '1' : '0', false );
            update_option( $this->get_interval_option_key( $resource ), $interval, false );
        }

        $this->maybe_schedule_crons();

        return $this->get_schedule_snapshot();
    }

    // =========================================================================
    // PUSH HANDLERS — hooked into UltraGuard security events
    // =========================================================================

    /**
     * Fired when the firewall blocks a request.
     *
     * @param string $ip      Blocked IP address.
     * @param string $reason  Human-readable reason.
     * @param string $threat  Threat level (low|medium|high|critical).
     */
    public function on_request_blocked( string $ip, string $reason, string $threat ): void {
        $this->push_event( 'hack_attempt', [
            'ip_address'   => $ip,
            'reason'       => $reason,
            'threat_level' => $threat,
            'request_uri'  => sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ) ),
            'method'       => sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ?? 'GET' ) ),
            'user_agent'   => sanitize_text_field( substr( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ), 0, 500 ) ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- unslashed and sanitized before dispatch.
        ] );
    }

    /**
     * Fired when the login limiter locks out an IP.
     *
     * @param string      $ip       Locked-out IP.
     * @param string|null $username Username that was attempted.
     * @param int         $duration Lockout duration in minutes.
     */
    public function on_ip_locked_out( string $ip, ?string $username, int $duration ): void {
        $this->push_event( 'brute_force', [
            'ip_address'   => $ip,
            'username'     => $username ?? '',
            'duration_min' => $duration,
            'threat_level' => 'high',
        ] );
    }

    /**
     * Fired when an antivirus scan completes with threats.
     *
     * @param string $scan_id  Scan session ID.
     * @param array  $summary  { files_scanned, threats_found, ... }
     */
    public function on_scan_completed( string $scan_id, array $summary ): void {
        $threats = (int) ( $summary['threats_found'] ?? 0 );
        if ( $threats <= 0 ) { return; }

        // Collect the actual infected file list (up to 20 samples)
        $samples = [];
        $db_tables = $this->plugin->modules->get_db_tables();
        $scan_t  = $db_tables ? $db_tables->get_table( 'scan_results' ) : null;
        if ( $scan_t ) {
            $rows = $scan_t->get_by( [
                'scan_id' => $scan_id,
                'status'  => 'infected',
            ], 20 );
            foreach ( $rows as $r ) {
                $samples[] = [
                    'file_hash'    => $r['file_hash']    ?? '',
                    'threat_type'  => $r['threat_type']  ?? '',
                    'threat_level' => $r['threat_level'] ?? 'medium',
                    'file_ext'     => strtolower( pathinfo( $r['file_path'] ?? '', PATHINFO_EXTENSION ) ),
                ];
            }
        }

        $this->push_event( 'malware_detected', [
            'scan_id'       => $scan_id,
            'threats_found' => $threats,
            'files_scanned' => (int) ( $summary['files_scanned'] ?? 0 ),
            'samples'       => $samples,
            'threat_level'  => 'critical',
        ] );
    }

    /**
     * Fired when a file is quarantined by the antivirus.
     *
     * @param string $original_path   Source path.
     * @param string $quarantine_path Destination path.
     */
    public function on_file_quarantined( string $original_path, string $quarantine_path ): void {
        $this->push_event( 'malware_quarantined', [
            'file_hash'    => is_readable( $quarantine_path ) ? md5_file( $quarantine_path ) : '',
            'file_ext'     => strtolower( pathinfo( $original_path, PATHINFO_EXTENSION ) ),
            'threat_level' => 'critical',
        ] );
    }

    /**
     * Fired when the antivirus finds a critical vulnerability in a component.
     *
     * @param string $component  Plugin/theme slug.
     * @param array  $vuln       Vulnerability record from the DB.
     */
    public function on_vulnerability( string $component, array $vuln ): void {
        $this->push_event( 'vulnerability_detected', [
            'component_slug'     => $component,
            'component_type'     => $vuln['component_type']     ?? 'plugin',
            'component_version'  => $vuln['component_version']  ?? '',
            'vulnerability_type' => $vuln['vulnerability_type'] ?? '',
            'severity'           => $vuln['severity']           ?? 'critical',
            'cve_id'             => $vuln['cve_id']             ?? '',
        ] );
    }

    /**
     * Fired when the file checker detects a suspicious file.
     *
     * @param string $path  Absolute file path.
     * @param array  $info  { file_hash, status, change_type, ... }
     */
    public function on_suspicious_file( string $path, array $info ): void {
        $this->push_event( 'suspicious_file', [
            'file_hash'   => $info['file_hash']   ?? '',
            'file_ext'    => strtolower( pathinfo( $path, PATHINFO_EXTENSION ) ),
            'change_type' => $info['change_type'] ?? '',
            'threat_level'=> 'high',
        ] );
    }

    /**
     * Fired when the file checker detects a modified core/plugin/theme file.
     *
     * @param string $path  Absolute file path.
     * @param array  $info  { file_hash, previous_hash, is_core_file, ... }
     */
    public function on_file_modified( string $path, array $info ): void {
        if ( empty( $info['is_core_file'] ) ) { return; } // Only report core file tampering
        $this->push_event( 'core_file_modified', [
            'file_hash'     => $info['file_hash']     ?? '',
            'previous_hash' => $info['previous_hash'] ?? '',
            'file_ext'      => strtolower( pathinfo( $path, PATHINFO_EXTENSION ) ),
            'threat_level'  => 'critical',
        ] );
    }

    public function on_integration_security_event( array $event ): void {
        $source   = sanitize_key( (string) ( $event['source'] ?? '' ) );
        $name     = sanitize_key( (string) ( $event['event'] ?? '' ) );
        $severity = sanitize_key( (string) ( $event['severity'] ?? 'medium' ) );
        $context  = is_array( $event['context'] ?? null ) ? $event['context'] : [];

        $map = [
            'login_protect/old_login_url_hit'       => 'protected_login_hit',
            'authenticator/two_factor_account_locked' => 'auth_lockout',
            'vulnerability/threats_found'           => 'vulnerability_detected',
            'database_scanner/scan_completed'       => 'database_injection_detected',
            'woo_security/skimmer_scan_completed'   => 'skimmer_detected',
            'woo_security/honeypot_triggered'       => 'bot_checkout_attempt',
        ];

        $key = $source . '/' . $name;
        if ( ! isset( $map[ $key ] ) ) {
            return;
        }

        if ( 'woo_security/skimmer_scan_completed' === $key ) {
            $threats = isset( $context['threats_found'] ) ? (int) $context['threats_found'] : count( $context );
            if ( $threats <= 0 ) {
                return;
            }
        }

        if ( 'database_scanner/scan_completed' === $key ) {
            $result = isset( $context['result'] ) && is_array( $context['result'] ) ? $context['result'] : $context;
            $threats = isset( $result['threats_found'] ) ? (int) $result['threats_found'] : 0;
            if ( $threats <= 0 ) {
                return;
            }
        }

        $payload = array_merge(
            [
                'source'       => $source,
                'event'        => $name,
                'severity'     => $severity,
                'message'      => sanitize_text_field( (string) ( $event['message'] ?? '' ) ),
                'threat_level' => $severity,
            ],
            $context
        );

        $this->push_event( $map[ $key ], $payload );
    }

    public function on_logged_security_event( string $event_type, string $message, array $data, string $level ): void {
        $event_type = sanitize_key( $event_type );
        $level      = sanitize_key( $level );

        $map = [
            'invalid_nonce'             => 'nonce_abuse',
            'unauthorized_ajax_access'  => 'unauthorized_access',
            'rate_limit_exceeded'       => 'rate_limit_abuse',
            'file_deletion_blocked'     => 'protected_file_operation',
        ];

        if ( ! isset( $map[ $event_type ] ) ) {
            return;
        }

        if ( in_array( $level, [ 'debug', 'info' ], true ) ) {
            return;
        }

        $payload = array_merge(
            [
                'signal'       => $event_type,
                'message'      => sanitize_text_field( $message ),
                'threat_level' => in_array( $level, [ 'error', 'critical' ], true ) ? 'high' : 'medium',
                'log_level'    => $level,
            ],
            $data
        );

        $this->push_event( $map[ $event_type ], $payload );
    }

    // =========================================================================
    // PUSH ENGINE
    // =========================================================================

    /**
     * Queue a security event for immediate non-blocking push to the server.
     *
     * The event is both:
     *   a) Dispatched immediately via a fire-and-forget wp_remote_post
     *      (blocking=false so it never delays the current HTTP response).
     *   b) Written to the persistent queue so the shutdown handler can
     *      batch-retry any that failed.
     *
     * @param string $type    Event type slug.
     * @param array  $payload Event-specific payload.
     */
    public function push_event( string $type, array $payload ): void {
        // Dedup identical events within one PHP request
        $dedup_key = md5( $type . wp_json_encode( $payload ) );
        if ( isset( $this->pushed_this_request[ $dedup_key ] ) ) { return; }
        $this->pushed_this_request[ $dedup_key ] = true;

        $event = $this->build_event( $type, $payload );

        // 1. Fire-and-forget non-blocking push
        $this->dispatch_nonblocking( self::EP_PUSH_EVENT, $event );

        // 2. Also persist to queue for retry / batch processing
        $this->enqueue( $event );
    }

    /**
     * Build a signed event envelope.
     *
     * @param string $type
     * @param array  $payload
     * @return array
     */
    private function build_event( string $type, array $payload ): array {
        $payload = $this->normalize_payload_for_server( $type, $payload );
        $event = [
            'event_id'     => wp_generate_uuid4(),
            'event_type'  => sanitize_key( $type ),
            'site_url'    => home_url(),
            'site_name'   => sanitize_text_field( get_bloginfo( 'name' ) ),
            'wp_version'  => get_bloginfo( 'version' ),
            'plugin_ver'  => defined( 'ULTRAGUARD_PRO_VERSION' ) ? ULTRAGUARD_PRO_VERSION : '',
            'license_key' => $this->get_license_key(),
            'is_pro'      => LicenseInfo::instance()->isPro(),
            'timestamp'   => gmdate( 'Y-m-d\TH:i:s\Z' ),
            'payload'     => $payload,
        ];

        // HMAC-SHA256 signature for server-side verification
        $event['signature'] = $this->sign( SecureRemoteTransport::canonical_json( $payload ) . $event['timestamp'] );

        return $event;
    }

    /**
     * Dispatch a non-blocking (fire-and-forget) HTTP POST.
     *
     * @param string $endpoint  API endpoint path.
     * @param array  $body      Request body.
     */
    private function dispatch_nonblocking( string $endpoint, array $body ): void {
        $transport_key = $this->get_transport_license_key();
        $request = SecureRemoteTransport::build_request_context(
            'POST',
            $endpoint,
            $body,
            $transport_key,
            home_url(),
            'UltraGuardPro/' . ( defined( 'ULTRAGUARD_PRO_VERSION' ) ? ULTRAGUARD_PRO_VERSION : '1.0' ),
            [],
            $this->get_transport_identity_key()
        );

        SecureRemoteTransport::request( 'POST', self::apiBaseUrl() . $endpoint, [
            'blocking'  => false,       // ← does NOT wait for response
            'timeout'   => 0.01,
            'sslverify' => self::shouldVerifySslForUrl( self::apiBaseUrl() . $endpoint ),
            'headers'   => $request['headers'],
            'body'      => $request['body_json'],
        ] );
    }

    /**
     * Add an event to the persistent push queue.
     *
     * @param array $event  Signed event envelope.
     */
    private function enqueue( array $event ): void {
        $queue   = (array) get_option( self::PUSH_QUEUE_OPTION, [] );
        $queue[] = array_merge( $event, [
            '_queued_at' => time(),
            '_attempts'  => 0,
        ] );

        // Prune stale items
        $now    = time();
        $queue  = array_values( array_filter( $queue, fn( $i ) =>
            isset( $i['_queued_at'] ) && ( $now - $i['_queued_at'] ) < self::QUEUE_TTL
        ) );

        // Auto-flush if queue is getting large
        if ( count( $queue ) >= self::QUEUE_FLUSH_SIZE ) {
            $this->flush_push_queue( $queue );
            return;
        }

        update_option( self::PUSH_QUEUE_OPTION, $queue, false );
    }

    /**
     * Flush the push queue by sending a batch to the server.
     * Hooked to `shutdown` so it runs at the end of every request.
     *
     * @param array|null $queue  Override queue (used by enqueue when auto-flushing).
     */
    public function flush_push_queue( ?array $queue = null ): void {
        if ( $queue === null ) {
            $queue = (array) get_option( self::PUSH_QUEUE_OPTION, [] );
        }

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

        // Split into chunks of 25
        $chunks   = array_chunk( $queue, 25 );
        $unsent   = [];

        foreach ( $chunks as $chunk ) {
            $transport_key = $this->get_transport_license_key();
            $body = [
                'events'      => $chunk,
                'site_url'    => home_url(),
                'license_key' => $this->get_license_key(),
                'timestamp'   => gmdate( 'Y-m-d\TH:i:s\Z' ),
            ];

            $accepted = false;

            foreach ( self::endpointUrlCandidates( self::EP_PUSH_BATCH ) as $url ) {
                $result = SecureRemoteTransport::signed_request_json(
                    'POST',
                    $url,
                    self::EP_PUSH_BATCH,
                    $body,
                    $transport_key,
                    home_url(),
                    'UltraGuardPro/' . ( defined( 'ULTRAGUARD_PRO_VERSION' ) ? ULTRAGUARD_PRO_VERSION : '1.0' ),
                    [
                        'blocking'  => true,
                        'timeout'   => 8,
                        'sslverify' => self::shouldVerifySslForUrl( $url ),
                    ],
                    $this->require_signed_responses(),
                    $this->get_transport_identity_key()
                );

                if ( ! empty( $result['success'] ) ) {
                    $accepted = true;
                    break;
                }
            }

            if ( ! $accepted ) {
                // Increment attempt counters and keep for next time
                foreach ( $chunk as &$item ) {
                    $item['_attempts'] = ( $item['_attempts'] ?? 0 ) + 1;
                    if ( $item['_attempts'] < 5 ) {
                        $unsent[] = $item;
                    }
                }
                unset( $item );
            }
        }

        update_option( self::PUSH_QUEUE_OPTION, $unsent, false );
    }

    // =========================================================================
    // PULL: BLACKLIST  (crowd-sourced global threat IP list)
    // =========================================================================

    /**
     * Pull the latest global threat IP blacklist from the server.
     * Stores results in the `ultraguard_threat_ips` table.
     */
    public function pull_blacklist(): void {
        $current_hash = get_option( self::OPT_BL_HASH, '' );

        $response = $this->blocking_get( self::EP_PULL_BLACKLIST, [
            'current_hash' => $current_hash,
        ] );

        if ( ! $response['success'] ) {
            $this->log( 'pull_blacklist failed: ' . ( $response['message'] ?? 'unknown' ) );
            return;
        }

        // No changes since last sync
        if ( ! empty( $response['up_to_date'] ) ) { return; }

        // Check tier delay
        if ( ! $this->is_update_available( $response['released_at'] ?? '' ) ) {
            $this->log( 'pull_blacklist: update not yet available for free tier.' );
            return;
        }

        $entries = $response['entries'] ?? [];
        if ( empty( $entries ) ) { return; }

        // Persist to the threat intelligence DB table
        $db_tables = $this->plugin->modules->get_db_tables();
        $ti_table = $db_tables ? $db_tables->get_table( 'threat_ips' ) : null;
        if ( $ti_table ) {
            $ti_table->upsert_batch( $entries, 'blacklist' );
        }

        // Also push to WP options cache so the firewall can do in-memory checks
        update_option( self::OPT_BL_HASH, $response['hash'] ?? '', false );
        delete_transient( 'ultraguard_threat_bl_cache' );

        update_option( self::OPT_LAST_PULL, time() );
        do_action( 'ultraguard_si/blacklist_updated', count( $entries ) );
        $this->log( sprintf( 'pull_blacklist: applied %d entries.', count( $entries ) ) );
    }

    // =========================================================================
    // PULL: MALWARE SIGNATURES
    // =========================================================================

    /**
     * Pull updated malware/scanner signature patterns.
     * Writes new signatures into the antivirus `malware_signatures` table.
     */
    public function pull_signatures(): void {
        $current_version = get_option( self::OPT_SIG_VERSION, '0' );

        $response = $this->blocking_get( self::EP_PULL_SIGNATURES, [
            'current_version' => $current_version,
        ] );

        if ( ! $response['success'] ) {
            $this->log( 'pull_signatures failed: ' . ( $response['message'] ?? 'unknown' ) );
            return;
        }
        if ( ! empty( $response['up_to_date'] ) ) { return; }
        if ( ! $this->is_update_available( $response['released_at'] ?? '' ) ) {
            $this->log( 'pull_signatures: update not yet available for free tier.' );
            return;
        }

        $signatures = $response['signatures'] ?? [];
        if ( empty( $signatures ) ) { return; }

        $filechecker_patterns = $this->extract_filechecker_patterns( $signatures );
        update_option( self::OPT_FILECHECKER_PATTERNS, $filechecker_patterns, false );

        $db_tables = $this->plugin->modules->get_db_tables();
        $sig_table = $db_tables ? $db_tables->get_table( 'malware_signatures' ) : null;
        if ( ! $sig_table ) {
            update_option( self::OPT_SIG_VERSION, sanitize_text_field( $response['version'] ?? $current_version ), false );
            do_action( 'ultraguard_si/signatures_updated', 0, 0 );
            $this->log( sprintf( 'pull_signatures: stored %d filechecker patterns without antivirus table access.', count( $filechecker_patterns ) ) );
            return;
        }

        $inserted = 0;
        $updated  = 0;
        foreach ( $signatures as $sig ) {
            $hash = sanitize_text_field( $sig['signature_hash'] ?? '' );
            if ( ! $hash ) { continue; }

            $existing = $sig_table->find_by_hash( $hash );
            if ( $existing ) {
                $sig_table->update( (int) $existing['id'], [
                    'name'         => sanitize_text_field( $sig['name']         ?? $existing['name'] ),
                    'type'         => sanitize_key(        $sig['type']         ?? $existing['type'] ),
                    'pattern'      => $sig['pattern']      ?? $existing['pattern'],
                    'threat_level' => sanitize_key(        $sig['threat_level'] ?? $existing['threat_level'] ),
                    'is_active'    => 1,
                    'source'       => sanitize_text_field( $sig['source']       ?? 'ultraguard_cloud' ),
                ] );
                $updated++;
            } else {
                $sig_table->insert( [
                    'signature_hash' => $hash,
                    'name'           => sanitize_text_field( $sig['name']         ?? '' ),
                    'type'           => sanitize_key(        $sig['type']         ?? 'pattern' ),
                    'pattern'        => $sig['pattern']        ?? null,
                    'threat_level'   => sanitize_key(        $sig['threat_level'] ?? 'medium' ),
                    'is_active'      => 1,
                    'source'         => sanitize_text_field( $sig['source']       ?? 'ultraguard_cloud' ),
                ] );
                $inserted++;
            }
        }

        update_option( self::OPT_SIG_VERSION, sanitize_text_field( $response['version'] ?? $current_version ), false );
        do_action( 'ultraguard_si/signatures_updated', $inserted, $updated );
        $this->log( sprintf( 'pull_signatures: +%d new, ~%d updated (v%s).', $inserted, $updated, $response['version'] ?? '?' ) );
    }

    // =========================================================================
    // PULL: VULNERABILITY DATABASE
    // =========================================================================

    /**
     * Pull updated CVE / plugin vulnerability records.
     * Stores in the antivirus `vulnerability_scans` table as templated records
     * (component_type, slug, fixed_in version) that the local scanner matches against.
     */
    public function pull_vulndb(): void {
        $current_version = get_option( self::OPT_VULN_VERSION, '0' );

        $response = $this->blocking_get( self::EP_PULL_VULNDB, [
            'current_version' => $current_version,
        ] );

        if ( ! $response['success'] ) {
            $this->log( 'pull_vulndb failed: ' . ( $response['message'] ?? 'unknown' ) );
            return;
        }
        if ( ! empty( $response['up_to_date'] ) ) { return; }
        if ( ! $this->is_update_available( $response['released_at'] ?? '' ) ) {
            $this->log( 'pull_vulndb: update not yet available for free tier.' );
            return;
        }

        $entries = $response['vulnerabilities'] ?? [];
        if ( empty( $entries ) ) { return; }

        // Store as signature entries of type 'vulnerability' so the scanner picks them up
        $db_tables = $this->plugin->modules->get_db_tables();
        $sig_table = $db_tables ? $db_tables->get_table( 'malware_signatures' ) : null;
        if ( ! $sig_table ) { return; }

        $inserted = 0;
        foreach ( $entries as $entry ) {
            $vuln_hash = 'vuln_' . md5( ( $entry['component_slug'] ?? '' ) . ( $entry['cve_id'] ?? '' ) . ( $entry['fixed_in'] ?? '' ) );

            if ( $sig_table->find_by_hash( $vuln_hash ) ) { continue; }

            $sig_table->insert( [
                'signature_hash' => $vuln_hash,
                'name'           => sanitize_text_field( $entry['title'] ?? $entry['component_slug'] ?? '' ),
                'type'           => 'vulnerability',
                'pattern'        => wp_json_encode( [
                    'component_slug' => sanitize_key( $entry['component_slug'] ?? '' ),
                    'component_type' => sanitize_key( $entry['component_type'] ?? 'plugin' ),
                    'fixed_in'       => sanitize_text_field( $entry['fixed_in'] ?? '' ),
                    'cve_id'         => sanitize_text_field( $entry['cve_id'] ?? '' ),
                    'description'    => wp_kses_post( $entry['description'] ?? '' ),
                ] ),
                'threat_level'   => sanitize_key( $entry['severity'] ?? 'medium' ),
                'is_active'      => 1,
                'source'         => 'vulnerability',
            ] );
            $inserted++;
        }

        update_option( self::OPT_VULN_VERSION, sanitize_text_field( $response['version'] ?? $current_version ), false );
        do_action( 'ultraguard_si/vulndb_updated', $inserted );
        $this->log( sprintf( 'pull_vulndb: +%d new vuln entries (v%s).', $inserted, $response['version'] ?? '?' ) );
    }

    // =========================================================================
    // PULL: FIREWALL RULES
    // =========================================================================

    /**
     * Pull updated cloud firewall rules (WAF patterns, geo rules, rate-limit presets).
     * Inserts into the `firewall_rules` table with source='cloud'.
     */
    public function pull_fw_rules(): void {
        $current_hash = get_option( self::OPT_FW_HASH, '' );

        $response = $this->blocking_get( self::EP_PULL_FW_RULES, [
            'current_hash' => $current_hash,
        ] );

        if ( ! $response['success'] ) {
            $this->log( 'pull_fw_rules failed: ' . ( $response['message'] ?? 'unknown' ) );
            return;
        }
        if ( ! empty( $response['up_to_date'] ) ) { return; }
        if ( ! $this->is_update_available( $response['released_at'] ?? '' ) ) {
            $this->log( 'pull_fw_rules: update not yet available for free tier.' );
            return;
        }

        $rules     = $response['rules'] ?? [];
        $db_tables = $this->plugin->modules->get_db_tables();
        $fw_table  = $db_tables ? $db_tables->get_table( 'firewall_rules' ) : null;
        if ( ! $fw_table || empty( $rules ) ) { return; }

        // Deactivate all previously synced cloud rules first (idempotent refresh)
        $fw_table->update_where( [ 'is_active' => 0 ], [ 'rule_type' => 'cloud' ] );

        $inserted = 0;
        foreach ( $rules as $rule ) {
            $name = sanitize_text_field( $rule['name'] ?? '' );
            if ( ! $name ) { continue; }

            if ( ! $fw_table->exists( [ 'name' => $name, 'rule_type' => 'cloud' ] ) ) {
                $fw_table->insert( [
                    'name'           => $name,
                    'description'    => sanitize_textarea_field( $rule['description'] ?? '' ),
                    'rule_type'      => 'cloud',
                    'match_field'    => sanitize_key(         $rule['match_field']    ?? 'uri' ),
                    'match_operator' => sanitize_key(         $rule['match_operator'] ?? 'regex' ),
                    'match_value'    => sanitize_textarea_field( $rule['match_value'] ?? '' ),
                    'action'         => sanitize_key(         $rule['action']         ?? 'block' ),
                    'priority'       => max( 1, absint(       $rule['priority']       ?? 50 ) ),
                    'is_active'      => 1,
                ] );
                $inserted++;
            } else {
                $fw_table->update_where( [ 'is_active' => 1 ], [ 'name' => $name, 'rule_type' => 'cloud' ] );
            }
        }

        update_option( self::OPT_FW_HASH, sanitize_text_field( $response['hash'] ?? '' ), false );
        do_action( 'ultraguard_si/fw_rules_updated', $inserted );
        $this->log( sprintf( 'pull_fw_rules: applied %d cloud rules.', $inserted ) );
    }

    // =========================================================================
    // PULL: HTACCESS SECURITY RULES
    // =========================================================================

    /**
     * Pull updated recommended .htaccess security rule templates.
     * Stored in a WP option; the htaccess module reads these when generating
     * its recommended-rules list.
     */
    public function pull_htaccess(): void {
        $current_hash = get_option( self::OPT_HTA_HASH, '' );

        $response = $this->blocking_get( self::EP_PULL_HTACCESS, [
            'current_hash' => $current_hash,
        ] );

        if ( ! $response['success'] ) {
            $this->log( 'pull_htaccess failed: ' . ( $response['message'] ?? 'unknown' ) );
            return;
        }
        if ( ! empty( $response['up_to_date'] ) ) { return; }
        if ( ! $this->is_update_available( $response['released_at'] ?? '' ) ) {
            $this->log( 'pull_htaccess: update not yet available for free tier.' );
            return;
        }

        $rules = $response['rules'] ?? [];
        if ( empty( $rules ) ) { return; }

        // Sanitize each rule template before storing
        $sanitized = array_map( function( $rule ) {
            return [
                'id'          => sanitize_key(        $rule['id']          ?? '' ),
                'title'       => sanitize_text_field( $rule['title']       ?? '' ),
                'description' => wp_kses_post(        $rule['description'] ?? '' ),
                'content'     => $this->sanitize_htaccess_rule( $rule['content'] ?? '' ),
                'category'    => sanitize_key(        $rule['category']    ?? 'general' ),
                'severity'    => sanitize_key(        $rule['severity']    ?? 'medium' ),
                'enabled'     => false, // Admins must explicitly enable cloud rules
            ];
        }, $rules );

        update_option( 'ultraguard_cloud_htaccess_rules', $sanitized, false );
        update_option( self::OPT_HTA_HASH, sanitize_text_field( $response['hash'] ?? '' ), false );

        do_action( 'ultraguard_si/htaccess_updated', count( $sanitized ) );
        $this->log( sprintf( 'pull_htaccess: stored %d rule templates.', count( $sanitized ) ) );
    }

    // =========================================================================
    // CRON SCHEDULING
    // =========================================================================

    /**
     * Register WP-Cron events on `wp_loaded` if they are not already scheduled.
     */
    public function maybe_schedule_crons(): void {
        foreach ( self::SYNC_RESOURCES as $resource => $config ) {
            $hook      = (string) $config['hook'];
            $enabled   = $this->is_resource_enabled( $resource );
            $interval  = $this->get_resource_interval( $resource );
            $scheduled = wp_get_scheduled_event( $hook );

            if ( ! $enabled ) {
                if ( $scheduled ) {
                    wp_clear_scheduled_hook( $hook );
                }
                continue;
            }

            if ( ! $scheduled || (string) ( $scheduled->schedule ?? '' ) !== $interval ) {
                wp_clear_scheduled_hook( $hook );
                wp_schedule_event( time() + (int) $config['offset'], $interval, $hook );
            }
        }
    }

    // =========================================================================
    // PRIVATE HELPERS
    // =========================================================================

    /**
     * Check whether a server-released update is available for the current
     * license tier.
     *
     * Pro  → always available immediately (returns true).
     * Free → only available if the release date is older than FREE_DELAY_DAYS.
     *
     * @param  string $released_at  ISO-8601 / MySQL datetime of when the package
     *                               was published on the server.
     * @return bool
     */
    private function is_update_available( string $released_at ): bool {
        if ( LicenseInfo::instance()->isPro() ) {
            return true; // Pro gets updates immediately, no delay
        }

        if ( empty( $released_at ) ) {
            return false; // No timestamp → cannot confirm delay has passed
        }

        $released_ts = strtotime( $released_at );
        if ( $released_ts === false || $released_ts <= 0 ) {
            return false;
        }

        $delay_seconds = max( 0, LicenseInfo::instance()->getUpdateDelay() ) * DAY_IN_SECONDS;
        return ( time() - $released_ts ) >= $delay_seconds;
    }

    /**
     * Blocking HTTP GET to the intelligence API.
     *
     * @param  string $endpoint  API path.
     * @param  array  $params    Query parameters.
     * @return array  { success, message?, ...response_fields }
     */
    private function blocking_get( string $endpoint, array $params = [] ): array {
        $params = array_merge( $params, [
            'site_url'    => home_url(),
            'is_pro'      => LicenseInfo::instance()->isPro() ? '1' : '0',
            'plugin_ver'  => defined( 'ULTRAGUARD_PRO_VERSION' ) ? ULTRAGUARD_PRO_VERSION : '',
            'ts'          => time(),
        ] );
        $transport_key = $this->get_transport_license_key();
        $last_error = 'Request failed';

        foreach ( self::endpointUrlCandidates( $endpoint ) as $base_url ) {
            $url    = add_query_arg( $params, $base_url );
            $result = SecureRemoteTransport::signed_request_json(
                'GET',
                $url,
                $endpoint,
                [],
                $transport_key,
                home_url(),
                'UltraGuardPro/' . ( defined( 'ULTRAGUARD_PRO_VERSION' ) ? ULTRAGUARD_PRO_VERSION : '1.0' ),
                [
                    'timeout'   => 20,
                    'sslverify' => self::shouldVerifySslForUrl( $url ),
                ],
                $this->require_signed_responses(),
                $this->get_transport_identity_key()
            );

            if ( ! $result['success'] ) {
                $last_error = (string) ( $result['message'] ?? $last_error );
                continue;
            }

            return array_merge( [ 'success' => true ], is_array( $result['body'] ?? null ) ? $result['body'] : [] );
        }

        return [ 'success' => false, 'message' => $last_error ];
    }

    private function get_transport_license_key(): string {
        $license_data = get_option( LicenseInfo::OPTION_DATA, [] );
        $identity_key = $this->get_transport_identity_key();
        if (
            is_array( $license_data )
            && ! empty( $license_data['transport_key'] )
            && ! empty( $identity_key )
            && hash( 'sha256', $identity_key ) === (string) ( $license_data['transport_identity_fingerprint'] ?? '' )
        ) {
            return (string) $license_data['transport_key'];
        }

        $license_key = $this->get_license_key();

        return '' !== $license_key ? $license_key : 'ultraguard-public';
    }

    private function get_transport_identity_key(): string {
        $license_key = $this->get_license_key();

        return '' !== $license_key ? $license_key : 'ultraguard-public';
    }

    /**
     * Shared HTTP headers for all API requests.
     *
     * @return array
     */
    private function default_headers(): array {
        return [
            'Content-Type' => 'application/json',
            'Accept'       => 'application/json',
            'User-Agent'   => 'UltraGuardPro/' . ( defined( 'ULTRAGUARD_PRO_VERSION' ) ? ULTRAGUARD_PRO_VERSION : '1.0' ),
            'X-License-Key'=> $this->get_license_key(),
            'X-Site-URL'   => home_url(),
        ];
    }

    /**
     * Compute HMAC-SHA256 signature for an outgoing payload.
     *
     * @param  string $data  Raw payload string.
     * @return string        Hex-encoded HMAC.
     */
    private function sign( string $data ): string {
        return hash_hmac( 'sha256', $data, $this->get_transport_license_key() );
    }

    /**
     * Check whether a resource is enabled for automatic fetch.
     */
    private function is_resource_enabled( string $resource ): bool {
        if ( ! isset( self::SYNC_RESOURCES[ $resource ] ) ) {
            return false;
        }

        $stored = get_option( $this->get_enabled_option_key( $resource ), null );
        if ( null === $stored || '' === $stored ) {
            return true;
        }

        return in_array( (string) $stored, [ '1', 'yes', 'true', 'on' ], true );
    }

    /**
     * Get the configured interval for a resource, falling back to tier defaults.
     */
    private function get_resource_interval( string $resource ): string {
        $allowed = $this->get_allowed_intervals();
        $stored  = sanitize_key( (string) get_option( $this->get_interval_option_key( $resource ), '' ) );

        if ( isset( $allowed[ $stored ] ) ) {
            return $stored;
        }

        return $this->get_default_interval( $resource );
    }

    /**
     * Get the default recurrence for a resource based on license tier.
     */
    private function get_default_interval( string $resource ): string {
        $config = self::SYNC_RESOURCES[ $resource ] ?? [];
        $key    = LicenseInfo::instance()->isPro() ? 'default_pro' : 'default_free';
        $value  = sanitize_key( (string) ( $config[ $key ] ?? 'daily' ) );
        $allowed = $this->get_allowed_intervals();

        if ( isset( $allowed[ $value ] ) ) {
            return $value;
        }

        if ( isset( $allowed['daily'] ) ) {
            return 'daily';
        }

        $keys = array_keys( $allowed );
        return isset( $keys[0] ) ? (string) $keys[0] : 'daily';
    }

    /**
     * Return valid schedule keys available in the current WordPress install.
     *
     * @return array<string,mixed>
     */
    private function get_allowed_intervals(): array {
        $supported = array_keys( self::get_interval_labels() );
        $registered = wp_get_schedules();

        return array_intersect_key( $registered, array_flip( $supported ) );
    }

    private function get_enabled_option_key( string $resource ): string {
        return self::OPT_SYNC_PREFIX . $resource . '_enabled';
    }

    private function get_interval_option_key( string $resource ): string {
        return self::OPT_SYNC_PREFIX . $resource . '_interval';
    }

    private function require_signed_responses(): bool {
        return (bool) apply_filters( 'ultraguard_pro/server_communication/require_signed_response', false );
    }

    private function normalize_payload_for_server( string $type, array $payload ): array {
        if ( isset( $payload['ip'] ) && ! isset( $payload['ip_address'] ) ) {
            $payload['ip_address'] = (string) $payload['ip'];
            unset( $payload['ip'] );
        }

        if ( isset( $payload['ip_address'] ) && is_string( $payload['ip_address'] ) && isset( $this->plugin->utils ) ) {
            $payload['ip_address'] = $this->plugin->utils->normalize_ip( $payload['ip_address'] );
        }

        if ( isset( $payload['request_uri'] ) && is_string( $payload['request_uri'] ) ) {
            $payload['request_uri'] = (string) wp_parse_url( $payload['request_uri'], PHP_URL_PATH );
        }

        if ( isset( $payload['username'] ) && is_string( $payload['username'] ) && '' !== $payload['username'] ) {
            $payload['username_hash'] = hash( 'sha256', strtolower( $payload['username'] ) );
            unset( $payload['username'] );
        }

        if ( isset( $payload['user_agent'] ) && is_string( $payload['user_agent'] ) ) {
            $payload['user_agent'] = substr( sanitize_text_field( $payload['user_agent'] ), 0, 255 );
        }

        if ( isset( $payload['payload'] ) && is_array( $payload['payload'] ) ) {
            $payload['payload'] = $this->normalize_payload_for_server( $type, $payload['payload'] );
        }

        $payload['incident_family'] = sanitize_key( $type );

        return $payload;
    }

    /**
     * Retrieve the stored license key.
     *
     * @return string
     */
    private function get_license_key(): string {
        static $key = null;
        if ( $key === null ) {
            $key = (string) get_option( LicenseInfo::OPTION_KEY, '' );
        }
        return $key;
    }

    /**
     * Sanitize an .htaccess rule block received from the server.
     * Strips anything that isn't standard Apache directive syntax.
     *
     * @param  string $content  Raw rule content.
     * @return string           Sanitized rule content.
     */
    /**
     * Sanitize and normalise an .htaccess rule block received from the server.
     *
     * Two-pass processing:
     *   1. Strip anything that cannot appear in a valid .htaccess file.
     *   2. Normalise known-bad header values to their modern safe equivalents.
     *
     * @param  string $content  Raw rule content from the cloud API.
     * @return string           Sanitized and normalised rule content.
     */
    private function sanitize_htaccess_rule( string $content ): string {
        // Strip PHP open/close tags and any code between them.
        $content = preg_replace( '/<\?.*?\?>/si', '', $content );

        // Remove script tags and their content.
        $content = preg_replace( '/<script.*?<\/script>/si', '', $content );

        // Allow only characters that are valid in Apache .htaccess directives.
        $content = preg_replace( '/[^\w\s\.\-\/\|\<\>\#\[\]\(\)\{\}\^\$\*\+\?\,\=\:\\\'\"@%!&;~_]/', '', $content );

        // Normalise outdated and unsafe header values before storing.
        $content = $this->normalize_htaccess_rule( $content );

        return trim( $content );
    }

    /**
     * Correct known-unsafe header values in an .htaccess rule block.
     *
     * BUG FIX — X-XSS-Protection: "1; mode=block" → "0"
     *   The XSS Auditor was removed from all modern browsers. OWASP documents
     *   that the "1; mode=block" value can actively introduce XSS vulnerabilities.
     *
     * BUG FIX — Strict-Transport-Security: add env=HTTPS if absent
     *   Per RFC 6797 §7.2 HSTS must only be sent over TLS. Without env=HTTPS
     *   the header fires on plain HTTP responses too (browsers ignore it per
     *   spec, but scanners flag it and it wastes response bytes).
     *
     * BUG FIX — GLOBALS / _REQUEST hex quantifier: {0,2} → {2}
     *   A URL-encoded byte is always exactly 2 hex digits. {0,2} allows a bare
     *   trailing percent sign to match, causing false positives on valid URLs.
     *
     * Safe to call multiple times — all replacements are idempotent.
     *
     * @param  string $content  Raw or partially sanitized .htaccess content.
     * @return string           Content with unsafe values replaced.
     */
    private function normalize_htaccess_rule( string $content ): string {

        // X-XSS-Protection — replace any non-zero value with "0".
        // Matches both `Header set` and `Header always set` with single or double quotes.
        $content = preg_replace(
            '/^(\s*Header\s+(?:always\s+)?set\s+X-XSS-Protection\s+)["\'"]1[^\"\']*["\'"]/im',
            '$1"0"',
            $content
        );

        // HSTS — append env=HTTPS when the directive lacks it.
        $content = preg_replace(
            '/^(\s*Header\s+(?:always\s+)?set\s+Strict-Transport-Security\s+"[^"]*")(?!\s+env=)/im',
            '$1 env=HTTPS',
            $content
        );

        // GLOBALS / _REQUEST hex quantifier: {0,2} → {2}
        $content = str_replace(
            [ 'GLOBALS(=|\\[|%[0-9A-Z]{0,2})', '_REQUEST(=|\\[|%[0-9A-Z]{0,2})' ],
            [ 'GLOBALS(=|\\[|%[0-9A-Z]{2})',   '_REQUEST(=|\\[|%[0-9A-Z]{2})'   ],
            $content
        );

        return $content;
    }

    /**
     * Extract a lightweight regex-pattern set for the Filechecker module.
     *
     * @param array $signatures
     * @return array<int,array<string,string>>
     */
    private function extract_filechecker_patterns( array $signatures ): array {
        $patterns = [];

        foreach ( $signatures as $signature ) {
            $type    = sanitize_key( (string) ( $signature['type'] ?? '' ) );
            $pattern = (string) ( $signature['pattern'] ?? '' );

            if ( ! in_array( $type, [ 'pattern', 'regex', 'heuristic' ], true ) ) {
                continue;
            }

            if ( '' === $pattern || @preg_match( $pattern, '' ) === false ) {
                continue;
            }

            $patterns[] = [
                'name'         => sanitize_text_field( (string) ( $signature['name'] ?? 'Cloud signature' ) ),
                'pattern'      => $pattern,
                'threat_level' => sanitize_key( (string) ( $signature['threat_level'] ?? 'high' ) ),
            ];
        }

        return $patterns;
    }

    /**
     * Write a debug log entry when WP_DEBUG is on.
     *
     * @param  string $message
     */
    private function log( string $message ): void {
        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
            \UltraguardPro\core\Logger::error( '[UltraGuard SI] ' . $message );
        }
    }
}
