<?php

/**
 * SSH/FTP Log Monitor Manager
 * 
 * Monitors SSH and FTP log files for failed login attempts and automatically
 * blocks IPs that exceed configured thresholds using service-level or 
 * iptables-based blocking methods.
 * 
 * Features:
 * - Real-time log parsing with position tracking
 * - High-performance in-memory caching with SQLite persistence  
 * - Service-level blocking via SSH/FTP configuration includes
 * - Configurable thresholds and block durations per service
 * - Exemption list integration
 * - Multiple blocking methods (service-level vs iptables)
 * 
 * @version 2025.08.73
 * @requires PHP 8.2+, SQLite3, systemctl access
 * @author WHP Development Team
 * @package WHP\Security
 */
class log_monitor_manager {
    
    private $db_path = '/docker/whp/sql/security.db';
    private $db;
    private $config = array();
    private $cache_file = '/docker/whp/sql/log_monitor.cache';
    private $attempts_cache = array(); // In-memory cache for performance
    private $last_cache_save = 0;
    
    /**
     * Constructor - Initialize database and configuration
     * 
     * Sets up SQLite database connection, creates required tables if they don't exist,
     * loads default configuration values, and initializes in-memory cache for
     * high-performance log monitoring operations.
     * 
     * @throws Exception If database connection or table creation fails
     * @since 2025.08.73
     */
    public function __construct() {
        $this->init_database();
        $this->load_config();
        $this->load_cache();
    }
    
    private function init_database() {
        $this->db = new SQLite3($this->db_path);
        $this->db->busyTimeout(6000);
        
        // Ensure all required tables exist
        $this->ensure_tables();
    }
    
    private function ensure_tables() {
        // Check if our new tables exist, create if not
        $tables = [
            'security_config' => 'CREATE TABLE IF NOT EXISTS security_config (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                setting_name TEXT UNIQUE NOT NULL,
                setting_value TEXT NOT NULL,
                description TEXT,
                last_modified INTEGER DEFAULT (strftime(\'%s\', \'now\'))
            )',
            'log_positions' => 'CREATE TABLE IF NOT EXISTS log_positions (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                log_file_path TEXT UNIQUE NOT NULL,
                last_position INTEGER DEFAULT 0,
                last_inode INTEGER DEFAULT 0,
                last_check INTEGER DEFAULT (strftime(\'%s\', \'now\'))
            )',
            'service_failed_attempts' => 'CREATE TABLE IF NOT EXISTS service_failed_attempts (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                service_type TEXT NOT NULL,
                ip_address TEXT NOT NULL,
                username TEXT NOT NULL,
                attempt_time INTEGER NOT NULL,
                log_line TEXT,
                user_agent TEXT DEFAULT NULL
            )',
            'service_blocked_ips' => 'CREATE TABLE IF NOT EXISTS service_blocked_ips (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                service_type TEXT NOT NULL,
                ip_address TEXT NOT NULL,
                block_time INTEGER NOT NULL,
                unblock_time INTEGER NOT NULL,
                reason TEXT,
                failed_attempts INTEGER DEFAULT 0,
                firewall_rule_added INTEGER DEFAULT 0,
                UNIQUE(service_type, ip_address)
            )'
        ];
        
        foreach ($tables as $table_name => $sql) {
            $stmt = $this->db->prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?");
            $stmt->bindValue(1, $table_name, SQLITE3_TEXT);
            $result = $stmt->execute();
            if (!$result->fetchArray()) {
                $this->db->exec($sql);
            }
        }
        
        // Create indexes
        $indexes = [
            'CREATE INDEX IF NOT EXISTS idx_service_attempts_ip ON service_failed_attempts(ip_address)',
            'CREATE INDEX IF NOT EXISTS idx_service_attempts_time ON service_failed_attempts(attempt_time)',
            'CREATE INDEX IF NOT EXISTS idx_service_attempts_service ON service_failed_attempts(service_type)',
            'CREATE INDEX IF NOT EXISTS idx_service_blocked_ip ON service_blocked_ips(ip_address)',
            'CREATE INDEX IF NOT EXISTS idx_service_blocked_time ON service_blocked_ips(unblock_time)',
            'CREATE INDEX IF NOT EXISTS idx_service_blocked_service ON service_blocked_ips(service_type)'
        ];
        
        foreach ($indexes as $index) {
            $this->db->exec($index);
        }
        
        // Insert default configuration if not exists
        $this->insert_default_config();
    }
    
    private function insert_default_config() {
        $defaults = [
            'ssh_monitoring_enabled' => ['1', 'Enable SSH failed login monitoring'],
            'ftp_monitoring_enabled' => ['1', 'Enable FTP failed login monitoring'],
            'max_ssh_attempts' => ['5', 'Maximum SSH failed attempts before IP block'],
            'max_ftp_attempts' => ['5', 'Maximum FTP failed attempts before IP block'],
            'ssh_block_duration' => ['3600', 'SSH IP block duration in seconds (1 hour)'],
            'ftp_block_duration' => ['3600', 'FTP IP block duration in seconds (1 hour)'],
            'log_check_interval' => ['300', 'How often to check logs in seconds (5 minutes)'],
            'ssh_log_file' => ['/var/log/secure', 'SSH/Auth log file path'],
            'ftp_log_file' => ['/var/log/proftpd/access.log', 'FTP log file path'],
            'blocking_method' => ['service', 'IP blocking method (service=service-level, iptables=firewall-level)'],
            'docker_network_preserve' => ['1', 'Preserve Docker network access when blocking IPs']
        ];
        
        foreach ($defaults as $name => $data) {
            $stmt = $this->db->prepare('INSERT OR IGNORE INTO security_config (setting_name, setting_value, description) VALUES (?, ?, ?)');
            $stmt->bindValue(1, $name, SQLITE3_TEXT);
            $stmt->bindValue(2, $data[0], SQLITE3_TEXT);
            $stmt->bindValue(3, $data[1], SQLITE3_TEXT);
            $stmt->execute();
        }
    }
    
