<?php

declare(strict_types=1);
/**
 * SecurWP Extended WAF Protection - Bootstrap
 *
 * This file is loaded via auto_prepend_file BEFORE WordPress.
 * It provides IP blocking, rate limiting, and basic WAF protection
 * at the PHP level, before any application code runs.
 *
 * DO NOT use any WordPress functions in this file.
 * This file must be pure PHP only -- no frameworks, no autoloaders.
 *
 */

if ( ! defined( 'ABSPATH' ) ) {
    $securwp_waf_bootstrap_script = (string) filter_input( INPUT_SERVER, 'SCRIPT_FILENAME', FILTER_UNSAFE_RAW );
    if ( is_string( $securwp_waf_bootstrap_script ) && realpath( $securwp_waf_bootstrap_script ) === __FILE__ ) {
        http_response_code( 404 );
        exit;
    }
}

if ( defined( 'SECURWP_WAF_LOADED' ) ) {
    return;
}
define( 'SECURWP_WAF_LOADED', true );

/**
 * Read a scalar server value in the standalone bootstrap context.
 *
 * @param string $key $_SERVER key.
 * @return string
 */
function securwp_waf_server_value( string $key ): string {
    $value = filter_input( INPUT_SERVER, $key, FILTER_UNSAFE_RAW );
    return is_string( $value ) ? str_replace( "\0", '', $value ) : '';
}

/**
 * Extract a request URI component without relying on parse_url().
 *
 * @param string $request_uri Full request URI.
 * @param string $component   Supported values: path, query.
 * @return string
 */
function securwp_waf_request_uri_component( string $request_uri, string $component ): string {
    $uri = explode( '#', $request_uri, 2 )[0];
    $pos = strpos( $uri, '?' );

    if ( $component === 'query' ) {
        return $pos === false ? '' : (string) substr( $uri, $pos + 1 );
    }

    $path = $pos === false ? $uri : (string) substr( $uri, 0, $pos );
    return $path !== '' ? $path : '/';
}

/**
 * Detect whether the current request targets xmlrpc.php.
 *
 * XML-RPC exposure is controlled by hardening. The pre-WordPress rate limiter
 * should not become a shadow XML-RPC protection feature.
 *
 * @param string $path Request URI path.
 * @return bool
 */
function securwp_waf_is_xmlrpc_path( string $path ): bool {
    $normalized = preg_replace( '#/+#', '/', str_replace( '\\', '/', $path ) );
    if ( ! is_string( $normalized ) ) {
        $normalized = $path;
    }

    return strtolower( basename( $normalized ) ) === 'xmlrpc.php';
}

/**
 * Verify whether the current request is a signed internal SecurWP
 * server-verification probe.
 *
 * This must run before normal WAF policy evaluation so SecurWP does not block
 * its own hardening probes on hosts where loopback requests arrive through the
 * public origin path instead of pure localhost.
 *
 * The signature binds the HTTP method, path, query string, nonce, and
 * timestamp to a secret written into the generated WAF config. A small
 * file-backed nonce cache prevents replay inside the validity window.
 *
 * @param array  $config   WAF config array.
 * @param string $data_dir SecurWP WAF data directory.
 * @return bool
 */
function securwp_waf_is_signed_server_verification_request( array $config, string $data_dir ): bool {
    $verification = is_array( $config['internal_verification'] ?? null ) ? $config['internal_verification'] : [];
    $version      = (string) ( $verification['version'] ?? '' );
    $window       = max( 1, (int) ( $verification['window'] ?? 120 ) );
    $secret       = (string) ( $verification['secret'] ?? '' );

    if ( $version !== '1' || $secret === '' ) {
        return false;
    }

    $header_version   = securwp_waf_server_value( 'HTTP_X_SECURWP_VERIFY_VERSION' );
    $header_timestamp = securwp_waf_server_value( 'HTTP_X_SECURWP_VERIFY_TIMESTAMP' );
    $header_nonce     = securwp_waf_server_value( 'HTTP_X_SECURWP_VERIFY_NONCE' );
    $header_signature = securwp_waf_server_value( 'HTTP_X_SECURWP_VERIFY_SIGNATURE' );

    if ( $header_version !== '1' || $header_timestamp === '' || $header_nonce === '' || $header_signature === '' ) {
        return false;
    }

    if (
        ! ctype_digit( $header_timestamp )
        || strlen( $header_nonce ) > 128
        || strlen( $header_signature ) > 128
    ) {
        return false;
    }

    $timestamp = (int) $header_timestamp;
    if ( abs( time() - $timestamp ) > $window ) {
        return false;
    }

    $request_uri = securwp_waf_server_value( 'REQUEST_URI' );
    $path        = securwp_waf_request_uri_component( $request_uri, 'path' );
    $query       = securwp_waf_request_uri_component( $request_uri, 'query' );
    $method      = strtoupper( securwp_waf_server_value( 'REQUEST_METHOD' ) ?: 'GET' );
    $expected    = hash_hmac(
        'sha256',
        $method . '|' . $path . '|' . $query . '|' . $header_nonce . '|' . $timestamp,
        $secret
    );

    if ( ! hash_equals( $expected, $header_signature ) ) {
        return false;
    }

    return securwp_waf_mark_verify_nonce_used( $data_dir, $header_nonce, $header_signature, $timestamp, $window );
}

/**
 * Mark a server-verification nonce as used in a small guarded cache file.
 *
 * @param string $data_dir
 * @param string $nonce
 * @param string $signature
 * @param int    $timestamp
 * @param int    $window
 * @return bool
 */
