<?php
/**
 * Visitor Classifier Model
 *
 * Handles visitor classification (human vs crawler) with caching.
 * Includes Google crawler verification via reverse DNS lookup.
 *
 * @package VMPFence
 * @since 2.0.0
 */

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

/**
 * VMPFence Visitor Classifier
 *
 * @since 2.0.0
 */
class VMPFence_Visitor_Classifier {

    /**
     * Known crawler user agent patterns
     *
     * @var array
     */
    private static $crawler_patterns = array(
        'googlebot',
        'bingbot',
        'slurp',           // Yahoo
        'duckduckbot',
        'baiduspider',
        'yandexbot',
        'sogou',
        'exabot',
        'facebot',         // Facebook
        'ia_archiver',     // Alexa
        'bot',
        'spider',
        'crawler',
        'curl',
        'wget',
        'python',
        'java',
        'perl',
        'ruby',
        'go-http-client',
        'http_request',
        'libwww',
        'scrapy',
        'httpclient',
        'axios',
        'node-fetch',
        'phantom',
        'headless',
    );

    /**
     * Known browser user agent patterns
     *
     * @var array
     */
    private static $browser_patterns = array(
        'mozilla',
        'chrome',
        'safari',
        'firefox',
        'edge',
        'opera',
        'msie',
        'trident',
    );

    /**
     * Classify visitor as human or crawler
     *
     * @since 2.0.0
     * @param string $user_agent The user agent string
     * @param string $ip The IP address
     * @return string 'human' or 'crawler'
     */
    public static function classify_visitor($user_agent, $ip) {
        if (empty($user_agent)) {
            return 'crawler'; // Don't cache empty user agents
        }

        // Check cache first
        $cached = self::get_cached_classification($ip);
        if ($cached !== false) {
            return $cached;
        }

        // Perform classification
        $classification = self::is_crawler($user_agent) ? 'crawler' : 'human';

        // Cache the result for 1 hour
        self::cache_classification($ip, $classification, 3600);

        return $classification;
    }

    /**
     * Check if user agent is a crawler
     *
     * @since 2.0.0
     * @param string $user_agent The user agent string
     * @return bool True if crawler, false if human
     */
    public static function is_crawler($user_agent) {
        $user_agent_lower = strtolower($user_agent);
        
        // First, check if it has browser patterns (most humans have these)
        $has_browser_pattern = false;
        foreach (self::$browser_patterns as $pattern) {
            if (strpos($user_agent_lower, $pattern) !== false) {
                $has_browser_pattern = true;
                break;
            }
        }

        // If it has browser patterns, check for explicit bot/crawler indicators
        if ($has_browser_pattern) {
            // These are definitive crawler indicators even in browser-like UAs
            $explicit_bot_patterns = array(
                'bot',
                'spider', 
                'crawl',
                'slurp',
                'curl',
                'wget',
                'python-requests',
                'scrapy',
                'headless',
                'phantom',
                'selenium'
            );
            
            foreach ($explicit_bot_patterns as $pattern) {
                if (strpos($user_agent_lower, $pattern) !== false) {
                    return true; // Crawler disguised as browser
                }
            }
            
            // Has browser pattern, no bot keywords = Human
            return false;
        }

        // No browser patterns = Crawler
        return true;
    }

    /**
     * Check if user agent is human browser
     *
     * @since 2.0.0
     * @param string $user_agent The user agent string
     * @return bool True if human, false if crawler
     */
    public static function is_human($user_agent) {
        return !self::is_crawler($user_agent);
    }

    /**
     * Check if user agent is Google crawler
     *
     * @since 2.0.0
     * @param string $user_agent The user agent string
     * @return bool True if Google crawler user agent
     */
    public static function is_google_crawler($user_agent) {
        $user_agent_lower = strtolower($user_agent);
        return strpos($user_agent_lower, 'googlebot') !== false || 
               strpos($user_agent_lower, 'google') !== false;
    }

