<?php
/**
 * Utility functions for BotFend Anti-Bot Firewall
 *
 * @package BotFend\Utilities
 * @since 1.0.0
 * @version 5.1.0 - Integrated TrustedServiceVerifier, Automattic verification
 * @version 5.0.0 - Integrated hybrid IPValidator
 * @version 4.8.3 - Added geolocation opt-in check for WordPress.org compliance
 */

namespace BotFend\Utilities;

use BotFend\Core\Config;
use BotFend\Database\Database;
use BotFend\Security\TrustedServiceVerifier;

defined('ABSPATH') || exit;

/**
 * BotFend Utilities Class
 *
 * Handles IP verification, crawler detection, and utility functions
 * with complete crawler verification to prevent impersonation.
 * Integrates TrustedServiceVerifier for enterprise-grade infrastructure verification.
 */
class Utilities {
    
    /**
     * Config instance
     *
     * @var Config
     */
    private $config;
    
    /**
     * IP Validator instance (hybrid version)
     *
     * @var IPValidator
     */
    private $ip_validator;
    
    /**
     * Trusted Service Verifier instance
     *
     * @var TrustedServiceVerifier
     */
    private $trusted_service_verifier;
    
    /**
     * Database instance (optional)
     *
     * @var Database|null
     */
    private $database;
    
    /**
     * Request ID for tracing
     *
     * @var string
     */
    private $request_id;
    
    /**
     * Cache for parsed whitelist (performance optimization)
     *
     * @var array|null
     */
    private static $parsed_whitelist_cache = null;
    
    /**
     * Cache for parsed permanent blocklist (performance optimization)
     *
     * @var array|null
     */
    private static $parsed_blocklist_cache = null;
    
    /**
     * Cache for high-risk country map (O(1) lookups)
     *
     * @var array|null
     */
    private static $high_risk_country_map = null;
    
    /**
     * Cache for reverse DNS lookups (prevents DNS exhaustion)
     *
     * @var array
     */
    private static $rdns_cache = [];
    
    /**
     * Google IP ranges (IPv4 + aggregated IPv6)
     *
     * @var array
     */
    const GOOGLE_IPS = [
        '8.8.8.0/24',
        '8.8.4.0/24',
        '64.18.0.0/20',
        '64.233.160.0/19',
        '66.102.0.0/20',
        '66.249.64.0/19',
        '72.14.192.0/18',
        '74.125.0.0/16',
        '108.177.8.0/21',
        '142.250.0.0/15',
        '172.217.0.0/16',
        '173.194.0.0/16',
        '207.126.144.0/20',
        '209.85.128.0/17',
        '216.58.192.0/19',
        '216.239.32.0/19',
        '104.154.0.0/15',
        '104.196.0.0/14',
        '107.167.160.0/19',
        '107.178.192.0/18',
        '108.170.192.0/18',
        '108.177.0.0/17',
        '130.211.0.0/16',
        '136.112.0.0/12',
        '146.148.0.0/17',
        '162.216.148.0/22',
        '162.222.176.0/21',
        '172.110.32.0/21',
        '172.253.0.0/16',
        '173.255.112.0/20',
        '192.158.28.0/22',
        '192.178.0.0/15',
        '199.192.112.0/22',
        '199.223.232.0/21',
        '2001:4860:4801::/48',
        '2607:f8b0:4000::/48',
    ];
    
    /**
     * Microsoft/Bing IP ranges
     *
     * @var array
     */
    const BING_IPS = [
        '13.64.0.0/11',
        '13.96.0.0/13',
        '13.104.0.0/14',
        '20.190.128.0/18',
        '40.74.0.0/15',
        '40.76.0.0/14',
        '40.80.0.0/12',
        '40.112.0.0/13',
        '40.120.0.0/14',
        '52.160.0.0/12',
        '65.52.0.0/14',
        '104.44.0.0/15',
        '104.47.0.0/17',
        '131.253.21.0/24',
        '131.253.22.0/23',
        '131.253.24.0/21',
        '131.253.32.0/20',
        '157.54.0.0/15',
        '157.56.0.0/14',
        '157.60.0.0/16',
        '168.62.0.0/15',
        '199.30.16.0/20',
        '207.46.0.0/16',
        '207.68.128.0/18',
    ];
    
    /**
     * Facebook IP ranges
     *
     * @var array
     */
    const FACEBOOK_IPS = [
        '2a03:2880::/32',
        '2a03:2880:f800::/48',
        '2620:0:1c00::/40',
        '2401:db00::/32',
        '31.13.0.0/16',
        '31.13.24.0/21',
        '31.13.64.0/18',
        '54.39.6.0/24',
        '66.220.0.0/16',
        '69.63.0.0/16',
        '69.171.0.0/16',
        '74.119.76.0/22',
        '103.4.96.0/22',
        '129.134.0.0/16',
        '157.240.0.0/16',
        '173.252.0.0/16',
        '179.60.192.0/22',
        '185.60.216.0/22',
        '204.15.20.0/22',
    ];
    
    /**
     * Yandex IP ranges
     *
     * @var array
     */
    const YANDEX_IPS = [
        '5.45.192.0/18',
        '37.9.64.0/18',
        '77.88.0.0/18',
        '87.250.224.0/19',
        '93.158.128.0/18',
        '95.108.128.0/17',
        '100.43.64.0/19',
        '141.8.128.0/18',
        '178.154.128.0/17',
        '199.21.96.0/22',
        '213.180.193.0/24',
        '213.180.204.0/24',
    ];
    
    /**
     * Apple IP ranges
     *
     * @var array
     */
    const APPLE_IPS = [
        '17.0.0.0/8',
        '100.64.0.0/10',
    ];
    
    /**
     * Pinterest IP ranges (AWS)
     *
     * @var array
     */
    const PINTEREST_IPS = [
        '54.236.0.0/18',
        '52.0.0.0/8',
        '34.0.0.0/8',
    ];
    
    /**
     * Automattic / WordPress.com IP ranges
     *
     * @since 5.1.0
     * @var array
     */
    const AUTOMATTIC_IPS = [
        '192.0.64.0/18',
        '192.0.112.0/20',
        '192.0.96.0/20',
        '192.0.80.0/20',
        '198.181.116.0/22',
        '198.181.120.0/22',
    ];
    
