<?php

class security_manager {
    
    private $db_path = '/docker/whp/sql/security.db';
    private $db;
    private $max_failed_attempts = 5;
    private $block_duration = 3600; // 1 hour in seconds
    private $whitelist_ips = array('127.0.0.1', '::1'); // Localhost
    
    public function __construct() {
        $this->init_database();
    }
    
    /**
     * Execute a database query with retry logic for handling locks
     * 
     * @param callable $operation The database operation to execute
     * @param int $max_retries Maximum number of retry attempts
     * @param int $base_delay Base delay in microseconds between retries
     * @return mixed Result of the operation, or false on failure
     */
    private function execute_with_retry($operation, $max_retries = 5, $base_delay = 100000) {
        for ($attempt = 1; $attempt <= $max_retries; $attempt++) {
            try {
                $result = $operation();
                
                // If operation succeeded, return the result
                if ($result !== false) {
                    return $result;
                }
                
                // If this is not the last attempt, wait and retry
                if ($attempt < $max_retries) {
                    usleep($base_delay * $attempt); // Progressive delay
                    continue;
                }
                
                // All retries failed
                error_log("Security Manager: Database operation failed after $max_retries attempts");
                return false;
                
            } catch (Exception $e) {
                error_log("Security Manager: Database operation error (attempt $attempt/$max_retries): " . $e->getMessage());
                
                // If this is not the last attempt, wait and retry
                if ($attempt < $max_retries) {
                    usleep($base_delay * $attempt); // Progressive delay
                    continue;
                }
                
                // All retries failed
                return false;
            }
        }
        
        return false;
    }
    
    private function init_database() {
        if (!file_exists($this->db_path)) {
            $this->db = new SQLite3($this->db_path);
            $this->db->busyTimeout(10000); // Increase timeout to 10 seconds
            $this->db->exec('PRAGMA journal_mode = WAL'); // Use Write-Ahead Logging for better concurrency
            
            // Create failed login attempts table
            $this->db->exec('CREATE TABLE IF NOT EXISTS failed_logins (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                ip_address TEXT NOT NULL,
                username TEXT NOT NULL,
                attempt_time INTEGER NOT NULL,
                user_agent TEXT
            )');
            
            // Create blocked IPs table
            $this->db->exec('CREATE TABLE IF NOT EXISTS blocked_ips (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                ip_address TEXT UNIQUE NOT NULL,
                block_time INTEGER NOT NULL,
                unblock_time INTEGER NOT NULL,
                reason TEXT,
                failed_attempts INTEGER DEFAULT 0,
                manual_block INTEGER DEFAULT 0
            )');
            
            // Create whitelist IPs table
            $this->db->exec('CREATE TABLE IF NOT EXISTS whitelist_ips (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                ip_address TEXT UNIQUE NOT NULL,
                description TEXT,
                added_time INTEGER NOT NULL
            )');
            
            // Create successful logins table
            $this->db->exec('CREATE TABLE IF NOT EXISTS successful_logins (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                username TEXT NOT NULL,
                ip_address TEXT NOT NULL,
                login_time INTEGER NOT NULL,
                user_agent TEXT
            )');
            
            // Create indexes for better performance
            $this->db->exec('CREATE INDEX IF NOT EXISTS idx_failed_logins_ip ON failed_logins(ip_address)');
            $this->db->exec('CREATE INDEX IF NOT EXISTS idx_failed_logins_time ON failed_logins(attempt_time)');
            $this->db->exec('CREATE INDEX IF NOT EXISTS idx_blocked_ips_time ON blocked_ips(unblock_time)');
            $this->db->exec('CREATE INDEX IF NOT EXISTS idx_successful_logins_user ON successful_logins(username)');
            $this->db->exec('CREATE INDEX IF NOT EXISTS idx_successful_logins_time ON successful_logins(login_time)');
        } else {
            $this->db = new SQLite3($this->db_path);
            $this->db->busyTimeout(10000); // Increase timeout to 10 seconds
            $this->db->exec('PRAGMA journal_mode = WAL'); // Use Write-Ahead Logging for better concurrency
            
            // Check if successful_logins table exists, create if not (for existing databases)
            $this->ensure_successful_logins_table();
        }
    }
    
