<?php

declare(strict_types=1);

namespace SiteFort\Modules\Firewall;

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

use SiteFort\Includes\Interfaces\Module;
use SiteFort\Includes\Capabilities;
use SiteFort\Includes\IpDetector;
use SiteFort\Modules\Firewall\RateLimiter\RateLimiter;
use SiteFort\Modules\Firewall\Blocklist\BlocklistService;
use SiteFort\Modules\Firewall\GeoIP\GeoIPService;
use SiteFort\Modules\Firewall\GeoIP\CountryBlocker;
use SiteFort\Modules\Firewall\FirewallBypass;
use SiteFort\Modules\Firewall\SensitivePathDetector;
use SiteFort\Modules\Firewall\Blocklist\ReportBuffer;
use SiteFort\Modules\Firewall\ExtendedProtection\ConfigCoordinator as ExtendedProtectionConfigCoordinator;
use SiteFort\Modules\Firewall\FirewallLogger;
use SiteFort\Includes\IpProbe;
use SiteFort\Modules\Cloud\Services\StatePush;
use SiteFort\Modules\Firewall\SafeNetwork\SafeNetworkRangeDownloader;
use SiteFort\Modules\Firewall\SafeNetwork\SafeNetworkRanges;
use SiteFort\Modules\Firewall\SearchTrust\SearchTrustDnsVerifier;
use SiteFort\Modules\Firewall\SearchTrust\SearchTrustRuntimeResolver;

/**
 * The Firewall module class.
 *
 * Handles IP blocking, rate limiting, community blocklist, and country blocking.
 */
class Firewall implements Module {
	private const LOCKOUT_CLEANUP_HOOK = 'sitefort_firewall_lockout_cleanup';
	private const WAF_LOG_INGEST_HOOK = 'sitefort_firewall_waf_log_ingest';
	private const BACKGROUND_RECONCILE_TRANSIENT = 'sitefort_firewall_bg_reconcile';
	private const BACKGROUND_RECONCILE_TTL = 21600;
	private const RUNTIME_ASSETS_RECONCILE_TRANSIENT = 'sitefort_firewall_waf_assets_reconcile';
	private const RUNTIME_ASSETS_FAILURE_TRANSIENT = 'sitefort_firewall_waf_assets_retry';
	private const RUNTIME_ASSETS_FAILURE_TTL = 60;
	private const SAFE_NETWORK_SYNC_LOCK_OPTION = 'sitefort_safe_network_ranges_sync_lock';
	private const SAFE_NETWORK_SYNC_LOCK_TTL = 300;

	private $plugin_name;
	private $version;
    private $settings;
    private $ip_blocking;
    private $api;

    /**
     * Rate limiter instance.
     *
     * @var RateLimiter|null
     */
    private ?RateLimiter $rate_limiter = null;

    /**
     * Sensitive path detector instance.
     *
     * @var SensitivePathDetector|null
     */
    private ?SensitivePathDetector $sensitive_path_detector = null;

    /**
     * Whether run_firewall confirmed active enforcement for this request.
     * Stays false when the firewall is disabled, bypassed, or in monitor
     * mode, so 404 counting cannot ban by an untrusted client IP.
     */
    private bool $origin_enforce = false;

    /**
     * IP carried from the early 404 pass to the late flood check.
     */
    private ?string $pending_404_flood_ip = null;

    /**
     * Community blocklist service instance.
     *
     * @var BlocklistService|null
     */
    private ?BlocklistService $blocklist_service = null;

    /**
     * GeoIP service instance.
     *
     * @var GeoIPService|null
     */
    private ?GeoIPService $geoip_service = null;

    /**
     * Country blocker instance.
     *
     * @var CountryBlocker|null
     */
    private ?CountryBlocker $country_blocker = null;

    private ?array $trusted_user_agent_patterns = null;

    private bool $background_tasks_reconciled = false;

    private ?SearchTrustRuntimeResolver $search_trust_runtime_resolver = null;

    public function __construct( string $plugin_name, string $version ) {
		$this->plugin_name = $plugin_name;
		$this->version = $version;
        $this->load_settings();
	}

    private function load_settings() {
        $this->settings = FirewallSettings::get();

        // Multisite: network admin can force-enable the firewall on all subsites.
        if ( is_multisite() ) {
            $network = get_site_option( 'sitefort_network_settings', [] );
            if ( ! empty( $network['enforce_firewall'] ) ) {
                $this->settings['enabled'] = true;
            }
        }
    }

