<?php
/**
 * Selective Scraping Friction
 * 
 * Detects scraping patterns and applies lightweight JS challenges
 * without CAPTCHAs or heavy databases.
 */

namespace HumanGate;

class SelectiveFriction {
    
    private $cookie_name = 'humang_human_verified';
    private $cookie_lifetime = 1800; // 30 minutes
    
    /**
     * Initialize friction system
     */
    public function init() {
        if (!get_option('humang_friction_enabled', true)) {
            return;
        }
        
        // Handle challenge verification
        add_action('wp_ajax_humang_verify_challenge', [$this, 'verify_challenge']);
        add_action('wp_ajax_nopriv_humang_verify_challenge', [$this, 'verify_challenge']);
        
        // Check for friction triggers before page load
        add_action('template_redirect', [$this, 'check_friction_triggers'], 1);
    }
    
    /**
     * Check if friction should be applied
     */
    public function check_friction_triggers() {
        // Skip for admin, ajax, cron, and already verified users
        if (is_admin() || wp_doing_ajax() || wp_doing_cron()) {
            return;
        }
        
        // Check if user already passed challenge
        $client_ip = $this->get_client_ip();
        if ($client_ip && isset($_COOKIE[$this->cookie_name])) {
            $cookie_value = sanitize_text_field(wp_unslash($_COOKIE[$this->cookie_name]));
            $expected_token = $this->get_verification_token($client_ip);
            // Use hash_equals for timing-safe comparison
            if (hash_equals($expected_token, $cookie_value)) {
                return;
            }
        }
        
        // Check IP whitelist (performance optimization)
        if ($client_ip && $this->is_ip_whitelisted($client_ip)) {
            return; // Whitelisted IP, allow access
        }
        
        // Check user agent whitelist
        $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
        if (!empty($user_agent) && $this->is_user_agent_whitelisted($user_agent)) {
            return; // Whitelisted user agent, allow access
        }
        
        // Check for scraping patterns
        $should_challenge = false;
        $reason = '';
        
        // Pattern A: Burst Traffic
        if ($this->detect_burst_traffic()) {
            $should_challenge = true;
            $reason = 'burst';
        }
        
        // Pattern B: Direct Deep-Link Access
        if ($this->detect_deep_link_access()) {
            $should_challenge = true;
            $reason = 'deep_link';
        }
        
        // Pattern C: Sequential Traversal
        if ($this->detect_sequential_traversal()) {
            $should_challenge = true;
            $reason = 'sequential';
        }
        
        if ($should_challenge) {
            $this->log_blocked_request($reason);
            $this->serve_challenge_page($reason);
        }
    }
    
    /**
     * Detect burst traffic pattern
     */
    private function detect_burst_traffic() {
        $ip = $this->get_client_ip();
        if (!$ip) {
            return false;
        }
        if (empty($ip)) {
            return false;
        }
        
        $burst_pages = get_option('humang_friction_burst_pages', 12);
        $burst_seconds = get_option('humang_friction_burst_seconds', 5);
        $rate_pages = get_option('humang_friction_rate_pages', 40);
        $rate_seconds = get_option('humang_friction_rate_seconds', 60);
        
        $key_burst = 'humang_burst_' . md5($ip);
        $key_rate = 'humang_rate_' . md5($ip);
        
        $burst_count = (int) get_transient($key_burst);
        $rate_count = (int) get_transient($key_rate);
        
        // Increment counters
        set_transient($key_burst, $burst_count + 1, $burst_seconds);
        set_transient($key_rate, $rate_count + 1, $rate_seconds);
        
        // Check thresholds
        if ($burst_count >= $burst_pages) {
            return true;
        }
        
        if ($rate_count >= $rate_pages) {
            return true;
        }
        
        return false;
    }
    
    /**
     * Detect direct deep-link access
     */
    private function detect_deep_link_access() {
        // Check if this is a direct request to old/archive content
        $request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
        
        // Patterns suggesting direct deep-link
        $deep_link_patterns = [
            '/\/(\d{4})\/(\d{2})\/(\d{2})\//', // Date archives
            '/\/attachment\//', // Media attachments
            '/\/page\/\d+/', // Pagination
            '/\.(jpg|jpeg|png|gif|pdf)$/i', // Direct media files
        ];
        
        foreach ($deep_link_patterns as $pattern) {
            if (preg_match($pattern, $request_uri)) {
                // Check if there's no referrer or session
                // Sanitize referer
                $referer = isset($_SERVER['HTTP_REFERER']) ? esc_url_raw(wp_unslash($_SERVER['HTTP_REFERER'])) : '';
                // Check for WordPress session cookies (sanitized)
                $has_session = false;
                foreach ($_COOKIE as $name => $value) {
                    $name = sanitize_text_field($name);
                    if (strpos($name, 'wordpress_logged_in_') === 0 || 
                        strpos($name, 'wp-settings-') === 0) {
                        $has_session = true;
                        break;
                    }
                }
                
                // If direct access with no referrer/session, likely scraping
                if (empty($referer) && !$has_session) {
                    return true;
                }
            }
        }
        
        return false;
    }
    