function securwp_waf_mark_verify_nonce_used( string $data_dir, string $nonce, string $signature, int $timestamp, int $window ): bool {
    $guard = "<?php exit; ?>\n";
    $file  = $data_dir . '/waf-verify-nonces.php';
    $key   = hash( 'sha256', $nonce . '|' . $signature . '|' . $timestamp );
    $now   = time();
    $cutoff = $now - $window;

    if ( ! is_file( $file ) ) {
        @file_put_contents( $file, $guard . json_encode( [] ), LOCK_EX );
    }

    $handle = @fopen( $file, 'c+' );
    if ( $handle === false ) {
        return false;
    }

    $result = false;

    if ( flock( $handle, LOCK_EX ) ) {
        $raw = stream_get_contents( $handle );
        if ( is_string( $raw ) && strncmp( $raw, $guard, strlen( $guard ) ) === 0 ) {
            $raw = substr( $raw, strlen( $guard ) );
        }

        $data = @json_decode( is_string( $raw ) ? $raw : '', true );
        if ( ! is_array( $data ) ) {
            $data = [];
        }

        $fresh = [];
        foreach ( $data as $stored_key => $stored_ts ) {
            $stored_key = is_string( $stored_key ) ? $stored_key : '';
            $stored_ts  = is_int( $stored_ts ) ? $stored_ts : (int) $stored_ts;
            if ( $stored_key !== '' && $stored_ts >= $cutoff ) {
                $fresh[ $stored_key ] = $stored_ts;
            }
        }

        if ( ! isset( $fresh[ $key ] ) ) {
            $fresh[ $key ] = $now;
            $payload = $guard . json_encode( $fresh );
            if ( is_string( $payload ) ) {
                rewind( $handle );
                ftruncate( $handle, 0 );
                $written = fwrite( $handle, $payload );
                fflush( $handle );
                $result = is_int( $written ) && $written === strlen( $payload );
            }
        }

        flock( $handle, LOCK_UN );
    }

    fclose( $handle );

    return $result;
}

$securwp_waf_data_dir = dirname( __FILE__ );
$securwp_waf_config_file = $securwp_waf_data_dir . '/waf-config.php';

if ( ! is_file( $securwp_waf_config_file ) || ! is_readable( $securwp_waf_config_file ) ) {
    // No config yet -- nothing to enforce. Let the request through.
    return;
}

/** @var array|false $securwp_waf_config */
$securwp_waf_config = @include $securwp_waf_config_file;

if ( ! is_array( $securwp_waf_config ) ) {
    return;
}

$securwp_waf_policy_signature = (string) ( $securwp_waf_config['policy_signature'] ?? '' );
if ( $securwp_waf_policy_signature !== '' && ! defined( 'SECURWP_WAF_POLICY_SIGNATURE' ) ) {
    define( 'SECURWP_WAF_POLICY_SIGNATURE', $securwp_waf_policy_signature );
}

$securwp_waf_origin_mode = (string) ( $securwp_waf_config['origin_enforcement_mode'] ?? 'pending_validation' );
if ( $securwp_waf_origin_mode !== 'enforce' ) {
    return;
}

$securwp_waf_engine_file = (string) ( $securwp_waf_config['engine_file'] ?? '' );
if ( $securwp_waf_engine_file === '' || ! is_file( $securwp_waf_engine_file ) || ! is_readable( $securwp_waf_engine_file ) ) {
    return;
}

$securwp_waf_search_trust_core_file = (string) ( $securwp_waf_config['search_trust_core_file'] ?? '' );
$securwp_waf_search_trust_context_file = (string) ( $securwp_waf_config['search_trust_context_file'] ?? '' );
$securwp_waf_search_trust_dns_verifier_file = (string) ( $securwp_waf_config['search_trust_dns_verifier_file'] ?? '' );
$securwp_waf_request_provenance_file = (string) ( $securwp_waf_config['request_provenance_file'] ?? '' );

if ( ! defined( 'SECURWP_STANDALONE_BOOTSTRAP' ) ) {
    define( 'SECURWP_STANDALONE_BOOTSTRAP', true );
}

foreach (
    [
        $securwp_waf_search_trust_core_file,
        $securwp_waf_search_trust_context_file,
        $securwp_waf_request_provenance_file,
        $securwp_waf_engine_file,
        $securwp_waf_search_trust_dns_verifier_file,
    ] as $securwp_waf_required_file
) {
    if ( $securwp_waf_required_file === '' || ! is_file( $securwp_waf_required_file ) || ! is_readable( $securwp_waf_required_file ) ) {
        return;
    }

    require_once $securwp_waf_required_file;
}

if (
    ! class_exists( '\SecurWP\Modules\Firewall\RuleEngine' )
    || ! class_exists( '\SecurWP\Modules\Firewall\SearchTrust\SearchTrustCore' )
    || ! class_exists( '\SecurWP\Modules\Firewall\SearchTrust\SearchTrustContext' )
    || ! class_exists( '\SecurWP\Modules\Firewall\SearchTrust\SearchTrustDnsVerifier' )
) {
    return;
}

if ( ! defined( 'SECURWP_WAF_POLICY_APPLIED' ) ) {
    define( 'SECURWP_WAF_POLICY_APPLIED', true );
}

if ( securwp_waf_is_signed_server_verification_request( $securwp_waf_config, $securwp_waf_data_dir ) ) {
    return;
}


/**
 * Determine the real client IP address.
 *
 * Algorithm mirrors the main IpDetector class. Reads proxy detection
 * settings from the WAF config so the same detection mode, header, and
 * trusted proxy list are used whether WordPress is loaded or not.
 *
 * Key decisions (matching IpDetector):
 *   - REMOTE_ADDR is always the baseline.
 *   - Proxy headers are only read if REMOTE_ADDR is a trusted proxy.
 *   - X-Forwarded-For uses RIGHTMOST public IP (not leftmost).
 *   - Port numbers are stripped before validation.
 *   - IPv6-mapped-IPv4 addresses are normalized.
 *
 * @param array $config The WAF config array.
 * @return string Client IP address.
 */
