<?php
/**
 * IPHub.info API integration for BotFend Anti-Bot Firewall
 *
 * @package BotFend\ThirdParty
 * @version 4.8.3 - External service disabled by default, proper escaping
 */

namespace BotFend\ThirdParty;

use BotFend\Core\Config;
use BotFend\Utilities\Utilities;
use WP_Error;

defined('ABSPATH') || exit;

/**
 * Class IPHub
 * 
 * IPHub is an external service that identifies non-residential IPs (VPNs, proxies, hosting).
 * This service is DISABLED BY DEFAULT and requires explicit user opt-in.
 */
class IPHub {
    
    /**
     * Config instance
     *
     * @var Config
     */
    private Config $config;
    
    /**
     * Utilities instance
     *
     * @var Utilities|null
     */
    private ?Utilities $utilities = null;
    
    /**
     * API key
     *
     * @var string
     */
    private string $api_key;
    
    /**
     * Blocking mode
     *
     * @var string
     */
    private string $blocking_mode;
    
    /**
     * Crawler verification cache
     *
     * @var array<string, bool>
     */
    private static array $crawler_cache = [];
    
    /**
     * Max cache size to prevent memory leaks
     */
    private const MAX_CRAWLER_CACHE = 100;
    
    /**
     * Constructor
     *
     * @param Utilities|null $utilities Utilities instance
     */
    public function __construct(?Utilities $utilities = null) {
        $this->config = Config::get_instance();
        $this->api_key = $this->config->get_string('iphub_api_key', '');
        // Defaults to 'moderate' to prevent false positives with standard VPNs
        $this->blocking_mode = $this->config->get_string('iphub_mode', 'moderate');
        $this->utilities = $utilities;
    }
    
    /**
     * Check if IPHub is enabled (user must opt-in)
     *
     * @return bool
     */
    private function is_enabled(): bool {
        // IPHub is DISABLED BY DEFAULT - requires explicit opt-in
        return $this->config->get_bool('enable_iphub', false);
    }
    
    /**
     * Set utilities (for dependency injection)
     *
     * @param Utilities $utilities
     * @return void
     */
    public function set_utilities(Utilities $utilities): void {
        $this->utilities = $utilities;
    }
    
    /**
     * Check if IP is a legitimate crawler (cached)
     *
     * @param string $ip
     * @param string $user_agent
     * @return bool
     */
    private function is_legitimate_crawler_cached(string $ip, string $user_agent): bool {
        if (!$this->utilities || empty($user_agent)) {
            return false;
        }
        
        if (!method_exists($this->utilities, 'verify_legitimate_crawler')) {
            return false;
        }
        
        $cache_key = md5($ip . $user_agent);
        
        if (isset(self::$crawler_cache[$cache_key])) {
            return self::$crawler_cache[$cache_key];
        }
        
        if (count(self::$crawler_cache) >= self::MAX_CRAWLER_CACHE) {
            self::$crawler_cache = array_slice(self::$crawler_cache, 20, null, true);
        }
        
        $result = $this->utilities->verify_legitimate_crawler($ip, $user_agent);
        self::$crawler_cache[$cache_key] = $result;
        
        return $result;
    }
    