    /**
     * Known crawler user agent strings
     *
     * @var array
     */
    const KNOWN_CRAWLERS = [
        'Googlebot',
        'Google-InspectionTool',
        'Mediapartners-Google',
        'AdsBot-Google',
        'Google-Safety',
        'Google Favicon',
        'Google-Read-Aloud',
        'Google-News',
        'Google-Site-Verification',
        'Google-AMPHTML',
        'Google-PageRenderer',
        'Google-Prober',
        'Google-Structured-Data-Testing-Tool',
        'Google-Youtube-Links',
        'Bingbot',
        'MSNBot',
        'BingPreview',
        'Slurp',
        'DuckDuckBot',
        'facebookexternalhit',
        'FacebookExternalHit',
        'meta-externalagent',
        'meta-externalagent/1.1',
        'Instagram',
        'Twitterbot',
        'LinkedInBot',
        'Pinterest',
        'Pinterestbot',
        'Pinterestbot/1.0',
        'Applebot',
        'WhatsApp',
        'TelegramBot',
        'Discordbot',
        'Slackbot',
        'YandexBot',
        'MojeekBot',
        'Baiduspider',
        'Sogou',
        'Exabot',
        'AhrefsBot',
        'SemrushBot',
        'MJ12bot',
        'DotBot',
    ];
    
    /**
     * Constructor
     *
     * @param Database|null $database Database instance (optional)
     */
    public function __construct(?Database $database = null) {
        $this->config = Config::get_instance();
        $this->ip_validator = new IPValidator($this->config);
        $this->trusted_service_verifier = new TrustedServiceVerifier($this, $this->config);
        $this->database = $database;
        $this->request_id = $this->generate_request_id();
    }
    
    /**
     * Set database instance (for dependency injection)
     *
     * @param Database $database
     * @return void
     */
    public function set_database(Database $database) {
        $this->database = $database;
    }
    
    /**
     * Get the Trusted Service Verifier instance
     *
     * @since 5.1.0
     * @return TrustedServiceVerifier
     */
    public function get_trusted_service_verifier(): TrustedServiceVerifier {
        return $this->trusted_service_verifier;
    }
    
    /**
     * Generate unique request ID for tracing
     *
     * @return string
     */
    private function generate_request_id() {
        return wp_generate_uuid4();
    }
    
    /**
     * Get user agent from server variable
     *
     * @return string User agent string.
     */
    public function get_user_agent() {
        return isset($_SERVER['HTTP_USER_AGENT']) ? 
               sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
    }
    
    /**
     * Hash an IP address for privacy-safe logging
     *
     * @param string $ip IP address
     * @return string Hashed IP
     */
    private function hash_ip($ip) {
        return hash('sha256', $ip . wp_salt('nonce'));
    }
    
    /**
     * Log an error with context
     *
     * @param string $message Error message.
     * @param array  $context Additional context data.
     * @param string $level   Log level (error, warning, info).
     * @return string Error key.
     */
    public function log_error($message, $context = [], $level = 'error') {
        $user_agent = $this->get_user_agent();
        $request_uri = isset($_SERVER['REQUEST_URI']) ? 
                       sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
        
        $ip = $this->get_client_ip();
        $ip_hash = $this->hash_ip($ip);
        
        $log_entry = [
            'timestamp'    => current_time('mysql'),
            'request_id'   => $this->request_id,
            'level'        => sanitize_text_field($level),
            'message'      => sanitize_text_field($message),
            'context'      => $this->sanitize_array($context),
            'ip_hash'      => $ip_hash,
            'user_agent'   => substr($user_agent, 0, 255),
            'request_uri'  => substr($request_uri, 0, 500),
        ];
        
        $error_key = 'botfend_error_' . md5(wp_json_encode($log_entry));
        $existing_errors = get_transient('botfend_recent_errors') ?: [];
        
        $existing_errors[] = $log_entry;
        if (count($existing_errors) > 50) {
            array_shift($existing_errors);
        }
        
        set_transient('botfend_recent_errors', $existing_errors, HOUR_IN_SECONDS);
        
        if (defined('WP_DEBUG') && WP_DEBUG && $this->config->get_bool('debug_mode', false)) {
            error_log('BotFend [' . strtoupper($level) . ']: ' . $message . ' - ' . wp_json_encode($context));
        }
        
        return $error_key;
    }
    
    /**
     * Recursively sanitize array values
     *
     * @param array $array Array to sanitize.
     * @return array Sanitized array.
     */
    private function sanitize_array($array) {
        foreach ($array as $key => $value) {
            if (is_array($value)) {
                $array[$key] = $this->sanitize_array($value);
            } else {
                $array[$key] = sanitize_text_field($value);
            }
        }
        return $array;
    }
    
    /**
     * Debug logging (privacy-safe)
     *
     * @param string $message Debug message.
     * @param mixed  $data    Additional debug data.
     * @return void
     */
    public function debug_log($message, $data = null) {
        if (defined('WP_DEBUG') && WP_DEBUG && $this->config->get_bool('debug_mode', false)) {
            $user_agent = $this->get_user_agent();
            $request_uri = isset($_SERVER['REQUEST_URI']) ? 
                          sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
            
            $ip = $this->get_client_ip();
            $ip_hash = $this->hash_ip($ip);
            
            $log_entry = [
                'timestamp'    => current_time('mysql'),
                'request_id'   => $this->request_id,
                'ip_hash'      => $ip_hash,
                'message'      => sanitize_text_field($message),
                'data'         => $this->sanitize_array($data ?: []),
                'request_uri'  => substr($request_uri, 0, 500),
                'user_agent'   => substr($user_agent, 0, 255),
            ];
            
            error_log('BotFend Debug: ' . wp_json_encode($log_entry));
        }
    }
    
    /**
     * Get validated client IP address
     * Delegates to hybrid IPValidator
     *
     * @param bool $skip_cache Bypass request cache
     * @return string Client IP or '0.0.0.0'
     */
    public function get_client_ip($skip_cache = false) {
        return $this->ip_validator->get_client_ip($skip_cache);
    }
    
