<?php
/**
 * Cloudflare Integration for BotFend Anti-Bot Firewall
 *
 * Main integration class that hooks into BotFend events and manages
 * the Cloudflare sync functionality with async processing,
 * security hardening, and comprehensive error handling.
 *
 * @package BotFend\Cloudflare
 * @since 4.9.0
 */

namespace BotFend\Cloudflare;

use BotFend\Core\Config;
use BotFend\Security\Encryption;

defined('ABSPATH') || exit;

/**
 * Cloudflare Integration Class
 *
 * Production-ready implementation with:
 * - Async sync processing to prevent blocking
 * - IP spoofing protection warnings
 * - Consolidated configuration checks
 * - Robust cron handling
 * - Comprehensive logging
 * - Health check capabilities
 * - Graceful degradation
 *
 * @since 4.9.0
 */
class CloudflareIntegration {

    /**
     * Singleton instance.
     *
     * @since 4.9.0
     * @var self|null
     */
    private static $instance = null;

    /**
     * Config instance.
     *
     * @since 4.9.0
     * @var Config
     */
    private $config;
    /**
     * Cloudflare API instance.
     *
     * @since 4.9.0
     * @var CloudflareAPI|null
     */
    private $api = null;

    /**
     * Cloudflare Sync instance.
     *
     * @since 4.9.0
     * @var CloudflareSync|null
     */
    private $sync = null;

    /**
     * Whether integration is enabled.
     *
     * @since 4.9.0
     * @var bool
     */
    private $enabled = false;

    /**
     * Whether sync is configured.
     *
     * @since 4.9.0
     * @var bool
     */
    private $configured = false;

    /**
     * Encryption instance.
     *
     * @since 4.9.0
     * @var Encryption
     */
    private $encryption;

    /**
     * Queue of IPs to sync (for async processing).
     *
     * @since 4.9.0
     * @var array
     */
    private $sync_queue = [];

    /**     * Private constructor for singleton pattern.
     *
     * @since 4.9.0
     */
    private function __construct() {
        $this->config     = Config::get_instance();
        $this->encryption = Encryption::get_instance();
        $this->enabled    = $this->config->get_bool('cf_enabled', false);
        
        // Check configuration early
        $this->configured = $this->check_configuration();

        // Initialize hooks
        $this->register_hooks();
    }