    /**
     * Validate IP address format
     * 
     * filter_var() with FILTER_VALIDATE_IP inherently rejects SQL injection,
     * XSS payloads, and malformed strings.
     *
     * @param string $ip IP address to validate
     * @return bool True if valid IPv4 or IPv6
     */
    private function is_valid_ip(string $ip): bool {
        if (empty($ip)) {
            return false;
        }
        
        $ip = trim($ip);
        
        // Block octal encoded IPs (e.g., 0177.0.0.1) - can bypass filter_var
        if (preg_match('/^0[0-9]+/', $ip)) {
            return false;
        }
        
        // Block hex encoded IPs (e.g., 0x7f000001) - can bypass filter_var
        if (preg_match('/^0x[a-fA-F0-9]+/i', $ip)) {
            return false;
        }
        
        return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE) !== false;
    }
    
    /**
     * Check if IP is private or reserved
     * Uses native PHP flags for optimal performance and IPv6 support
     *
     * @param string $ip
     * @return bool
     */
    private function is_private_ip(string $ip): bool {
        $is_public = filter_var(
            $ip, 
            FILTER_VALIDATE_IP, 
            FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
        );
        
        return $is_public === false;
    }
    
    /**
     * Make API request with retry logic
     *
     * @param string $ip
     * @param int    $max_retries
     * @return array|WP_Error
     */
    private function make_api_request_with_retry(string $ip, int $max_retries = 2) {
        $url = "https://v2.api.iphub.info/ip/" . urlencode($ip);
        $headers = [
            'X-Key' => $this->api_key,
            'User-Agent' => 'BotFend Anti-Bot Firewall/' . BOTFEND_VERSION
        ];
        
        $retry_count = 0;
        
        while ($retry_count <= $max_retries) {
            $response = wp_remote_get($url, [
                'timeout' => 3,
                'headers' => $headers,
                'sslverify' => true,
                'httpversion' => '1.1'
            ]);
            
            if (!is_wp_error($response)) {
                $status_code = absint(wp_remote_retrieve_response_code($response));
                
                if ($status_code >= 500 || $status_code === 429) {
                    $retry_count++;
                    if ($retry_count <= $max_retries) {
                        usleep(500000 * $retry_count);
                        continue;
                    }
                }
            } else {
                $retry_count++;
                if ($retry_count <= $max_retries) {
                    usleep(500000 * $retry_count);
                    continue;
                }
            }
            
            return $response;
        }
        
        return new WP_Error('max_retries_exceeded', __('Maximum retry attempts exceeded', 'botfend-anti-bot-firewall'));
    }
    
    /**
     * Get cache TTL based on IP type
     *
     * @param int $block_code
     * @return int
     */
    private function get_cache_ttl_for_ip(int $block_code): int {
        switch ($block_code) {
            case 0:
                return DAY_IN_SECONDS * 5;
            case 1:
                return DAY_IN_SECONDS;
            case 2:
                return HOUR_IN_SECONDS * 12;
            default:
                return DAY_IN_SECONDS;
        }
    }
    
    /**
     * Interpret IPHub block codes with proper mode handling
     *
     * @param int   $block_code
     * @param array $data
     * @return array
     */
    private function interpret_block_code(int $block_code, array $data): array {
        switch ($block_code) {
            case 0:
                return [
                    'action' => 'allow', 
                    'reason' => __('Residential IP', 'botfend-anti-bot-firewall'),
                    'block_code' => $block_code
                ];
                
            case 1:
                switch ($this->blocking_mode) {
                    case 'strict':
                        return [
                            'action' => 'block', 
                            'reason' => __('Non-residential IP (Hosting/Proxy/VPN)', 'botfend-anti-bot-firewall'),
                            'block_code' => $block_code
                        ];
                    case 'moderate':
                    case 'lenient':
                    default:
                        return [
                            'action' => 'allow', 
                            'reason' => __('Non-residential IP (VPN/Proxy allowed)', 'botfend-anti-bot-firewall'),
                            'block_code' => $block_code
                        ];
                }
                
            case 2:
                switch ($this->blocking_mode) {
                    case 'strict':
                    case 'moderate':
                        return [
                            'action' => 'block', 
                            'reason' => __('Non-residential IP (Suspicious)', 'botfend-anti-bot-firewall'),
                            'block_code' => $block_code
                        ];
                    case 'lenient':
                    default:
                        return [
                            'action' => 'allow', 
                            'reason' => __('Non-residential IP (Suspicious - Allowed in lenient mode)', 'botfend-anti-bot-firewall'),
                            'block_code' => $block_code
                        ];
                }
                
            default:
                return [
                    'action' => 'allow', 
                    'reason' => __('Unknown IP type', 'botfend-anti-bot-firewall'),
                    'block_code' => $block_code
                ];
        }
    }
    
    /**
     * Sanitize API data
     *
     * @param mixed $data
     * @return array
     */
    private function sanitize_api_data($data): array {
        if (!is_array($data)) {
            return [];
        }
        
        $sanitized = [];
        foreach ($data as $key => $value) {
            $safe_key = sanitize_key((string) $key);
            
            if (is_array($value)) {
                $sanitized[$safe_key] = $this->sanitize_api_data($value);
            } elseif (is_string($value)) {
                $sanitized[$safe_key] = sanitize_text_field($value);
            } elseif (is_numeric($value)) {
                $sanitized[$safe_key] = is_int($value) ? $value : (float) $value;
            } elseif (is_bool($value)) {
                $sanitized[$safe_key] = $value;
            } else {
                $sanitized[$safe_key] = null;
            }
        }
        
        return $sanitized;
    }
    
    /**
     * Sanitize API result
     *
     * @param mixed $result
     * @return mixed
     */
    private function sanitize_api_result($result) {
        if (!is_array($result)) {
            return $result;
        }
        
        $sanitized = [];
        foreach ($result as $key => $value) {
            $safe_key = sanitize_key($key);
            
            if (is_array($value)) {
                $sanitized[$safe_key] = $this->sanitize_api_result($value);
            } elseif ($key === 'action' || $key === 'reason') {
                $sanitized[$safe_key] = sanitize_text_field($value);
            } elseif ($key === 'block_code') {
                $sanitized[$safe_key] = absint($value);
            } else {
                $sanitized[$safe_key] = sanitize_text_field((string) $value);
            }
        }
        
        return $sanitized;
    }
    
    /**
     * Check IP against IPHub
     * 
     * This method will return false (no action) if IPHub is not enabled by user.
     *
     * @param string $ip
     * @return array|false
     */
    public function check_ip(string $ip): array|false {
        // CRITICAL: IPHub is DISABLED BY DEFAULT - return false if not enabled
        if (!$this->is_enabled()) {
            return false;
        }
        
        if (empty($this->api_key)) {
            return false;
        }
        
        if (!$this->is_valid_ip($ip)) {
            return false;
        }
        
        if ($this->is_private_ip($ip)) {
            return false;
        }
        
        // Skip during cron jobs to prevent API quota waste
        if (wp_doing_cron()) {
            return false;
        }
        
        // Skip Facebook IPs - they should never be checked by IPHub
        if ($this->utilities && method_exists($this->utilities, 'is_facebook_ip')) {
            if ($this->utilities->is_facebook_ip($ip)) {
                return [
                    'action' => 'allow', 
                    'reason' => __('Facebook infrastructure IP', 'botfend-anti-bot-firewall'),
                    'block_code' => 0
                ];
            }
        }
        
        // Skip Apple Private Relay IPs to prevent blocking iPhone users
        if ($this->utilities && method_exists($this->utilities, 'is_apple_ip')) {
            if ($this->utilities->is_apple_ip($ip)) {
                return [
                    'action' => 'allow', 
                    'reason' => __('Apple Private Relay / iCloud', 'botfend-anti-bot-firewall'),
                    'block_code' => 0
                ];
            }
        }
        
        // Check for legitimate crawlers with caching
        $user_agent = isset($_SERVER['HTTP_USER_AGENT']) 
            ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) 
            : '';
        
        if ($this->is_legitimate_crawler_cached($ip, $user_agent)) {
            return [
                'action' => 'allow',
                'reason' => __('Legitimate crawler - skipping IPHub check', 'botfend-anti-bot-firewall'),
                'block_code' => 0
            ];
        }
        
        // Versioned cache key for instant invalidation across all cache backends
        $cache_version = get_option('botfend_iphub_cache_version', '1');
        $cache_key = 'botfend_iphub_v' . $cache_version . '_' . md5($ip);
        
        $cached = get_transient($cache_key);
        
        if (false !== $cached) {
            return is_array($cached) ? $this->sanitize_api_result($cached) : false;
        }
        
        $response = $this->make_api_request_with_retry($ip);
        
        if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
            set_transient($cache_key, false, HOUR_IN_SECONDS);
            return false;
        }
        
        $body = json_decode(wp_remote_retrieve_body($response), true);
        
        if (!isset($body['block'])) {
            set_transient($cache_key, false, HOUR_IN_SECONDS);
            return false;
        }
        
        $result = $this->interpret_block_code(
            absint($body['block']), 
            $this->sanitize_api_data($body)
        );
        
        $cache_ttl = $this->get_cache_ttl_for_ip(absint($body['block']));
        set_transient($cache_key, $result, $cache_ttl);
        
        return $this->sanitize_api_result($result);
    }
    
    /**
     * Test API connection
     *
     * @return array
     */
    public function test_api(): array {
        if (!$this->is_enabled()) {
            return [
                'success' => false, 
                'message' => __('IPHub is disabled. Please enable it in settings first.', 'botfend-anti-bot-firewall')
            ];
        }
        
        if (empty($this->api_key)) {
            return [
                'success' => false, 
                'message' => __('API key not set', 'botfend-anti-bot-firewall')
            ];
        }
        
        $result = $this->check_ip('8.8.8.8');
        
        if ($result === false) {
            return [
                'success' => false, 
                'message' => __('API connection failed. Check your API key.', 'botfend-anti-bot-firewall')
            ];
        }
        
        return [
            'success' => true, 
            'message' => __('API connected successfully', 'botfend-anti-bot-firewall'),
            'result' => $this->sanitize_api_result($result)
        ];
    }
    
    /**
     * Get API usage statistics
     *
     * @return array
     */
    public function get_api_statistics(): array {
        global $wpdb;
        
        $table = $wpdb->prefix . 'botfend_logs';
        
        $table_exists = $wpdb->get_var($wpdb->prepare(
            "SHOW TABLES LIKE %s",
            $wpdb->esc_like($table)
        ));
        
        if ($table_exists !== $table) {
            return [
                'enabled' => $this->is_enabled(),
                'api_key_set' => !empty($this->api_key),
                'blocking_mode' => $this->blocking_mode,
                'total_blocks' => 0,
                'last_check' => __('Never', 'botfend-anti-bot-firewall'),
            ];
        }
        
        $total_blocks = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM `{$table}` WHERE action = %s",
            'iphub_block'
        ));
        
        $last_check = $wpdb->get_var($wpdb->prepare(
            "SELECT created_at FROM `{$table}` WHERE action = %s ORDER BY created_at DESC LIMIT 1",
            'iphub_block'
        ));
        
        return [
            'enabled' => $this->is_enabled(),
            'api_key_set' => !empty($this->api_key),
            'blocking_mode' => $this->blocking_mode,
            'total_blocks' => $total_blocks,
            'last_check' => $last_check ?: __('Never', 'botfend-anti-bot-firewall'),
        ];
    }
    
    /**
     * Validate API key format
     *
     * @param string|null $api_key
     * @return array
     */
    public function validate_api_key(?string $api_key = null): array {
        if (!$this->is_enabled()) {
            return [
                'valid' => false,
                'message' => __('IPHub is disabled. Please enable it in settings first.', 'botfend-anti-bot-firewall')
            ];
        }
        
        $api_key = $api_key ?: $this->api_key;
        
        if (empty($api_key)) {
            return [
                'valid' => false,
                'message' => __('API key is empty', 'botfend-anti-bot-firewall')
            ];
        }
        
        if (!preg_match('/^[a-zA-Z0-9]{20,}$/', $api_key)) {
            return [
                'valid' => false,
                'message' => __('API key format appears invalid', 'botfend-anti-bot-firewall')
            ];
        }
        
        $test_result = $this->test_api();
        
        if (!$test_result['success']) {
            return [
                'valid' => false,
                'message' => $test_result['message']
            ];
        }
        
        return [
            'valid' => true,
            'message' => __('API key is valid', 'botfend-anti-bot-firewall')
        ];
    }
    
    /**
     * Clear API cache for specific IP or all IPs
     * Uses version-based invalidation for O(1) performance across all cache backends
     *
     * @param string|null $ip
     * @return bool
     */
    public function clear_cache(?string $ip = null): bool {
        $cache_version = get_option('botfend_iphub_cache_version', '1');
        
        if ($ip) {
            $cache_key = 'botfend_iphub_v' . $cache_version . '_' . md5($ip);
            delete_transient($cache_key);
            return true;
        }
        
        update_option('botfend_iphub_cache_version', time());
        self::$crawler_cache = [];
        
        return true;
    }
    
    /**
     * Get mode description for display
     *
     * @param string|null $mode
     * @return string
     */
    public function get_mode_description(?string $mode = null): string {
        $mode = $mode ?: $this->blocking_mode;
        
        $descriptions = [
            'strict' => __('Strict - Block ALL non-residential IPs (VPN/Proxy & Suspicious)', 'botfend-anti-bot-firewall'),
            'moderate' => __('Moderate - Block ONLY suspicious IPs (Allow VPN/Proxy)', 'botfend-anti-bot-firewall'),
            'lenient' => __('Lenient - Allow all IPs (Minimal blocking)', 'botfend-anti-bot-firewall')
        ];
        
        return $descriptions[$mode] ?? $descriptions['moderate'];
    }
    
    /**
     * Clear crawler cache
     *
     * @return void
     */
    public static function clear_crawler_cache(): void {
        self::$crawler_cache = [];
    }
    
    /**
     * Get cache statistics
     *
     * @return array
     */
    public function get_cache_stats(): array {
        return [
            'enabled' => $this->is_enabled(),
            'crawler_cache_size' => count(self::$crawler_cache),
            'max_crawler_cache' => self::MAX_CRAWLER_CACHE,
        ];
    }
}