function securwp_waf_get_ip( array $config ): string {
    $remote_addr = securwp_waf_sanitize_ip( securwp_waf_server_value( 'REMOTE_ADDR' ) );
    if ( ! filter_var( $remote_addr, FILTER_VALIDATE_IP ) ) {
        return '0.0.0.0';
    }

    $ip_config = $config['ip_detection'] ?? [];
    $mode      = $ip_config['mode'] ?? 'automatic';
    $cf_ranges = $ip_config['cloudflare_ranges'] ?? [];

    // Mode: disabled. Only use the TCP connection IP.
    if ( 'disabled' === $mode ) {
        return securwp_waf_normalize_ip( $remote_addr );
    }

    // Only read proxy headers if REMOTE_ADDR belongs to a trusted proxy.
    $trusted = $ip_config['trusted_proxies'] ?? [];
    if ( empty( $trusted ) && defined( 'SECURWP_TRUSTED_PROXIES' ) ) {
        $trusted = (array) SECURWP_TRUSTED_PROXIES;
    }

    // Zero-config Cloudflare parity with runtime RequestProvenance.
    $provenance = securwp_waf_resolve_request_provenance( $config );
    if (
        ! empty( $provenance['cloudflare_peer_verified'] )
        && (
            'automatic' === $mode
            || ( 'manual' === $mode && ( $ip_config['header'] ?? '' ) === 'HTTP_CF_CONNECTING_IP' )
        )
    ) {
        $cf_ip = securwp_waf_extract_ip( 'HTTP_CF_CONNECTING_IP', [] );
        if ( '' !== $cf_ip ) {
            return securwp_waf_normalize_ip( $cf_ip );
        }
    }

    if ( empty( $trusted ) ) {
        return securwp_waf_normalize_ip( $remote_addr );
    }

    $is_trusted = false;
    foreach ( $trusted as $proxy ) {
        if ( securwp_waf_ip_matches( $remote_addr, (string) $proxy ) ) {
            $is_trusted = true;
            break;
        }
    }

    if ( ! $is_trusted ) {
        return securwp_waf_normalize_ip( $remote_addr );
    }

    // Supported proxy headers in priority order.
    $all_headers = [
        'HTTP_CF_CONNECTING_IP',
        'HTTP_X_FORWARDED_FOR',
        'HTTP_FORWARDED',
        'HTTP_X_REAL_IP',
        'HTTP_X_CLIENT_IP',
        'HTTP_CLIENT_IP',
        'HTTP_X_CLUSTER_CLIENT_IP',
    ];

    // Mode: manual. Only read the configured header.
    if ( 'manual' === $mode ) {
        $header = $ip_config['header'] ?? '';
        if ( ! empty( $header ) && in_array( $header, $all_headers, true ) ) {
            $ip = securwp_waf_extract_ip( $header, $trusted );
            if ( '' !== $ip ) {
                return securwp_waf_normalize_ip( $ip );
            }
        }
        return securwp_waf_normalize_ip( $remote_addr );
    }

    // Mode: automatic. Iterate all headers in priority order.
    foreach ( $all_headers as $header ) {
        $ip = securwp_waf_extract_ip( $header, $trusted );
        if ( '' !== $ip ) {
            return securwp_waf_normalize_ip( $ip );
        }
    }

    return securwp_waf_normalize_ip( $remote_addr );
}

/**
 * Resolve the request country code from trusted Cloudflare headers.
 *
 * The standalone bootstrap does not perform local MMDB lookups. Country
 * enforcement is only available here when the request is known to be coming
 * through Cloudflare and CF-IPCountry is present.
 *
 * @param array $config The WAF config array.
 * @return string ISO 3166-1 alpha-2 country code or '' when unavailable.
 */
function securwp_waf_get_country_code( array $config ): string {
    $provenance = securwp_waf_resolve_request_provenance( $config );
    if ( empty( $provenance['cloudflare_peer_verified'] ) || empty( $provenance['cloudflare_country_valid'] ) ) {
        return '';
    }

    return strtoupper( trim( (string) ( $provenance['cf_ipcountry'] ?? '' ) ) );
}

/**
 * Resolve request provenance using the shared runtime/bootstrap contract.
 *
 * @param array $config The WAF config array.
 * @return array<string,mixed>
 */
function securwp_waf_resolve_request_provenance( array $config ): array {
    static $cache = null;

    if ( is_array( $cache ) ) {
        return $cache;
    }

    if ( ! class_exists( '\SecurWP\Includes\RequestProvenance' ) ) {
        return [];
    }

    $ip_config = is_array( $config['ip_detection'] ?? null ) ? $config['ip_detection'] : [];
    $cache = \SecurWP\Includes\RequestProvenance::resolve_from_server(
        $_SERVER,
        [
            'cloudflare_ranges' => is_array( $ip_config['cloudflare_ranges'] ?? null ) ? $ip_config['cloudflare_ranges'] : [],
        ]
    );

    return $cache;
}

/**
 * Extract the best IP from a proxy header.
 *
 * For comma-separated headers (X-Forwarded-For), walks RIGHT-to-LEFT
 * skipping trusted proxies, returning the first public IP. Falls back
 * to the first valid private IP if no public one is found.
 *
 * @param string $header  $_SERVER key.
 * @param array  $trusted Trusted proxy list (IPs and/or CIDRs).
 * @return string Validated IP or ''.
 */
function securwp_waf_extract_ip( string $header, array $trusted ): string {
    $raw = securwp_waf_server_value( $header );
    if ( empty( $raw ) || ! is_string( $raw ) ) {
        return '';
    }

    // RFC 7239 Forwarded header.
    if ( 'HTTP_FORWARDED' === $header ) {
        if ( preg_match_all( '/for=["\']?\[?([a-f0-9.:]+)\]?["\']?/i', $raw, $m ) ) {
            $raw = implode( ',', $m[1] );
        } else {
            return '';
        }
    }

    // Comma-separated chain.
    if ( strpos( $raw, ',' ) !== false ) {
        $parts            = array_map( 'trim', explode( ',', $raw ) );
        $fallback_private = '';

        for ( $i = count( $parts ) - 1; $i >= 0; $i-- ) {
            $ip = securwp_waf_sanitize_ip( $parts[ $i ] );
            if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
                continue;
            }
            // Skip trusted proxies.
            $skip = false;
            foreach ( $trusted as $t ) {
                if ( securwp_waf_ip_matches( $ip, (string) $t ) ) {
                    $skip = true;
                    break;
                }
            }
            if ( $skip ) {
                continue;
            }
            if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
                return $ip;
            }
            if ( '' === $fallback_private ) {
                $fallback_private = $ip;
            }
        }

        return $fallback_private;
    }

    // Single value.
    $ip = securwp_waf_sanitize_ip( $raw );
    if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
        return securwp_waf_normalize_cloudflare_header_ip( $header, $ip );
    }

    return '';
}

/**
 * Prefer CF-Connecting-IPv6 when Cloudflare overwrote CF-Connecting-IP with
 * a pseudo IPv4 address.
 *
 * @param string $header
 * @param string $ip
 * @return string
 */
function securwp_waf_normalize_cloudflare_header_ip( string $header, string $ip ): string {
    if ( 'HTTP_CF_CONNECTING_IP' !== $header || ! securwp_waf_is_cloudflare_pseudo_ipv4( $ip ) ) {
        return $ip;
    }

    $ipv6 = securwp_waf_sanitize_ip( securwp_waf_server_value( 'HTTP_CF_CONNECTING_IPV6' ) );
    if ( filter_var( $ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) === false ) {
        return $ip;
    }

    return $ipv6;
}