    /**
     * Get singleton instance.
     *
     * @since 4.9.0
     *
     * @return self
     */
    public static function get_instance(): self {
        if (null === self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Register all WordPress hooks.
     *
     * @since 4.9.0
     *
     * @return void
     */
    private function register_hooks(): void {
        // Core initialization
        add_action('plugins_loaded', [$this, 'init'], 20);
        
        // BotFend event hooks
        add_action('botfend_ip_blocked', [$this, 'on_ip_blocked'], 10, 4);
        add_action('botfend_ip_unblocked', [$this, 'on_ip_unblocked'], 10, 1);
        
        // IP restoration (high priority)
        add_filter('botfend_get_client_ip', [$this, 'restore_visitor_ip'], 5);
        
        // Cron jobs
        add_action('init', [$this, 'register_cron_jobs']);        add_action('botfend_cf_clean_expired', [$this, 'clean_expired_blocks']);
        add_action('botfend_cf_sync_ip', [$this, 'process_sync_queue_item'], 10, 5);
        
        // Async processing on shutdown
        add_action('shutdown', [$this, 'process_shutdown_queue']);
        
        // Admin notices
        add_action('admin_notices', [$this, 'display_admin_notices']);
        
        // Health check
        add_filter('site_status_tests', [$this, 'add_health_check']);
    }

    /**
     * Initialize after plugins loaded.
     *
     * @since 4.9.0
     *
     * @return void
     */
    public function init(): void {
        if (!$this->enabled) {
            return;
        }

        if (!$this->configured) {
            return;
        }

        $this->load_dependencies();
        $this->initialize_services();
        
        /**
         * Fires after Cloudflare integration is initialized.
         *
         * @since 4.9.0
         */
        do_action('botfend_cf_initialized', $this);
    }

    /**
     * Load required dependency files.
     *
     * @since 4.9.0
     *
     * @return void
     */
    private function load_dependencies(): void {
        // Check if classes already loaded (autoloader)
        if (!class_exists('BotFend\\Cloudflare\\CloudflareAPI')) {            $api_file = BOTFEND_PATH . 'includes/cloudflare/CloudflareAPI.php';
            if (file_exists($api_file)) {
                require_once $api_file;
            }
        }
        
        if (!class_exists('BotFend\\Cloudflare\\CloudflareSync')) {
            $sync_file = BOTFEND_PATH . 'includes/cloudflare/CloudflareSync.php';
            if (file_exists($sync_file)) {
                require_once $sync_file;
            }
        }
    }

    /**
     * Initialize API and Sync services.
     *
     * @since 4.9.0
     *
     * @return void
     */
    private function initialize_services(): void {
        try {
            $this->api = new CloudflareAPI($this->config, $this->encryption);
            $this->sync = new CloudflareSync($this->api, $this->config);
            
            // Verify API connection periodically (once per hour)
            $this->maybe_verify_connection();
            
        } catch (\Exception $e) {
            $this->log_error('init_failed', 'N/A', $e->getMessage());
            $this->api = null;
            $this->sync = null;
        }
    }

    /**
     * Check if Cloudflare is properly configured.
     *
     * @since 4.9.0
     *
     * @return bool
     */
    private function check_configuration(): bool {
        $auth_method = $this->config->get_string('cf_auth_method', 'token');
        $zone_id     = $this->config->get_string('cf_zone_id', '');

        if (empty($zone_id)) {
            return false;
        }
        // Validate zone ID format
        if (!preg_match('/^[a-f0-9]{32}$/i', $zone_id)) {
            return false;
        }

        if ('token' === $auth_method) {
            $token = $this->get_decrypted_credential('cf_api_token');
            return !empty($token);
        }

        $email = $this->config->get_string('cf_email', '');
        $key   = $this->get_decrypted_credential('cf_api_key');
        
        return !empty($email) && !empty($key) && is_email($email);
    }

    /**
     * Get decrypted credential value.
     *
     * @since 4.9.0
     *
     * @param string $key Option key.
     * @return string Decrypted value.
     */
    private function get_decrypted_credential(string $key): string {
        $value = $this->config->get_string($key, '');
        
        if (empty($value)) {
            return '';
        }
        
        if ($this->encryption->is_encrypted($value)) {
            $decrypted = $this->encryption->decrypt($value);
            return $decrypted !== false ? $decrypted : '';
        }
        
        return $value;
    }

    /**
     * Handle BotFend IP blocked event.
     *
     * @since 4.9.0
     *
     * @param string $ip     Blocked IP address.
     * @param string $type   Block type.
     * @param int    $score  Threat score.
     * @param string $reason Block reason.
     * @return void     */
    public function on_ip_blocked(string $ip, string $type, int $score, string $reason): void {
        if (!$this->should_sync_block($ip, $type, $score)) {
            return;
        }

        /**
         * Filter whether to sync this specific block.
         *
         * @since 4.9.0
         * @param bool   $sync   Whether to sync.
         * @param string $ip     IP address.
         * @param string $type   Block type.
         * @param int    $score  Threat score.
         */
        $should_sync = apply_filters('botfend_cf_should_sync_block', true, $ip, $type, $score);
        
        if (!$should_sync) {
            return;
        }

        // Determine if async processing is enabled
        $async = $this->config->get_bool('cf_async_sync', true);
        $async = apply_filters('botfend_cf_use_async_sync', $async, $ip, $type, $score);

        if ($async) {
            // Queue for async processing
            $this->queue_sync_action('add', $ip, $type, $score, $reason);
        } else {
            // Synchronous processing
            $this->sync_ip_block($ip, $type, $score, $reason);
        }
    }

    /**
     * Handle BotFend IP unblocked event.
     *
     * @since 4.9.0
     *
     * @param string $ip Unblocked IP address.
     * @return void
     */
    public function on_ip_unblocked(string $ip): void {
        if (!$this->sync) {
            return;
        }

        $sync_enabled = $this->config->get_bool('cf_sync_blocks', true);
        if (!$sync_enabled) {
            return;        }

        // Determine if async processing is enabled
        $async = $this->config->get_bool('cf_async_sync', true);
        $async = apply_filters('botfend_cf_use_async_sync', $async, $ip, 'unblock', 0);

        if ($async) {
            $this->queue_sync_action('remove', $ip);
        } else {
            $this->sync->remove_ip_from_blocklist($ip);
        }
    }

    /**
     * Queue a sync action for async processing.
     *
     * @since 4.9.0
     *
     * @param string $action Action type (add/remove).
     * @param string $ip     IP address.
     * @param string $type   Block type.
     * @param int    $score  Threat score.
     * @param string $reason Block reason.
     * @return void
     */
    private function queue_sync_action(string $action, string $ip, string $type = '', int $score = 0, string $reason = ''): void {
        $this->sync_queue[] = [
            'action' => $action,
            'ip'     => $ip,
            'type'   => $type,
            'score'  => $score,
            'reason' => $reason,
        ];

        // Schedule immediate async processing
        if (false === wp_next_scheduled('botfend_cf_sync_ip', [$action, $ip, $type, $score, $reason])) {
            wp_schedule_single_event(
                time(),
                'botfend_cf_sync_ip',
                [$action, $ip, $type, $score, $reason]
            );
        }
    }

    /**
     * Process sync queue item (called by cron).
     *
     * @since 4.9.0
     *
     * @param string $action Action type.     * @param string $ip     IP address.
     * @param string $type   Block type.
     * @param int    $score  Threat score.
     * @param string $reason Block reason.
     * @return void
     */
    public function process_sync_queue_item(string $action, string $ip, string $type = '', int $score = 0, string $reason = ''): void {
        // Ensure services are initialized in cron context
        $this->ensure_services_initialized();

        if (!$this->sync) {
            $this->log_error('sync_not_available', $ip, 'Cannot process sync queue - sync not initialized');
            return;
        }

        if ($action === 'add') {
            $this->sync_ip_block($ip, $type, $score, $reason);
        } elseif ($action === 'remove') {
            $this->sync->remove_ip_from_blocklist($ip);
        }
    }

    /**
     * Process any remaining queue items on shutdown.
     *
     * @since 4.9.0
     *
     * @return void
     */
    public function process_shutdown_queue(): void {
        // Queue is processed via wp_schedule_single_event
        // This is a fallback for edge cases
        if (empty($this->sync_queue)) {
            return;
        }

        // Log that items were queued
        $this->log_debug('shutdown', 'N/A', sprintf('Queued %d sync actions', count($this->sync_queue)));
    }

    /**
     * Sync IP block to Cloudflare.
     *
     * @since 4.9.0
     *
     * @param string $ip     IP address.
     * @param string $type   Block type.
     * @param int    $score  Threat score.
     * @param string $reason Block reason.
     * @return bool     */
    private function sync_ip_block(string $ip, string $type, int $score, string $reason): bool {
        if (!$this->sync) {
            return false;
        }

        $result = $this->sync->add_ip_to_blocklist($ip, $type, $score, $reason);

        if (!$result) {
            $this->log_error('sync_failed', $ip, sprintf('Failed to sync block (type: %s, score: %d)', $type, $score));
        }

        return $result;
    }

    /**
     * Check if block should be synced.
     *
     * @since 4.9.0
     *
     * @param string $ip    IP address.
     * @param string $type  Block type.
     * @param int    $score Threat score.
     * @return bool
     */
    private function should_sync_block(string $ip, string $type, int $score): bool {
        if (!$this->sync) {
            return false;
        }

        $sync_enabled = $this->config->get_bool('cf_sync_blocks', true);
        if (!$sync_enabled) {
            return false;
        }

        $min_score = $this->config->get_int('cf_min_block_score', 5);
        if ($score < $min_score) {
            return false;
        }

        // Check IP whitelist
        $whitelist = $this->config->get_array('cf_sync_whitelist', []);
        if (in_array($ip, $whitelist, true)) {
            return false;
        }

        return true;
    }

    /**     * Restore real visitor IP from Cloudflare headers.
     *
     * ⚠️ SECURITY NOTE: This is only safe if your server is configured to
     * only accept traffic from Cloudflare IPs. Otherwise, attackers can
     * spoof the CF-Connecting-IP header.
     *
     * @see https://developers.cloudflare.com/fundamentals/reference/cloudflare-ip-addresses/
     *
     * @since 4.9.0
     *
     * @param string $ip Original IP.
     * @return string Real visitor IP.
     */
    public function restore_visitor_ip(string $ip): string {
        // Check if Cloudflare IP restoration is enabled
        if (!$this->config->get_bool('cf_restore_ip', true)) {
            return $ip;
        }

        // Priority order of Cloudflare headers
        $headers = [
            'HTTP_CF_CONNECTING_IP',      // Primary
            'HTTP_CF_VISITOR',            // JSON encoded
            'HTTP_X_FORWARDED_FOR',       // Fallback (less reliable)
        ];

        foreach ($headers as $header) {
            if (!isset($_SERVER[$header])) {
                continue;
            }

            $value = sanitize_text_field(wp_unslash($_SERVER[$header]));

            if (empty($value)) {
                continue;
            }

            // Handle CF-Visitor JSON format
            if ($header === 'HTTP_CF_VISITOR') {
                $decoded = json_decode($value, true);
                if (isset($decoded['addr']) && $this->is_valid_ip($decoded['addr'])) {
                    return $decoded['addr'];
                }
                continue;
            }

            // Handle X-Forwarded-For (may contain multiple IPs)
            if ($header === 'HTTP_X_FORWARDED_FOR') {
                $ips = array_map('trim', explode(',', $value));
                foreach ($ips as $forwarded_ip) {                    if ($this->is_valid_ip($forwarded_ip)) {
                        return $forwarded_ip;
                    }
                }
                continue;
            }

            // Direct IP
            if ($this->is_valid_ip($value)) {
                return $value;
            }
        }

        return $ip;
    }

    /**
     * Validate IP address.
     *
     * @since 4.9.0
     *
     * @param string $ip IP address.
     * @return bool
     */
    private function is_valid_ip(string $ip): bool {
        return filter_var($ip, FILTER_VALIDATE_IP) !== false;
    }

    /**
     * Ensure services are initialized (for cron/context switching).
     *
     * @since 4.9.0
     *
     * @return void
     */
    private function ensure_services_initialized(): void {
        if ($this->sync && $this->api) {
            return;
        }

        if (!$this->enabled || !$this->configured) {
            return;
        }

        $this->load_dependencies();
        $this->initialize_services();
    }

    /**
     * Register cron jobs.     *
     * @since 4.9.0
     *
     * @return void
     */
    public function register_cron_jobs(): void {
        // Expired blocks cleanup
        if (!wp_next_scheduled('botfend_cf_clean_expired')) {
            wp_schedule_event(time(), 'daily', 'botfend_cf_clean_expired');
        }

        // Connection verification (hourly)
        if (!wp_next_scheduled('botfend_cf_verify_connection')) {
            wp_schedule_event(time(), 'hourly', 'botfend_cf_verify_connection');
        }
        add_action('botfend_cf_verify_connection', [$this, 'verify_connection']);
    }

    /**
     * Verify Cloudflare connection periodically.
     *
     * @since 4.9.0
     *
     * @return void
     */
    private function maybe_verify_connection(): void {
        $last_check = get_option('botfend_cf_last_connection_check', 0);
        $check_interval = HOUR_IN_SECONDS;

        if (time() - $last_check < $check_interval) {
            return;
        }

        $this->verify_connection();
    }

    /**
     * Verify Cloudflare connection.
     *
     * @since 4.9.0
     *
     * @return bool
     */
    public function verify_connection(): bool {
        $this->ensure_services_initialized();

        if (!$this->api) {
            update_option('botfend_cf_connection_status', 'error');
            return false;
        }
        $result = $this->api->test_connection();
        $status = is_wp_error($result) ? 'error' : 'connected';

        update_option('botfend_cf_connection_status', $status);
        update_option('botfend_cf_last_connection_check', time());

        if (is_wp_error($result)) {
            $this->log_error('connection_failed', 'N/A', $result->get_error_message());
            return false;
        }

        return true;
    }

    /**
     * Clean expired blocks (called by cron).
     *
     * @since 4.9.0
     *
     * @return void
     */
    public function clean_expired_blocks(): void {
        $this->ensure_services_initialized();

        if (!$this->sync) {
            $this->log_error('cleanup_failed', 'N/A', 'Sync not initialized');
            return;
        }

        try {
            $removed = $this->sync->clean_expired_blocks();
            
            if ($removed > 0) {
                $this->log_info('cleanup', 'N/A', sprintf('Cleaned %d expired IP blocks', $removed));
            }
            
        } catch (\Exception $e) {
            $this->log_error('cleanup_exception', 'N/A', $e->getMessage());
        }
    }

    /**
     * Display admin notices for configuration issues.
     *
     * @since 4.9.0
     *
     * @return void
     */
    public function display_admin_notices(): void {        if (!current_user_can('manage_options')) {
            return;
        }

        // Check if enabled but not configured
        if ($this->enabled && !$this->configured) {
            $this->display_notice(
                __('BotFend Cloudflare integration is enabled but not properly configured. Please check your API credentials and Zone ID.', 'botfend-anti-bot-firewall'),
                'error'
            );
            return;
        }

        // Check connection status
        $status = get_option('botfend_cf_connection_status', '');
        if ($status === 'error') {
            $this->display_notice(
                __('BotFend Cloudflare connection failed. Please verify your API credentials and Zone ID.', 'botfend-anti-bot-firewall'),
                'error'
            );
        }

        // Check for IP restoration security warning
        if ($this->config->get_bool('cf_restore_ip', true) && !$this->is_behind_cloudflare()) {
            $this->display_notice(
                sprintf(
                    /* translators: %s: Cloudflare documentation link */
                    __('BotFend is restoring visitor IPs from Cloudflare headers. Ensure your server is configured to only accept traffic from Cloudflare IPs to prevent IP spoofing. <a href="%s" target="_blank">Learn more</a>', 'botfend-anti-bot-firewall'),
                    'https://developers.cloudflare.com/fundamentals/reference/cloudflare-ip-addresses/'
                ),
                'warning'
            );
        }
    }

    /**
     * Display an admin notice.
     *
     * @since 4.9.0
     *
     * @param string $message Notice message.
     * @param string $type    Notice type (error, warning, success, info).
     * @return void
     */
    private function display_notice(string $message, string $type): void {
        $class = 'notice notice-' . sanitize_html_class($type);
        printf(
            '<div class="%1$s is-dismissible"><p><strong>BotFend:</strong> %2$s</p></div>',
            esc_attr($class),
            wp_kses_post($message)        );
    }

    /**
     * Check if server appears to be behind Cloudflare.
     *
     * @since 4.9.0
     *
     * @return bool
     */
    private function is_behind_cloudflare(): bool {
        // Check for Cloudflare headers
        $cf_headers = [
            'HTTP_CF_CONNECTING_IP',
            'HTTP_CF_RAY',
            'HTTP_CF_VISITOR',
            'HTTP_CF_IPCOUNTRY',
        ];

        foreach ($cf_headers as $header) {
            if (!empty($_SERVER[$header])) {
                return true;
            }
        }

        // Check for Cloudflare in remote address ranges
        // This is a basic check; production should validate against CF IP ranges
        return false;
    }

    /**
     * Add health check tests.
     *
     * @since 4.9.0
     *
     * @param array $tests Health check tests.
     * @return array
     */
    public function add_health_check(array $tests): array {
        $tests['direct']['botfend_cloudflare'] = [
            'label' => __('BotFend Cloudflare Integration', 'botfend-anti-bot-firewall'),
            'test'  => [$this, 'health_check_test'],
        ];

        return $tests;
    }

    /**
     * Health check test callback.
     *     * @since 4.9.0
     *
     * @return array
     */
    public function health_check_test(): array {
        $result = [
            'label'       => __('Cloudflare integration is working', 'botfend-anti-bot-firewall'),
            'status'      => 'good',
            'badge'       => [
                'label' => __('Security', 'botfend-anti-bot-firewall'),
                'color' => 'blue',
            ],
            'description' => '<p>' . __('BotFend is successfully syncing blocked IPs to Cloudflare.', 'botfend-anti-bot-firewall') . '</p>',
            'actions'     => '',
            'test'        => 'botfend_cloudflare',
        ];

        if (!$this->enabled) {
            $result['status'] = 'recommended';
            $result['label'] = __('Cloudflare integration is disabled', 'botfend-anti-bot-firewall');
            $result['description'] = '<p>' . __('Enable Cloudflare integration to sync blocked IPs to Cloudflare firewall.', 'botfend-anti-bot-firewall') . '</p>';
            return $result;
        }

        if (!$this->configured) {
            $result['status'] = 'critical';
            $result['label'] = __('Cloudflare integration not configured', 'botfend-anti-bot-firewall');
            $result['description'] = '<p>' . __('Please configure your Cloudflare API credentials and Zone ID.', 'botfend-anti-bot-firewall') . '</p>';
            return $result;
        }

        $status = get_option('botfend_cf_connection_status', '');
        if ($status === 'error') {
            $result['status'] = 'critical';
            $result['label'] = __('Cloudflare connection failed', 'botfend-anti-bot-firewall');
            $result['description'] = '<p>' . __('BotFend cannot connect to Cloudflare API. Check your credentials and network.', 'botfend-anti-bot-firewall') . '</p>';
            return $result;
        }

        return $result;
    }

    /**
     * Get API instance.
     *
     * @since 4.9.0
     *
     * @return CloudflareAPI|null
     */
    public function get_api(): ?CloudflareAPI {        return $this->api;
    }

    /**
     * Get sync instance.
     *
     * @since 4.9.0
     *
     * @return CloudflareSync|null
     */
    public function get_sync(): ?CloudflareSync {
        return $this->sync;
    }

    /**
     * Check if integration is enabled.
     *
     * @since 4.9.0
     *
     * @return bool
     */
    public function is_enabled(): bool {
        return $this->enabled;
    }

    /**
     * Check if integration is configured.
     *
     * @since 4.9.0
     *
     * @return bool
     */
    public function is_configured(): bool {
        return $this->configured;
    }

    /**
     * Get sync status summary.
     *
     * @since 4.9.0
     *
     * @return array
     */
    public function get_status(): array {
        $this->ensure_services_initialized();

        $status = [
            'enabled'     => $this->enabled,
            'configured'  => $this->configured,
            'connected'   => get_option('botfend_cf_connection_status', '') === 'connected',            'last_check'  => get_option('botfend_cf_last_connection_check', 0),
            'async_sync'  => $this->config->get_bool('cf_async_sync', true),
        ];

        if ($this->sync) {
            $status['sync'] = $this->sync->get_sync_status();
        }

        return $status;
    }

    /**
     * Log error message.
     *
     * @since 4.9.0
     *
     * @param string $context Error context.
     * @param string $ip      IP address.
     * @param string $message Error message.
     * @return void
     */
    private function log_error(string $context, string $ip, string $message): void {
        $this->log('error', $context, $ip, $message);
    }

    /**
     * Log info message.
     *
     * @since 4.9.0
     *
     * @param string $context Context.
     * @param string $ip      IP address.
     * @param string $message Message.
     * @return void
     */
    private function log_info(string $context, string $ip, string $message): void {
        $this->log('info', $context, $ip, $message);
    }

    /**
     * Log debug message.
     *
     * @since 4.9.0
     *
     * @param string $context Context.
     * @param string $ip      IP address.
     * @param string $message Message.
     * @return void
     */
    private function log_debug(string $context, string $ip, string $message): void {        $this->log('debug', $context, $ip, $message);
    }

    /**
     * Log message with level.
     *
     * @since 4.9.0
     *
     * @param string $level   Log level.
     * @param string $context Context.
     * @param string $ip      IP address.
     * @param string $message Message.
     * @return void
     */
    private function log(string $level, string $context, string $ip, string $message): void {
        $should_log = (defined('WP_DEBUG') && WP_DEBUG) ||
                      $this->config->get_bool('cf_debug_log', false);

        if ($level === 'debug' && !defined('BOTFEND_DEBUG')) {
            return;
        }

        if (!$should_log && $level !== 'error') {
            return;
        }

        error_log(sprintf(
            '[BotFend CF] [%s] [%s] IP: %s - %s',
            strtoupper($level),
            $context,
            $ip,
            $message
        ));

        /**
         * Fires when a log message is recorded.
         *
         * @since 4.9.0
         */
        do_action('botfend_cf_log', $level, $context, $ip, $message);
    }
}