    /**
     * Validate IP address format
     * Delegates to hybrid IPValidator
     *
     * @param string $ip IP to validate
     * @return bool
     */
    public function is_valid_ip($ip) {
        return $this->ip_validator->is_valid_ip($ip);
    }
    
    /**
     * Check if IP is in CIDR range
     * Delegates to hybrid IPValidator
     *
     * @param string $ip IP to check
     * @param string $range CIDR range
     * @return bool
     */
    public function ip_in_range($ip, $range) {
        return $this->ip_validator->ip_in_range($ip, $range);
    }
    
    /**
     * Check if IP is private/internal
     * Delegates to hybrid IPValidator
     *
     * @param string $ip IP to check
     * @return bool
     */
    public function is_private_ip($ip) {
        return $this->ip_validator->is_private_ip($ip);
    }
    
    /**
     * Clear IP validation cache
     *
     * @return void
     */
    public function clear_ip_cache() {
        $this->ip_validator->clear_cache();
    }
    
    /**
     * Check if header value is safe (prevents CRLF injection)
     *
     * @param string $value Header value to check
     * @return bool
     */
    private function is_safe_header_value($value) {
        if (!is_string($value)) {
            return false;
        }
        
        if (preg_match('/[\x00-\x1F\x7F]/', $value)) {
            return false;
        }
        
        $decoded = urldecode($value);
        if (preg_match('/[\r\n]/', $decoded)) {
            return false;
        }
        
        if (stripos($decoded, '%0d') !== false || stripos($decoded, '%0a') !== false) {
            return false;
        }
        
        if (strpos($value, "\0") !== false) {
            return false;
        }
        
        $dangerous = ['../', '..\\', '://', ';', '|', '&', '`', '$', '(', ')', '{', '}', '[', ']'];
        $lower = strtolower($decoded);
        
        foreach ($dangerous as $pattern) {
            if (strpos($lower, $pattern) !== false) {
                return false;
            }
        }
        
        return true;
    }
    
    /**
     * Verify legitimate crawler with complete verification
     *
     * This is the MAIN method to check if a request is from a legitimate crawler.
     * Now integrates TrustedServiceVerifier for enterprise-grade infrastructure verification.
     *
     * @param string $ip         IP address.
     * @param string $user_agent User agent string.
     * @return bool True if legitimate crawler.
     */
    public function verify_legitimate_crawler($ip, $user_agent) {
        if (!$this->config->get_bool('enable_crawler_verification', true)) {
            return false;
        }
        
        if (!$this->is_valid_ip($ip)) {
            return false;
        }
        
        // ============================================
        // NEW: Trusted Service Verification (Enterprise)
        // ============================================
        $trust_result = $this->trusted_service_verifier->verify($ip, $user_agent, $_SERVER);
        
        if ($trust_result['verified']) {
            $this->debug_log('Crawler verified via TrustedServiceVerifier', [
                'ip'        => $ip,
                'service'   => $trust_result['service_name'],
                'trust'     => $trust_result['trust_level'],
            ]);
            return true;
        }
        
        // If TrustedServiceVerifier gives partial trust, still run legacy checks
        if ($trust_result['trust_level'] >= TrustedServiceVerifier::TRUST_MEDIUM) {
            $this->debug_log('Crawler has partial trust from TrustedServiceVerifier', [
                'ip'        => $ip,
                'service'   => $trust_result['service_name'],
                'trust'     => $trust_result['trust_level'],
            ]);
        }
        
        // ============================================
        // Legacy Crawler Verification (fallback)
        // ============================================
        $crawler_name = $this->extract_crawler_from_ua($user_agent);
        
        if (!$crawler_name) {
            return false;
        }
        
        $ip_verified = $this->verify_crawler_ip($ip, $crawler_name);
        
        if (!$ip_verified) {
            $this->log_error(
                'Crawler impersonation detected',
                [
                    'ip'           => $ip,
                    'user_agent'   => substr($user_agent, 0, 100),
                    'claimed_as'   => $crawler_name,
                ]
            );
            return false;
        }
        
        if (in_array($crawler_name, ['google', 'bing', 'facebook', 'automattic'], true) && 
            $this->config->get_bool('enable_reverse_dns', false)) {
            
            if (!$this->verify_reverse_dns($ip, $crawler_name)) {
                $this->log_error(
                    'Crawler reverse DNS verification failed',
                    [
                        'ip'         => $ip,
                        'crawler'    => $crawler_name,
                        'user_agent' => substr($user_agent, 0, 100),
                    ]
                );
                return false;
            }
        }
        
        if ($this->config->get_bool('enable_tor_detection', false) && $this->database) {
            if ($this->database->is_tor_exit_node($ip)) {
                $this->debug_log('Crawler detected but IP is TOR exit node', [
                    'ip' => $ip,
                    'crawler' => $crawler_name
                ]);
                return false;
            }
        }
        
        $this->debug_log('Legitimate crawler verified', [
            'ip'       => $ip,
            'crawler'  => $crawler_name,
            'user_agent' => substr($user_agent, 0, 50),
        ]);
        
        return true;
    }
    
    /**
     * Extract crawler name from User-Agent
     * PERFORMANCE: Optimized with O(1) hash map lookup
     *
     * @param string $user_agent User agent string.
     * @return string|false Crawler name or false if not found.
     */
    public function extract_crawler_from_ua($user_agent) {
        if (empty($user_agent)) {
            return false;
        }
        
        static $crawler_map = null;
        
        // Lazy load crawler map for O(1) lookups
        if ($crawler_map === null) {
            $crawler_map = [
                'googlebot'              => 'google',
                'google-inspectiontool'  => 'google',
                'mediapartners-google'   => 'google',
                'adsbot-google'          => 'google',
                'bingbot'                => 'bing',
                'msnbot'                 => 'bing',
                'bingpreview'            => 'bing',
                'slurp'                  => 'yahoo',
                'duckduckbot'            => 'duckduckgo',
                'yandexbot'              => 'yandex',
                'applebot'               => 'apple',
                'facebookexternalhit'    => 'facebook',
                'facebookcatalog'        => 'facebook',
                'meta-externalagent'     => 'facebook',
                'twitterbot'             => 'twitter',
                'linkedinbot'            => 'linkedin',
                'pinterest'              => 'pinterest',
                'pinterestbot'           => 'pinterest',
                'whatsapp'               => 'messenger',
                'telegrambot'            => 'messenger',
                'discordbot'             => 'messenger',
                'slackbot'               => 'messenger',
                'baiduspider'            => 'baidu',
                'sogou'                  => 'sogou',
                'ahrefsbot'              => 'ahrefs',
                'semrushbot'             => 'semrush',
                'mj12bot'                => 'mj12',
                'dotbot'                 => 'dotbot',
                // NEW: Automattic services
                'photon/1.0'             => 'automattic',
                'jetpack'                => 'automattic',
                'wordpress.com'          => 'automattic',
                'wp.com'                 => 'automattic',
            ];
        }
        
        $ua_lower = strtolower($user_agent);
        
        foreach ($crawler_map as $keyword => $name) {
            if (strpos($ua_lower, $keyword) !== false) {
                return $name;
            }
        }
        
        return false;
    }
    