    private function ensure_successful_logins_table() {
        // Check if successful_logins table exists with retry logic
        $max_retries = 5;
        $retry_delay = 100000; // 100ms in microseconds
        
        for ($attempt = 1; $attempt <= $max_retries; $attempt++) {
            try {
                $result = $this->db->query("SELECT name FROM sqlite_master WHERE type='table' AND name='successful_logins'");
                
                // Check if query was successful
                if ($result === false) {
                    // If this is not the last attempt, wait and retry
                    if ($attempt < $max_retries) {
                        usleep($retry_delay * $attempt); // Progressive delay
                        continue;
                    }
                    // If all retries failed, log error and return
                    error_log("Security Manager: Failed to query sqlite_master after $max_retries attempts");
                    return;
                }
                
                // Check if table exists
                if (!$result->fetchArray()) {
                    // Create the table and indexes
                    $this->db->exec('CREATE TABLE IF NOT EXISTS successful_logins (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        username TEXT NOT NULL,
                        ip_address TEXT NOT NULL,
                        login_time INTEGER NOT NULL,
                        user_agent TEXT
                    )');
                    
                    // Create indexes if they don't exist
                    $this->db->exec('CREATE INDEX IF NOT EXISTS idx_successful_logins_user ON successful_logins(username)');
                    $this->db->exec('CREATE INDEX IF NOT EXISTS idx_successful_logins_time ON successful_logins(login_time)');
                }
                
                // Success - exit the retry loop
                return;
                
            } catch (Exception $e) {
                // Log the error
                error_log("Security Manager: Database operation failed (attempt $attempt/$max_retries): " . $e->getMessage());
                
                // If this is not the last attempt, wait and retry
                if ($attempt < $max_retries) {
                    usleep($retry_delay * $attempt); // Progressive delay
                    continue;
                }
                
                // If all retries failed, log final error
                error_log("Security Manager: Failed to ensure successful_logins table after $max_retries attempts");
                return;
            }
        }
    }
    
    public function record_failed_login($ip_address, $username, $user_agent = '') {
        // Don't track whitelisted IPs
        if ($this->is_ip_whitelisted($ip_address)) {
            return true;
        }
        
        $stmt = $this->db->prepare('INSERT INTO failed_logins (ip_address, username, attempt_time, user_agent) VALUES (:ip, :user, :time, :agent)');
        $stmt->bindValue(':ip', $ip_address, SQLITE3_TEXT);
        $stmt->bindValue(':user', $username, SQLITE3_TEXT);
        $stmt->bindValue(':time', time(), SQLITE3_INTEGER);
        $stmt->bindValue(':agent', $user_agent, SQLITE3_TEXT);
        $stmt->execute();
        
        // Check if IP should be blocked
        $this->check_and_block_ip($ip_address);
        
        return true;
    }
    