    /**
     * Detect sequential traversal pattern
     */
    private function detect_sequential_traversal() {
        // Sanitize and validate REQUEST_URI
        $request_uri = isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : '';
        if (empty($request_uri) || strlen($request_uri) > 2048) {
            return false;
        }
        
        // Check for pagination sequences
        if (preg_match('/\/page\/(\d+)/', $request_uri, $matches)) {
            $current_page = (int) $matches[1];
            
            // Check if previous page was exactly -1
            $ip = $this->get_client_ip();
            if (!$ip) {
                return false;
            }
            $previous_key = 'humang_prev_page_' . md5($ip);
            $previous_page = get_transient($previous_key);
            
            if ($previous_page !== false && $current_page === ($previous_page + 1)) {
                // Check for multiple sequential hits
                $seq_key = 'humang_seq_' . md5($ip);
                $seq_count = (int) get_transient($seq_key);
                
                if ($seq_count >= 2) { // 3+ sequential pages
                    return true;
                }
                
                set_transient($seq_key, $seq_count + 1, 300); // 5 minute window
            }
            
            set_transient($previous_key, $current_page, 300);
        }
        
        return false;
    }
    
    /**
     * Serve challenge page
     */
    private function serve_challenge_page($reason) {
        // Rate limiting: prevent DoS attacks
        $ip = $this->get_client_ip();
        if (!$ip) {
            // Can't verify IP, deny access
            wp_die(esc_html__('Access denied', 'humangate'), esc_html__('Access Denied', 'humangate'), ['response' => 403]);
        }
        
        // Check rate limit for challenges (max 10 challenges per IP per minute)
        $rate_limit_key = 'humang_challenge_rate_' . md5($ip);
        $challenge_count = get_transient($rate_limit_key) ?: 0;
        if ($challenge_count >= 10) {
            // Too many challenges, block for a minute
            wp_die(esc_html__('Too many requests. Please try again later.', 'humangate'), esc_html__('Rate Limited', 'humangate'), ['response' => 429]);
        }
        
        // Increment rate limit counter
        set_transient($rate_limit_key, $challenge_count + 1, 60);
        
        // Generate challenge token and nonce
        $challenge_token = wp_create_nonce('humang_challenge');
        $challenge_nonce = wp_create_nonce('humangate_challenge');
        // Store IP securely
        set_transient('humang_challenge_' . $challenge_token, $ip, 300); // 5 min
        
        http_response_code(200);
        header('Content-Type: text/html; charset=UTF-8');
        
        // Escape URL and nonce for JavaScript context
        $verify_url = esc_js(admin_url('admin-ajax.php?action=humang_verify_challenge&token=' . esc_attr($challenge_token)));
        $nonce_js = esc_js($challenge_nonce);
        
        $title_text = esc_html__('Verifying...', 'humangate');
        $verifying_text = esc_html__('Verifying browser...', 'humangate');
        $failed_text = esc_html__('Verification failed. Please try again.', 'humangate');
        $error_text = esc_html__('Verification error. Please refresh the page.', 'humangate');
        
        // Note: Inline styles and scripts are used here because this is a challenge page that exits immediately,
        // so wp_enqueue_style() and wp_enqueue_script() cannot be used. This is acceptable for error/challenge pages.
        echo '<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="robots" content="noindex, nofollow">
    <title>' . esc_html($title_text) . '</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            margin: 0;
            background: #f5f5f5;
            color: #333;
        }
        .container {
            text-align: center;
            padding: 2rem;
        }
        .spinner {
            border: 3px solid #f3f3f3;
            border-top: 3px solid #333;
            border-radius: 50%;
            width: 40px;
            height: 40px;
            animation: spin 1s linear infinite;
            margin: 0 auto 1rem;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="spinner"></div>
        <p>' . esc_html($verifying_text) . '</p>
    </div>
    <script>
        (function() {
            // Generate browser entropy token
            function generateToken() {
                const data = {
                    timestamp: Date.now(),
                    random: Math.random().toString(36).substring(2),
                    performance: performance.now(),
                    screen: window.screen.width + "x" + window.screen.height,
                    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
                    languages: navigator.languages.join(",")
                };
                
                // Create hash-like token
                const str = JSON.stringify(data);
                let hash = 0;
                for (let i = 0; i < str.length; i++) {
                    const char = str.charCodeAt(i);
                    hash = ((hash << 5) - hash) + char;
                    hash = hash & hash;
                }
                
                return btoa(str).substring(0, 32) + Math.abs(hash).toString(36);
            }
            
            const token = generateToken();
            const verifyUrl = ' . wp_json_encode($verify_url) . ';
            const challengeNonce = ' . wp_json_encode($challenge_nonce) . ';
            
            // POST token to server
            fetch(verifyUrl, {
                method: "POST",
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                },
                body: "browser_token=" + encodeURIComponent(token) + "&humangate_challenge_nonce=" + encodeURIComponent(challengeNonce)
            })
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    // Reload original page
                    window.location.reload();
                } else {
                    document.querySelector(".container p").textContent = ' . wp_json_encode($failed_text) . ';
                }
            })
            .catch(error => {
                document.querySelector(".container p").textContent = ' . wp_json_encode($error_text) . ';
            });
        })();
    </script>