    private function load_config() {
        $result = $this->db->query('SELECT setting_name, setting_value FROM security_config');
        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
            $this->config[$row['setting_name']] = $row['setting_value'];
        }
    }
    
    /**
     * Retrieves a configuration value with optional default
     * 
     * Looks up configuration value from in-memory cache first, then database
     * if not cached. Returns the default value if the key is not found.
     * 
     * @param string $key Configuration key name
     * @param mixed $default Default value to return if key not found
     * @return string|mixed Configuration value or default value
     * @since 2025.08.73
     * 
     * @example
     * $max_attempts = $monitor->get_config('max_ssh_attempts', '5');
     * $log_file = $monitor->get_config('ssh_log_file', '/var/log/auth.log');
     */
    public function get_config($key, $default = null) {
        return isset($this->config[$key]) ? $this->config[$key] : $default;
    }
    
    private function is_safe_log_path($path) {
        // List of allowed log directories
        $allowed_paths = [
            '/var/log/',
            '/docker/whp/logs/',
            '/tmp/whp-logs/'
        ];
        
        // Get the real path to prevent symlink attacks
        $real_path = realpath($path);
        if ($real_path === false) {
            // If file doesn't exist yet, check the directory
            $dir_path = dirname($path);
            $real_path = realpath($dir_path);
            if ($real_path === false) {
                return false;
            }
            $real_path = $real_path . '/' . basename($path);
        }
        
        // Check if the path is within allowed directories
        foreach ($allowed_paths as $allowed) {
            if (strpos($real_path, $allowed) === 0) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Sets a configuration value with validation and persistence
     * 
     * Validates the configuration key against allowed keys, performs value-specific
     * validation, and stores the setting in the database. All configuration changes
     * are logged for audit purposes.
     * 
     * @param string $key Configuration key name (must be in allowed keys list)
     * @param string $value Configuration value (validated based on key type)
     * @return bool True on success, false on validation failure or database error
     * @throws InvalidArgumentException If key is not allowed or value is invalid
     * @since 2025.08.73
     * 
     * @example
     * $monitor->set_config('max_ssh_attempts', '10');
     * $monitor->set_config('blocking_method', 'service');
     */
    public function set_config($key, $value) {
        // Validate log file paths before saving
        if (in_array($key, ['ssh_log_file', 'ftp_log_file'])) {
            if (!$this->is_safe_log_path($value)) {
                return false;
            }
        }
        
        // Validate numeric settings
        if (in_array($key, ['max_ssh_attempts', 'max_ftp_attempts'])) {
            if (!is_numeric($value) || $value < 1 || $value > 50) {
                return false;
            }
        }
        
        if (in_array($key, ['ssh_block_duration', 'ftp_block_duration', 'log_check_interval'])) {
            if (!is_numeric($value) || $value < 60 || $value > 86400) {
                return false;
            }
        }
        
        $stmt = $this->db->prepare('INSERT OR REPLACE INTO security_config (setting_name, setting_value, last_modified) VALUES (?, ?, ?)');
        $stmt->bindValue(1, $key, SQLITE3_TEXT);
        $stmt->bindValue(2, $value, SQLITE3_TEXT);
        $stmt->bindValue(3, time(), SQLITE3_INTEGER);
        $result = $stmt->execute();
        
        if ($result) {
            $this->config[$key] = $value;
        }
        
        return $result;
    }
    
    public function get_all_config() {
        $result = $this->db->query('SELECT * FROM security_config ORDER BY setting_name');
        $config = array();
        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
            $config[] = $row;
        }
        return $config;
    }
    
    /**
     * Main log monitoring entry point
     * 
     * Scans configured log files for new failed login attempts and applies blocking rules
     * based on configured thresholds. Processes both SSH and FTP logs incrementally,
     * tracking file positions to avoid reprocessing entries.
     * 
     * @return array Statistics about processed attempts and blocks applied
     * @throws Exception If database operations fail or log files cannot be accessed
     * @since 2025.08.73
     * 
     * @example
     * $monitor = new log_monitor_manager();
     * $stats = $monitor->check_logs();
     * echo "Processed {$stats['ssh_attempts']} SSH attempts";
     */
    public function check_logs() {
        if ($this->get_config('ssh_monitoring_enabled') == '1') {
            $this->check_ssh_logs();
        }
        
        if ($this->get_config('ftp_monitoring_enabled') == '1') {
            $this->check_ftp_logs();
        }
        
        // Clean up expired blocks
        $this->cleanup_expired_service_blocks();
    }
    
    private function check_ssh_logs() {
        $log_file = $this->get_config('ssh_log_file', '/var/log/secure');
        
        // Validate log file path to prevent path traversal
        if (!$this->is_safe_log_path($log_file)) {
            return false;
        }
        
        if (!file_exists($log_file)) {
            return false;
        }
        
        $position = $this->get_log_position($log_file);
        $new_lines = $this->read_new_log_lines($log_file, $position);
        
        foreach ($new_lines as $line) {
            $this->parse_ssh_log_line($line);
        }
        
        return true;
    }
    
    private function check_ftp_logs() {
        $log_file = $this->get_config('ftp_log_file', '/var/log/proftpd/access.log');
        
        // Validate log file path to prevent path traversal
        if (!$this->is_safe_log_path($log_file)) {
            return false;
        }
        
        if (!file_exists($log_file)) {
            return false;
        }
        
        $position = $this->get_log_position($log_file);
        $new_lines = $this->read_new_log_lines($log_file, $position);
        
        foreach ($new_lines as $line) {
            $this->parse_ftp_log_line($line);
        }
        
        return true;
    }
    
    private function get_log_position($log_file) {
        $stmt = $this->db->prepare('SELECT last_position, last_inode FROM log_positions WHERE log_file_path = ?');
        $stmt->bindValue(1, $log_file, SQLITE3_TEXT);
        $result = $stmt->execute();
        $row = $result->fetchArray(SQLITE3_ASSOC);
        
        if ($row) {
            $current_inode = fileinode($log_file);
            // If inode changed, file was rotated, start from beginning
            if ($current_inode != $row['last_inode']) {
                return $this->get_safe_starting_position($log_file);
            }
            return $row['last_position'];
        }
        
        // No position stored - for large files, start near the end
        return $this->get_safe_starting_position($log_file);
    }
    
    /**
     * Get a safe starting position for large log files
     * 
     * For files larger than 1MB, starts from near the end (last 100KB)
     * to avoid memory exhaustion when processing very large log files.
     * 
     * @param string $log_file Path to the log file
     * @return int Starting position in bytes
     * @since 2025.08.73
     */
    private function get_safe_starting_position($log_file) {
        $file_size = filesize($log_file);
        
        // For files larger than 1MB, start from last 100KB
        if ($file_size > 1048576) { // 1MB = 1048576 bytes
            // Start 100KB from the end (or from beginning if file is smaller)
            $safe_position = max(0, $file_size - 102400); // 100KB = 102400 bytes
            
            // Try to find a line break near our position to avoid partial lines
            $file = fopen($log_file, 'r');
            if ($file) {
                fseek($file, $safe_position);
                // Read and discard partial line
                fgets($file);
                $aligned_position = ftell($file);
                fclose($file);
                return $aligned_position;
            }
            
            return $safe_position;
        }
        
        // For smaller files, start from the beginning
        return 0;
    }
    
    private function update_log_position($log_file, $position) {
        $inode = fileinode($log_file);
        $stmt = $this->db->prepare('INSERT OR REPLACE INTO log_positions (log_file_path, last_position, last_inode, last_check) VALUES (?, ?, ?, ?)');
        $stmt->bindValue(1, $log_file, SQLITE3_TEXT);
        $stmt->bindValue(2, $position, SQLITE3_INTEGER);
        $stmt->bindValue(3, $inode, SQLITE3_INTEGER);
        $stmt->bindValue(4, time(), SQLITE3_INTEGER);
        $stmt->execute();
    }
    
    private function read_new_log_lines($log_file, $start_position) {
        $lines = array();
        $file = fopen($log_file, 'r');
        
        if (!$file) {
            return $lines;
        }
        
        fseek($file, $start_position);
        
        // Limit to 10000 lines per run to prevent memory issues
        $max_lines = 10000;
        $line_count = 0;
        
        while (($line = fgets($file)) !== false && $line_count < $max_lines) {
            $lines[] = trim($line);
            $line_count++;
        }
        
        $new_position = ftell($file);
        fclose($file);
        
        $this->update_log_position($log_file, $new_position);
        
        // Log a warning if we hit the limit
        if ($line_count >= $max_lines) {
            error_log("WHP Log Monitor: Processed maximum of $max_lines lines from $log_file. Will continue in next run.");
        }
        
        return $lines;
    }
    
    private function parse_ssh_log_line($line) {
        // Limit line length to prevent excessive memory usage
        if (strlen($line) > 2048) {
            return;
        }
        
        // SSH failed login patterns for /var/log/secure format
        $patterns = [
            // SSH password authentication failed (with or without "invalid user")
            '/Failed password for (?:invalid user )?(\S+) from (\d+\.\d+\.\d+\.\d+) port \d+ ssh2/',
            // Invalid user attempt
            '/Invalid user (\S+) from (\d+\.\d+\.\d+\.\d+) port \d+/',
            // PAM authentication failure
            '/pam_unix\(sshd:auth\): authentication failure;.*rhost=(\d+\.\d+\.\d+\.\d+)(?:\s+user=(\S+))?/',
            // Connection closed (potential brute force)
            '/Connection closed by (\d+\.\d+\.\d+\.\d+) port \d+/',
        ];
        
        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $line, $matches)) {
                // Determine IP and username based on pattern matched
                if (strpos($line, 'pam_unix') !== false) {
                    // PAM pattern: IP is in $matches[1], username in $matches[2] (if present)
                    $ip = $matches[1];
                    $username = isset($matches[2]) && !empty($matches[2]) ? $matches[2] : 'unknown';
                } elseif (strpos($line, 'Connection closed') !== false) {
                    // Connection closed: IP is in $matches[1], no username
                    $ip = $matches[1];
                    $username = 'unknown';
                } else {
                    // Failed password or Invalid user: username in $matches[1], IP in $matches[2]
                    $username = $matches[1];
                    $ip = $matches[2];
                }
                
                // Validate extracted data
                if (filter_var($ip, FILTER_VALIDATE_IP)) {
                    // Sanitize username (allow alphanumeric, underscore, dash, max 32 chars)
                    $username = substr(preg_replace('/[^a-zA-Z0-9_-]/', '', $username), 0, 32);
                    if (empty($username)) {
                        $username = 'unknown';
                    }
                    
                    $this->record_service_failed_attempt('ssh', $ip, $username, $line);
                }
                break;
            }
        }
    }
    
    private function parse_ftp_log_line($line) {
        // Limit line length to prevent excessive memory usage
        if (strlen($line) > 2048) {
            return;
        }
        
        // ProFTPD access.log format:
        // IP_ADDRESS USERNAME - [timestamp] "COMMAND" STATUS_CODE -
        // Common failure status codes:
        // 530 = authentication failed (PASS command)
        // 550 = permission denied (USER command, file access)
        // 502 = command not implemented (potential scanning)
        
        // Pattern to match various failed FTP attempts
        if (preg_match('/^(\d+\.\d+\.\d+\.\d+)\s+(\S+)\s+-\s+\[.*?\]\s+"([^"]+)"\s+(530|550|502)/', $line, $matches)) {
            $ip = $matches[1];
            $username_field = $matches[2];
            $status_code = $matches[3];
            
            // Validate IP address
            if (filter_var($ip, FILTER_VALIDATE_IP)) {
                // Clean up username - if it's UNKNOWN, use 'unknown'
                $username = ($username_field !== 'UNKNOWN' && $username_field !== '-') ? $username_field : 'unknown';
                
                // Sanitize username (allow alphanumeric, underscore, dash, max 32 chars)
                $username = substr(preg_replace('/[^a-zA-Z0-9_-]/', '', $username), 0, 32);
                if (empty($username)) {
                    $username = 'unknown';
                }
                
                // Record the failed attempt
                $this->record_service_failed_attempt('ftp', $ip, $username, $line);
                return true;
            }
        }
        
        return false;
    }
    
    private function record_service_failed_attempt($service, $ip, $username, $log_line) {
        // Check if IP is whitelisted
        require_once('/docker/whp/web/libs/security_manager.php');
        $sec_manager = new security_manager();
        if ($sec_manager->is_ip_whitelisted($ip)) {
            return false;
        }
        
        // Use in-memory cache for performance
        $key = $service . '_' . $ip;
        if (!isset($this->attempts_cache[$key])) {
            $this->attempts_cache[$key] = array(
                'service' => $service,
                'ip' => $ip,
                'attempts' => array(),
                'blocked' => false
            );
        }
        
        // Add attempt to cache
        $attempt_time = time();
        $this->attempts_cache[$key]['attempts'][] = array(
            'username' => $username,
            'time' => $attempt_time,
            'log_line' => $log_line
        );
        
        // Also save to database for statistics and reporting
        $stmt = $this->db->prepare('INSERT INTO service_failed_attempts (service_type, ip_address, username, attempt_time, log_line) VALUES (?, ?, ?, ?, ?)');
        $stmt->bindValue(1, $service, SQLITE3_TEXT);
        $stmt->bindValue(2, $ip, SQLITE3_TEXT);
        $stmt->bindValue(3, $username, SQLITE3_TEXT);
        $stmt->bindValue(4, $attempt_time, SQLITE3_INTEGER);
        $stmt->bindValue(5, $log_line, SQLITE3_TEXT);
        $stmt->execute();
        
        // Clean old attempts (older than 1 hour)
        $cutoff = time() - 3600;
        $this->attempts_cache[$key]['attempts'] = array_filter(
            $this->attempts_cache[$key]['attempts'],
            function($attempt) use ($cutoff) {
                return $attempt['time'] > $cutoff;
            }
        );
        
        // Check if this IP should be blocked
        $this->check_and_block_service_ip($service, $ip);
        
        // Save cache periodically (every 30 seconds) to avoid constant disk writes
        if (time() - $this->last_cache_save > 30) {
            $this->save_cache();
        }
        
        return true;
    }
    
    private function check_and_block_service_ip($service, $ip) {
        $max_attempts_key = 'max_' . $service . '_attempts';
        $max_attempts = (int)$this->get_config($max_attempts_key, 5);
        
        // Use cache for fast lookup
        $key = $service . '_' . $ip;
        if (!isset($this->attempts_cache[$key])) {
            return;
        }
        
        $recent_attempts = count($this->attempts_cache[$key]['attempts']);
        
        if ($recent_attempts >= $max_attempts && !$this->attempts_cache[$key]['blocked']) {
            $this->attempts_cache[$key]['blocked'] = true;
            $this->block_service_ip($service, $ip, "Too many failed {$service} login attempts", $recent_attempts);
        }
    }
    
    private function load_cache() {
        if (file_exists($this->cache_file)) {
            $data = file_get_contents($this->cache_file);
            if ($data) {
                $cache = json_decode($data, true);
                if ($cache && is_array($cache)) {
                    $this->attempts_cache = $cache;
                    // Clean old entries from cache
                    $cutoff = time() - 3600;
                    foreach ($this->attempts_cache as $key => $data) {
                        if (isset($data['attempts'])) {
                            $this->attempts_cache[$key]['attempts'] = array_filter(
                                $data['attempts'],
                                function($attempt) use ($cutoff) {
                                    return $attempt['time'] > $cutoff;
                                }
                            );
                            // Remove empty entries
                            if (empty($this->attempts_cache[$key]['attempts'])) {
                                unset($this->attempts_cache[$key]);
                            }
                        }
                    }
                }
            }
        }
    }
    
    private function save_cache() {
        $json = json_encode($this->attempts_cache);
        if ($json) {
            file_put_contents($this->cache_file, $json, LOCK_EX);
            $this->last_cache_save = time();
        }
    }
    
    /**
     * Blocks an IP address for a specific service
     * 
     * Applies blocking using the configured method (service-level or iptables).
     * Checks exemption list before blocking and validates all inputs. Updates
     * service configuration files and reloads services gracefully.
     * 
     * @param string $service Service name ('ssh' or 'ftp')
     * @param string $ip IP address to block (IPv4 format)
     * @param string $reason Human-readable reason for blocking
     * @param int $failed_attempts Number of failed attempts that triggered block
     * @return bool True on success, false on failure or if IP is exempted
     * @throws InvalidArgumentException If service or IP format is invalid
     * @since 2025.08.73
     * 
     * @example
     * $result = $monitor->block_service_ip('ssh', '203.0.113.45', 'Too many failed attempts', 15);
     * if ($result) echo "IP blocked successfully";
     */
    public function block_service_ip($service, $ip, $reason = 'Manual block', $failed_attempts = 0, $permanent = false) {
        // Check if IP is whitelisted
        require_once('/docker/whp/web/libs/security_manager.php');
        $sec_manager = new security_manager();
        if ($sec_manager->is_ip_whitelisted($ip)) {
            return false;
        }
        
        // Check if IP is already blocked for this service to prevent infinite loops
        $stmt = $this->db->prepare('SELECT * FROM service_blocked_ips WHERE service_type = ? AND ip_address = ? AND (unblock_time > ? OR unblock_time = -1)');
        $stmt->bindValue(1, $service, SQLITE3_TEXT);
        $stmt->bindValue(2, $ip, SQLITE3_TEXT);
        $stmt->bindValue(3, time(), SQLITE3_INTEGER);
        $result = $stmt->execute();
        if ($result->fetchArray()) {
            // IP is already blocked for this service, return success without doing anything
            return true;
        }
        
        $block_duration_key = $service . '_block_duration';
        $block_duration = (int)$this->get_config($block_duration_key, 3600);
        
        // 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, "$service: $reason", 'WHP Log Monitor');
        } catch (Exception $e) {
            error_log("Failed to block IP in HAProxy from log monitor: " . $e->getMessage());
            // Continue even if HAProxy blocking fails - service block is still active
        }
        
        $firewall_rule_added = 0;
        $blocking_method = $this->get_config('blocking_method', 'service');
        if ($blocking_method === 'iptables') {
            $firewall_rule_added = $this->add_iptables_rule($ip) ? 1 : 0;
        } else {
            // Use service-level blocking by default
            $firewall_rule_added = $this->add_service_blocks($ip) ? 1 : 0;
        }
        
        // Calculate unblock time - -1 means permanent block
        $unblock_time = $permanent ? -1 : (time() + $block_duration);
        
        $stmt = $this->db->prepare('INSERT OR REPLACE INTO service_blocked_ips (service_type, ip_address, block_time, unblock_time, reason, failed_attempts, firewall_rule_added) VALUES (?, ?, ?, ?, ?, ?, ?)');
        $stmt->bindValue(1, $service, SQLITE3_TEXT);
        $stmt->bindValue(2, $ip, SQLITE3_TEXT);
        $stmt->bindValue(3, time(), SQLITE3_INTEGER);
        $stmt->bindValue(4, $unblock_time, SQLITE3_INTEGER);
        $stmt->bindValue(5, $reason, SQLITE3_TEXT);
        $stmt->bindValue(6, $failed_attempts, SQLITE3_INTEGER);
        $stmt->bindValue(7, $firewall_rule_added, SQLITE3_INTEGER);
        $stmt->execute();
        
        // Also add to main security manager for web interface compatibility (skip service blocks to prevent loops)
        $sec_manager->block_ip($ip, $reason, $failed_attempts, false, true);
        
        return true;
    }
    
    /**
     * Unblock an IP address from ALL services
     * 
     * @param string $ip IP address to unblock
     * @return bool True on success, false on failure
     */
    public function unblock_ip_from_all_services($ip) {
        if (!filter_var($ip, FILTER_VALIDATE_IP)) {
            return false;
        }
        
        // Get all services this IP is blocked from
        $stmt = $this->db->prepare('SELECT DISTINCT service_type FROM service_blocked_ips WHERE ip_address = ? AND (unblock_time > ? OR unblock_time = -1)');
        $stmt->bindValue(1, $ip, SQLITE3_TEXT);
        $stmt->bindValue(2, time(), SQLITE3_INTEGER);
        $result = $stmt->execute();
        
        $services = [];
        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
            $services[] = $row['service_type'];
        }
        
        // Unblock from each service
        foreach ($services as $service) {
            $this->unblock_service_ip($service, $ip);
        }
        
        return !empty($services);
    }

    /**
     * Removes an IP address from service-specific block list
     * 
     * Removes blocking rules using the same method that was used to apply them.
     * Updates service configuration files and reloads services gracefully.
     * Also clears failed attempt records for the IP.
     * 
     * @param string $service Service name ('ssh' or 'ftp')
     * @param string $ip IP address to unblock (IPv4 format)
     * @return bool True on success, false on failure
     * @throws InvalidArgumentException If service or IP format is invalid
     * @since 2025.08.73
     * 
     * @example
     * $result = $monitor->unblock_service_ip('ssh', '203.0.113.45');
     * if ($result) echo "IP unblocked successfully";
     */
    public function unblock_service_ip($service, $ip) {
        // Check what type of blocking was used
        $stmt = $this->db->prepare('SELECT firewall_rule_added FROM service_blocked_ips WHERE service_type = ? AND ip_address = ?');
        $stmt->bindValue(1, $service, SQLITE3_TEXT);
        $stmt->bindValue(2, $ip, SQLITE3_TEXT);
        $result = $stmt->execute();
        $row = $result->fetchArray(SQLITE3_ASSOC);
        
        if ($row && $row['firewall_rule_added']) {
            $blocking_method = $this->get_config('blocking_method', 'service');
            if ($blocking_method === 'iptables') {
                $this->remove_iptables_rule($ip);
            } else {
                $this->remove_service_blocks($ip);
            }
        }
        
        // Remove from service blocked IPs
        $stmt = $this->db->prepare('DELETE FROM service_blocked_ips WHERE service_type = ? AND ip_address = ?');
        $stmt->bindValue(1, $service, SQLITE3_TEXT);
        $stmt->bindValue(2, $ip, SQLITE3_TEXT);
        $stmt->execute();
        
        // Clear failed attempts
        $stmt = $this->db->prepare('DELETE FROM service_failed_attempts WHERE service_type = ? AND ip_address = ?');
        $stmt->bindValue(1, $service, SQLITE3_TEXT);
        $stmt->bindValue(2, $ip, SQLITE3_TEXT);
        $stmt->execute();
        
        return true;
    }
    
    private function remove_service_blocks($ip) {
        // Validate IP address first
        if (!filter_var($ip, FILTER_VALIDATE_IP)) {
            return false;
        }
        
        $success = true;
        
        // Remove SSH service block
        if (!$this->remove_ssh_service_block($ip)) {
            $success = false;
        }
        
        // Remove FTP service block
        if (!$this->remove_ftp_service_block($ip)) {
            $success = false;
        }
        
        return $success;
    }
    
    private function remove_ssh_service_block($ip) {
        $ssh_blocked_file = '/etc/ssh/whp_blocked_ips.conf';
        
        if (!file_exists($ssh_blocked_file)) {
            return true; // Nothing to remove
        }
        
        // Read existing blocked IPs
        $content = file_get_contents($ssh_blocked_file);
        preg_match_all('/DenyUsers \*@([0-9\.]+)/', $content, $matches);
        
        if (!empty($matches[1])) {
            $blocked_ips = $matches[1];
            
            // Remove the IP from the list
            $blocked_ips = array_filter($blocked_ips, function($blocked_ip) use ($ip) {
                return $blocked_ip !== $ip;
            });
            
            // Write updated configuration
            $config_content = "# WHP SSH Blocked IPs - Auto-generated\n";
            foreach ($blocked_ips as $blocked_ip) {
                $config_content .= "DenyUsers *@$blocked_ip\n";
            }
            
            if (file_put_contents($ssh_blocked_file, $config_content, LOCK_EX) === false) {
                return false;
            }
            
            // Set secure file permissions
            chmod($ssh_blocked_file, 0600);
            
            // Reload SSH service gracefully using proc_open for security
            $descriptors = [1 => ["pipe", "w"], 2 => ["pipe", "w"]];
            $process = proc_open(["systemctl", "reload", "sshd"], $descriptors, $pipes);
            if (is_resource($process)) {
                fclose($pipes[1]);
                fclose($pipes[2]);
                $return = proc_close($process);
                return $return === 0;
            }
            return false;
        }
        
        return true;
    }
    
    private function remove_ftp_service_block($ip) {
        $ftp_blocked_file = '/etc/proftpd/conf.d/whp_blocked_ips.conf';
        
        if (!file_exists($ftp_blocked_file)) {
            return true; // Nothing to remove
        }
        
        // Read existing blocked IPs
        $content = file_get_contents($ftp_blocked_file);
        preg_match_all('/Deny from ([0-9\.]+)/', $content, $matches);
        
        if (!empty($matches[1])) {
            $blocked_ips = $matches[1];
            
            // Remove the IP from the list
            $blocked_ips = array_filter($blocked_ips, function($blocked_ip) use ($ip) {
                return $blocked_ip !== $ip;
            });
            
            // Write updated configuration - use Global context to avoid Directory conflicts
            $config_content = "# WHP FTP Blocked IPs - Auto-generated\n";
            $config_content .= "# Global FTP access restrictions\n";
            $config_content .= "<Global>\n";
            $config_content .= "  <Limit LOGIN>\n";
            $config_content .= "    Order allow,deny\n";
            $config_content .= "    Allow from all\n";
            foreach ($blocked_ips as $blocked_ip) {
                $config_content .= "    Deny from $blocked_ip\n";
            }
            $config_content .= "  </Limit>\n";
            $config_content .= "</Global>\n";
            
            if (file_put_contents($ftp_blocked_file, $config_content, LOCK_EX) === false) {
                return false;
            }
            
            // Set secure file permissions
            chmod($ftp_blocked_file, 0600);
            
            // Reload ProFTPD service gracefully using proc_open for security
            $descriptors = [1 => ["pipe", "w"], 2 => ["pipe", "w"]];
            $process = proc_open(["systemctl", "reload", "proftpd"], $descriptors, $pipes);
            if (is_resource($process)) {
                fclose($pipes[1]);
                fclose($pipes[2]);
                $return = proc_close($process);
                return $return === 0;
            }
            return false;
        }
        
        return true;
    }
    
    public function is_service_ip_blocked($service, $ip) {
        $stmt = $this->db->prepare('SELECT * FROM service_blocked_ips WHERE service_type = ? AND ip_address = ? AND (unblock_time > ? OR unblock_time = -1)');
        $stmt->bindValue(1, $service, SQLITE3_TEXT);
        $stmt->bindValue(2, $ip, SQLITE3_TEXT);
        $stmt->bindValue(3, time(), SQLITE3_INTEGER);
        $result = $stmt->execute();
        $row = $result->fetchArray(SQLITE3_ASSOC);
        
        return $row ? $row : false;
    }
    
    private function add_service_blocks($ip) {
        // Validate IP address first
        if (!filter_var($ip, FILTER_VALIDATE_IP)) {
            return false;
        }
        
        $success = true;
        
        // Block SSH service
        if ($this->get_config('ssh_monitoring_enabled') == '1') {
            if (!$this->add_ssh_service_block($ip)) {
                $success = false;
            }
        }
        
        // Block FTP service
        if ($this->get_config('ftp_monitoring_enabled') == '1') {
            if (!$this->add_ftp_service_block($ip)) {
                $success = false;
            }
        }
        
        return $success;
    }
    
    private function add_ssh_service_block($ip) {
        $ssh_blocked_file = '/etc/ssh/whp_blocked_ips.conf';
        
        // Read existing blocked IPs
        $blocked_ips = array();
        if (file_exists($ssh_blocked_file)) {
            $content = file_get_contents($ssh_blocked_file);
            preg_match_all('/DenyUsers \*@([0-9\.]+)/', $content, $matches);
            if (!empty($matches[1])) {
                $blocked_ips = $matches[1];
            }
        }
        
        // Add IP if not already blocked
        if (!in_array($ip, $blocked_ips)) {
            $blocked_ips[] = $ip;
            
            // Write updated configuration
            $config_content = "# WHP SSH Blocked IPs - Auto-generated\n";
            foreach ($blocked_ips as $blocked_ip) {
                $config_content .= "DenyUsers *@$blocked_ip\n";
            }
            
            if (file_put_contents($ssh_blocked_file, $config_content, LOCK_EX) === false) {
                return false;
            }
            
            // Ensure main sshd_config includes our file
            $this->ensure_ssh_config_include();
            
            // Set secure file permissions
            chmod($ssh_blocked_file, 0600);
            
            // Reload SSH service gracefully using proc_open for security
            $descriptors = [1 => ["pipe", "w"], 2 => ["pipe", "w"]];
            $process = proc_open(["systemctl", "reload", "sshd"], $descriptors, $pipes);
            if (is_resource($process)) {
                fclose($pipes[1]);
                fclose($pipes[2]);
                $return = proc_close($process);
                return $return === 0;
            }
            return false;
        }
        
        return true; // Already blocked
    }
    
    private function add_ftp_service_block($ip) {
        $ftp_blocked_file = '/etc/proftpd/conf.d/whp_blocked_ips.conf';
        
        // Read existing blocked IPs
        $blocked_ips = array();
        if (file_exists($ftp_blocked_file)) {
            $content = file_get_contents($ftp_blocked_file);
            preg_match_all('/Deny from ([0-9\.]+)/', $content, $matches);
            if (!empty($matches[1])) {
                $blocked_ips = $matches[1];
            }
        }
        
        // Add IP if not already blocked
        if (!in_array($ip, $blocked_ips)) {
            $blocked_ips[] = $ip;
            
            // Write updated configuration - use Global context to avoid Directory conflicts
            $config_content = "# WHP FTP Blocked IPs - Auto-generated\n";
            $config_content .= "# Global FTP access restrictions\n";
            $config_content .= "<Global>\n";
            $config_content .= "  <Limit LOGIN>\n";
            $config_content .= "    Order allow,deny\n";
            $config_content .= "    Allow from all\n";
            foreach ($blocked_ips as $blocked_ip) {
                $config_content .= "    Deny from $blocked_ip\n";
            }
            $config_content .= "  </Limit>\n";
            $config_content .= "</Global>\n";
            
            if (file_put_contents($ftp_blocked_file, $config_content, LOCK_EX) === false) {
                return false;
            }
            
            // Ensure main proftpd.conf includes our file
            $this->ensure_ftp_config_include();
            
            // Set secure file permissions
            chmod($ftp_blocked_file, 0600);
            
            // Reload ProFTPD service gracefully using proc_open for security
            $descriptors = [1 => ["pipe", "w"], 2 => ["pipe", "w"]];
            $process = proc_open(["systemctl", "reload", "proftpd"], $descriptors, $pipes);
            if (is_resource($process)) {
                fclose($pipes[1]);
                fclose($pipes[2]);
                $return = proc_close($process);
                return $return === 0;
            }
            return false;
        }
        
        return true; // Already blocked
    }
    
    private function ensure_ssh_config_include() {
        $main_config = '/etc/ssh/sshd_config';
        $include_line = 'Include /etc/ssh/whp_blocked_ips.conf';
        
        if (file_exists($main_config)) {
            $content = file_get_contents($main_config);
            if (strpos($content, $include_line) === false) {
                // Add include at the beginning of the file (before other configurations)
                $content = "# WHP Blocked IPs Include\n$include_line\n\n" . $content;
                file_put_contents($main_config, $content, LOCK_EX);
            }
        }
    }
    
    private function ensure_ftp_config_include() {
        // Files in /etc/proftpd/conf.d/ are automatically included by most ProFTPD installations
        // So we don't need to manually add include statements
        // Just ensure the conf.d directory exists
        $conf_dir = '/etc/proftpd/conf.d';
        if (!is_dir($conf_dir)) {
            @mkdir($conf_dir, 0755, true);
        }
        
        // For compatibility, still check if manual include is needed
        $main_config = '/etc/proftpd/proftpd.conf';
        $include_line = 'Include /etc/proftpd/conf.d/whp_blocked_ips.conf';
        
        if (file_exists($main_config)) {
            $content = file_get_contents($main_config);
            // Only add include if conf.d directory include is not already present
            if (strpos($content, 'Include /etc/proftpd/conf.d') === false && 
                strpos($content, $include_line) === false) {
                // Add include at the end of the file
                file_put_contents($main_config, "\n# WHP Blocked IPs Include\n$include_line\n", FILE_APPEND | LOCK_EX);
            }
        }
    }
    
    private function add_iptables_rule($ip) {
        // Validate IP address first
        if (!filter_var($ip, FILTER_VALIDATE_IP)) {
            return false;
        }
        
        // Escape the IP for shell command
        $escaped_ip = escapeshellarg($ip);
        
        if ($this->get_config('docker_network_preserve') == '1') {
            // Block only specific services, not all traffic
            $services = ['22', '21', '80', '443']; // SSH, FTP, HTTP, HTTPS
            $return = 0;
            foreach ($services as $port) {
                // Validate port number
                if (!is_numeric($port) || $port < 1 || $port > 65535) {
                    continue;
                }
                $escaped_port = escapeshellarg($port);
                exec("iptables -I INPUT -s $escaped_ip -p tcp --dport $escaped_port -j DROP 2>/dev/null", $output, $cmd_return);
                if ($cmd_return !== 0) {
                    $return = $cmd_return;
                }
            }
        } else {
            // Block all traffic from IP
            exec("iptables -I INPUT -s $escaped_ip -j DROP 2>/dev/null", $output, $return);
        }
        
        return $return === 0;
    }
    
    private function remove_iptables_rule($ip) {
        // Validate IP address first
        if (!filter_var($ip, FILTER_VALIDATE_IP)) {
            return false;
        }
        
        // Escape the IP for shell command
        $escaped_ip = escapeshellarg($ip);
        
        if ($this->get_config('docker_network_preserve') == '1') {
            // Remove service-specific blocks
            $services = ['22', '21', '80', '443'];
            foreach ($services as $port) {
                // Validate port number
                if (!is_numeric($port) || $port < 1 || $port > 65535) {
                    continue;
                }
                $escaped_port = escapeshellarg($port);
                exec("iptables -D INPUT -s $escaped_ip -p tcp --dport $escaped_port -j DROP 2>/dev/null");
            }
        } else {
            // Remove full IP block
            exec("iptables -D INPUT -s $escaped_ip -j DROP 2>/dev/null");
        }
        
        return true;
    }
    
    private function cleanup_expired_service_blocks() {
        // unblock_time = -1 means "permanent" — never expire those.
        $stmt = $this->db->prepare('SELECT service_type, ip_address, firewall_rule_added FROM service_blocked_ips WHERE unblock_time != -1 AND unblock_time <= ?');
        $stmt->bindValue(1, time(), SQLITE3_INTEGER);
        $result = $stmt->execute();

        $affected_services = [];
        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
            $affected_services[$row['service_type']] = true;
            if ($row['firewall_rule_added']) {
                $this->remove_iptables_rule($row['ip_address']);
            }
        }

        $stmt = $this->db->prepare('DELETE FROM service_blocked_ips WHERE unblock_time != -1 AND unblock_time <= ?');
        $stmt->bindValue(1, time(), SQLITE3_INTEGER);
        $stmt->execute();

        // Rewrite sshd/proftpd block files from current DB state so DenyUsers /
        // Deny lines for expired blocks actually disappear from the live config.
        // Without this the files grow unbounded and eventually break sshd.
        if (!empty($affected_services['ssh'])) {
            $this->rebuild_ssh_block_conf();
        }
        if (!empty($affected_services['ftp'])) {
            $this->rebuild_ftp_block_conf();
        }

        // Clean up old failed attempts (older than 24 hours)
        $stmt = $this->db->prepare('DELETE FROM service_failed_attempts WHERE attempt_time <= ?');
        $stmt->bindValue(1, time() - 86400, SQLITE3_INTEGER);
        $stmt->execute();
    }

    public function rebuild_ssh_block_conf() {
        $ssh_blocked_file = '/etc/ssh/whp_blocked_ips.conf';

        $stmt = $this->db->prepare('SELECT DISTINCT ip_address FROM service_blocked_ips WHERE service_type = ? AND (unblock_time > ? OR unblock_time = -1) ORDER BY ip_address');
        $stmt->bindValue(1, 'ssh', SQLITE3_TEXT);
        $stmt->bindValue(2, time(), SQLITE3_INTEGER);
        $result = $stmt->execute();

        $config_content = "# WHP SSH Blocked IPs - Auto-generated\n";
        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
            $config_content .= "DenyUsers *@{$row['ip_address']}\n";
        }

        $existing = file_exists($ssh_blocked_file) ? file_get_contents($ssh_blocked_file) : '';
        if ($existing === $config_content) {
            return true;
        }

        if (file_put_contents($ssh_blocked_file, $config_content, LOCK_EX) === false) {
            return false;
        }
        chmod($ssh_blocked_file, 0600);
        $this->ensure_ssh_config_include();

        $descriptors = [1 => ["pipe", "w"], 2 => ["pipe", "w"]];
        $process = proc_open(["systemctl", "reload", "sshd"], $descriptors, $pipes);
        if (is_resource($process)) {
            fclose($pipes[1]);
            fclose($pipes[2]);
            return proc_close($process) === 0;
        }
        return false;
    }

    public function rebuild_ftp_block_conf() {
        $ftp_blocked_file = '/etc/proftpd/conf.d/whp_blocked_ips.conf';

        $stmt = $this->db->prepare('SELECT DISTINCT ip_address FROM service_blocked_ips WHERE service_type = ? AND (unblock_time > ? OR unblock_time = -1) ORDER BY ip_address');
        $stmt->bindValue(1, 'ftp', SQLITE3_TEXT);
        $stmt->bindValue(2, time(), SQLITE3_INTEGER);
        $result = $stmt->execute();

        $config_content  = "# WHP FTP Blocked IPs - Auto-generated\n";
        $config_content .= "# Global FTP access restrictions\n";
        $config_content .= "<Global>\n";
        $config_content .= "  <Limit LOGIN>\n";
        $config_content .= "    Order allow,deny\n";
        $config_content .= "    Allow from all\n";
        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
            $config_content .= "    Deny from {$row['ip_address']}\n";
        }
        $config_content .= "  </Limit>\n";
        $config_content .= "</Global>\n";

        $existing = file_exists($ftp_blocked_file) ? file_get_contents($ftp_blocked_file) : '';
        if ($existing === $config_content) {
            return true;
        }

        if (file_put_contents($ftp_blocked_file, $config_content, LOCK_EX) === false) {
            return false;
        }
        chmod($ftp_blocked_file, 0600);
        $this->ensure_ftp_config_include();

        $descriptors = [1 => ["pipe", "w"], 2 => ["pipe", "w"]];
        $process = proc_open(["systemctl", "reload", "proftpd"], $descriptors, $pipes);
        if (is_resource($process)) {
            fclose($pipes[1]);
            fclose($pipes[2]);
            return proc_close($process) === 0;
        }
        return false;
    }
    
    public function get_service_blocked_ips($service = null) {
        if ($service) {
            $stmt = $this->db->prepare('SELECT * FROM service_blocked_ips WHERE service_type = ? AND (unblock_time > ? OR unblock_time = -1) ORDER BY block_time DESC');
            $stmt->bindValue(1, $service, SQLITE3_TEXT);
            $stmt->bindValue(2, time(), SQLITE3_INTEGER);
        } else {
            $stmt = $this->db->prepare('SELECT * FROM service_blocked_ips WHERE unblock_time > ? OR unblock_time = -1 ORDER BY block_time DESC');
            $stmt->bindValue(1, time(), SQLITE3_INTEGER);
        }
        
        $result = $stmt->execute();
        $blocked = array();
        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
            $blocked[] = $row;
        }
        
        return $blocked;
    }
    
    /**
     * Get unique blocked IPs grouped by IP address showing original blocking service
     * 
     * @return array Array of unique blocked IPs with original blocking service
     */
    public function get_unique_blocked_ips() {
        $stmt = $this->db->prepare('SELECT * FROM service_blocked_ips WHERE unblock_time > ? OR unblock_time = -1 ORDER BY ip_address, block_time ASC');
        $stmt->bindValue(1, time(), SQLITE3_INTEGER);
        
        $result = $stmt->execute();
        $blocked_by_ip = array();
        
        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
            $ip = $row['ip_address'];
            
            if (!isset($blocked_by_ip[$ip])) {
                // First occurrence of this IP - use it as the blocking method/service
                $blocked_by_ip[$ip] = $row;
                $blocked_by_ip[$ip]['blocking_service'] = $row['service_type']; // Store original blocking service
                $blocked_by_ip[$ip]['service'] = $row['service_type']; // Also add 'service' field for UI compatibility
            } else {
                // Additional service for this IP - keep original service but update unblock time if later
                if ($row['unblock_time'] > $blocked_by_ip[$ip]['unblock_time']) {
                    $blocked_by_ip[$ip]['unblock_time'] = $row['unblock_time'];
                }
                
                // Sum failed attempts from all services
                $blocked_by_ip[$ip]['failed_attempts'] += $row['failed_attempts'];
            }
        }
        
        return array_values($blocked_by_ip); // Return indexed array
    }
    
    /**
     * Sync blocked IPs across all services to ensure consistency
     * 
     * @return array Array with sync results and statistics
     */
    public function sync_blocked_ips() {
        $results = [
            'success' => true,
            'synced_count' => 0,
            'errors' => [],
            'details' => []
        ];
        
        try {
            // Get all currently blocked IPs from service database
            $stmt = $this->db->prepare('SELECT DISTINCT ip_address, reason FROM service_blocked_ips WHERE unblock_time > ? OR unblock_time = -1');
            $stmt->bindValue(1, time(), SQLITE3_INTEGER);
            $result = $stmt->execute();
            
            $service_blocked_ips = [];
            while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
                $service_blocked_ips[] = [
                    'ip' => $row['ip_address'],
                    'reason' => $row['reason'] ?? 'Service block sync'
                ];
            }
            
            if (empty($service_blocked_ips)) {
                $results['details'][] = 'No blocked IPs found in service database';
                return $results;
            }
            
            // Initialize managers
            require_once('/docker/whp/web/libs/security_manager.php');
            $security_manager = new security_manager();
            
            $haproxy_available = class_exists('haproxy_manager');
            $haproxy_manager = null;
            if ($haproxy_available) {
                $haproxy_manager = new haproxy_manager();
            }
            
            // Sync each IP to all services
            foreach ($service_blocked_ips as $blocked_ip) {
                $ip = $blocked_ip['ip'];
                $reason = $blocked_ip['reason'];
                $ip_synced = false;
                
                // Sync to Security Manager
                try {
                    if (!$this->is_ip_blocked_in_security_manager($security_manager, $ip)) {
                        $security_manager->block_ip($ip, $reason . ' (sync)', 0, false, true); // Skip service blocks to prevent loop
                        $ip_synced = true;
                        $results['details'][] = "Synced $ip to Security Manager";
                    }
                } catch (Exception $e) {
                    $results['errors'][] = "Security Manager sync failed for $ip: " . $e->getMessage();
                    error_log("Security Manager sync error for IP $ip: " . $e->getMessage());
                }
                
                // Sync to HAProxy if available
                if ($haproxy_available && $haproxy_manager) {
                    try {
                        if (!$this->is_ip_blocked_in_haproxy($haproxy_manager, $ip)) {
                            $haproxy_manager->block_ip($ip, $reason . ' (sync)', 'WHP Sync');
                            $ip_synced = true;
                            $results['details'][] = "Synced $ip to HAProxy";
                        }
                    } catch (Exception $e) {
                        $results['errors'][] = "HAProxy sync failed for $ip: " . $e->getMessage();
                        error_log("HAProxy sync error for IP $ip: " . $e->getMessage());
                    }
                }
                
                if ($ip_synced) {
                    $results['synced_count']++;
                }
            }
            
            if ($results['synced_count'] == 0 && empty($results['errors'])) {
                $results['details'][] = 'All services are already in sync - no changes needed';
            }
            
        } catch (Exception $e) {
            $results['success'] = false;
            $results['errors'][] = 'Sync operation failed: ' . $e->getMessage();
            error_log("IP sync operation failed: " . $e->getMessage());
        }
        
        return $results;
    }
    
    /**
     * Check if IP is blocked in Security Manager
     */
    private function is_ip_blocked_in_security_manager($security_manager, $ip) {
        $blocked_ips = $security_manager->get_blocked_ips();
        foreach ($blocked_ips as $blocked) {
            if ($blocked['ip_address'] === $ip) {
                return true;
            }
        }
        return false;
    }
    
    /**
     * Check if IP is blocked in HAProxy
     */
    private function is_ip_blocked_in_haproxy($haproxy_manager, $ip) {
        $blocked_ips = $haproxy_manager->get_blocked_ips();
        if ($blocked_ips && is_array($blocked_ips)) {
            foreach ($blocked_ips as $blocked) {
                if (is_array($blocked) && isset($blocked['ip_address']) && $blocked['ip_address'] === $ip) {
                    return true;
                }
            }
        }
        return false;
    }
    
    public function get_service_failed_attempts($service, $hours = 24) {
        $stmt = $this->db->prepare('SELECT * FROM service_failed_attempts WHERE service_type = ? AND attempt_time > ? ORDER BY attempt_time DESC');
        $stmt->bindValue(1, $service, SQLITE3_TEXT);
        $stmt->bindValue(2, time() - ($hours * 3600), SQLITE3_INTEGER);
        $result = $stmt->execute();
        
        $attempts = array();
        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
            $attempts[] = $row;
        }
        
        return $attempts;
    }
    
    public function get_service_stats() {
        $stats = array();
        
        foreach (['ssh', 'ftp'] as $service) {
            $stats[$service] = array(
                'blocked_ips' => 0,
                'failed_attempts_24h' => 0,
                'monitoring_enabled' => $this->get_config($service . '_monitoring_enabled') == '1'
            );
            
            // Get blocked IPs count
            $stmt = $this->db->prepare('SELECT COUNT(*) as count FROM service_blocked_ips WHERE service_type = ? AND (unblock_time > ? OR unblock_time = -1)');
            $stmt->bindValue(1, $service, SQLITE3_TEXT);
            $stmt->bindValue(2, time(), SQLITE3_INTEGER);
            $result = $stmt->execute();
            $row = $result->fetchArray(SQLITE3_ASSOC);
            $stats[$service]['blocked_ips'] = $row['count'];
            
            // Get failed attempts in last 24 hours
            $stmt = $this->db->prepare('SELECT COUNT(*) as count FROM service_failed_attempts WHERE service_type = ? AND attempt_time > ?');
            $stmt->bindValue(1, $service, SQLITE3_TEXT);
            $stmt->bindValue(2, time() - 86400, SQLITE3_INTEGER);
            $result = $stmt->execute();
            $row = $result->fetchArray(SQLITE3_ASSOC);
            $stats[$service]['failed_attempts_24h'] = $row['count'];
        }
        
        return $stats;
    }
    
    public function __destruct() {
        // Always save cache on destruction to prevent data loss
        $this->save_cache();
        
        if ($this->db) {
            $this->db->close();
        }
    }
}