    public function record_successful_login($ip_address, $username = '', $user_agent = '') {
        // Clear failed attempts for this IP on successful login
        $stmt = $this->db->prepare('DELETE FROM failed_logins WHERE ip_address = :ip');
        $stmt->bindValue(':ip', $ip_address, SQLITE3_TEXT);
        $stmt->execute();
        
        // Record successful login if username is provided
        if (!empty($username)) {
            // Ensure table exists before trying to use it
            $this->ensure_successful_logins_table();
            
            // Insert new login record
            $stmt = $this->db->prepare('INSERT INTO successful_logins (username, ip_address, login_time, user_agent) VALUES (:user, :ip, :time, :agent)');
            if ($stmt) {
                $stmt->bindValue(':user', $username, SQLITE3_TEXT);
                $stmt->bindValue(':ip', $ip_address, SQLITE3_TEXT);
                $stmt->bindValue(':time', time(), SQLITE3_INTEGER);
                $stmt->bindValue(':agent', $user_agent, SQLITE3_TEXT);
                $stmt->execute();
                
                // Keep only the last 3 logins for this user
                $stmt = $this->db->prepare('DELETE FROM successful_logins WHERE username = :user AND id NOT IN (
                    SELECT id FROM successful_logins WHERE username = :user ORDER BY login_time DESC LIMIT 3
                )');
                if ($stmt) {
                    $stmt->bindValue(':user', $username, SQLITE3_TEXT);
                    $stmt->execute();
                }
            }
        }
        
        return true;
    }
    
    private function check_and_block_ip($ip_address) {
        // Count recent failed attempts
        $stmt = $this->db->prepare('SELECT COUNT(*) as count FROM failed_logins WHERE ip_address = :ip AND attempt_time > :time');
        $stmt->bindValue(':ip', $ip_address, SQLITE3_TEXT);
        $stmt->bindValue(':time', time() - 3600, SQLITE3_INTEGER); // Last hour
        $result = $stmt->execute();
        $row = $result->fetchArray(SQLITE3_ASSOC);
        
        if ($row['count'] >= $this->max_failed_attempts) {
            $this->block_ip($ip_address, 'Too many failed login attempts', $row['count']);
        }
    }
    
    public function block_ip($ip_address, $reason = 'Manual block', $failed_attempts = 0, $permanent = false, $skip_service_blocks = false) {
        // Validate IP or CIDR format
        if (!$this->validate_ip_or_cidr($ip_address)) {
            error_log("Invalid IP or CIDR format: " . $ip_address);
            return false;
        }

        // Normalize CIDR notation to canonical form
        $ip_address = $this->normalize_cidr($ip_address);
        if ($ip_address === false) {
            error_log("Failed to normalize IP/CIDR: " . $ip_address);
            return false;
        }

        // Check if this IP/CIDR is whitelisted or overlaps with exempted ranges
        if ($this->is_ip_whitelisted($ip_address)) {
            error_log("Cannot block whitelisted IP/CIDR: " . $ip_address);
            return false;
        }

        // For CIDR ranges, check if they overlap with any exempted ranges
        if (strpos($ip_address, '/') !== false) {
            // Limit number of CIDR ranges to prevent DoS
            $stmt = $this->db->prepare("SELECT COUNT(*) as count FROM blocked_ips WHERE ip_address LIKE '%/%'");
            $result = $stmt->execute();
            $row = $result->fetchArray(SQLITE3_ASSOC);

            if ($row['count'] >= 1000) {
                error_log("CIDR range limit exceeded: {$row['count']}/1000");
                return false;
            }

            // Check if this CIDR range overlaps with any exempted range
            $exemption_ips = $this->get_exemption_ips();
            foreach ($exemption_ips as $exempt) {
                if ($this->cidr_ranges_overlap($ip_address, $exempt['ip_address'])) {
                    error_log("Cannot block CIDR range that overlaps with exempted range: " . $exempt['ip_address']);
                    return false;
                }
            }
        }

        // Check if IP/CIDR is already blocked in security manager to prevent duplicates
        // For CIDR, check exact match only
        $stmt = $this->db->prepare('SELECT * FROM blocked_ips WHERE ip_address = :ip');
        $stmt->bindValue(':ip', $ip_address, SQLITE3_TEXT);
        $result = $stmt->execute();
        if ($result->fetchArray(SQLITE3_ASSOC)) {
            return true; // Already blocked, return success
        }

        // Store in local database
        $unblock_time = $permanent ? -1 : (time() + $this->block_duration); // -1 means permanent/manual unblock only
        $stmt = $this->db->prepare('INSERT OR REPLACE INTO blocked_ips (ip_address, block_time, unblock_time, reason, failed_attempts, manual_block) VALUES (:ip, :block_time, :unblock_time, :reason, :attempts, :manual)');
        $stmt->bindValue(':ip', $ip_address, SQLITE3_TEXT);
        $stmt->bindValue(':block_time', time(), SQLITE3_INTEGER);
        $stmt->bindValue(':unblock_time', $unblock_time, SQLITE3_INTEGER);
        $stmt->bindValue(':reason', $reason, SQLITE3_TEXT);
        $stmt->bindValue(':attempts', $failed_attempts, SQLITE3_INTEGER);
        $stmt->bindValue(':manual', ($reason === 'Manual block') ? 1 : 0, SQLITE3_INTEGER);
        $stmt->execute();

        // Also block in HAProxy for full service-wide blocking
        try {
            require_once('/docker/whp/web/libs/haproxy_manager.php');
            $haproxy = new haproxy_manager();
            $haproxy->block_ip($ip_address, $reason, 'WHP Security Manager');
        } catch (Exception $e) {
            error_log("Failed to block IP in HAProxy: " . $e->getMessage());
            // Continue even if HAProxy blocking fails - local block is still active
        }

        // Also block in SSH/FTP services via log monitor (only if not called from service blocks to prevent loops)
        // Note: SSH/FTP services may not support CIDR directly, so skip for CIDR ranges
        if (!$skip_service_blocks && strpos($ip_address, '/') === false) {
            try {
                require_once('/docker/whp/web/libs/log_monitor_manager.php');
                $log_monitor = new log_monitor_manager();
                // Block on both SSH and FTP services with the same permanent flag
                $log_monitor->block_service_ip('ssh', $ip_address, $reason, $failed_attempts, $permanent);
                $log_monitor->block_service_ip('ftp', $ip_address, $reason, $failed_attempts, $permanent);
            } catch (Exception $e) {
                error_log("Failed to block IP in SSH/FTP services: " . $e->getMessage());
                // Continue even if service blocking fails
            }
        }

        return true;
    }
    