    /**
     * Verify if IP is actually from Google via reverse DNS lookup
     *
     * @since 2.0.0
     * @param string $ip The IP address
     * @param string $user_agent The user agent string
     * @return bool True if verified Google crawler, false otherwise
     */
    public static function is_verified_google($ip, $user_agent) {
        // First check if user agent claims to be Google
        if (!self::is_google_crawler($user_agent)) {
            return false;
        }

        // Check cache for verification result
        global $wpdb;
        $reverse_cache_table = VMPFence_Constants::get_table_name(VMPFence_Constants::TABLE_REVERSE_CACHE);
        $ip_binary = inet_pton($ip);

        if ($ip_binary === false) {
            return false;
        }

        // Check if we have a recent verification in cache (valid for 24 hours)
        $cached_host = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT host, lastUpdate FROM {$reverse_cache_table} 
                 WHERE IP = %s AND lastUpdate > %d",
                $ip_binary,
                time() - 86400
            )
        );

        if ($cached_host) {
            $hostname = $cached_host->host;
        } else {
            // Perform reverse DNS lookup
            $hostname = gethostbyaddr($ip);
            
            if ($hostname === $ip) {
                // Reverse DNS failed
                return false;
            }

            // Cache the result
            $wpdb->query(
                $wpdb->prepare(
                    "INSERT INTO {$reverse_cache_table} (IP, host, lastUpdate) 
                     VALUES (%s, %s, %d)
                     ON DUPLICATE KEY UPDATE host = %s, lastUpdate = %d",
                    $ip_binary, $hostname, time(),
                    $hostname, time()
                )
            );
        }

        // Verify hostname ends with .googlebot.com or .google.com
        $is_google = (
            substr($hostname, -14) === '.googlebot.com' ||
            substr($hostname, -11) === '.google.com'
        );

        return $is_google;
    }

    /**
     * Get cached visitor classification
     *
     * @since 2.0.0
     * @param string $ip The IP address
     * @return string|false Classification ('human' or 'crawler') or false if not cached
     */
    private static function get_cached_classification($ip) {
        global $wpdb;

        $table_name = VMPFence_Constants::get_table_name(VMPFence_Constants::TABLE_LIVE_TRAFFIC_HUMAN);
        $ip_binary = inet_pton($ip);

        if ($ip_binary === false) {
            return false;
        }

        $current_time = time();

        $result = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT identifier FROM {$table_name} 
                 WHERE IP = %s AND expiration > %d
                 LIMIT 1",
                $ip_binary,
                $current_time
            )
        );

        return $result ? $result : false;
    }

    /**
     * Cache visitor classification
     *
     * @since 2.0.0
     * @param string $ip The IP address
     * @param string $classification Classification ('human' or 'crawler')
     * @param int $duration Cache duration in seconds
     * @return bool True on success, false on failure
     */
    public static function cache_classification($ip, $classification, $duration = 3600) {
        global $wpdb;

        $table_name = VMPFence_Constants::get_table_name(VMPFence_Constants::TABLE_LIVE_TRAFFIC_HUMAN);
        $ip_binary = inet_pton($ip);

        if ($ip_binary === false) {
            return false;
        }

        $expiration = time() + $duration;

        $result = $wpdb->query(
            $wpdb->prepare(
                "INSERT INTO {$table_name} (IP, identifier, expiration) 
                 VALUES (%s, %s, %d)
                 ON DUPLICATE KEY UPDATE identifier = %s, expiration = %d",
                $ip_binary, $classification, $expiration,
                $classification, $expiration
            )
        );

        return $result !== false;
    }

    /**
     * Clean up expired classification cache entries
     *
     * @since 2.0.0
     * @return int Number of records deleted
     */
    public static function cleanup_expired_cache() {
        global $wpdb;

        $table_name = VMPFence_Constants::get_table_name(VMPFence_Constants::TABLE_LIVE_TRAFFIC_HUMAN);
        $current_time = time();

        $deleted = $wpdb->query(
            $wpdb->prepare(
                "DELETE FROM {$table_name} WHERE expiration < %d",
                $current_time
            )
        );

        return $deleted ? $deleted : 0;
    }

    /**
     * Handle Google crawler based on configuration
     *
     * @since 2.0.0
     * @param string $user_agent The user agent string
     * @param string $ip The IP address
     * @return string|false Returns 'bypass' if should bypass rate limits, 'crawler' if treat as crawler, false if no special handling
     */
    public static function handle_google_crawler($user_agent, $ip) {
        if (!self::is_google_crawler($user_agent)) {
            return false;
        }

        $google_treatment = VMPFence_Config::get('google_crawler_treatment', 'verified_google');

        switch ($google_treatment) {
            case 'verified_google':
                // Verify via reverse DNS
                if (self::is_verified_google($ip, $user_agent)) {
                    return 'bypass';
                }
                // If verification fails, treat as regular crawler
                return 'crawler';

            case 'treat_as_crawler':
                // Apply crawler rate limits without verification
                return 'crawler';

            case 'no_verification':
                // Bypass all rate limits without verification
                return 'bypass';
            default:
                // No special handling, treat like any other crawler
                return false;
        }
    }
}
