<?php

declare(strict_types=1);

namespace SecurWP\Modules\Firewall;

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

use SecurWP\Includes\Interfaces\Module;
use SecurWP\Includes\Capabilities;
use SecurWP\Includes\IpDetector;
use SecurWP\Modules\Firewall\RateLimiter\RateLimiter;
use SecurWP\Modules\Firewall\Blocklist\BlocklistService;
use SecurWP\Modules\Firewall\GeoIP\GeoIPService;
use SecurWP\Modules\Firewall\GeoIP\CountryBlocker;
use SecurWP\Modules\Firewall\FirewallBypass;
use SecurWP\Modules\Firewall\SensitivePathDetector;
use SecurWP\Modules\Firewall\Blocklist\ReportBuffer;
use SecurWP\Modules\Firewall\ExtendedProtection\ConfigCoordinator as ExtendedProtectionConfigCoordinator;
use SecurWP\Modules\Firewall\FirewallLogger;
use SecurWP\Includes\IpProbe;
use SecurWP\Modules\Cloud\Services\StatePush;
use SecurWP\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 = 'securwp_firewall_lockout_cleanup';
	private const WAF_LOG_INGEST_HOOK = 'securwp_firewall_waf_log_ingest';
	private const BACKGROUND_RECONCILE_TRANSIENT = 'securwp_firewall_bg_reconcile';
	private const BACKGROUND_RECONCILE_TTL = 21600;

	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;

    /**
     * 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( 'securwp_network_settings', [] );
            if ( ! empty( $network['enforce_firewall'] ) ) {
                $this->settings['enabled'] = true;
            }
        }
    }

    public function initialize( \SecurWP\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'] ?? 300 );
            $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 );
        }

        ExtendedProtectionConfigCoordinator::ensure_runtime_assets_current();

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

        add_action( 'securwp_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( 'securwp_firewall_block', [ FirewallLogger::class, 'log' ], 5, 1 );
        add_action( 'securwp_firewall_lockout_changed', [ $this, 'on_firewall_lockout_changed' ], 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_securwp_check_ip', $this, 'handle_check_ip' );
        $loader->add_action( 'wp_ajax_securwp_auto_configure_ip', $this, 'handle_auto_configure_ip' );

        if ( ! empty( $this->settings['rate_limit_enabled'] ) && $this->rate_limiter ) {
            $loader->add_action( 'template_redirect', $this, 'handle_404_rate_limit' );
        }

        if ( $this->rate_limiter ) {
            $loader->add_action( 'securwp_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( 'securwp_settings_saved_firewall', null, [ BlocklistService::class, 'flush_cache' ] );
        $loader->add_action( 'securwp_settings_saved_firewall', $this, 'on_firewall_settings_saved', 10, 1 );
        $loader->add_action( 'securwp_settings_saved_hardening', $this, 'on_hardening_settings_saved', 10, 1 );

        $loader->add_action( 'securwp_geoip_update', $this, 'handle_geoip_update' );

        add_action( 'securwp_lockout_activated', [ $this, 'on_lockout_activated' ], 10, 2 );

        $loader->add_action( 'securwp_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();
        }

	}

    /**
     * Handle 404 rate limiting on template_redirect.
     */
    public function handle_404_rate_limit(): void {
        if ( $this->rate_limiter ) {
            $ip         = $this->get_visitor_ip();
            $user_agent = IpDetector::get_server_value( 'HTTP_USER_AGENT' );
            $search_trust_context = $this->get_search_trust_context( $ip, $user_agent );
            $this->rate_limiter->record_404( $ip, ! empty( $search_trust_context['exception'] ) );
        }
    }

    /**
     * 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 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( 'securwp_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( 'securwp_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( 'SecurWP\\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( 'securwp_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'];

        // 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 = IpDetector::get_server_value( 'HTTP_USER_AGENT' );
        $search_trust_context = $this->get_search_trust_context( $ip, $user_agent );
        $verified_search_exception = ! empty( $search_trust_context['exception'] );
        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;

            // 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 ) {
                $runtime_country = $this->get_runtime_country_result( $ip );
            }

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

            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->sensitive_path_detector->enforce_match();
            }

            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'] ?? '' ),
            ]
        );

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

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

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

        if ( $this->rate_limiter ) {
            $this->rate_limiter->check_request( $ip, $verified_search_exception );
        }

    }

    /**
     * @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( 'securwp_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';
        securwp_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();
    }

    /**
     * @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( $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 );
        ExtendedProtectionConfigCoordinator::refresh_if_installed();
    }

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

        $this->search_trust_runtime_resolver = null;
        ExtendedProtectionConfigCoordinator::refresh_if_installed();
    }

    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( 'securwp_rate_limit_cleanup' ) ) {
            wp_schedule_event( time(), 'hourly', 'securwp_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( 'securwp_firewall_log_cleanup' ) ) {
            wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'securwp_firewall_log_cleanup' );
        }

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

    /**
     * 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( 'securwp_geoip_update' );

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

        if ( ! $has_credentials && $scheduled ) {
            wp_clear_scheduled_hook( 'securwp_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;
        }

        ExtendedProtectionConfigCoordinator::refresh_if_installed();
    }

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

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

        $expected_signature = (string) get_option( 'securwp_waf_policy_signature', '' );
        $runtime_signature  = (string) SECURWP_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( 'SECURWP_WAF_COMMUNITY_BLOCKLIST_APPLIED' ) || ! SECURWP_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( 'SECURWP_WAF_COUNTRY_POLICY_APPLIED' ) || ! SECURWP_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 should_reconcile_background_tasks_now(): bool {
        if ( defined( 'WP_CLI' ) && WP_CLI ) {
            return true;
        }

        if ( function_exists( 'wp_doing_cron' ) && wp_doing_cron() ) {
            return true;
        }

        if ( is_admin() ) {
            return true;
        }

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