/**
 * Detect Cloudflare Pseudo IPv4 overwrite addresses (Class E 240.0.0.0/4).
 *
 * @param string $ip
 * @return bool
 */
function securwp_waf_is_cloudflare_pseudo_ipv4( string $ip ): bool {
    return filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) !== false
        && securwp_waf_ip_matches( $ip, '240.0.0.0/4' );
}

/**
 * Strip port from an IP string and trim whitespace.
 *
 * @param string $ip Raw IP value.
 * @return string Cleaned IP.
 */
function securwp_waf_sanitize_ip( string $ip ): string {
    $ip = trim( $ip );
    if ( '' === $ip ) {
        return '';
    }
    // IPv6 in brackets: [::1]:port
    if ( $ip[0] === '[' ) {
        $close = strpos( $ip, ']' );
        if ( false !== $close ) {
            $ip = substr( $ip, 1, $close - 1 );
        }
    } elseif ( strpos( $ip, '.' ) !== false && substr_count( $ip, ':' ) === 1 ) {
        // IPv4 with port: 1.2.3.4:8080
        $ip = explode( ':', $ip, 2 )[0];
    }
    return $ip;
}

/**
 * Normalize an IP: convert IPv6-mapped-IPv4 to plain IPv4.
 *
 * @param string $ip Validated IP.
 * @return string Normalized IP.
 */
function securwp_waf_normalize_ip( string $ip ): string {
    return \SecurWP\Modules\Firewall\SearchTrust\SearchTrustCore::normalize_ip( $ip );
}

/**
 * Send a block response and terminate execution.
 *
 * @param string $message Human-readable reason displayed to the visitor.
 * @param int    $code    HTTP status code (403 or 429).
 */
function securwp_waf_block( string $message, int $code = 403 ): void {
    if ( ! headers_sent() ) {
        http_response_code( $code );
        header( 'Content-Type: text/html; charset=UTF-8' );
        header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0' );
        if ( $code === 429 ) {
            header( 'Retry-After: 60' );
        }
    }

    // Try to locate block-page.php. When running from the plugin directory,
    // it is one level up. When running as a copied bootstrap in the data dir,
    // walk up to find the plugin's Firewall module directory.
    $block_page = dirname( __DIR__ ) . '/block-page.php';
    if ( ! is_file( $block_page ) ) {
        // Fallback: the data dir lives at wp-content/securwp-data; the plugin
        // Firewall module is at wp-content/plugins/securwp/modules/Firewall.
        $block_page = dirname( __DIR__, 2 ) . '/plugins/securwp/modules/Firewall/block-page.php';
    }
    if ( is_file( $block_page ) ) {
        require_once $block_page;
        $ip = securwp_waf_server_value( 'REMOTE_ADDR' );
        securwp_render_block_page( $code, $message, $ip );
    } else {
        // Fallback if template is missing (should not happen in normal installs).
        $status_text = ( $code === 429 ) ? 'Too Many Requests' : 'Forbidden';
        $safe_code   = htmlspecialchars( (string) $code, ENT_QUOTES, 'UTF-8' );
        $safe_msg    = htmlspecialchars( $message, ENT_QUOTES, 'UTF-8' );
        $safe_status = htmlspecialchars( $status_text, ENT_QUOTES, 'UTF-8' );
        // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
        echo '<!DOCTYPE html><html><head><title>' . $safe_code . ' ' . $safe_status . '</title></head><body>';
        echo '<h1>' . $safe_code . ' ' . $safe_status . '</h1>';
        echo '<p>' . $safe_msg . '</p>';
        echo '</body></html>';
        // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
    }
    exit;
}

/**
 * Append a log entry to the WAF block log file.
 *
 * Performs automatic rotation when the log exceeds 1 MB.
 *
 * @param string $data_dir Absolute path to the data directory.
 * @param string $ip       Client IP address.
 * @param string $reason   Block reason.
 */