    public function initialize( \SiteFort\Includes\Module_Loader $loader ) {
        $this->ip_blocking = new IpBlocking( $this->settings );

        // Sensitive path detector (application-level complement to .htaccess rules).
        if ( ! empty( $this->settings['sensitive_path_enabled'] ?? true ) ) {
            $threshold = (int) ( $this->settings['sensitive_path_threshold'] ?? 3 );
            $window    = (int) ( $this->settings['sensitive_path_window'] ?? 600 );
            $ban_days  = (int) ( $this->settings['ban_duration'] ?? 0 );
            $this->sensitive_path_detector = new SensitivePathDetector( $threshold, $window, $ban_days );
        }

        if ( ! empty( $this->settings['rate_limit_enabled'] ) || $this->sensitive_path_detector !== null ) {
            $this->rate_limiter = new RateLimiter( $this->settings );
        }

        $this->maybe_ensure_runtime_assets_current();

        if ( ! class_exists( 'SiteFort\\Modules\\Firewall\\API\\FirewallApi' ) ) {
            require_once __DIR__ . '/API/FirewallApi.php';
        }
        $this->api = new API\FirewallApi();

        add_action( 'sitefort_firewall_block', static function ( $data ) {
            $ip   = $data['ip'] ?? '';
            $type = $data['attack_type'] ?? '';

            if ( $type === '404 Flood' ) {
                ReportBuffer::add( (string) $ip, '404_flood' );
            } elseif ( $type === 'Sensitive Path' ) {
                ReportBuffer::add( (string) $ip, 'sensitive_path' );
            } elseif ( $type === 'XML-RPC' ) {
                ReportBuffer::add( (string) $ip, 'xmlrpc' );
            }
        } );

        add_action( 'sitefort_firewall_block', [ FirewallLogger::class, 'log' ], 5, 1 );
        add_action( 'sitefort_firewall_lockout_changed', [ $this, 'on_firewall_lockout_changed' ], 10, 1 );
        add_filter( 'robots_txt', [ $this, 'filter_robots_txt' ], 10, 1 );

        // Enforce immediately after direct firewall hooks are attached; queued loader
        // hooks are registered later when the module loader finishes initialization.
        $this->run_firewall();

        $loader->add_action( 'admin_init', $this, 'refresh_admin_whitelist', 0 );
        $loader->add_action( 'admin_init', $this, 'refresh_ip_readiness_cache', 1 );
        $loader->add_action( 'admin_init', $this, 'ingest_waf_log', 2 );
        $loader->add_action( 'admin_init', $this, 'reconcile_background_tasks', 3 );
        $loader->add_action( 'rest_api_init', $this->api, 'register_routes' );
        $loader->add_action( 'wp_ajax_sitefort_check_ip', $this, 'handle_check_ip' );
        $loader->add_action( 'wp_ajax_sitefort_auto_configure_ip', $this, 'handle_auto_configure_ip' );

        // Count probes before redirect/SEO plugins reshape the 404; throttle after.
        if ( $this->rate_limiter ) {
            $loader->add_action( 'template_redirect', $this, 'handle_404_request', 0 );
            if ( ! empty( $this->settings['rate_limit_enabled'] ) ) {
                $loader->add_action( 'template_redirect', $this, 'enforce_404_flood_limit', 9999 );
            }
            $loader->add_action( 'sitefort_rate_limit_cleanup', $this->rate_limiter, 'cleanup' );
        }

        $loader->add_action( self::LOCKOUT_CLEANUP_HOOK, $this, 'handle_lockout_cleanup' );

        if ( ! empty( $this->settings['blocklist_enabled'] ) ) {
            $loader->add_action( BlocklistService::CRON_HOOK, $this, 'handle_blocklist_sync' );
        }
        $loader->add_action( 'sitefort_settings_saved_firewall', null, [ BlocklistService::class, 'flush_cache' ] );
        $loader->add_action( 'sitefort_settings_saved_firewall', $this, 'on_firewall_settings_saved', 10, 1 );
        $loader->add_action( 'sitefort_settings_saved_hardening', $this, 'on_hardening_settings_saved', 10, 1 );
        $loader->add_action( 'upgrader_process_complete', null, [ self::class, 'handle_upgrader_process_complete' ], 5, 2 );

        $loader->add_action( 'sitefort_geoip_update', $this, 'handle_geoip_update' );
        $loader->add_action( SafeNetworkRanges::CRON_HOOK, $this, 'handle_safe_network_ranges_update' );
        $loader->add_action( SafeNetworkRanges::PRE_SAFE_NETWORK_CRON_HOOK, $this, 'handle_pre_safe_network_ranges_update' );

        add_action( 'sitefort_lockout_activated', [ $this, 'on_lockout_activated' ], 10, 2 );
        add_action( 'sitefort_admin_recovery_ip_added', [ $this, 'on_admin_recovery_ip_added' ], 10, 0 );

        $loader->add_action( 'sitefort_firewall_log_cleanup', null, [ FirewallLogger::class, 'cleanup' ] );
        $loader->add_action( self::WAF_LOG_INGEST_HOOK, null, [ FirewallLogger::class, 'ingest_waf_log' ] );

        if ( $this->should_reconcile_background_tasks_now() ) {
            $this->reconcile_background_tasks();
        }

	}