    public function unblock_ip($ip_address) {
        // Remove from local database
        $stmt = $this->db->prepare('DELETE FROM blocked_ips WHERE ip_address = :ip');
        $stmt->bindValue(':ip', $ip_address, SQLITE3_TEXT);
        $stmt->execute();
        
        // Also clear failed attempts for this IP
        $stmt = $this->db->prepare('DELETE FROM failed_logins WHERE ip_address = :ip');
        $stmt->bindValue(':ip', $ip_address, SQLITE3_TEXT);
        $stmt->execute();
        
        // Also unblock from HAProxy
        try {
            require_once('/docker/whp/web/libs/haproxy_manager.php');
            $haproxy = new haproxy_manager();
            $haproxy->unblock_ip($ip_address);
        } catch (Exception $e) {
            error_log("Failed to unblock IP from HAProxy: " . $e->getMessage());
            // Continue even if HAProxy unblocking fails
        }
        
        // Also unblock from SSH/FTP services
        try {
            require_once('/docker/whp/web/libs/log_monitor_manager.php');
            $log_monitor = new log_monitor_manager();
            // Unblock from both SSH and FTP services
            $log_monitor->unblock_service_ip('ssh', $ip_address);
            $log_monitor->unblock_service_ip('ftp', $ip_address);
        } catch (Exception $e) {
            error_log("Failed to unblock IP from SSH/FTP services: " . $e->getMessage());
            // Continue even if service unblocking fails
        }
        
        return true;
    }
    
