<?php
/**
 * AbuseIPDB API integration for BotFend Anti-Bot Firewall
 *
 * @package BotFend\ThirdParty
 * @version 2.1.3 - Ultimate: Combined circuit breaker, type safety, octal/hex blocking, cron optimization
 * @version 4.8.3 - Rebranded from SafeBot, external service disabled by default
 */

namespace BotFend\ThirdParty;

use BotFend\Core\Config;
use BotFend\Database\Database;
use BotFend\Utilities\Utilities;

defined('ABSPATH') || exit;

/**
 * Class AbuseIPDB
 *
 * Handles AbuseIPDB API integration for IP reputation checks
 * 
 * IMPORTANT: AbuseIPDB is an external service that requires explicit opt-in.
 * This service is DISABLED BY DEFAULT for privacy compliance.
 * 
 * Features:
 * - Per-IP 24-hour throttling (each IP checked once per day)
 * - Circuit breaker pattern (stops all calls when globally rate limited)
 * - Granular error type tracking
 * - Rate-limited error logging
 * - Crawler verification caching
 * - Apple Private Relay & Facebook infrastructure bypasses
 * - Full type safety with PHP 8.0+ features
 * - Octal/hex IP blocking for security
 * - Cron skip check for quota optimization
 * - Redis/Memcached Object Cache compatible invalidation
 */
class AbuseIPDB {

    /**
     * Config instance
     *
     * @var Config
     */
    private Config $config;

    /**
     * Database instance
     *
     * @var Database|null
     */
    private ?Database $database = null;

    /**
     * API key
     *
     * @var string
     */
    private string $api_key;

    /**
     * Confidence threshold
     *
     * @var int
     */
    private int $confidence_threshold;

    /**
     * Action mode
     *
     * @var string
     */
    private string $action_mode;

    /**
     * Whether module is enabled
     *
     * @var bool
     */
    private bool $enabled;

    /**
     * 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;

    /**
     * Error logging rate limit TTL (1 hour)
     */
    private const ERROR_LOG_RATE_LIMIT = HOUR_IN_SECONDS;

    /**
     * Circuit breaker suspension TTL (24 hours - matches free API quota reset)
     */
    private const CIRCUIT_BREAKER_TTL = DAY_IN_SECONDS;

    /**
     * Per-IP cache TTL (24 hours - check each IP once per day)
     */
    private const IP_CACHE_TTL = DAY_IN_SECONDS;

    /**
     * Constructor
     *
     * @param Database|null $database Database instance
     */
    public function __construct(?Database $database = null) {
        $this->config = Config::get_instance();
        $this->database = $database;
        
        $this->load_configuration();
    }

    /**
     * Load configuration from settings
     *
     * @return void
     */
    private function load_configuration(): void {
        // AbuseIPDB is DISABLED BY DEFAULT (requires explicit opt-in)
        $this->enabled = $this->config->get_bool('enable_abuseipdb', false);
        $this->api_key = $this->config->get_string('abuseipdb_api_key', '');
        $this->confidence_threshold = $this->config->get_int('abuseipdb_confidence', 80);
        $this->action_mode = $this->config->get_string('abuseipdb_action', 'block');
    }

    /**
     * Check if module is enabled
     * Returns false unless user has explicitly enabled AND provided API key
     *
     * @return bool
     */
    public function is_enabled(): bool {
        return $this->enabled && !empty($this->api_key);
    }

    /**
     * Get dynamic cache prefix to support Object Cache invalidation
     *
     * @return string
     */
    private function get_cache_prefix(): string {
        $version = get_option('botfend_abuseipdb_cache_version', '1');
        return 'botfend_abuseipdb_v' . $version . '_';
    }