    /**
     * Early 404 pass: count scanner signals without touching the response.
     */
    public function handle_404_request(): void {
        if ( ! $this->origin_enforce || FirewallBypass::should_bypass() ) {
            return;
        }

        if ( ! is_404() && http_response_code() !== 404 ) {
            return;
        }

        if ( is_user_logged_in() ) {
            return;
        }

        $path  = SensitivePathDetector::current_request_path();
        $class = SensitivePathDetector::classify_404_path( $path );
        if ( $class === SensitivePathDetector::CLASS_ASSET ) {
            return;
        }

        $ip         = $this->get_visitor_ip();
        $user_agent = (string) IpDetector::get_server_value( 'HTTP_USER_AGENT' );

        if ( $this->should_eager_verify_search_claims() ) {
            $search_trust_context = $this->get_search_trust_context( $ip, $user_agent );
            if ( ! empty( $search_trust_context['exception'] ) ) {
                return;
            }
        }

        if ( $this->is_safe_service_ip( $ip ) ) {
            return;
        }

        if ( RuleEngine::find_ip_match( $ip, $this->ip_blocking->get_allowlisted_entries() ) !== null ) {
            return;
        }

        $trusted_user_agents = RuleEngine::normalize_user_agent_patterns( $this->settings['trusted_user_agents'] ?? '' );
        if ( RuleEngine::find_user_agent_pattern_match( $user_agent, $trusted_user_agents ) !== null ) {
            return;
        }

        if ( $class === SensitivePathDetector::CLASS_STRONG && $this->sensitive_path_detector ) {
            $this->sensitive_path_detector->record_probe_404( $path );
        }

        if ( ! empty( $this->settings['rate_limit_enabled'] ) ) {
            // Recognized, policy-allowed crawlers skip the weak-404 probe counter.
            $bot_class = RuleEngine::classify_user_agent( $user_agent, [ 'trusted_user_agents' => $trusted_user_agents ] );
            $recognized_crawler = RuleEngine::is_recognized_crawler_class( $bot_class )
                && RuleEngine::get_bot_policy_source( $this->get_bot_policy_settings(), $bot_class ) === null;

            if ( $class === SensitivePathDetector::CLASS_STRONG || ( $class !== null && ! $recognized_crawler ) ) {
                $this->rate_limiter->record_404_probe( $ip );
            }
            $this->pending_404_flood_ip = $ip;
        }
    }

    /**
     * Late 404 pass: throttle only if no redirect, 410, or output claimed the request.
     */
    public function enforce_404_flood_limit(): void {
        $ip = $this->pending_404_flood_ip;
        if ( $ip === null ) {
            return;
        }
        $this->pending_404_flood_ip = null;

        if ( ! is_404() || headers_sent() || http_response_code() !== 404 ) {
            return;
        }

        $this->rate_limiter->enforce_404_flood(
            $ip,
            fn(): bool => $this->is_lazy_verified_search_request( $ip )
        );
    }

    /**
     * Handle scheduled GeoIP database update.
     */
    public function handle_geoip_update(): void {
        $account_id  = $this->settings['geoip_account_id'] ?? '';
        $license_key = $this->settings['geoip_license_key'] ?? '';
        if ( empty( $account_id ) || empty( $license_key ) ) {
            return;
        }

        $downloader = new GeoIP\MaxMindDownloader();
        if ( $downloader->needs_update() ) {
            $downloader->download( (string) $account_id, (string) $license_key );
        }
    }

    public function filter_robots_txt( $output ): string {
        return AiTrainingRobots::append_directives(
            (string) $output,
            ! empty( $this->settings['block_ai_training_crawlers'] )
        );
    }

    public function handle_safe_network_ranges_update(): void {
        if ( ! $this->acquire_safe_network_ranges_sync_lock() ) {
            return;
        }

        try {
            $downloader = new SafeNetworkRangeDownloader();
            $attempted_download = false;

            if ( $downloader->needs_update() ) {
                $attempted_download = true;
                $result = $downloader->download();
                if ( is_wp_error( $result ) ) {
                    SafeNetworkRanges::record_update_failure( $result );
                    return;
                }
            }

            if ( ! ExtendedProtection\ConfigWriter::write() ) {
                SafeNetworkRanges::record_update_failure(
                    new \WP_Error(
                        'sitefort_safe_network_ranges_config_write_failed',
                        'Safe network ranges were refreshed, but the WAF runtime configuration could not be rebuilt.'
                    )
                );
                return;
            }

            $status = SafeNetworkRanges::get_update_status();
            if (
                $attempted_download
                || (string) ( $status['last_error_code'] ?? '' ) === 'sitefort_safe_network_ranges_config_write_failed'
            ) {
                SafeNetworkRanges::record_update_success();
            }
        } finally {
            delete_option( self::SAFE_NETWORK_SYNC_LOCK_OPTION );
        }
    }

    public function handle_pre_safe_network_ranges_update(): void {
        wp_clear_scheduled_hook( SafeNetworkRanges::PRE_SAFE_NETWORK_CRON_HOOK );
        $this->reconcile_safe_network_ranges_schedule();
        $this->handle_safe_network_ranges_update();
    }

    public function handle_lockout_cleanup(): void {
        ( new LockoutManager() )->purge_expired();
    }

    public function handle_blocklist_sync(): void {
        $service = $this->get_blocklist_service();
        if ( $service ) {
            $service->sync();
        }
    }

    public function ingest_waf_log(): void {
        FirewallLogger::ingest_waf_log();
    }