    public function is_ip_blocked($ip_address) {
        // Check if IP is whitelisted
        if ($this->is_ip_whitelisted($ip_address)) {
            return false;
        }

        // Check if IP is blocked (including CIDR ranges)
        $stmt = $this->db->prepare('SELECT * FROM blocked_ips WHERE (unblock_time > :time OR unblock_time = -1)');
        $stmt->bindValue(':time', time(), SQLITE3_INTEGER);
        $result = $stmt->execute();

        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
            $blocked_entry = $row['ip_address'];

            // Check if IP matches this blocked entry (handles both single IPs and CIDR)
            if ($this->ip_in_cidr($ip_address, $blocked_entry)) {
                // Auto-unblock if time has expired (but not for permanent blocks with unblock_time = -1)
                if ($row['unblock_time'] > 0 && $row['unblock_time'] <= time()) {
                    $this->unblock_ip($blocked_entry);
                    return false;
                }
                return $row;
            }
        }

        return false;
    }
    
    /**
     * Validates if a string is a valid IP address or CIDR notation
     *
     * @param string $input IP address or CIDR notation (e.g., "192.168.1.0/24")
     * @return bool True if valid IP or CIDR, false otherwise
     */
    private function validate_ip_or_cidr($input) {
        // Check if it's a plain IP address
        if (filter_var($input, FILTER_VALIDATE_IP)) {
            return true;
        }

        // Check if it's CIDR notation
        if (strpos($input, '/') !== false) {
            list($ip, $mask) = explode('/', $input, 2);

            // Validate IP part
            if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
                return false;
            }

            // Validate mask (0-32 for IPv4)
            if (!is_numeric($mask) || $mask < 0 || $mask > 32) {
                return false;
            }

            return true;
        }

        return false;
    }

    /**
     * Checks if an IP address matches a CIDR range
     *
     * @param string $ip IP address to check
     * @param string $cidr CIDR notation (e.g., "192.168.1.0/24")
     * @return bool True if IP is in CIDR range, false otherwise
     */
    private function ip_in_cidr($ip, $cidr) {
        // If CIDR doesn't contain /, it's just a single IP
        if (strpos($cidr, '/') === false) {
            return $ip === $cidr;
        }

        list($subnet, $mask) = explode('/', $cidr, 2);

        // Validate mask value
        $mask = (int)$mask;
        if ($mask < 0 || $mask > 32) {
            error_log("Invalid CIDR mask value: $mask");
            return false;
        }

        // Convert IP and subnet to long integers
        $ip_long = ip2long($ip);
        $subnet_long = ip2long($subnet);

        if ($ip_long === false || $subnet_long === false) {
            return false;
        }

        // Handle edge cases to prevent integer overflow
        if ($mask === 0) {
            // /0 matches all IPs
            return true;
        }

        if ($mask === 32) {
            // /32 is a single IP
            return $ip_long === $subnet_long;
        }

        // Calculate subnet mask with overflow protection
        $mask_long = (0xFFFFFFFF << (32 - $mask)) & 0xFFFFFFFF;

        // Apply mask to both IP and subnet and compare
        return ($ip_long & $mask_long) === ($subnet_long & $mask_long);
    }

    /**
     * Normalize CIDR notation to canonical form
     *
     * @param string $cidr CIDR notation or IP address
     * @return string|false Normalized CIDR or false on error
     */
    private function normalize_cidr($cidr) {
        if (strpos($cidr, '/') === false) {
            return $cidr; // Single IP, no normalization needed
        }

        list($ip, $mask) = explode('/', $cidr, 2);
        $mask = (int)$mask;

        // Calculate network address
        $ip_long = ip2long($ip);
        if ($ip_long === false) {
            return false;
        }

        if ($mask === 0) {
            $network_long = 0;
        } else if ($mask === 32) {
            $network_long = $ip_long;
        } else {
            $mask_long = (0xFFFFFFFF << (32 - $mask)) & 0xFFFFFFFF;
            $network_long = $ip_long & $mask_long;
        }

        $network_ip = long2ip($network_long);
        return $network_ip . '/' . $mask;
    }

    /**
     * Check if two CIDR ranges overlap
     *
     * @param string $cidr1 First CIDR range or IP
     * @param string $cidr2 Second CIDR range or IP
     * @return bool True if ranges overlap, false otherwise
     */
    private function cidr_ranges_overlap($cidr1, $cidr2) {
        $cidr1_has_mask = strpos($cidr1, '/') !== false;
        $cidr2_has_mask = strpos($cidr2, '/') !== false;

        // If both are single IPs, just compare
        if (!$cidr1_has_mask && !$cidr2_has_mask) {
            return $cidr1 === $cidr2;
        }

        // If one is single IP, check if it's in the other's range
        if (!$cidr1_has_mask) {
            return $this->ip_in_cidr($cidr1, $cidr2);
        }
        if (!$cidr2_has_mask) {
            return $this->ip_in_cidr($cidr2, $cidr1);
        }

        // Both are CIDR ranges - check if they overlap
        list($ip1, $mask1) = explode('/', $cidr1);
        list($ip2, $mask2) = explode('/', $cidr2);

        $ip1_long = ip2long($ip1);
        $ip2_long = ip2long($ip2);

        if ($ip1_long === false || $ip2_long === false) {
            return false;
        }

        $mask1 = (int)$mask1;
        $mask2 = (int)$mask2;

        $mask1_long = $mask1 === 0 ? 0 : ((0xFFFFFFFF << (32 - $mask1)) & 0xFFFFFFFF);
        $mask2_long = $mask2 === 0 ? 0 : ((0xFFFFFFFF << (32 - $mask2)) & 0xFFFFFFFF);

        $network1 = $ip1_long & $mask1_long;
        $network2 = $ip2_long & $mask2_long;

        // Check if one range contains the other's network address
        return ($network1 & $mask2_long) === $network2 ||
               ($network2 & $mask1_long) === $network1;
    }

    public function is_ip_whitelisted($ip_address) {
        // Check built-in whitelist
        if (in_array($ip_address, $this->whitelist_ips)) {
            return true;
        }

        // Check database whitelist (including CIDR ranges)
        $stmt = $this->db->prepare('SELECT ip_address FROM whitelist_ips');
        $result = $stmt->execute();

        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
            $whitelist_entry = $row['ip_address'];

            // Check if IP matches this whitelist entry (handles both single IPs and CIDR)
            if ($this->ip_in_cidr($ip_address, $whitelist_entry)) {
                return true;
            }
        }

        return false;
    }
    
    public function add_whitelist_ip($ip_address, $description = '') {
        // Validate IP or CIDR format
        if (!$this->validate_ip_or_cidr($ip_address)) {
            error_log("Invalid IP or CIDR format: " . $ip_address);
            return false;
        }

        $stmt = $this->db->prepare('INSERT OR REPLACE INTO whitelist_ips (ip_address, description, added_time) VALUES (:ip, :desc, :time)');
        $stmt->bindValue(':ip', $ip_address, SQLITE3_TEXT);
        $stmt->bindValue(':desc', $description, SQLITE3_TEXT);
        $stmt->bindValue(':time', time(), SQLITE3_INTEGER);
        $stmt->execute();

        return true;
    }
    
    public function remove_whitelist_ip($ip_address) {
        $stmt = $this->db->prepare('DELETE FROM whitelist_ips WHERE ip_address = :ip');
        $stmt->bindValue(':ip', $ip_address, SQLITE3_TEXT);
        $stmt->execute();
        
        return true;
    }
    
    public function get_failed_attempts($ip_address, $hours = 1) {
        $stmt = $this->db->prepare('SELECT * FROM failed_logins WHERE ip_address = :ip AND attempt_time > :time ORDER BY attempt_time DESC');
        $stmt->bindValue(':ip', $ip_address, SQLITE3_TEXT);
        $stmt->bindValue(':time', time() - ($hours * 3600), SQLITE3_INTEGER);
        $result = $stmt->execute();
        
        $attempts = array();
        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
            $attempts[] = $row;
        }
        
        return $attempts;
    }
    
    public function get_blocked_ips() {
        $stmt = $this->db->prepare('SELECT * FROM blocked_ips WHERE unblock_time > :time OR unblock_time = -1 ORDER BY block_time DESC');
        $stmt->bindValue(':time', time(), SQLITE3_INTEGER);
        $result = $stmt->execute();
        
        $blocked = array();
        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
            $blocked[] = $row;
        }
        
        return $blocked;
    }
    
    public function get_whitelist_ips() {
        $stmt = $this->db->prepare('SELECT * FROM whitelist_ips ORDER BY added_time DESC');
        $result = $stmt->execute();
        
        $whitelist = array();
        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
            $whitelist[] = $row;
        }
        
        return $whitelist;
    }
    
    public function cleanup_expired_blocks() {
        $stmt = $this->db->prepare('DELETE FROM blocked_ips WHERE unblock_time <= :time');
        $stmt->bindValue(':time', time(), SQLITE3_INTEGER);
        $stmt->execute();
        
        // Clean up old failed attempts (older than 24 hours)
        $stmt = $this->db->prepare('DELETE FROM failed_logins WHERE attempt_time <= :time');
        $stmt->bindValue(':time', time() - 86400, SQLITE3_INTEGER);
        $stmt->execute();
        
        // Clean up old permission logs (older than 90 days)
        if (file_exists('/docker/whp/sql/permissions.db')) {
            require_once('/docker/whp/web/libs/permission_manager.php');
            $PermManager = new permission_manager();
            $PermManager->cleanup_old_logs(90);
        }
        
        return true;
    }
    
    public function get_last_logins($username) {
        $this->ensure_successful_logins_table();
        
        return $this->execute_with_retry(function() use ($username) {
            $stmt = $this->db->prepare('SELECT * FROM successful_logins WHERE username = :user ORDER BY login_time DESC LIMIT 3');
            if (!$stmt) {
                return array(); // Return empty array if table doesn't exist
            }
            
            $stmt->bindValue(':user', $username, SQLITE3_TEXT);
            $result = $stmt->execute();
            
            if ($result === false) {
                return false; // Trigger retry
            }
            
            $logins = array();
            while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
                $logins[] = $row;
            }
            
            return $logins;
        }) ?: array(); // Return empty array on complete failure
    }
    
    public function get_all_recent_logins($limit = 10) {
        $this->ensure_successful_logins_table();
        
        return $this->execute_with_retry(function() use ($limit) {
            $stmt = $this->db->prepare('SELECT * FROM successful_logins ORDER BY login_time DESC LIMIT :limit');
            if (!$stmt) {
                return array(); // Return empty array if table doesn't exist
            }
            
            $stmt->bindValue(':limit', $limit, SQLITE3_INTEGER);
            $result = $stmt->execute();
            
            if ($result === false) {
                return false; // Trigger retry
            }
            
            $logins = array();
            while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
                $logins[] = $row;
            }
            
            return $logins;
        }) ?: array(); // Return empty array on complete failure
    }
    
    public function get_security_stats() {
        // Get current blocked IPs count (including permanent blocks)
        $stmt = $this->db->prepare('SELECT COUNT(*) as count FROM blocked_ips WHERE unblock_time > :time OR unblock_time = -1');
        $stmt->bindValue(':time', time(), SQLITE3_INTEGER);
        $result = $stmt->execute();
        $blocked_count = $result->fetchArray(SQLITE3_ASSOC)['count'];
        
        // Get failed attempts in last 24 hours from both old and new tables
        $cutoff_time = time() - 86400;
        
        // Get from old failed_logins table
        $stmt = $this->db->prepare('SELECT COUNT(*) as count FROM failed_logins WHERE attempt_time > :time');
        $stmt->bindValue(':time', $cutoff_time, SQLITE3_INTEGER);
        $result = $stmt->execute();
        $failed_24h_old = $result->fetchArray(SQLITE3_ASSOC)['count'];
        
        // Get from new service_failed_attempts table (if it exists)
        $failed_24h_new = 0;
        try {
            $stmt = $this->db->prepare('SELECT COUNT(*) as count FROM service_failed_attempts WHERE attempt_time > :time');
            $stmt->bindValue(':time', $cutoff_time, SQLITE3_INTEGER);
            $result = $stmt->execute();
            $failed_24h_new = $result->fetchArray(SQLITE3_ASSOC)['count'];
        } catch (Exception $e) {
            // Table might not exist in older installations
        }
        
        $failed_24h = $failed_24h_old + $failed_24h_new;
        
        // Get exemption list count
        $stmt = $this->db->prepare('SELECT COUNT(*) as count FROM whitelist_ips');
        $result = $stmt->execute();
        $exemption_count = $result->fetchArray(SQLITE3_ASSOC)['count'];
        
        return array(
            'blocked_ips' => $blocked_count,
            'failed_attempts_24h' => $failed_24h,
            'exemption_ips' => $exemption_count,
            'whitelist_ips' => $exemption_count // Backward compatibility
        );
    }
    
    // New exemption terminology methods (for clearer semantics)
    
    /**
     * Adds an IP address to the exemption list (modern terminology)
     * 
     * Exempted IPs are never blocked by any monitoring system. This method
     * provides modern, clear terminology as an alias for add_whitelist_ip().
     * 
     * @param string $ip_address IPv4 address to exempt from blocking
     * @param string $description Optional description for the exemption
     * @return bool True on success, false on failure
     * @since 2025.08.73
     * 
     * @example
     * $security->add_exemption_ip('192.168.1.100', 'Office gateway');
     */
    public function add_exemption_ip($ip_address, $description = '') {
        return $this->add_whitelist_ip($ip_address, $description);
    }
    
    /**
     * Removes an IP address from the exemption list (modern terminology)
     * 
     * Removes IP from exemption list, allowing it to be blocked by monitoring
     * systems. Provides modern, clear terminology as alias for remove_whitelist_ip().
     * 
     * @param string $ip_address IPv4 address to remove from exemption list
     * @return bool True on success, false on failure
     * @since 2025.08.73
     * 
     * @example
     * $security->remove_exemption_ip('192.168.1.100');
     */
    public function remove_exemption_ip($ip_address) {
        return $this->remove_whitelist_ip($ip_address);
    }
    
    /**
     * Retrieves all IP addresses in the exemption list (modern terminology)
     * 
     * Returns array of exempted IP addresses with descriptions. Provides
     * modern, clear terminology as alias for get_whitelist_ips().
     * 
     * @return array Array of exemption records with ip_address and description
     * @since 2025.08.73
     * 
     * @example
     * $exempted = $security->get_exemption_ips();
     * foreach ($exempted as $exempt) {
     *     echo $exempt['ip_address'] . ' - ' . $exempt['description'];
     * }
     */
    public function get_exemption_ips() {
        return $this->get_whitelist_ips();
    }
    
    /**
     * Checks if an IP address is in the exemption list (modern terminology)
     * 
     * Returns true if IP is exempted from blocking. Provides modern, clear
     * terminology as alias for is_ip_whitelisted().
     * 
     * @param string $ip_address IPv4 address to check
     * @return bool True if IP is exempted, false otherwise
     * @since 2025.08.73
     * 
     * @example
     * if ($security->is_ip_exempted('192.168.1.100')) {
     *     echo "IP is exempted from blocking";
     * }
     */
    public function is_ip_exempted($ip_address) {
        return $this->is_ip_whitelisted($ip_address);
    }
    
    public function __destruct() {
        if ($this->db) {
            $this->db->close();
        }
    }
} 