    /**
     * Verify crawler IP against known ranges
     *
     * @param string $ip         IP address.
     * @param string $crawler_name Crawler name.
     * @return bool True if IP matches known ranges.
     */
    public function verify_crawler_ip($ip, $crawler_name) {
        if (!$this->is_valid_ip($ip)) {
            return false;
        }
        
        switch ($crawler_name) {
            case 'google':
                return $this->is_google_ip($ip);
            case 'bing':
            case 'yahoo':
                return $this->is_bing_ip($ip);
            case 'facebook':
                return $this->is_facebook_ip($ip);
            case 'apple':
                return $this->is_apple_ip($ip);
            case 'yandex':
                return $this->is_yandex_ip($ip);
            case 'pinterest':
                return $this->is_pinterest_ip($ip);
            case 'automattic':
                return $this->is_automattic_ip($ip);
            default:
                return false;
        }
    }
    
    /**
     * Check if IP is a Google IP
     *
     * @param string $ip IP address.
     * @return bool True if Google IP.
     */
    public function is_google_ip($ip) {
        foreach (self::GOOGLE_IPS as $range) {
            if ($this->ip_in_range($ip, $range)) {
                $this->debug_log('Google IP matched', ['ip' => $ip, 'range' => $range]);
                return true;
            }
        }
        return false;
    }
    
    /**
     * Check if IP is a Bing/Microsoft IP
     *
     * @param string $ip IP address.
     * @return bool True if Bing IP.
     */
    public function is_bing_ip($ip) {
        foreach (self::BING_IPS as $range) {
            if ($this->ip_in_range($ip, $range)) {
                $this->debug_log('Bing IP matched', ['ip' => $ip, 'range' => $range]);
                return true;
            }
        }
        return false;
    }
    
    /**
     * Check if IP is a Facebook IP
     *
     * @param string $ip IP address.
     * @return bool True if Facebook IP.
     */
    public function is_facebook_ip($ip) {
        foreach (self::FACEBOOK_IPS as $range) {
            if ($this->ip_in_range($ip, $range)) {
                $this->debug_log('Facebook IP matched', ['ip' => $ip, 'range' => $range]);
                return true;
            }
        }
        return false;
    }
    
    /**
     * Check if IP is an Apple IP
     *
     * @param string $ip IP address.
     * @return bool True if Apple IP.
     */
    public function is_apple_ip($ip) {
        foreach (self::APPLE_IPS as $range) {
            if ($this->ip_in_range($ip, $range)) {
                $this->debug_log('Apple IP matched', ['ip' => $ip, 'range' => $range]);
                return true;
            }
        }
        return false;
    }
    
    /**
     * Check if IP is a Yandex IP
     *
     * @param string $ip IP address.
     * @return bool True if Yandex IP.
     */
    public function is_yandex_ip($ip) {
        foreach (self::YANDEX_IPS as $range) {
            if ($this->ip_in_range($ip, $range)) {
                $this->debug_log('Yandex IP matched', ['ip' => $ip, 'range' => $range]);
                return true;
            }
        }
        return false;
    }
    
    /**
     * Check if IP is a Pinterest IP
     *
     * @param string $ip IP address.
     * @return bool True if Pinterest IP.
     */
    public function is_pinterest_ip($ip) {
        foreach (self::PINTEREST_IPS as $range) {
            if ($this->ip_in_range($ip, $range)) {
                $this->debug_log('Pinterest IP matched', ['ip' => $ip, 'range' => $range]);
                return true;
            }
        }
        return false;
    }
    
    /**
     * Check if IP belongs to Automattic
     *
     * @since 5.1.0
     * @param string $ip IP address.
     * @return bool True if Automattic IP.
     */
    public function is_automattic_ip($ip) {
        foreach (self::AUTOMATTIC_IPS as $range) {
            if ($this->ip_in_range($ip, $range)) {
                $this->debug_log('Automattic IP matched', ['ip' => $ip, 'range' => $range]);
                return true;
            }
        }
        return false;
    }
    