    public function handle_auto_configure_ip() {
        check_ajax_referer( 'sitefort_nonce', '_ajax_nonce' );
        if ( ! Capabilities::can_manage_plugin() ) {
            wp_send_json_error( [ 'message' => 'Unauthorized' ] );
            return;
        }

        $config = IpDetector::detect_best_configuration();

        $recommended = $config['recommended'] ?? [];
        $mode   = $config['recommended_mode'] ?? ( ( $config['has_proxy'] ?? false ) ? 'manual' : 'disabled' );
        $header = 'manual' === $mode ? ( $recommended['header'] ?? '' ) : '';
        $ip     = $recommended['ip'] ?? '';

        $config['mode']   = $mode;
        $config['header'] = $header;
        $config['ip']     = $ip;

        wp_send_json_success( $config );
    }

    public function handle_check_ip() {
        check_ajax_referer( 'sitefort_nonce', '_ajax_nonce' );
        if ( ! Capabilities::can_manage_plugin() ) {
            wp_send_json_error( [ 'message' => 'Unauthorized' ] );
            return;
        }

        IpDetector::reset_cache();
        $ip = IpDetector::get_ip( true );

        wp_send_json_success( [ 'ip' => $ip ] );
    }

    /**
     * Refresh the admin IP auto-whitelist.
     *
     * Hooked to admin_init at priority 0 so it runs on every admin page load.
     * This is the primary lockout prevention mechanism.
     */
    public function refresh_admin_whitelist(): void {
        FirewallBypass::refresh_admin_whitelist();
    }

    /**
     * Refresh cached IP readiness in a real admin request and schedule a probe if stale.
     */
    public function refresh_ip_readiness_cache(): void {
        $cached   = get_transient( IpDetector::READINESS_TRANSIENT );
        $prev_sev = is_array( $cached ) ? ( $cached['severity'] ?? '' ) : null;
        $readiness = null;
        $stale    = ! is_array( $cached )
            || ( time() - (int) ( $cached['cached_at'] ?? 0 ) ) > DAY_IN_SECONDS;

        if ( $stale ) {
            $readiness = IpDetector::refresh_readiness();

            $fresh     = get_transient( IpDetector::READINESS_TRANSIENT );
            $fresh_sev = is_array( $fresh ) ? ( $fresh['severity'] ?? '' ) : '';
            if ( $prev_sev !== null && $prev_sev !== $fresh_sev ) {
                StatePush::mark_firewall_dirty();
            }
        }

        if ( ! class_exists( 'SiteFort\\Includes\\IpProbe' ) ) {
            require_once dirname( __DIR__, 2 ) . '/includes/IpProbe.php';
        }
        IpProbe::maybe_schedule( is_array( $readiness ) ? $readiness : null );
    }