    /**
     * 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 (empty($user_agent)) {
            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);
        }
        
        $utilities = new Utilities();
        $result = $utilities->verify_legitimate_crawler($ip, $user_agent);
        
        self::$crawler_cache[$cache_key] = $result;
        
        return $result;
    }

    /**
     * Validate IP address with security hardening
     *
     * @param string $ip
     * @return bool
     */
    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;
        }
        
        // filter_var inherently prevents SQL injection/XSS payloads
        return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE) !== false;
    }

    /**
     * Check if IP is private
     *
     * @param string $ip
     * @return bool
     */
    private function is_private_ip(string $ip): bool {
        $private_ranges = [
            '10.0.0.0/8',
            '172.16.0.0/12',
            '192.168.0.0/16',
            '127.0.0.0/8',
            '169.254.0.0/16',
            'fc00::/7',
            'fe80::/10',
            '::1/128'
        ];
        
        $utilities = new Utilities();
        
        foreach ($private_ranges as $range) {
            if ($utilities->ip_in_range($ip, $range)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Check if error should be logged (rate-limited)
     *
     * @param string $error_type Type of error
     * @return bool True if should log
     */
    private function should_log_error(string $error_type): bool {
        $cache_key = $this->get_cache_prefix() . 'botfend_error_log_' . $error_type;
        $last_logged = get_transient($cache_key);
        
        if ($last_logged === false) {
            set_transient($cache_key, time(), self::ERROR_LOG_RATE_LIMIT);
            return true;
        }
        
        return false;
    }

    /**
     * Log API errors (rate-limited)
     *
     * @param string $ip IP address or 'SYSTEM' for global issues
     * @param string $error Error message
     * @param string $error_type Type of error for rate limiting
     * @return void
     */
    private function log_api_error(string $ip, string $error, string $error_type = 'general'): void {
        if (!$this->database) {
            return;
        }
        
        if (!$this->should_log_error($error_type)) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log(sprintf(
                    'BotFend AbuseIPDB (suppressed): %s - IP: %s - Type: %s',
                    $error,
                    $ip,
                    $error_type
                ));
            }
            return;
        }
        
        $this->database->log_request(
            sanitize_text_field($ip),
            0,
            'abuseipdb_error',
            [sprintf(
                __('AbuseIPDB API Error: %s', 'botfend-anti-bot-firewall'),
                sanitize_text_field($error)
            )],
            'AbuseIPDB API',
            false,
            null
        );
        
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log(sprintf(
                'BotFend AbuseIPDB Error (logged): %s - IP: %s - Type: %s',
                $error,
                $ip,
                $error_type
            ));
        }
    }

    /**
     * Sanitize AbuseIPDB data
     *
     * @param array $data
     * @return array
     */
    private function sanitize_abuse_data(array $data): array {
        $sanitized = [];
        
        if (isset($data['abuseConfidenceScore'])) {
            $sanitized['abuseConfidenceScore'] = absint($data['abuseConfidenceScore']);
        }
        
        if (isset($data['totalReports'])) {
            $sanitized['totalReports'] = absint($data['totalReports']);
        }
        
        if (isset($data['countryCode'])) {
            $sanitized['countryCode'] = sanitize_text_field($data['countryCode']);
        }
        
        if (isset($data['isp'])) {
            $sanitized['isp'] = sanitize_text_field($data['isp']);
        }
        
        if (isset($data['domain'])) {
            $sanitized['domain'] = sanitize_text_field($data['domain']);
        }
        
        if (isset($data['isWhitelisted'])) {
            $sanitized['isWhitelisted'] = (bool) $data['isWhitelisted'];
        }
        
        return $sanitized;
    }

    /**
     * Interpret AbuseIPDB data
     *
     * @param array $data
     * @return array
     */
    private function interpret_abuse_data(array $data): array {
        $confidence_score = $data['abuseConfidenceScore'] ?? 0;
        $is_whitelisted = $data['isWhitelisted'] ?? false;
        
        if ($is_whitelisted) {
            return [
                'action' => 'allow',
                'reason' => sprintf(
                    __('AbuseIPDB: Whitelisted (%d%% confidence)', 'botfend-anti-bot-firewall'),
                    $confidence_score
                ),
                'confidence' => $confidence_score,
                'whitelisted' => true
            ];
        }
        
        if ($confidence_score >= $this->confidence_threshold) {
            return [
                'action' => $this->action_mode,
                'reason' => sprintf(
                    __('AbuseIPDB: High risk (%d%% confidence)', 'botfend-anti-bot-firewall'),
                    $confidence_score
                ),
                'confidence' => $confidence_score,
                'score' => min(10, (int) ceil($confidence_score / 10))
            ];
        }
        
        if ($confidence_score >= 50) {
            return [
                'action' => 'challenge',
                'reason' => sprintf(
                    __('AbuseIPDB: Moderate risk (%d%% confidence)', 'botfend-anti-bot-firewall'),
                    $confidence_score
                ),
                'confidence' => $confidence_score
            ];
        }
        
        return [
            'action' => 'allow',
            'reason' => sprintf(
                __('AbuseIPDB: Low risk (%d%% confidence)', 'botfend-anti-bot-firewall'),
                $confidence_score
            ),
            'confidence' => $confidence_score
        ];
    }

    /**
     * Get cached result for an IP
     *
     * @param string $cache_key
     * @return array|false
     */
    private function get_cached_result(string $cache_key): array|false {
        $cached = get_transient($cache_key);
        
        if (false !== $cached) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('BotFend AbuseIPDB: Returning cached result');
            }
            return $cached;
        }
        
        return false;
    }

    /**
     * Cache the result for an IP
     *
     * @param string $cache_key
     * @param array|false $result
     * @return void
     */
    private function cache_result(string $cache_key, $result): void {
        set_transient($cache_key, $result, self::IP_CACHE_TTL);
        
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log(sprintf(
                'BotFend AbuseIPDB: Cached result for 24 hours (Result: %s)',
                $result === false ? 'false' : ($result['action'] ?? 'unknown')
            ));
        }
    }

    /**
     * Check IP against AbuseIPDB
     * 
     * Returns false if AbuseIPDB is not enabled (default state)
     *
     * @param string $ip
     * @return array|false
     */
    public function check_ip(string $ip): array|false {
        // CRITICAL: AbuseIPDB is DISABLED BY DEFAULT
        if (!$this->is_enabled()) {
            return false;
        }
        
        // Skip during cron jobs to prevent API quota waste
        if (wp_doing_cron()) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('BotFend AbuseIPDB: Skipping check during cron job');
            }
            return false;
        }

        // Check if the API is globally rate-limited (Circuit Breaker)
        if (get_transient('botfend_abuseipdb_suspended')) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('BotFend AbuseIPDB: Circuit breaker active - skipping API call');
            }
            return false;
        }
        
        // Validate IP
        if (!$this->is_valid_ip($ip)) {
            return false;
        }
        
        // Skip private IPs
        if ($this->is_private_ip($ip)) {
            return false;
        }

        $utilities = new Utilities();

        // Skip Facebook IPs - they should never be checked
        if (method_exists($utilities, 'is_facebook_ip')) {
            if ($utilities->is_facebook_ip($ip)) {
                return [
                    'action' => 'allow',
                    'reason' => __('Facebook infrastructure IP', 'botfend-anti-bot-firewall'),
                    'confidence' => 0,
                    'whitelisted' => true
                ];
            }
        }

        // Skip Apple Private Relay IPs to prevent blocking iPhone users
        if (method_exists($utilities, 'is_apple_ip')) {
            if ($utilities->is_apple_ip($ip)) {
                return [
                    'action' => 'allow',
                    'reason' => __('Apple Private Relay / iCloud', 'botfend-anti-bot-firewall'),
                    'confidence' => 0,
                    'whitelisted' => true
                ];
            }
        }
        
        // Check for legitimate crawlers first 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 AbuseIPDB check', 'botfend-anti-bot-firewall'),
                'confidence' => 0,
                'crawler' => true
            ];
        }
        
        // Check per-IP cache (24 hours) using dynamically versioned key
        $cache_key = $this->get_cache_prefix() . md5($ip);
        $cached = $this->get_cached_result($cache_key);
        
        if (false !== $cached) {
            return $cached;
        }
        
        // Make API request
        $response = wp_safe_remote_get(
            add_query_arg(
                [
                    'ipAddress' => urlencode($ip),
                    'maxAgeInDays' => 30
                ],
                'https://api.abuseipdb.com/api/v2/check'
            ),
            [
                'timeout' => 5,
                'headers' => [
                    'Key' => $this->api_key,
                    'Accept' => 'application/json',
                    'User-Agent' => 'BotFend Anti-Bot Firewall/' . BOTFEND_VERSION
                ],
                'sslverify' => true,
                'httpversion' => '1.1'
            ]
        );
        
        if (is_wp_error($response)) {
            $this->log_api_error($ip, $response->get_error_message(), 'connection_error');
            set_transient($cache_key, false, HOUR_IN_SECONDS);
            return false;
        }
        
        $status_code = wp_remote_retrieve_response_code($response);
        
        switch ($status_code) {
            case 200:
                break;
                
            case 401:
                $this->log_api_error('SYSTEM', 'Invalid API key (401 Unauthorized)', '401_error');
                set_transient('botfend_abuseipdb_suspended', time() + (DAY_IN_SECONDS * 30), DAY_IN_SECONDS * 30);
                $this->cache_result($cache_key, false);
                return false;
                
            case 429:
                $this->log_api_error('SYSTEM', 'API rate limit exceeded (429). Pausing checks for 24 hours.', '429_error');
                set_transient('botfend_abuseipdb_suspended', time() + DAY_IN_SECONDS, DAY_IN_SECONDS);
                set_transient($cache_key, false, HOUR_IN_SECONDS * 6);
                return false;
                
            case 403:
                $this->log_api_error('SYSTEM', 'Access forbidden (403). Account may be blocked.', '403_error');
                set_transient('botfend_abuseipdb_suspended', time() + (HOUR_IN_SECONDS * 6), HOUR_IN_SECONDS * 6);
                set_transient($cache_key, false, HOUR_IN_SECONDS * 6);
                return false;
                
            case 500:
            case 502:
            case 503:
            case 504:
                $this->log_api_error('SYSTEM', "AbuseIPDB server error ({$status_code})", 'server_error');
                set_transient($cache_key, false, MINUTE_IN_SECONDS * 15);
                return false;
                
            default:
                if ($status_code >= 400) {
                    $this->log_api_error($ip, "HTTP {$status_code} error", "http_{$status_code}_error");
                }
                set_transient($cache_key, false, HOUR_IN_SECONDS);
                return false;
        }
        
        $body = wp_remote_retrieve_body($response);
        
        if (empty($body)) {
            $this->log_api_error($ip, 'Empty response body', 'empty_response');
            set_transient($cache_key, false, HOUR_IN_SECONDS);
            return false;
        }
        
        $data = json_decode($body, true);
        
        if (json_last_error() !== JSON_ERROR_NONE) {
            $this->log_api_error($ip, 'Invalid JSON response: ' . json_last_error_msg(), 'json_error');
            set_transient($cache_key, false, HOUR_IN_SECONDS);
            return false;
        }
        
        if (!isset($data['data']) || !is_array($data['data'])) {
            $this->log_api_error($ip, 'Invalid data structure in response', 'invalid_structure');
            set_transient($cache_key, false, HOUR_IN_SECONDS);
            return false;
        }
        
        $data = $this->sanitize_abuse_data($data['data']);
        $result = $this->interpret_abuse_data($data);
        
        $this->cache_result($cache_key, $result);
        
        return $result;
    }

    /**
     * Test API connection
     *
     * @param string $test_ip Optional IP to test. Defaults to Google DNS.
     * @return array
     */
    public function test_api(string $test_ip = '8.8.8.8'): array {
        if (!$this->is_enabled()) {
            return [
                'success' => false,
                'message' => __('AbuseIPDB 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')
            ];
        }
        
        if (!$this->is_valid_ip($test_ip)) {
            return [
                'success' => false,
                'message' => __('Invalid test IP address', 'botfend-anti-bot-firewall')
            ];
        }
        
        try {
            $cache_key = $this->get_cache_prefix() . md5($test_ip);
            delete_transient($cache_key);
            delete_transient('botfend_abuseipdb_suspended');
            
            $error_types = ['429_error', '401_error', '403_error', 'connection_error', 'server_error'];
            foreach ($error_types as $error_type) {
                delete_transient($this->get_cache_prefix() . 'error_log_' . $error_type);
            }
            
            $result = $this->check_ip($test_ip);
            
            if ($result === false) {
                return [
                    'success' => false,
                    'message' => __('API connection failed or rate limited.', 'botfend-anti-bot-firewall')
                ];
            }
            
            if (!isset($result['action']) || !isset($result['reason'])) {
                return [
                    'success' => false,
                    'message' => __('Invalid API response format', 'botfend-anti-bot-firewall')
                ];
            }
            
            return [
                'success' => true,
                'message' => __('API connected successfully', 'botfend-anti-bot-firewall'),
                'result' => [
                    'action' => sanitize_text_field($result['action']),
                    'reason' => sanitize_text_field($result['reason']),
                    'confidence' => $result['confidence'] ?? 0
                ]
            ];
        } catch (\Exception $e) {
            return [
                'success' => false,
                'message' => sprintf(
                    __('Exception: %s', 'botfend-anti-bot-firewall'),
                    $e->getMessage()
                )
            ];
        }
    }

    /**
     * Clear all cached AbuseIPDB results
     *
     * @return bool
     */
    public function clear_cache(): bool {
        global $wpdb;
        
        $current_version = (int) get_option('botfend_abuseipdb_cache_version', '1');
        update_option('botfend_abuseipdb_cache_version', $current_version + 1);
        
        if (!wp_using_ext_object_cache()) {
            $wpdb->query(
                $wpdb->prepare(
                    "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
                    $wpdb->esc_like('_transient_botfend_abuseipdb_') . '%',
                    $wpdb->esc_like('_transient_timeout_botfend_abuseipdb_') . '%'
                )
            );
        }
        
        delete_transient('botfend_abuseipdb_suspended');
        self::$crawler_cache = [];
        
        return true;
    }

    /**
     * Clear cache for a specific IP
     *
     * @param string $ip IP address
     * @return bool
     */
    public function clear_ip_cache(string $ip): bool {
        if (!$this->is_valid_ip($ip)) {
            return false;
        }
        
        $cache_key = $this->get_cache_prefix() . md5($ip);
        return delete_transient($cache_key);
    }

    /**
     * Clear crawler cache (useful for testing)
     *
     * @return void
     */
    public static function clear_crawler_cache(): void {
        self::$crawler_cache = [];
    }

    /**
     * Manually reset circuit breaker (for admin tools)
     *
     * @return void
     */
    public function reset_circuit_breaker(): void {
        delete_transient('botfend_abuseipdb_suspended');
        
        $error_types = ['429_error', '401_error', '403_error', 'connection_error', 'server_error', 'json_error', 'empty_response', 'invalid_structure'];
        foreach ($error_types as $error_type) {
            delete_transient($this->get_cache_prefix() . 'error_log_' . $error_type);
        }
        
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('BotFend AbuseIPDB: Circuit breaker manually reset');
        }
    }

    /**
     * Get circuit breaker status
     *
     * @return array
     */
    public function get_circuit_breaker_status(): array {
        $suspended_until = get_transient('botfend_abuseipdb_suspended');
        $suspended = (bool) $suspended_until;
        
        $error_status = [];
        $error_types = ['429_error', '401_error', '403_error', 'connection_error', 'server_error'];
        foreach ($error_types as $error_type) {
            $last_logged = get_transient($this->get_cache_prefix() . 'error_log_' . $error_type);
            $error_status[$error_type] = $last_logged ? date('Y-m-d H:i:s', $last_logged) : null;
        }
        
        return [
            'enabled' => $this->is_enabled(),
            'suspended' => $suspended,
            'suspended_until' => $suspended_until ? date('Y-m-d H:i:s', $suspended_until) : null,
            'error_log_status' => $error_status,
            'crawler_cache_size' => count(self::$crawler_cache),
            'max_crawler_cache' => self::MAX_CRAWLER_CACHE,
        ];
    }

    /**
     * Get cache statistics (for debugging)
     *
     * @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,
            'circuit_breaker_active' => (bool) get_transient('botfend_abuseipdb_suspended'),
        ];
    }
}