function securwp_waf_log( string $data_dir, string $ip, string $reason ): void {
    $log_file    = $data_dir . '/waf-blocks.php';
    $legacy_file = $data_dir . '/waf-blocks.log';
    $max_size = 1048576; // 1 MB

    if ( ! is_file( $log_file ) && is_file( $legacy_file ) ) {
        $legacy_contents = @file_get_contents( $legacy_file );
        if ( is_string( $legacy_contents ) ) {
            securwp_waf_write_guarded_payload( $log_file, $legacy_contents );
            @unlink( $legacy_file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
        }
    }

    // Rotate if the log file exceeds the size limit.
    if ( is_file( $log_file ) && @filesize( $log_file ) > $max_size ) {
        $rotated = substr( $log_file, 0, -4 ) . '.' . gmdate( 'Ymd-His' ) . '.php';
        // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename -- Standalone pre-WordPress bootstrap rotates its own flat-file log before WordPress is loaded.
        @rename( $log_file, $rotated );
    }

    $uri     = securwp_waf_server_value( 'REQUEST_URI' );
    $payload = json_encode(
        [
            'timestamp'   => time(),
            'ip'          => $ip,
            'reason'      => $reason,
            'request_uri' => $uri,
            'host'        => securwp_waf_server_value( 'HTTP_HOST' ),
            'method'      => strtoupper( securwp_waf_server_value( 'REQUEST_METHOD' ) ?: 'GET' ),
        ]
    );
    $line = is_string( $payload ) && $payload !== ''
        ? $payload . "\n"
        : gmdate( 'Y-m-d H:i:s' ) . ' UTC | ' . $ip . ' | ' . $reason . ' | ' . $uri . "\n";
    securwp_waf_append_guarded_payload( $log_file, $line );
}

function securwp_waf_guard_header(): string {
    return "<?php exit('Access denied'); __halt_compiler(); ?>\n";
}

function securwp_waf_write_guarded_payload( string $path, string $payload ): bool {
    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Standalone bootstrap writes a PHP-guarded container before WordPress loads.
    return @file_put_contents( $path, securwp_waf_guard_header() . $payload, LOCK_EX ) !== false;
}

function securwp_waf_append_guarded_payload( string $path, string $payload ): bool {
    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Standalone bootstrap needs a lockable handle for guarded log appends.
    $handle = @fopen( $path, 'c+' );
    if ( false === $handle ) {
        return false;
    }

    $ok = false;

    if ( flock( $handle, LOCK_EX ) ) {
        if ( securwp_waf_ensure_guarded_stream( $handle ) && 0 === fseek( $handle, 0, SEEK_END ) ) {
            $ok = securwp_waf_write_all( $handle, $payload );
            fflush( $handle );
        }

        flock( $handle, LOCK_UN );
    }

    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing the bootstrap guarded log handle after append.
    fclose( $handle );

    return $ok;
}

/**
 * @param resource $handle
 */
function securwp_waf_ensure_guarded_stream( $handle ): bool {
    $header = securwp_waf_guard_header();

    if ( 0 !== fseek( $handle, 0 ) ) {
        return false;
    }

    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- Bootstrap log migration reads the existing prefix before appending.
    $prefix = fread( $handle, strlen( $header ) );
    if ( is_string( $prefix ) && strncmp( $prefix, $header, strlen( $header ) ) === 0 ) {
        return true;
    }

    if ( 0 !== fseek( $handle, 0 ) ) {
        return false;
    }

    $existing = stream_get_contents( $handle );
    if ( false === $existing ) {
        $existing = '';
    }

    if ( ! ftruncate( $handle, 0 ) || 0 !== fseek( $handle, 0 ) ) {
        return false;
    }

    return securwp_waf_write_all( $handle, $header . $existing );
}

/**
 * @param resource $handle
 */
function securwp_waf_write_all( $handle, string $data ): bool {
    $written = 0;
    $length  = strlen( $data );

    while ( $written < $length ) {
        $result = fwrite( $handle, substr( $data, $written ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
        if ( false === $result || 0 === $result ) {
            return false;
        }

        $written += $result;
    }

    return true;
}

/**
 * Check whether an IP address falls within a given CIDR range.
 *
 * Supports both IPv4 and IPv6.
 *
 * @param string $ip   IP address to check.
 * @param string $cidr CIDR notation (e.g. "192.168.1.0/24" or "::1/128").
 * @return bool
 */
function securwp_waf_ip_in_cidr( string $ip, string $cidr ): bool {
    $parts = explode( '/', $cidr, 2 );
    if ( count( $parts ) !== 2 ) {
        return false;
    }

    $subnet      = $parts[0];
    $prefix_len  = (int) $parts[1];

    $ip_bin     = @inet_pton( $ip );
    $subnet_bin = @inet_pton( $subnet );

    if ( $ip_bin === false || $subnet_bin === false ) {
        return false;
    }

    // Both addresses must be the same family (same byte length).
    if ( strlen( $ip_bin ) !== strlen( $subnet_bin ) ) {
        return false;
    }

    $byte_len  = strlen( $ip_bin ); // 4 for IPv4, 16 for IPv6
    $max_bits  = $byte_len * 8;

    if ( $prefix_len < 0 || $prefix_len > $max_bits ) {
        return false;
    }

    // Build the network mask.
    $mask = str_repeat( "\xff", (int) floor( $prefix_len / 8 ) );
    $remainder = $prefix_len % 8;
    if ( $remainder > 0 ) {
        $mask .= chr( 0xff << ( 8 - $remainder ) & 0xff );
    }
    $mask = str_pad( $mask, $byte_len, "\x00" );

    return ( $ip_bin & $mask ) === ( $subnet_bin & $mask );
}

/**
 * Check whether an IP matches a single entry (exact or CIDR).
 *
 * @param string $ip    IP to test.
 * @param string $entry Entry from the IP list (exact IP or CIDR).
 * @return bool
 */
function securwp_waf_ip_matches( string $ip, string $entry ): bool {
    if ( strpos( $entry, '/' ) !== false ) {
        return securwp_waf_ip_in_cidr( $ip, $entry );
    }
    // Binary comparison so different IPv6 representations match.
    $a = @inet_pton( $ip );
    $b = @inet_pton( $entry );
    return $a !== false && $b !== false && $a === $b;
}

/**
 * Determine whether the request should bypass firewall policy as a verified
 * search engine crawler.
 *
 * @param string $data_dir Data directory used for the verification cache.
 * @param string $ip       Client IP address.
 * @param string $engine   Verified-search engine identifier.
 * @return bool
 */
function securwp_waf_is_verified_search_bot( string $data_dir, string $ip, string $engine ): bool {
    return \SecurWP\Modules\Firewall\SearchTrust\SearchTrustDnsVerifier::verify(
        $ip,
        $engine,
        static function ( string $cache_key ) use ( $data_dir ): ?bool {
            $cache_file = securwp_waf_search_trust_cache_file( $data_dir, $cache_key );
            return securwp_waf_search_trust_cache_get( $cache_file, $cache_key );
        },
        static function ( string $cache_key, bool $verified, int $ttl ) use ( $data_dir ): void {
            $cache_file = securwp_waf_search_trust_cache_file( $data_dir, $cache_key );
            securwp_waf_search_trust_cache_set( $cache_file, $cache_key, $verified, $ttl );
        }
    );
}

/**
 * Resolve the sharded search-trust cache file path for a cache key.
 *
 * @param string $data_dir
 * @param string $cache_key
 * @return string
 */
function securwp_waf_search_trust_cache_file( string $data_dir, string $cache_key ): string {
    $cache_dir = $data_dir . '/waf-search-trust';
    if ( ! is_dir( $cache_dir ) ) {
        @mkdir( $cache_dir, 0755, true );
    }

    return $cache_dir . '/shard-' . \SecurWP\Modules\Firewall\SearchTrust\SearchTrustCore::get_verified_search_cache_shard( $cache_key ) . '.php';
}

/**
 * Read a cached search-trust decision.
 *
 * @param string $cache_file Cache file path.
 * @param string $cache_key  MD5 cache key for the claimed engine and client IP.
 * @return bool|null
 */
function securwp_waf_search_trust_cache_get( string $cache_file, string $cache_key ): ?bool {
    $guard = "<?php exit; ?>\n";
    if ( ! is_file( $cache_file ) || ! is_readable( $cache_file ) ) {
        return null;
    }

    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Standalone bootstrap needs a lockable cache file handle before WordPress loads.
    $fp = @fopen( $cache_file, 'c+' );
    if ( $fp === false ) {
        return null;
    }

    if ( ! flock( $fp, LOCK_SH ) ) {
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing standalone bootstrap cache file handle after lock failure.
        fclose( $fp );
        return null;
    }

    $raw = stream_get_contents( $fp );
    flock( $fp, LOCK_UN );
    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing standalone bootstrap cache file handle after reading.
    fclose( $fp );

    if ( is_string( $raw ) && strncmp( $raw, $guard, strlen( $guard ) ) === 0 ) {
        $raw = substr( $raw, strlen( $guard ) );
    }

    $data = is_string( $raw ) ? @json_decode( $raw, true ) : null;
    if ( ! is_array( $data ) || ! isset( $data[ $cache_key ] ) || ! is_array( $data[ $cache_key ] ) ) {
        return null;
    }

    $entry = $data[ $cache_key ];
    if ( (int) ( $entry['expires_at'] ?? 0 ) <= time() ) {
        return null;
    }

    return ! empty( $entry['verified'] );
}

/**
 * Persist a search-trust decision with a bounded shard payload.
 *
 * @param string $cache_file Cache file path.
 * @param string $cache_key  MD5 cache key for the claimed engine and client IP.
 * @param bool   $verified   Verification result.
 * @param int    $ttl        Cache lifetime in seconds.
 * @return void
 */
function securwp_waf_search_trust_cache_set( string $cache_file, string $cache_key, bool $verified, int $ttl ): void {
    $guard = "<?php exit; ?>\n";
    if ( ! is_dir( dirname( $cache_file ) ) ) {
        return;
    }

    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Standalone bootstrap needs a lockable cache file handle before WordPress loads.
    $fp = @fopen( $cache_file, 'c+' );
    if ( $fp === false ) {
        return;
    }

    if ( ! flock( $fp, LOCK_EX ) ) {
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing standalone bootstrap cache file handle after lock failure.
        fclose( $fp );
        return;
    }

    $raw = stream_get_contents( $fp );
    if ( is_string( $raw ) && strncmp( $raw, $guard, strlen( $guard ) ) === 0 ) {
        $raw = substr( $raw, strlen( $guard ) );
    }

    $data = is_string( $raw ) ? @json_decode( $raw, true ) : null;
    if ( ! is_array( $data ) ) {
        $data = [];
    }

    $now = time();
    $data = securwp_waf_search_trust_cache_compact( $data, $now );

    $data[ $cache_key ] = [
        'verified'   => $verified,
        'expires_at' => $now + $ttl,
    ];

    $data = securwp_waf_search_trust_cache_compact( $data, $now );

    ftruncate( $fp, 0 );
    rewind( $fp );
    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Standalone bootstrap persists its own search-trust cache before WordPress loads.
    fwrite( $fp, $guard . json_encode( $data ) );
    fflush( $fp );
    flock( $fp, LOCK_UN );
    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing standalone bootstrap cache file handle after writing.
    fclose( $fp );
}

/**
 * Remove expired/invalid entries and bound shard growth.
 *
 * @param array<string, mixed> $data
 * @param int                  $now
 * @return array<string, array{verified: bool, expires_at: int}>
 */
function securwp_waf_search_trust_cache_compact( array $data, int $now ): array {
    $compacted = [];

    foreach ( $data as $key => $entry ) {
        if ( ! is_string( $key ) || $key === '' || ! is_array( $entry ) ) {
            continue;
        }

        $expires_at = (int) ( $entry['expires_at'] ?? 0 );
        if ( $expires_at <= $now ) {
            continue;
        }

        $compacted[ $key ] = [
            'verified'   => ! empty( $entry['verified'] ),
            'expires_at' => $expires_at,
        ];
    }

    if ( count( $compacted ) > 512 ) {
        uasort(
            $compacted,
            static function ( array $left, array $right ): int {
                return $right['expires_at'] <=> $left['expires_at'];
            }
        );
        $compacted = array_slice( $compacted, 0, 512, true );
    }

    return $compacted;
}

/**
 * Resolve the sharded rate-limit file path for an IP address.
 *
 * @param string $data_dir SecurWP data directory.
 * @param string $ip       Client IP address.
 * @return string
 */
function securwp_waf_rate_limit_file( string $data_dir, string $ip ): string {
    $rate_dir = $data_dir . '/waf-rates';
    if ( ! is_dir( $rate_dir ) ) {
        @mkdir( $rate_dir, 0755, true );
    }

    $hash  = hash( 'crc32b', $ip );
    $shard = (int) ( hexdec( substr( $hash, -4 ) ) % 32 );
    return $rate_dir . '/shard-' . str_pad( (string) $shard, 2, '0', STR_PAD_LEFT ) . '.php';
}

$securwp_waf_ip = securwp_waf_get_ip( $securwp_waf_config );

// Loopback requests (wp-cron, REST API health, scanner workers)
// must never be blocked by the WAF.
if ( in_array( $securwp_waf_ip, [ '127.0.0.1', '::1' ], true ) ) {
    return;
}

$securwp_waf_policy = is_array( $securwp_waf_config['policy'] ?? null ) ? $securwp_waf_config['policy'] : [];
$securwp_waf_community_config = is_array( $securwp_waf_config['community_blocklist'] ?? null ) ? $securwp_waf_config['community_blocklist'] : [];
$securwp_waf_search_scope = is_array( $securwp_waf_config['search_exception'] ?? null ) ? $securwp_waf_config['search_exception'] : [];
$securwp_waf_search_scope_hash = trim( (string) ( $securwp_waf_search_scope['scope_hash'] ?? '' ) );
$securwp_waf_user_agent = securwp_waf_server_value( 'HTTP_USER_AGENT' );
$securwp_waf_search_engine = \SecurWP\Modules\Firewall\SearchTrust\SearchTrustCore::get_verified_search_engine_claim( $securwp_waf_user_agent ) ?? '';
$securwp_waf_verified_search_eligible = false;
$securwp_waf_verified_search_checked = false;
$securwp_waf_verified_search = false;

if (
    \SecurWP\Modules\Firewall\SearchTrust\SearchTrustCore::should_resolve_verified_search_exception(
        ! empty( $securwp_waf_policy['rate_limit_enabled'] ),
        (string) ( $securwp_waf_policy['country_mode'] ?? 'off' ) !== 'off' && ! empty( $securwp_waf_policy['country_codes'] ),
        ! empty( $securwp_waf_community_config['enabled'] )
    )
    && $securwp_waf_search_scope_hash !== ''
    &&
    $securwp_waf_search_engine !== ''
    && \SecurWP\Modules\Firewall\SearchTrust\SearchTrustCore::is_verified_search_request_eligible(
        [
            'method' => strtoupper( securwp_waf_server_value( 'REQUEST_METHOD' ) ?: 'GET' ),
            'request_uri' => securwp_waf_server_value( 'REQUEST_URI' ),
        ],
        $securwp_waf_search_scope
    )
) {
    $securwp_waf_verified_search_eligible = true;
    $securwp_waf_verified_search_checked = true;
    $securwp_waf_verified_search = securwp_waf_is_verified_search_bot(
        $securwp_waf_data_dir,
        $securwp_waf_ip,
        $securwp_waf_search_engine
    );
}

if ( ! defined( \SecurWP\Modules\Firewall\SearchTrust\SearchTrustContext::BOOTSTRAP_CONSTANT ) ) {
    define(
        \SecurWP\Modules\Firewall\SearchTrust\SearchTrustContext::BOOTSTRAP_CONSTANT,
        \SecurWP\Modules\Firewall\SearchTrust\SearchTrustContext::create(
            [
                'checked' => $securwp_waf_verified_search_checked,
                'eligible' => $securwp_waf_verified_search_eligible,
                'verified' => $securwp_waf_verified_search,
                'exception' => $securwp_waf_verified_search,
                'engine' => $securwp_waf_search_engine,
                'scope_hash' => $securwp_waf_search_scope_hash,
            ]
        )
    );
}

$securwp_waf_search_trust_context = \SecurWP\Modules\Firewall\SearchTrust\SearchTrustContext::from_bootstrap_constant();
$securwp_waf_verified_search_exception = is_array( $securwp_waf_search_trust_context ) && ! empty( $securwp_waf_search_trust_context['exception'] );
$securwp_waf_search_engine = is_array( $securwp_waf_search_trust_context ) ? (string) ( $securwp_waf_search_trust_context['engine'] ?? '' ) : '';

$securwp_waf_country_code = $securwp_waf_verified_search_exception ? '' : securwp_waf_get_country_code( $securwp_waf_config );
$securwp_waf_country_policy_applied = (
    $securwp_waf_verified_search_exception
    || (
    (string) ( $securwp_waf_policy['country_mode'] ?? 'off' ) === 'off'
    || empty( $securwp_waf_policy['country_codes'] )
    || $securwp_waf_country_code !== ''
    )
);
$securwp_waf_community_blocked = false;
$securwp_waf_community_blocklist_applied = $securwp_waf_verified_search_exception;
$securwp_waf_community_enabled = ! empty( $securwp_waf_community_config['enabled'] );
$securwp_waf_expected_blocklist_signature = (string) ( $securwp_waf_community_config['artifact_signature'] ?? '' );
$securwp_waf_blocklist_file = $securwp_waf_data_dir . '/waf-blocklist.php';
if (
    ! $securwp_waf_verified_search_exception
    &&
    $securwp_waf_community_enabled
    && $securwp_waf_expected_blocklist_signature !== ''
    && is_file( $securwp_waf_blocklist_file )
    && is_readable( $securwp_waf_blocklist_file )
) {
    $securwp_waf_blocklist = @include $securwp_waf_blocklist_file;

    if (
        is_array( $securwp_waf_blocklist )
        && hash_equals(
            $securwp_waf_expected_blocklist_signature,
            (string) ( $securwp_waf_blocklist['signature'] ?? '' )
        )
    ) {
        $securwp_waf_community_blocklist_applied = true;

        if ( ! empty( $securwp_waf_blocklist['ips'] )
            && isset( $securwp_waf_blocklist['ips'][ $securwp_waf_ip ] ) ) {
            $securwp_waf_community_blocked = true;
        }

        if ( ! $securwp_waf_community_blocked && ! empty( $securwp_waf_blocklist['cidrs'] ) && is_array( $securwp_waf_blocklist['cidrs'] ) ) {
            foreach ( $securwp_waf_blocklist['cidrs'] as $securwp_waf_bl_cidr ) {
                if ( securwp_waf_ip_in_cidr( $securwp_waf_ip, $securwp_waf_bl_cidr ) ) {
                    $securwp_waf_community_blocked = true;
                    break;
                }
            }
        }
    }

    unset( $securwp_waf_blocklist, $securwp_waf_bl_cidr );
}
unset( $securwp_waf_blocklist_file );
unset( $securwp_waf_community_config, $securwp_waf_community_enabled, $securwp_waf_expected_blocklist_signature );

if ( ! defined( 'SECURWP_WAF_COMMUNITY_BLOCKLIST_APPLIED' ) ) {
    define( 'SECURWP_WAF_COMMUNITY_BLOCKLIST_APPLIED', $securwp_waf_community_blocklist_applied );
}

if ( ! defined( 'SECURWP_WAF_COUNTRY_POLICY_APPLIED' ) ) {
    define( 'SECURWP_WAF_COUNTRY_POLICY_APPLIED', $securwp_waf_country_policy_applied );
}

$securwp_waf_decision = \SecurWP\Modules\Firewall\RuleEngine::evaluate(
    $securwp_waf_policy,
    [
        'ip' => $securwp_waf_ip,
        'user_agent' => $securwp_waf_user_agent,
        'community_blocked' => $securwp_waf_community_blocked,
        'country_code' => $securwp_waf_country_code,
        'country_lookup_performed' => $securwp_waf_country_code !== '',
        'sensitive_path_match' => false,
        'verified_search_exception' => $securwp_waf_verified_search_exception,
        'verified_search_engine' => $securwp_waf_verified_search_exception ? $securwp_waf_search_engine : '',
    ]
);

if ( ( $securwp_waf_decision['action'] ?? '' ) === 'allow_bypass' ) {
    return;
}

if ( ( $securwp_waf_decision['action'] ?? '' ) === 'block' ) {
    $securwp_waf_reason = (string) ( $securwp_waf_decision['reason'] ?? 'Access denied' );
    $securwp_waf_source = (string) ( $securwp_waf_decision['source'] ?? '' );
    $securwp_waf_message = 'This request was blocked due to suspicious activity.';
    if ( $securwp_waf_source === 'blocked_ip' ) {
        $securwp_waf_message = (string) ( $securwp_waf_config['lockout_message'] ?? 'Access denied.' );
    } elseif ( $securwp_waf_source === 'blocked_country' ) {
        $securwp_waf_message = 'Access from your region is restricted by this site\'s security policy.';
    }

    securwp_waf_log( $securwp_waf_data_dir, $securwp_waf_ip, $securwp_waf_reason );
    securwp_waf_block( $securwp_waf_message, 403 );
}

if ( ! empty( $securwp_waf_policy['rate_limit_enabled'] ) ) {
    $securwp_waf_rate_limit_path = securwp_waf_request_uri_component(
        securwp_waf_server_value( 'REQUEST_URI' ),
        'path'
    );

    if ( securwp_waf_is_xmlrpc_path( $securwp_waf_rate_limit_path ) ) {
        return;
    }

    if ( $securwp_waf_verified_search_exception ) {
        return;
    }

    $securwp_waf_rl_max            = (int) ( $securwp_waf_policy['rate_limit_requests'] ?? 60 );
    $securwp_waf_rl_file           = securwp_waf_rate_limit_file( $securwp_waf_data_dir, $securwp_waf_ip );
    $securwp_waf_now               = time();
    $securwp_waf_current_bucket    = (int) floor( $securwp_waf_now / 60 );
    $securwp_waf_boundary_bucket   = $securwp_waf_current_bucket - 1;

    // Guard prefix prevents Nginx from serving raw JSON with IP data.
    // On Nginx the .php extension triggers PHP-FPM which hits exit().
    $securwp_waf_rl_guard = "<?php exit; ?>\n";

    if ( ! is_file( $securwp_waf_rl_file ) ) {
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Standalone pre-WordPress bootstrap initializes its own rate-limit state file before WordPress is loaded.
        @file_put_contents( $securwp_waf_rl_file, $securwp_waf_rl_guard . json_encode( [] ), LOCK_EX );
    }

    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Standalone pre-WordPress bootstrap needs a lockable file handle for rate limiting before WordPress is loaded.
    $securwp_waf_rl_fp = @fopen( $securwp_waf_rl_file, 'c+' );
    if ( $securwp_waf_rl_fp !== false ) {
        // Acquire an exclusive lock (blocking).
        if ( flock( $securwp_waf_rl_fp, LOCK_EX ) ) {
            $securwp_waf_rl_raw  = stream_get_contents( $securwp_waf_rl_fp );
            // Strip the PHP guard prefix before decoding JSON.
            if ( strncmp( $securwp_waf_rl_raw, $securwp_waf_rl_guard, strlen( $securwp_waf_rl_guard ) ) === 0 ) {
                $securwp_waf_rl_raw = substr( $securwp_waf_rl_raw, strlen( $securwp_waf_rl_guard ) );
            }
            $securwp_waf_rl_data = @json_decode( $securwp_waf_rl_raw, true );

            if ( ! is_array( $securwp_waf_rl_data ) ) {
                $securwp_waf_rl_data = [];
            }

            foreach ( $securwp_waf_rl_data as $securwp_waf_rl_ip => &$securwp_waf_rl_item ) {
                if ( ! is_array( $securwp_waf_rl_item ) || ! isset( $securwp_waf_rl_item['buckets'] ) || ! is_array( $securwp_waf_rl_item['buckets'] ) ) {
                    unset( $securwp_waf_rl_data[ $securwp_waf_rl_ip ] );
                    continue;
                }

                foreach ( $securwp_waf_rl_item['buckets'] as $securwp_waf_rl_bucket => $securwp_waf_rl_hits ) {
                    $securwp_waf_rl_bucket = (int) $securwp_waf_rl_bucket;
                    if ( $securwp_waf_rl_bucket < $securwp_waf_current_bucket - 1 ) {
                        unset( $securwp_waf_rl_item['buckets'][ $securwp_waf_rl_bucket ] );
                    }
                }

                if ( empty( $securwp_waf_rl_item['buckets'] ) ) {
                    unset( $securwp_waf_rl_data[ $securwp_waf_rl_ip ] );
                }
            }
            unset( $securwp_waf_rl_item );

            $securwp_waf_rl_key = $securwp_waf_ip;

            if ( ! isset( $securwp_waf_rl_data[ $securwp_waf_rl_key ] ) || ! is_array( $securwp_waf_rl_data[ $securwp_waf_rl_key ] ) ) {
                $securwp_waf_rl_data[ $securwp_waf_rl_key ] = [];
            }

            $securwp_waf_rl_entry = &$securwp_waf_rl_data[ $securwp_waf_rl_key ];
            if ( ! isset( $securwp_waf_rl_entry['buckets'] ) || ! is_array( $securwp_waf_rl_entry['buckets'] ) ) {
                $securwp_waf_rl_entry['buckets'] = [];
            }

            if ( ! isset( $securwp_waf_rl_entry['buckets'][ $securwp_waf_current_bucket ] ) ) {
                $securwp_waf_rl_entry['buckets'][ $securwp_waf_current_bucket ] = 0;
            }

            $securwp_waf_rl_entry['buckets'][ $securwp_waf_current_bucket ]++;

            $securwp_waf_rl_current_hits  = (int) ( $securwp_waf_rl_entry['buckets'][ $securwp_waf_current_bucket ] ?? 0 );
            $securwp_waf_rl_boundary_hits = (int) ( $securwp_waf_rl_entry['buckets'][ $securwp_waf_boundary_bucket ] ?? 0 );
            $securwp_waf_rl_seconds_in    = $securwp_waf_now % 60;
            $securwp_waf_rl_weighted_hits = $securwp_waf_rl_current_hits
                + (int) floor( $securwp_waf_rl_boundary_hits * ( 60 - $securwp_waf_rl_seconds_in ) / 60 );
            $securwp_waf_rl_exceeded = ( $securwp_waf_rl_weighted_hits > $securwp_waf_rl_max );

            // Write the updated data back (with PHP guard prefix).
            ftruncate( $securwp_waf_rl_fp, 0 );
            rewind( $securwp_waf_rl_fp );
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Standalone pre-WordPress bootstrap persists rate-limit state via its locked file handle.
            fwrite( $securwp_waf_rl_fp, $securwp_waf_rl_guard . json_encode( $securwp_waf_rl_data ) );
            fflush( $securwp_waf_rl_fp );
            flock( $securwp_waf_rl_fp, LOCK_UN );
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Standalone pre-WordPress bootstrap closes its locked file handle after writing rate-limit state.
            fclose( $securwp_waf_rl_fp );

            if ( $securwp_waf_rl_exceeded ) {
                securwp_waf_log( $securwp_waf_data_dir, $securwp_waf_ip, 'Rate limit exceeded' );
                securwp_waf_block( 'Request rate limit exceeded. Please wait before trying again.', 429 );
            }
        } else {
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Standalone pre-WordPress bootstrap closes its file handle when locking fails.
            fclose( $securwp_waf_rl_fp );
        }
    }
}