    public function run_firewall() {
        // Enforce network-wide lockouts before site-level runtime checks.
        if ( is_multisite() && ! FirewallBypass::should_bypass() ) {
            $network = get_site_option( 'sitefort_network_settings', [] );
            if ( ! empty( $network['network_lockout'] ) ) {
                $this->block_network_lockout();
                return;
            }
        }

        if ( empty( $this->settings['enabled'] ) ) {
            return;
        }

        $origin_state = FirewallConfigService::resolve_runtime_mode_from_cached_state( $this->settings );
        $this->settings = $origin_state['settings'];
        $this->origin_enforce = ! empty( $origin_state['enforce'] );

        // Respect global bypass conditions before evaluating firewall rules.
        if ( FirewallBypass::should_bypass() ) {
            return;
        }

        if ( empty( $origin_state['enforce'] ) ) {
            return;
        }

        $ip = $this->get_visitor_ip();
        $user_agent = (string) IpDetector::get_server_value( 'HTTP_USER_AGENT' );
        // Strict verifies eagerly to block proven spoofs; other profiles verify lazily.
        $search_trust_context = $this->should_eager_verify_search_claims()
            ? $this->get_search_trust_context( $ip, $user_agent )
            : null;
        $verified_search_exception = $search_trust_context !== null && ! empty( $search_trust_context['exception'] );
        $spoofed_search_claim = $search_trust_context !== null
            && ( $search_trust_context['status'] ?? '' ) === SearchTrustDnsVerifier::TRUST_SPOOFED;
        $safe_service_exception = $this->is_safe_service_ip( $ip );
        if ( $this->is_pre_wordpress_waf_active() ) {
            $runtime_policy_bypassed = $this->is_runtime_policy_bypassed( $ip, $user_agent );
            $runtime_country = null;
            $runtime_community_blocked = false;

            if ( $spoofed_search_claim && ! $runtime_policy_bypassed && ! $safe_service_exception ) {
                $this->block_spoofed_crawler();
                return;
            }

            // Keep runtime country/community enforcement as a parity fallback in case
            // the bootstrap policy is stale or only partially applied for this request.
            if ( $this->should_runtime_check_country_block() && ! $runtime_policy_bypassed && ! $verified_search_exception && ! $safe_service_exception ) {
                $runtime_country = $this->get_runtime_country_result( $ip );
            }

            if (
                $this->should_runtime_check_community_blocklist()
                && ! $runtime_policy_bypassed
                && ! $verified_search_exception
                && ! $safe_service_exception
            ) {
                $runtime_community_blocked = $this->is_runtime_community_blocked( $ip );
            }

            $parity_blocked = $runtime_community_blocked
                || ( is_array( $runtime_country ) && ! empty( $runtime_country['blocked'] ) );
            if ( $parity_blocked && $this->is_lazy_verified_search_request( $ip, $user_agent ) ) {
                $runtime_country = null;
                $runtime_community_blocked = false;
            }

            if (
                is_array( $runtime_country )
                && ! empty( $runtime_country['blocked'] )
            ) {
                $this->ip_blocking->block_request(
                    $this->get_block_message_for_decision( [ 'source' => 'blocked_country' ] ),
                    $this->get_runtime_country_block_reason( $runtime_country )
                );
                return;
            }

            if ( $runtime_community_blocked ) {
                $this->ip_blocking->block_request(
                    $this->get_block_message_for_decision( [ 'source' => 'community_blocklist' ] ),
                    'Community Blocklist'
                );
                return;
            }

            if (
                $this->sensitive_path_detector
                && ! $runtime_policy_bypassed
                && $this->sensitive_path_detector->matches_request()
            ) {
                $this->enforce_sensitive_path( ! $verified_search_exception && ! $safe_service_exception );
            }

            return;
        }

        $policy = FirewallSettings::compile_policy_from_sanitized(
            $this->settings,
            [
                'allowlisted_ips' => $this->ip_blocking->get_allowlisted_entries(),
                'blocked_ips' => $this->ip_blocking->get_active_banned_entries(),
            ]
        );
        $decision = ( new RequestEvaluator(
            $policy,
            $this->get_blocklist_service(),
            $this->get_country_blocker(),
            $this->sensitive_path_detector
        ) )->evaluate(
            $ip,
            $user_agent,
            [
                'verified_search_exception' => $verified_search_exception,
                'verified_search_engine' => (string) ( $search_trust_context['engine'] ?? '' ),
                'is_api_request' => $this->is_api_request(),
            ]
        );

        if ( $decision['action'] === 'allow_bypass' || $decision['action'] === 'allow_safe_service' ) {
            return;
        }

        if ( $decision['source'] === 'sensitive_path' && $this->sensitive_path_detector ) {
            $this->enforce_sensitive_path( ! $verified_search_exception && ! $safe_service_exception );
            return;
        }

        if ( $decision['action'] === 'block' && ! $this->should_waive_block_for_verified_search( $decision, $ip, $user_agent ) ) {
            $message = $this->get_block_message_for_decision( $decision );
            $this->ip_blocking->block_request( $message, (string) ( $decision['reason'] ?? 'Access denied' ) );
            return;
        }

        if ( $spoofed_search_claim ) {
            $this->block_spoofed_crawler();
            return;
        }

        if ( $this->rate_limiter ) {
            $this->rate_limiter->check_request(
                $ip,
                $safe_service_exception,
                fn(): bool => $this->is_lazy_verified_search_request( $ip, $user_agent )
            );
        }

    }

    private function block_spoofed_crawler(): void {
        $this->ip_blocking->block_request(
            'This request was blocked due to suspicious activity.',
            'Spoofed Crawler'
        );
    }

    /**
     * @param array<string, mixed> $decision
     */
    private function should_waive_block_for_verified_search( array $decision, string $ip, string $user_agent ): bool {
        if ( ! in_array( (string) ( $decision['source'] ?? '' ), [ 'blocked_country', 'community_blocklist' ], true ) ) {
            return false;
        }

        return $this->is_lazy_verified_search_request( $ip, $user_agent );
    }

    /**
     * @param array<string, mixed> $decision
     */
    private function get_block_message_for_decision( array $decision ): string {
        $source = (string) ( $decision['source'] ?? '' );

        if ( $source === 'blocked_ip' ) {
            return 'Access for this IP address is temporarily restricted under this site\'s security policy. If you believe this is an error, contact the site administrator and provide the reference ID below.';
        }

        if ( $source === 'blocked_country' ) {
            return 'Access from your region is restricted by this site\'s security policy.';
        }

        return 'This request was blocked due to suspicious activity.';
    }

    /**
     * Terminate the request with 503 when a network-wide lockout is active.
     */
    private function block_network_lockout(): void {
        do_action( 'sitefort_network_lockout_block', [
            'site_url'    => home_url(),
            'ip'          => IpDetector::get_ip(),
            'request_uri' => IpDetector::get_server_value( 'REQUEST_URI' ),
        ] );
        if ( ! headers_sent() ) {
            http_response_code( 503 );
            header( 'Content-Type: text/html; charset=UTF-8' );
            header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0' );
        }
        require_once __DIR__ . '/block-page.php';
        sitefort_render_block_page( 503, 'Access is temporarily restricted while site-wide security controls are in effect.', IpDetector::get_ip() );
        exit;
    }

    /**
     * Get the visitor's IP address using the configured detection method.
     *
     * @return string The detected IP address.
     */
    private function get_visitor_ip(): string {
        return IpDetector::get_ip();
    }

    private function is_safe_service_ip( string $ip ): bool {
        return SafeNetworkRanges::is_ip_in_custom_safe_ranges( $ip );
    }