    /**
     * Verify reverse DNS for crawlers with aggressive caching
     * PERFORMANCE PATCH: Prevents DNS exhaustion attacks via 24-hour cache
     * SEO SAFETY PATCH: Failures cached for only 15 minutes to prevent DNS Blip lockouts
     *
     * @param string $ip          IP address.
     * @param string $crawler_name Crawler name.
     * @return bool True if reverse DNS matches expected pattern.
     */
    public function verify_reverse_dns($ip, $crawler_name) {
        if (!function_exists('gethostbyaddr') || !function_exists('gethostbyname')) {
            return false;
        }
        
        // Cache key for DNS caching (prevents DNS hammering)
        $cache_key = 'botfend_rdns_' . md5($ip . '_' . $crawler_name);
        
        if (isset(self::$rdns_cache[$cache_key])) {
            return self::$rdns_cache[$cache_key];
        }
        
        $cached_result = get_transient($cache_key);
        if (false !== $cached_result) {
            self::$rdns_cache[$cache_key] = (bool) $cached_result;
            return (bool) $cached_result;
        }
        
        // Manage cache size
        if (count(self::$rdns_cache) > 500) {
            self::$rdns_cache = array_slice(self::$rdns_cache, 100, null, true);
        }
        
        $hostname = @gethostbyaddr($ip);
        
        if (!$hostname || $hostname === $ip) {
            // SEO SAFETY: Only cache DNS failures for 15 minutes
            set_transient($cache_key, 0, 15 * MINUTE_IN_SECONDS);
            self::$rdns_cache[$cache_key] = false;
            return false;
        }
        
        // FCrDNS (Forward-Confirmed reverse DNS) Security Patch
        $forward_ip = @gethostbyname($hostname);
        if ($forward_ip !== $ip) {
            $this->debug_log('Crawler spoofing detected: FCrDNS failed', [
                'ip' => $ip,
                'hostname' => $hostname,
                'forward_resolved_ip' => $forward_ip
            ]);
            set_transient($cache_key, 0, 15 * MINUTE_IN_SECONDS);
            self::$rdns_cache[$cache_key] = false;
            return false;
        }
        
        $hostname_lower = strtolower($hostname);
        $is_valid = false;
        
        switch ($crawler_name) {
            case 'google':
                $is_valid = (bool) preg_match('/\.(googlebot|google)\.com$/', $hostname_lower);
                break;
            case 'bing':
                $is_valid = (bool) preg_match('/\.search\.msn\.com$/', $hostname_lower);
                break;
            case 'facebook':
                $is_valid = (bool) preg_match('/\.(facebook|fb)\.com$/', $hostname_lower);
                break;
            case 'automattic':
                $is_valid = (bool) preg_match('/\.(wordpress\.com|wp\.com|automattic\.com)$/', $hostname_lower);
                break;
            default:
                $is_valid = true;
        }
        
        // Cache VALID lookups for 24 hours
        set_transient($cache_key, $is_valid ? 1 : 0, $is_valid ? DAY_IN_SECONDS : (15 * MINUTE_IN_SECONDS));
        self::$rdns_cache[$cache_key] = $is_valid;
        
        return $is_valid;
    }
    
    /**
     * Legacy method for backward compatibility
     * Only checks User-Agent, not IP!
     *
     * @param string $ip         IP address.
     * @param string $user_agent User agent string.
     * @return bool True if Google crawler.
     * @deprecated Use verify_legitimate_crawler() instead.
     */
    public function is_google_crawler($ip, $user_agent) {
        if ($this->is_google_ip($ip)) {
            return true;
        }
        
        if (!empty($user_agent)) {
            $ua_lower = strtolower(sanitize_text_field($user_agent));
            $google_agents = ['googlebot', 'google-inspectiontool', 'mediapartners-google', 'adsbot-google', 'google-safety'];
            foreach ($google_agents as $agent) {
                if (strpos($ua_lower, $agent) !== false) {
                    return true;
                }
            }
        }
        
        return false;
    }
    
    /**
     * Legacy method for backward compatibility
     * Only checks User-Agent, not IP!
     *
     * @param string $user_agent User agent string.
     * @return bool True if known crawler.
     * @deprecated Use verify_legitimate_crawler() instead.
     */
    public function is_known_crawler($user_agent) {
        if (empty($user_agent)) {
            return false;
        }
        
        $ua_lower = strtolower(sanitize_text_field($user_agent));
        foreach (self::KNOWN_CRAWLERS as $crawler) {
            if (strpos($ua_lower, strtolower($crawler)) !== false) {
                $this->debug_log('Known crawler detected (UA only)', [
                    'crawler'    => $crawler,
                    'user_agent' => substr($user_agent, 0, 50),
                ]);
                return true;
            }
        }
        return false;
    }
    