</body>
</html>';
        
        exit;
    }
    
    /**
     * Verify challenge response
     */
    public function verify_challenge() {
        // Verify nonce for POST requests
        if (isset($_POST['browser_token'])) {
            if (!isset($_POST['humangate_challenge_nonce'])) {
                wp_send_json_error(['message' => esc_html__('Invalid security token', 'humangate')]);
                return false;
            }
            $nonce = sanitize_text_field(wp_unslash($_POST['humangate_challenge_nonce']));
            if (!wp_verify_nonce($nonce, 'humangate_challenge')) {
                wp_send_json_error(['message' => esc_html__('Invalid security token', 'humangate')]);
                return false;
            }
        }
        
        // Sanitize and validate inputs
        $token = isset($_GET['token']) ? sanitize_text_field(wp_unslash($_GET['token'])) : '';
        $browser_token = isset($_POST['browser_token']) ? sanitize_text_field(wp_unslash($_POST['browser_token'])) : '';
        
        if (empty($token) || empty($browser_token)) {
            wp_send_json_error(['message' => esc_html__('Invalid challenge', 'humangate')]);
        }
        
        // Validate token format (nonce should be alphanumeric, max 10 chars)
        if (!preg_match('/^[a-zA-Z0-9]{1,10}$/', $token)) {
            wp_send_json_error(['message' => esc_html__('Invalid challenge token format', 'humangate')]);
        }
        
        // Validate browser token format (prevent injection)
        if (strlen($browser_token) > 256 || !preg_match('/^[a-zA-Z0-9+/=_-]+$/', $browser_token)) {
            wp_send_json_error(['message' => esc_html__('Invalid browser token format', 'humangate')]);
        }
        
        // Get and validate IP address
        $client_ip = $this->get_client_ip();
        if (!$client_ip) {
            wp_send_json_error(['message' => esc_html__('Unable to verify client', 'humangate')]);
        }
        
        // Verify nonce and IP match
        $stored_ip = get_transient('humang_challenge_' . $token);
        if ($stored_ip === false || !hash_equals($stored_ip, $client_ip)) {
            wp_send_json_error(['message' => esc_html__('Invalid challenge token', 'humangate')]);
        }
        
        // Challenge passed - set verification cookie
        $verification_token = $this->get_verification_token($client_ip);
        setcookie(
            $this->cookie_name,
            $verification_token,
            time() + $this->cookie_lifetime,
            '/',
            '',
            is_ssl(),
            true // HttpOnly
        );
        
        // Clean up challenge token immediately after use
        delete_transient('humang_challenge_' . $token);
        
        wp_send_json_success(['message' => esc_html__('Verified', 'humangate')]);
    }
    
    /**
     * Get and validate client IP address
     * Handles proxy headers securely
     */
    private function get_client_ip() {
        $ip_keys = [
            'HTTP_CF_CONNECTING_IP',     // Cloudflare
            'HTTP_X_REAL_IP',            // Nginx proxy
            'HTTP_X_FORWARDED_FOR',      // Standard proxy header
            'REMOTE_ADDR',                // Standard
        ];
        
        foreach ($ip_keys as $key) {
            if (!isset($_SERVER[$key])) {
                continue;
            }
            
            $ip = sanitize_text_field(wp_unslash($_SERVER[$key]));
            
            // Handle X-Forwarded-For (can contain multiple IPs)
            if ($key === 'HTTP_X_FORWARDED_FOR') {
                $ips = explode(',', $ip);
                $ip = trim($ips[0]); // Take first IP (original client)
            }
            
            // Validate IP format
            if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
                return $ip;
            }
        }
        
        // Fallback to REMOTE_ADDR if available
        if (isset($_SERVER['REMOTE_ADDR'])) {
            $ip = sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR']));
            if (filter_var($ip, FILTER_VALIDATE_IP)) {
                return $ip;
            }
        }
        
        return false;
    }
    
    /**
     * Generate verification token for IP
     */
    private function get_verification_token($ip) {
        $secret = wp_salt('auth');
        return hash_hmac('sha256', $ip . $secret, wp_salt('logged_in'));
    }
    
    /**
     * Log blocked request for telemetry
     */
    private function log_blocked_request($reason) {
        $telemetry = new Telemetry();
        $telemetry->increment_blocked($reason);
    }
    
    /**
     * Check if IP is whitelisted
     * 
     * @param string $ip IP address to check
     * @return bool True if IP is whitelisted
     */
    private function is_ip_whitelisted($ip) {
        if (empty($ip)) {
            return false;
        }
        
        // Get whitelist from options (cached per request)
        static $whitelist_cache = null;
        if ($whitelist_cache === null) {
            $whitelist_raw = get_option('humang_whitelist_ips', '');
            $whitelist_cache = array_filter(array_map('trim', explode("\n", $whitelist_raw)));
        }
        
        if (empty($whitelist_cache)) {
            return false;
        }
        
        // Check exact match and CIDR ranges
        foreach ($whitelist_cache as $whitelist_entry) {
            $whitelist_entry = trim($whitelist_entry);
            if (empty($whitelist_entry)) {
                continue;
            }
            
            // Exact match
            if ($ip === $whitelist_entry) {
                return true;
            }
            
            // CIDR range match
            if (strpos($whitelist_entry, '/') !== false) {
                if ($this->ip_in_cidr($ip, $whitelist_entry)) {
                    return true;
                }
            }
        }
        
        return false;
    }
    
    /**
     * Check if user agent is whitelisted
     * 
     * @param string $user_agent User agent string to check
     * @return bool True if user agent is whitelisted
     */
    private function is_user_agent_whitelisted($user_agent) {
        if (empty($user_agent)) {
            return false;
        }
        
        // Get whitelist from options (cached per request)
        static $whitelist_cache = null;
        if ($whitelist_cache === null) {
            $whitelist_raw = get_option('humang_whitelist_user_agents', '');
            $whitelist_cache = array_filter(array_map('trim', explode("\n", $whitelist_raw)));
        }
        
        if (empty($whitelist_cache)) {
            return false;
        }
        
        // Check if user agent contains any whitelisted pattern
        foreach ($whitelist_cache as $pattern) {
            $pattern = trim($pattern);
            if (empty($pattern)) {
                continue;
            }
            
            if (stripos($user_agent, $pattern) !== false) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Check if IP is within CIDR range
     * 
     * @param string $ip IP address to check
     * @param string $cidr CIDR range (e.g., "192.168.1.0/24")
     * @return bool True if IP is in range
     */
    private function ip_in_cidr($ip, $cidr) {
        list($subnet, $mask) = explode('/', $cidr);
        
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            // IPv4
            $ip_long = ip2long($ip);
            $subnet_long = ip2long($subnet);
            
            // Validate that both IPs converted successfully
            if ($ip_long === false || $subnet_long === false) {
                return false;
            }
            
            $mask_long = -1 << (32 - (int)$mask);
            return ($ip_long & $mask_long) === ($subnet_long & $mask_long);
        } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
            // IPv6 (simplified check)
            $ip_bin = inet_pton($ip);
            $subnet_bin = inet_pton($subnet);
            
            // Validate that both IPs converted successfully
            if ($ip_bin === false || $subnet_bin === false) {
                return false;
            }
            
            $mask_bits = (int)$mask;
            $bytes = (int)($mask_bits / 8);
            $bits = $mask_bits % 8;
            
            if (strncmp($ip_bin, $subnet_bin, $bytes) !== 0) {
                return false;
            }
            
            if ($bits > 0) {
                $mask_byte = 0xFF << (8 - $bits);
                return (ord($ip_bin[$bytes]) & $mask_byte) === (ord($subnet_bin[$bytes]) & $mask_byte);
            }
            
            return true;
        }
        
        return false;
    }
}