    // Admins legitimately fetch backups and dumps. Capabilities aren't resolved
    // this early, so defer to init when a login cookie is present and exempt
    // managers there; cookieless requests are blocked inline as before.
    private function enforce_sensitive_path( bool $countable ): void {
        if ( ! $this->sensitive_path_detector ) {
            return;
        }

        if ( ! FirewallBypass::has_wordpress_login_cookie() ) {
            $this->sensitive_path_detector->enforce_match( $countable );
            return;
        }

        $detector = $this->sensitive_path_detector;
        add_action(
            'init',
            static function () use ( $detector, $countable ): void {
                if ( Capabilities::can_manage_plugin() ) {
                    return;
                }

                if ( $detector->matches_request() ) {
                    $detector->enforce_match( $countable );
                }
            },
            0
        );
    }

    private function is_api_request(): bool {
        $request_uri = (string) IpDetector::get_server_value( 'REQUEST_URI' );
        if ( $request_uri === '' ) {
            return false;
        }

        $rest_prefix = function_exists( 'rest_get_url_prefix' )
            ? (string) rest_get_url_prefix()
            : 'wp-json';

        return RuleEngine::is_api_request_path(
            (string) ( wp_parse_url( $request_uri, PHP_URL_PATH ) ?? '' ),
            (string) ( wp_parse_url( $request_uri, PHP_URL_QUERY ) ?? '' ),
            $rest_prefix
        );
    }

    private function should_eager_verify_search_claims(): bool {
        return RuleEngine::should_block_spoofed_search_engine(
            ! empty( $this->settings['bot_policy_enabled'] ),
            (string) ( $this->settings['bot_policy_profile'] ?? '' )
        );
    }

    private function is_lazy_verified_search_request( string $ip, ?string $user_agent = null ): bool {
        if ( $user_agent === null ) {
            $user_agent = (string) IpDetector::get_server_value( 'HTTP_USER_AGENT' );
        }

        return $this->get_search_trust_runtime_resolver()->resolve_enforcement_status( $ip, $user_agent, true )
            === SearchTrustDnsVerifier::TRUST_VERIFIED;
    }