    /**
     * Check Instagram crawler
     *
     * @param string $user_agent User agent string.
     * @return bool True if Instagram crawler.
     */
    public function is_instagram_crawler($user_agent) {
        if (empty($user_agent)) {
            return false;
        }
        
        $ua_lower = strtolower(sanitize_text_field($user_agent));
        $instagram_agents = ['instagram', 'facebookexternalhit', 'facebookcatalog'];
        
        foreach ($instagram_agents as $agent) {
            if (strpos($ua_lower, $agent) !== false) {
                $this->debug_log('Instagram crawler detected', ['user_agent' => substr($user_agent, 0, 50)]);
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Check Instagram request with IP verification
     *
     * @param string $ip         IP address.
     * @param string $user_agent User agent string.
     * @return bool True if Instagram request.
     */
    public function is_instagram_request($ip, $user_agent) {
        if ($this->is_facebook_ip($ip)) {
            return true;
        }
        
        if ($this->is_instagram_crawler($user_agent)) {
            return true;
        }
        
        return false;
    }
    
    /**
     * Check for suspicious User-Agent
     *
     * @param string $ua User agent string.
     * @return bool True if suspicious.
     */
    public function is_suspicious_ua($ua) {
        if (empty($ua)) {
            return true;
        }
        
        // NEW: Skip known legitimate crawlers and services
        if ($this->extract_crawler_from_ua($ua)) {
            return false;
        }
        
        $ua_lower = strtolower(sanitize_text_field($ua));
        
        $legitimate_browsers = ['chrome', 'firefox', 'safari', 'edge', 'opera', 'msie', 'trident', 'mozilla'];
        
        $has_legitimate_browser = false;
        foreach ($legitimate_browsers as $browser) {
            if (strpos($ua_lower, $browser) !== false) {
                $has_legitimate_browser = true;
                break;
            }
        }
        
        $suspicious_keywords = ['bot', 'crawler', 'spider', 'scraper', 'curl', 'wget', 'python-requests', 'java/', 'go-http-client'];
        
        foreach ($suspicious_keywords as $keyword) {
            if (strpos($ua_lower, $keyword) !== false) {
                return true;
            }
        }
        
        if (strlen($ua) < 10) {
            return true;
        }
        
        if (!$has_legitimate_browser) {
            return true;
        }
        
        return false;
    }
    
    /**
     * Check if request is from proxy
     *
     * @param string $ip IP address.
     * @return bool True if proxy detected.
     */
    public function is_proxy($ip) {
        $headers = ['HTTP_VIA', 'HTTP_X_FORWARDED_FOR', 'HTTP_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED', 'HTTP_CLIENT_IP'];
        
        foreach ($headers as $header) {
            if (isset($_SERVER[$header])) {
                $header_value = sanitize_text_field(wp_unslash($_SERVER[$header]));
                if (!empty($header_value) && $this->is_safe_header_value($header_value)) {
                    $this->debug_log('Proxy header detected', [
                        'header' => $header,
                        'value'  => substr($header_value, 0, 100),
                    ]);
                    return true;
                }
            }
        }
        
        if ($this->database && $this->database->is_tor_exit_node($ip)) {
            $this->debug_log('TOR exit node detected', ['ip' => $ip]);
            return true;
        }
        
        return false;
    }
    
    /**
     * Check for suspicious headers
     *
     * @return bool True if suspicious headers found.
     */
    public function has_suspicious_headers() {
        $suspicious_headers = ['HTTP_X_SCANNER', 'HTTP_X_ATTACKER', 'HTTP_X_MALWARE'];
        
        foreach ($suspicious_headers as $header) {
            if (isset($_SERVER[$header])) {
                $header_value = sanitize_text_field(wp_unslash($_SERVER[$header]));
                if ($this->is_safe_header_value($header_value)) {
                    $this->debug_log('Suspicious header detected', [
                        'header' => $header,
                        'value'  => substr($header_value, 0, 100),
                    ]);
                    return true;
                }
            }
        }
        return false;
    }
    
    /**
     * Get country from IP with multiple fallback services, circuit breaker, and proper caching
     * 
     * CRITICAL: Geolocation is DISABLED BY DEFAULT for WordPress.org compliance.
     * Users must explicitly opt-in by enabling 'enable_geolocation' setting.
     *
     * @param string $ip IP address.
     * @return string|null Country code or null.
     */
    public function get_country($ip) {
        // CRITICAL: Geolocation is DISABLED BY DEFAULT - requires explicit opt-in
        if (!$this->config->get_bool('enable_geolocation', false)) {
            return null;
        }
        
        if (!function_exists('get_transient') || !function_exists('wp_remote_get')) {
            return null;
        }

        if (!$this->is_valid_ip($ip)) {
            return null;
        }

        $cache_key = 'botfend_country_' . md5($ip . BOTFEND_VERSION);
        $country_code = get_transient($cache_key);

        if (false !== $country_code && $country_code !== 'N/A' && $country_code !== 'ERROR') {
            return sanitize_text_field($country_code);
        }

        $services = $this->get_geo_services();
        
        foreach ($services as $service_name => $service) {
            if (!$this->is_service_healthy($service_name)) {
                continue;
            }
            
            try {
                $args = [
                    'timeout' => $service['timeout'],
                    'sslverify' => (strpos($service['url'], 'https://') === 0),
                    'user-agent' => 'BotFend Anti-Bot Firewall/' . BOTFEND_VERSION,
                    'headers' => [
                        'Accept' => isset($service['expect_json']) && !$service['expect_json'] ? 'text/plain' : 'application/json',
                    ]
                ];

                $url = str_replace('{ip}', $ip, $service['url']);
                $response = wp_remote_get($url, $args);
                
                if (is_wp_error($response)) {
                    $this->record_service_failure($service_name);
                    continue;
                }

                $status_code = wp_remote_retrieve_response_code($response);
                if ($status_code !== 200) {
                    $this->record_service_failure($service_name);
                    continue;
                }

                $body = wp_remote_retrieve_body($response);
                if (empty($body)) {
                    $this->record_service_failure($service_name);
                    continue;
                }

                if (isset($service['expect_json']) && !$service['expect_json']) {
                    $data = $body;
                } else {
                    $data = json_decode($body, true);
                    if (json_last_error() !== JSON_ERROR_NONE) {
                        $this->record_service_failure($service_name);
                        continue;
                    }
                }

                $country_code = $service['parser']($data);
                
                if (!empty($country_code) && preg_match('/^[A-Z]{2}$/', $country_code)) {
                    set_transient($cache_key, $country_code, WEEK_IN_SECONDS);
                    $this->reset_service_failures($service_name);
                    
                    if (defined('WP_DEBUG') && WP_DEBUG && $this->config->get_bool('debug_mode', false)) {
                        error_log(sprintf(
                            'BotFend Geo: IP %s -> %s (via %s)',
                            $ip,
                            $country_code,
                            $service_name
                        ));
                    }
                    
                    return $country_code;
                } else {
                    $this->record_service_failure($service_name);
                }
            } catch (\Exception $e) {
                $this->record_service_failure($service_name);
                if (defined('WP_DEBUG') && WP_DEBUG && $this->config->get_bool('debug_mode', false)) {
                    error_log(sprintf(
                        'BotFend Geo: Service %s failed for IP %s: %s',
                        $service_name,
                        $ip,
                        $e->getMessage()
                    ));
                }
                continue;
            }
        }

        set_transient('botfend_geo_all_failed_' . md5($ip), time(), HOUR_IN_SECONDS);
        set_transient($cache_key, 'N/A', HOUR_IN_SECONDS);
        
        if (defined('WP_DEBUG') && WP_DEBUG && $this->config->get_bool('debug_mode', false)) {
            error_log(sprintf('BotFend Geo: All services failed for IP: %s', $ip));
        }
        
        return null;
    }
    
    /**
     * Get configured geo services
     *
     * @return array
     */
    private function get_geo_services() {
        return [
            'ip-api' => [
                'url' => 'http://ip-api.com/json/{ip}?fields=status,countryCode',
                'parser' => function($data) {
                    if (isset($data['status']) && $data['status'] === 'success' && !empty($data['countryCode'])) {
                        return sanitize_text_field($data['countryCode']);
                    }
                    return null;
                },
                'timeout' => 3,
                'expect_json' => true
            ],
            'ipapi' => [
                'url' => 'https://ipapi.co/{ip}/country_code/',
                'parser' => function($data) {
                    $code = trim($data);
                    if (preg_match('/^[A-Z]{2}$/', $code)) {
                        return sanitize_text_field($code);
                    }
                    return null;
                },
                'timeout' => 3,
                'expect_json' => false
            ],
            'ipwhois' => [
                'url' => 'https://ipwho.is/{ip}',
                'parser' => function($data) {
                    if (isset($data['success']) && $data['success'] === true && !empty($data['country_code'])) {
                        return sanitize_text_field($data['country_code']);
                    }
                    return null;
                },
                'timeout' => 3,
                'expect_json' => true
            ],
        ];
    }
    
    /**
     * Check if a geo service is healthy (circuit breaker)
     *
     * @param string $service_name
     * @return bool
     */
    private function is_service_healthy($service_name) {
        $key = 'botfend_geo_health_' . $service_name;
        $failures = (int) get_transient($key);
        return $failures < 5;
    }
    
    /**
     * Record a service failure
     *
     * @param string $service_name
     * @return void
     */
    private function record_service_failure($service_name) {
        $key = 'botfend_geo_health_' . $service_name;
        $failures = (int) get_transient($key) + 1;
        set_transient($key, $failures, HOUR_IN_SECONDS);
    }
    
    /**
     * Reset service failure count on success
     *
     * @param string $service_name
     * @return void
     */
    private function reset_service_failures($service_name) {
        $key = 'botfend_geo_health_' . $service_name;
        delete_transient($key);
    }

    /**
     * Helper method to clear geo cache for a specific IP
     *
     * @param string|null $ip IP address to clear cache for
     * @return bool Success
     */
    public function clear_geo_cache($ip = null) {
        if ($ip) {
            if (!$this->is_valid_ip($ip)) {
                return false;
            }
            $cache_key = 'botfend_country_' . md5($ip . BOTFEND_VERSION);
            $failure_key = 'botfend_geo_all_failed_' . md5($ip);
            delete_transient($cache_key);
            delete_transient($failure_key);
            return true;
        }
        
        global $wpdb;
        
        if (!wp_using_ext_object_cache()) {
            $keys = $wpdb->get_col($wpdb->prepare(
                "SELECT option_name FROM {$wpdb->options} 
                 WHERE option_name LIKE %s 
                 OR option_name LIKE %s 
                 OR option_name LIKE %s",
                $wpdb->esc_like('_transient_botfend_country_') . '%',
                $wpdb->esc_like('_transient_botfend_geo_failure_') . '%',
                $wpdb->esc_like('_transient_botfend_geo_health_') . '%'
            ));
            
            if (!empty($keys)) {
                foreach ($keys as $key) {
                    $transient_name = str_replace(['_transient_timeout_', '_transient_'], '', $key);
                    delete_transient($transient_name);
                }
            }
        }
        
        return true;
    }
    
    /**
     * Get authoritative high-risk country list from config
     * PERFORMANCE PATCH: O(1) Hash Map instead of Array
     *
     * @return array List of high-risk country codes (as array for backward compatibility)
     */
    public function get_high_risk_countries() {
        $high_risk_string = $this->config->get_string('high_risk_countries', '');
        
        if (empty($high_risk_string)) {
            // Fallback to default if not configured
            return ['CN', 'RU', 'UA', 'BR', 'IN', 'VN', 'ID', 'TH', 'IR', 'PK', 'NL', 'FR', 'DE', 'AU', 'KR'];
        }
        
        return $this->parse_country_list($high_risk_string);
    }

    /**
     * Check if country is high-risk
     * PERFORMANCE PATCH: isset() is magnitudes faster than in_array()
     *
     * @param string $country Country code.
     * @return bool True if high-risk country.
     */
    public function is_high_risk_country($country) {
        if (empty($country)) {
            return false;
        }
        
        // Lazy load hash map for O(1) lookups
        if (self::$high_risk_country_map === null) {
            $high_risk_array = $this->get_high_risk_countries();
            self::$high_risk_country_map = [];
            foreach ($high_risk_array as $code) {
                self::$high_risk_country_map[strtoupper($code)] = true;
            }
        }
        
        $result = isset(self::$high_risk_country_map[strtoupper(sanitize_text_field($country))]);
        
        if ($result) {
            $this->debug_log('High risk country detected', ['country' => $country]);
        }
        
        return $result;
    }

    /**
     * Check if country is blocked
     *
     * @param string $country Country code.
     * @return bool True if blocked.
     */
    public function is_country_blocked($country) {
        if (empty($country)) {
            return false;
        }
        
        $blocked_string = $this->config->get_string('blocked_countries', '');
        
        if (empty($blocked_string)) {
            return false;
        }
        
        $blocked_countries = $this->parse_country_list($blocked_string);
        
        if (empty($blocked_countries)) {
            return false;
        }
        
        return in_array(strtoupper(sanitize_text_field($country)), $blocked_countries, true);
    }

    /**
     * Get country block action
     *
     * @param string $country Country code.
     * @return string Action type ('block' or 'none')
     */
    public function get_country_block_action($country) {
        if ($this->is_country_blocked($country)) {
            return 'block';
        }
        
        return 'none';
    }

    /**
     * Get blocked countries list
     *
     * @return array List of blocked country codes.
     */
    public function get_blocked_countries() {
        $blocked_string = $this->config->get_string('blocked_countries', '');
        return $this->parse_country_list($blocked_string);
    }

    /**
     * Get high-risk country score value
     *
     * @return int Score value
     */
    public function get_high_risk_country_score() {
        return $this->config->get_int('score_high_risk_country', 4);
    }

    /**
     * Parse comma-separated country list into array
     *
     * @param string $list Comma-separated country codes
     * @return array Array of uppercase country codes
     */
    private function parse_country_list($list) {
        if (empty($list) || !is_string($list)) {
            return [];
        }
        
        $countries = array_map('trim', explode(',', $list));
        $countries = array_filter($countries, function($code) {
            return preg_match('/^[A-Z]{2}$/', strtoupper($code));
        });
        
        return array_values(array_unique(array_map('strtoupper', $countries)));
    }

    /**
     * Refresh high-risk country cache (call after settings update)
     *
     * @return void
     */
    public function refresh_high_risk_country_cache() {
        self::$high_risk_country_map = null;
    }
    
    /**
     * Check if IP is whitelisted
     * PERFORMANCE PATCH: Cache the parsed array to prevent repetitive CPU loads
     *
     * @param string|null $ip IP address.
     * @return bool True if whitelisted.
     */
    public function is_ip_whitelisted($ip = null) {
        if (is_null($ip)) {
            $ip = $this->get_client_ip();
        }
        
        if (!$this->is_valid_ip($ip)) {
            return false;
        }
        
        if ($ip === '127.0.0.1' || $ip === '::1') {
            return true;
        }
        
        $whitelist_text = $this->config->get_string('whitelist_ips', '');
        
        if (empty(trim($whitelist_text))) {
            return false;
        }
        
        // Use cached parsed whitelist
        if (self::$parsed_whitelist_cache === null) {
            $lines = explode("\n", $whitelist_text);
            self::$parsed_whitelist_cache = [];
            
            foreach ($lines as $line) {
                $line = trim($line);
                if (empty($line) || strpos($line, '#') === 0) {
                    continue;
                }
                self::$parsed_whitelist_cache[] = $line;
            }
        }
        
        foreach (self::$parsed_whitelist_cache as $line) {
            if (strpos($line, '/') !== false) {
                if ($this->ip_in_range($ip, $line)) {
                    return true;
                }
            } else {
                if ($ip === $line) {
                    return true;
                }
            }
        }
        
        return false;
    }
    
    /**
     * Legacy method for backward compatibility
     *
     * @param string $ip IP address.
     * @return bool True if whitelisted.
     * @deprecated Use is_ip_whitelisted() instead.
     */
    public function is_whitelisted($ip) {
        return $this->is_ip_whitelisted($ip);
    }
    
    /**
     * Check if IP is permanently blocked
     * PERFORMANCE PATCH: Cache the parsed array to prevent repetitive CPU loads
     *
     * @param string $ip IP address.
     * @return bool True if permanently blocked.
     */
    public function is_permanently_blocked($ip) {
        $permanent_blocklist = $this->config->get_string('permanent_blocklist', '');
        if (empty($permanent_blocklist)) {
            return false;
        }
        
        // Use cached parsed blocklist
        if (self::$parsed_blocklist_cache === null) {
            $lines = explode("\n", $permanent_blocklist);
            self::$parsed_blocklist_cache = [];
            
            foreach ($lines as $line) {
                $line = trim($line);
                if (empty($line) || strpos($line, '#') === 0) {
                    continue;
                }
                self::$parsed_blocklist_cache[] = $line;
            }
        }
        
        foreach (self::$parsed_blocklist_cache as $blocked_ip) {
            if (strpos($blocked_ip, '/') !== false) {
                if ($this->ip_in_range($ip, $blocked_ip)) {
                    return true;
                }
            } elseif ($ip === $blocked_ip) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Get request fingerprint with SHA-256
     *
     * @return string SHA-256 hash of request fingerprint.
     */
    public function get_request_fingerprint() {
        $user_agent = $this->get_user_agent();
        $accept = isset($_SERVER['HTTP_ACCEPT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_ACCEPT'])) : '';
        $accept_language = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_ACCEPT_LANGUAGE'])) : '';
        $accept_encoding = isset($_SERVER['HTTP_ACCEPT_ENCODING']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_ACCEPT_ENCODING'])) : '';
        $connection = isset($_SERVER['HTTP_CONNECTION']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_CONNECTION'])) : '';
        
        $components = [
            'user_agent'       => $user_agent,
            'accept'           => $accept,
            'accept_language'  => $accept_language,
            'accept_encoding'  => $accept_encoding,
            'connection'       => $connection,
            'platform'         => $this->detect_platform(),
            'browser'          => $this->detect_browser(),
        ];
        
        return 'fp_' . hash('sha256', wp_json_encode($components));
    }
    
    /**
     * Detect platform from User-Agent
     *
     * @return string Platform name.
     */
    private function detect_platform() {
        $user_agent = $this->get_user_agent();
        
        $platforms = [
            '/windows nt 11/i' => 'Windows 11',
            '/windows nt 10/i' => 'Windows 10',
            '/macintosh|mac os x/i' => 'Mac OS X',
            '/linux/i' => 'Linux',
            '/iphone|ipad|ipod/i' => 'iOS',
            '/android/i' => 'Android',
        ];
        
        foreach ($platforms as $regex => $platform) {
            if (preg_match($regex, $user_agent)) {
                return sanitize_text_field($platform);
            }
        }
        
        return 'unknown';
    }
    
    /**
     * Detect browser from User-Agent
     *
     * @return string Browser name.
     */
    private function detect_browser() {
        $user_agent = $this->get_user_agent();
        
        $browsers = [
            '/edg/i' => 'Edge',
            '/chrome/i' => 'Chrome',
            '/firefox/i' => 'Firefox',
            '/safari/i' => 'Safari',
            '/opera/i' => 'Opera',
            '/msie|trident/i' => 'Internet Explorer',
        ];
        
        foreach ($browsers as $regex => $browser) {
            if (preg_match($regex, $user_agent)) {
                return sanitize_text_field($browser);
            }
        }
        
        return 'unknown';
    }
    
    /**
     * Rate limit API requests with endpoint-specific keys
     * PERFORMANCE PATCH: Use object cache group for better Redis/Memcached compatibility
     *
     * @param string $endpoint     API endpoint.
     * @param string $ip           IP address.
     * @param int    $max_requests Maximum requests allowed.
     * @param int    $time_window  Time window in seconds.
     * @return bool True if rate limited.
     */
    public function is_rate_limited_api($endpoint, $ip, $max_requests = 10, $time_window = 60) {
        $cache_key = 'botfend_api_rate_' . md5($endpoint . '|' . $ip);
        $attempts = absint(get_transient($cache_key) ?: 0);
        
        if ($attempts >= $max_requests) {
            $this->debug_log('API rate limit exceeded', [
                'endpoint' => $endpoint,
                'ip'       => $ip,
                'attempts' => $attempts,
                'max'      => $max_requests,
            ]);
            return true;
        }
        
        $attempts++;
        set_transient($cache_key, $attempts, $time_window);
        
        $this->debug_log('API request counted', [
            'endpoint' => $endpoint,
            'ip'       => $ip,
            'attempts' => $attempts,
            'max'      => $max_requests,
        ]);
        
        return false;
    }
    
    /**
     * Clear all parsed caches
     *
     * @return void
     */
    public static function clear_parsed_caches() {
        self::$parsed_whitelist_cache = null;
        self::$parsed_blocklist_cache = null;
        self::$high_risk_country_map = null;
        self::$rdns_cache = [];
    }
}