    /**
     * @return array<string, mixed>
     */
    private function get_bot_policy_settings(): array {
        return [
            'bot_policy_enabled' => ! empty( $this->settings['bot_policy_enabled'] ),
            'bot_policy_profile' => (string) ( $this->settings['bot_policy_profile'] ?? '' ),
            'block_ai_training_crawlers' => ! empty( $this->settings['block_ai_training_crawlers'] ),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function get_search_trust_context( string $ip, string $user_agent ): array {
        return $this->get_search_trust_runtime_resolver()->resolve_current_request(
            $ip,
            $user_agent,
            $this->is_pre_wordpress_waf_active(),
            $this->should_runtime_check_country_block(),
            $this->should_runtime_check_community_blocklist(),
            [
                'method' => IpDetector::get_server_value( 'REQUEST_METHOD' ),
                'request_uri' => IpDetector::get_server_value( 'REQUEST_URI' ),
            ]
        );
    }

    private function get_search_trust_runtime_resolver(): SearchTrustRuntimeResolver {
        if ( $this->search_trust_runtime_resolver === null ) {
            $this->search_trust_runtime_resolver = new SearchTrustRuntimeResolver( $this->settings );
        }

        return $this->search_trust_runtime_resolver;
    }

    /**
     * Keep runtime services aligned on every firewall settings mutation.
     *
     * @param array<string,mixed> $settings
     */
    public function on_firewall_settings_saved( array $settings ): void {
        $settings = FirewallSettings::sanitize( FirewallSettings::with_runtime_secrets( $settings ) );
        $this->settings = $settings;
        $this->trusted_user_agent_patterns = null;
        $this->search_trust_runtime_resolver = null;
        IpDetector::reset_cache();
        $blocklist_service = new BlocklistService( $settings );
        if ( ! empty( $settings['blocklist_enabled'] ) ) {
            $blocklist_service->schedule_sync();
        } else {
            $blocklist_service->unschedule_sync();
        }

        $this->reconcile_geoip_update_schedule( $settings );
        self::refresh_runtime_assets();
    }

    /**
     * @param array<string,mixed> $settings
     */
    public function on_hardening_settings_saved( array $settings ): void {
        unset( $settings );

        $this->search_trust_runtime_resolver = null;
        self::refresh_runtime_assets();
    }

    public function on_admin_recovery_ip_added(): void {
        self::refresh_runtime_assets();
    }

    public function on_lockout_activated( string $identifier, string $type ): void {
        if ( $type === 'ip' ) {
            ReportBuffer::add( $identifier, 'failed_login' );
        }

        FirewallLogger::log( [
            'ip'          => $type === 'ip' ? $identifier : (string) IpDetector::get_ip(),
            'attack_type' => 'Login Lockout',
            'request_uri' => IpDetector::get_server_value( 'REQUEST_URI' ),
        ] );
    }

    public function reconcile_background_tasks(): void {
        if ( $this->background_tasks_reconciled ) {
            return;
        }

        $this->background_tasks_reconciled = true;

        if ( ! empty( $this->settings['blocklist_enabled'] ) ) {
            ( new BlocklistService( $this->settings ) )->schedule_sync();
        } else {
            ( new BlocklistService( $this->settings ) )->unschedule_sync();
        }

        if ( $this->rate_limiter && ! wp_next_scheduled( 'sitefort_rate_limit_cleanup' ) ) {
            wp_schedule_event( time(), 'hourly', 'sitefort_rate_limit_cleanup' );
        }

        if ( ! wp_next_scheduled( self::LOCKOUT_CLEANUP_HOOK ) ) {
            wp_schedule_event( time() + MINUTE_IN_SECONDS, 'hourly', self::LOCKOUT_CLEANUP_HOOK );
        }

        if ( ! wp_next_scheduled( self::WAF_LOG_INGEST_HOOK ) ) {
            wp_schedule_event( time() + 5 * MINUTE_IN_SECONDS, 'hourly', self::WAF_LOG_INGEST_HOOK );
        }

        if ( ! wp_next_scheduled( 'sitefort_firewall_log_cleanup' ) ) {
            wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'sitefort_firewall_log_cleanup' );
        }

        $this->reconcile_geoip_update_schedule( $this->settings );
        $this->reconcile_safe_network_ranges_schedule();
        set_transient( self::BACKGROUND_RECONCILE_TRANSIENT, 1, self::BACKGROUND_RECONCILE_TTL );
    }

    private function reconcile_safe_network_ranges_schedule(): void {
        if ( wp_next_scheduled( SafeNetworkRanges::PRE_SAFE_NETWORK_CRON_HOOK ) ) {
            wp_clear_scheduled_hook( SafeNetworkRanges::PRE_SAFE_NETWORK_CRON_HOOK );
        }

        if ( ! wp_next_scheduled( SafeNetworkRanges::CRON_HOOK ) ) {
            wp_schedule_event( time() + 2 * MINUTE_IN_SECONDS, 'daily', SafeNetworkRanges::CRON_HOOK );
        }
    }

    private function acquire_safe_network_ranges_sync_lock(): bool {
        $now     = time();
        $expires = $now + self::SAFE_NETWORK_SYNC_LOCK_TTL;

        if ( add_option( self::SAFE_NETWORK_SYNC_LOCK_OPTION, $expires, '', false ) ) {
            return true;
        }

        $current = (int) get_option( self::SAFE_NETWORK_SYNC_LOCK_OPTION, 0 );
        if ( $current > $now ) {
            return false;
        }

        delete_option( self::SAFE_NETWORK_SYNC_LOCK_OPTION );
        return add_option( self::SAFE_NETWORK_SYNC_LOCK_OPTION, $expires, '', false );
    }

    /**
     * Schedule automatic MaxMind refreshes only when both required credentials exist.
     *
     * @param array<string,mixed> $settings
     */
    private function reconcile_geoip_update_schedule( array $settings ): void {
        $has_credentials = ! empty( $settings['geoip_account_id'] ) && ! empty( $settings['geoip_license_key'] );
        $scheduled       = wp_next_scheduled( 'sitefort_geoip_update' );

        if ( $has_credentials && ! $scheduled ) {
            wp_schedule_event( time(), 'weekly', 'sitefort_geoip_update' );
            return;
        }

        if ( ! $has_credentials && $scheduled ) {
            wp_clear_scheduled_hook( 'sitefort_geoip_update' );
        }
    }

    /**
     * Keep pre-WordPress WAF config aligned with runtime lockout changes.
     *
     * @param array<string,mixed> $payload
     */
    public function on_firewall_lockout_changed( array $payload ): void {
        $type = (string) ( $payload['type'] ?? '' );

        if ( $type !== 'ban' && $type !== 'allow' ) {
            return;
        }

        self::refresh_runtime_assets();
    }

    private function is_pre_wordpress_waf_active(): bool {
        if ( ! defined( 'SITEFORT_WAF_POLICY_APPLIED' ) || ! SITEFORT_WAF_POLICY_APPLIED ) {
            return false;
        }

        if ( ! defined( 'SITEFORT_WAF_POLICY_SIGNATURE' ) ) {
            return false;
        }

        $expected_signature = (string) get_option( 'sitefort_waf_policy_signature', '' );
        $runtime_signature  = (string) SITEFORT_WAF_POLICY_SIGNATURE;

        return $expected_signature !== '' && hash_equals( $expected_signature, $runtime_signature );
    }

    private function is_runtime_policy_bypassed( string $ip, string $user_agent ): bool {
        return RuleEngine::find_ip_match( $ip, $this->ip_blocking->get_allowlisted_entries() ) !== null
            || RuleEngine::find_user_agent_pattern_match( $user_agent, $this->get_trusted_user_agent_patterns() ) !== null;
    }

    private function should_runtime_check_community_blocklist(): bool {
        return ! empty( $this->settings['blocklist_enabled'] )
            && ( ! defined( 'SITEFORT_WAF_COMMUNITY_BLOCKLIST_APPLIED' ) || ! SITEFORT_WAF_COMMUNITY_BLOCKLIST_APPLIED );
    }

    private function should_runtime_check_country_block(): bool {
        return ! empty( $this->settings['geoip_enabled'] )
            && ! empty( RuleEngine::normalize_country_codes( $this->settings['geoip_countries'] ?? '' ) )
            && ( ! defined( 'SITEFORT_WAF_COUNTRY_POLICY_APPLIED' ) || ! SITEFORT_WAF_COUNTRY_POLICY_APPLIED );
    }

    private function is_runtime_community_blocked( string $ip ): bool {
        $service = $this->get_blocklist_service();
        return $service ? $service->is_blocked( $ip ) : false;
    }

    /**
     * @return array<string, mixed>|null
     */
    private function get_runtime_country_result( string $ip ): ?array {
        $blocker = $this->get_country_blocker();
        if ( ! $blocker ) {
            return null;
        }

        return $blocker->should_block( $ip );
    }

    /**
     * @param array<string, mixed>|null $result
     */
    private function get_runtime_country_block_reason( ?array $result ): string {
        if ( ! is_array( $result ) ) {
            return 'Country Block: Unknown Country';
        }

        $country_code = strtoupper( trim( (string) ( $result['country_code'] ?? '' ) ) );
        if ( $country_code !== '' ) {
            return 'Country Block: ' . $country_code;
        }

        return 'Country Block: Unknown Country';
    }

    private function get_trusted_user_agent_patterns(): array {
        if ( $this->trusted_user_agent_patterns !== null ) {
            return $this->trusted_user_agent_patterns;
        }

        $this->trusted_user_agent_patterns = RuleEngine::normalize_user_agent_patterns(
            (string) ( $this->settings['trusted_user_agents'] ?? '' )
        );

        return $this->trusted_user_agent_patterns;
    }

    private function get_blocklist_service(): ?BlocklistService {
        if ( empty( $this->settings['blocklist_enabled'] ) ) {
            return null;
        }

        if ( $this->blocklist_service === null ) {
            $this->blocklist_service = new BlocklistService( $this->settings );
        }

        return $this->blocklist_service;
    }

    private function get_country_blocker(): ?CountryBlocker {
        if ( empty( $this->settings['geoip_enabled'] ) ) {
            return null;
        }

        if ( $this->country_blocker === null ) {
            if ( $this->geoip_service === null ) {
                $this->geoip_service = new GeoIPService();
            }

            $this->country_blocker = new CountryBlocker( $this->settings, $this->geoip_service );
        }

        return $this->country_blocker;
    }

    private function maybe_ensure_runtime_assets_current(): void {
        if ( ! \SiteFort\Includes\RequestContext::is_privileged() ) {
            $marker = get_transient( self::RUNTIME_ASSETS_RECONCILE_TRANSIENT );
            if ( is_string( $marker ) && $marker === self::runtime_assets_version() ) {
                return;
            }
            if ( get_transient( self::RUNTIME_ASSETS_FAILURE_TRANSIENT ) ) {
                return;
            }
        }

        if ( ExtendedProtectionConfigCoordinator::ensure_runtime_assets_current() ) {
            set_transient(
                self::RUNTIME_ASSETS_RECONCILE_TRANSIENT,
                self::runtime_assets_version(),
                self::BACKGROUND_RECONCILE_TTL
            );
            delete_transient( self::RUNTIME_ASSETS_FAILURE_TRANSIENT );
            return;
        }

        if ( ! \SiteFort\Includes\RequestContext::is_privileged() ) {
            set_transient( self::RUNTIME_ASSETS_FAILURE_TRANSIENT, 1, self::RUNTIME_ASSETS_FAILURE_TTL );
        }
    }

    public static function invalidate_runtime_assets_reconcile_cache(): void {
        delete_transient( self::RUNTIME_ASSETS_RECONCILE_TRANSIENT );
        delete_transient( self::RUNTIME_ASSETS_FAILURE_TRANSIENT );
    }

    private static function refresh_runtime_assets(): void {
        self::invalidate_runtime_assets_reconcile_cache();
        ExtendedProtectionConfigCoordinator::ensure_runtime_assets_current();
    }

    /**
     * @param mixed               $upgrader   WordPress upgrader instance.
     * @param array<string,mixed> $hook_extra Upgrade details.
     */
    public static function handle_upgrader_process_complete( $upgrader, array $hook_extra ): void {
        unset( $upgrader );

        if ( ( $hook_extra['action'] ?? '' ) !== 'update' || ( $hook_extra['type'] ?? '' ) !== 'plugin' ) {
            return;
        }

        $plugins = is_array( $hook_extra['plugins'] ?? null ) ? $hook_extra['plugins'] : [];
        if ( is_string( $hook_extra['plugin'] ?? null ) ) {
            $plugins[] = $hook_extra['plugin'];
        }

        $plugin_file = defined( 'SITEFORT_PLUGIN_FILE' ) ? plugin_basename( SITEFORT_PLUGIN_FILE ) : '';
        if ( $plugin_file !== '' && in_array( $plugin_file, $plugins, true ) ) {
            self::invalidate_runtime_assets_reconcile_cache();
        }
    }

    private static function runtime_assets_version(): string {
        return defined( 'SITEFORT_VERSION' ) ? (string) SITEFORT_VERSION : 'unknown';
    }

    private function should_reconcile_background_tasks_now(): bool {
        if ( \SiteFort\Includes\RequestContext::is_privileged() ) {
            return true;
        }

        return ! get_transient( self::BACKGROUND_RECONCILE_TRANSIENT );
    }
}
