<?php


class usermgmt {

    public $base_dir = "/docker/users/";
    
    // Protected system users that cannot be deleted or modified
    private $protected_users = array(
        'root',
        'nobody',
        'daemon',
        'bin',
        'sys',
        'sync',
        'games',
        'man',
        'lp',
        'mail',
        'news',
        'uucp',
        'proxy',
        'www-data',
        'backup',
        'list',
        'irc',
        'gnats',
        'systemd-network',
        'systemd-resolve',
        'systemd-timesync',
        'messagebus',
        'syslog',
        '_apt',
        'tss',
        'uuidd',
        'tcpdump',
        'avahi-autoipd',
        'usbmux',
        'rtkit',
        'dnsmasq',
        'cups-pk-helper',
        'speech-dispatcher',
        'avahi',
        'kernoops',
        'saned',
        'nm-openvpn',
        'hplip',
        'whoopsie',
        'colord',
        'geoclue',
        'pulse',
        'gnome-initial-setup',
        'gdm',
        'mysql',
        'postgres',
        'redis',
        'mongodb',
        'nginx',
        'apache',
        'www',
        'httpd',
        'php-fpm',
        'docker'
    );
    
    // Check if a user is protected
    private function is_protected_user($username) {
        return in_array(strtolower($username), array_map('strtolower', $this->protected_users));
    }

    /**
     * Validate that a username is OS-safe before passing it to any shell.
     *
     * Throws InvalidArgumentException if the username does not match the
     * conservative pattern (lowercase letter start, lowercase alphanumerics,
     * underscore or hyphen, max 32 chars). This is the single chokepoint that
     * defends every shell_exec/exec/system call site in this file against
     * shell-injection via $_POST['username'].
     */
    private function validate_username_or_throw($username) {
        $username = (string)$username;
        if (!preg_match('/^[a-z][a-z0-9_-]{0,31}$/', $username)) {
            throw new \InvalidArgumentException(
                'Invalid username: must start with a lowercase letter and contain only ' .
                'lowercase letters, digits, underscore, or hyphen (max 32 chars).'
            );
        }
        // Defence in depth — block known-bad sequences even if regex passes.
        if (str_contains($username, '..') || str_contains($username, '/')) {
            throw new \InvalidArgumentException('Invalid username (path-like sequence rejected).');
        }
        return $username;
    }

    /**
     * Ensure $user_dir is contained under an allowed root before any
     * destructive filesystem operation (rm -rf, etc.). Throws on escape.
     */
    private function assert_user_dir_contained($user_dir) {
        $expected_root = rtrim($this->base_dir, '/') . '/';
        $resolved = realpath($user_dir);
        if ($resolved === false) {
            // realpath fails for non-existent paths; fall back to the literal
            // path so we still reject obvious escapes.
            $resolved = $user_dir;
        }
        // Normalise trailing slash for comparison
        $resolved_with_slash = rtrim($resolved, '/') . '/';
        if (!str_starts_with($resolved_with_slash, $expected_root)) {
            throw new \RuntimeException("Refusing to operate outside user-home root: $user_dir");
        }
        return $resolved;
    }
    
    /**
     * Set filesystem quota for a user
     */
    public function setUserQuota($username, $disk_limit_mb) {
        if ($disk_limit_mb <= 0) {
            error_log("Invalid disk limit for $username: {$disk_limit_mb}MB");
            return false;
        }
        
        // Use the bash quota script for consistency and proper filesystem detection
        $cmd = "/root/whp/scripts/setup-quotas.sh user " . escapeshellarg($username) . " " . escapeshellarg($disk_limit_mb) . " 2>&1";
        $output = [];
        $exit_code = 0;
        exec($cmd, $output, $exit_code);
        $result = implode("\n", $output);
        
        // Check if the command was successful (bash script returns 0 on success)
        if ($exit_code === 0 || strpos($result, '✓') !== false) {
            error_log("Set quota for $username: {$disk_limit_mb}MB");
            return true;
        } else {
            error_log("Failed to set quota for $username (exit code: $exit_code): $result");
            return false;
        }
    }
    
    /**
     * Get current user quota usage
     */
    public function getUserQuotaUsage($username) {
        $cmd = "quota -u " . escapeshellarg($username) . " 2>/dev/null | tail -n 1";
        $output = shell_exec($cmd);
        
        if (empty($output)) {
            return ['used_kb' => 0, 'soft_limit_kb' => 0, 'hard_limit_kb' => 0, 'used_mb' => 0];
        }
        
        // Parse quota output
        $parts = preg_split('/\s+/', trim($output));
        if (count($parts) >= 4) {
            $used_kb = intval($parts[1]);
            $soft_limit_kb = intval($parts[2]);
            $hard_limit_kb = intval($parts[3]);
            
            return [
                'used_kb' => $used_kb,
                'soft_limit_kb' => $soft_limit_kb,
                'hard_limit_kb' => $hard_limit_kb,
                'used_mb' => round($used_kb / 1024, 2),
                'soft_limit_mb' => round($soft_limit_kb / 1024, 2),
                'hard_limit_mb' => round($hard_limit_kb / 1024, 2)
            ];
        }
        
        return ['used_kb' => 0, 'soft_limit_kb' => 0, 'hard_limit_kb' => 0, 'used_mb' => 0];
    }
    
    /**
     * Remove user quota
     */
    public function removeUserQuota($username) {
        // Use the bash quota script for consistency and proper filesystem detection
        $cmd = "/root/whp/scripts/setup-quotas.sh remove " . escapeshellarg($username) . " 2>&1";
        $output = [];
        $exit_code = 0;
        exec($cmd, $output, $exit_code);
        $result = implode("\n", $output);
        
        // Check if the command was successful
        if ($exit_code === 0 || strpos($result, '✓') !== false) {
            error_log("Removed quota for $username");
            return true;
        } else {
            error_log("Failed to remove quota for $username (exit code: $exit_code): $result");
            return false;
        }
    }

    /**
     * Set up FTP access for a user using ProFTPD MySQL integration
     */
    private function setupFTPAccess($username) {
        try {
            $username = $this->validate_username_or_throw($username);

            require_once('/docker/whp/web/libs/mysqlmgmt.php');
            $MySQLMgmt = new mysqlmgmt();
            $pdo = $MySQLMgmt->getMySQLConnection();

            if (!$pdo) {
                error_log("Failed to connect to MySQL for FTP user setup: $username");
                return false;
            }

            // Get user information
            $uid = shell_exec('/usr/bin/id -u ' . escapeshellarg($username) . ' 2>/dev/null');
            $gid = shell_exec('/usr/bin/id -g ' . escapeshellarg($username) . ' 2>/dev/null');
            
            if (!$uid || !$gid) {
                error_log("Failed to get UID/GID for user: $username");
                return false;
            }
            
            $uid = intval(trim($uid));
            $gid = intval(trim($gid));
            $homedir = "/docker/users/$username";
            
            // Get user's system password from database
            $mysql_password = $this->getUserMySQLPassword($username);
            if (!$mysql_password) {
                error_log("Failed to get MySQL password for FTP user: $username");
                return false;
            }
            
            // Hash the password for FTP authentication
            $password_hash = password_hash($mysql_password, PASSWORD_DEFAULT);
            
            // Insert or update FTP user record
            $stmt = $pdo->prepare("INSERT INTO whp.ftp_users (username, password, uid, gid, homedir, shell) 
                                  VALUES (?, ?, ?, ?, ?, '/bin/false') 
                                  ON DUPLICATE KEY UPDATE 
                                  password = VALUES(password), 
                                  uid = VALUES(uid), 
                                  gid = VALUES(gid), 
                                  homedir = VALUES(homedir),
                                  modified = NOW()");
            
            $result = $stmt->execute([$username, $password_hash, $uid, $gid, $homedir]);
            
            if ($result) {
                // Set up default quota (1GB)
                $this->setupFTPQuota($username, 1000 * 1024 * 1024); // 1GB in bytes
                return true;
            } else {
                error_log("Failed to insert/update FTP user record for: $username");
                return false;
            }
            
        } catch (Exception $e) {
            error_log("Exception setting up FTP access for user $username: " . $e->getMessage());
            return false;
        }
    }
    
    /**
     * Remove FTP access for a user using ProFTPD MySQL integration
     */
    private function removeFTPAccess($username) {
        try {
            require_once('/docker/whp/web/libs/mysqlmgmt.php');
            $MySQLMgmt = new mysqlmgmt();
            $pdo = $MySQLMgmt->getMySQLConnection();
            
            if (!$pdo) {
                error_log("Failed to connect to MySQL for FTP user removal: $username");
                return false;
            }
            
            // Remove FTP user record
            $stmt = $pdo->prepare("DELETE FROM whp.ftp_users WHERE username = ?");
            $result = $stmt->execute([$username]);
            
            if ($result) {
                // Also remove quota record
                $stmt2 = $pdo->prepare("DELETE FROM whp.ftp_quotas WHERE username = ?");
                $stmt2->execute([$username]);
                return true;
            } else {
                error_log("Failed to remove FTP user record for: $username");
                return false;
            }
            
        } catch (Exception $e) {
            error_log("Exception removing FTP access for user $username: " . $e->getMessage());
            return false;
        }
    }

    /**
     * Update FTP password for a user
     */
    private function updateFTPPassword($username, $new_password) {
        try {
            require_once('/docker/whp/web/libs/mysqlmgmt.php');
            $MySQLMgmt = new mysqlmgmt();
            $pdo = $MySQLMgmt->getMySQLConnection();
            
            if (!$pdo) {
                error_log("Failed to connect to MySQL for FTP password update: $username");
                return false;
            }
            
            // Check if user has FTP access
            $stmt = $pdo->prepare("SELECT username FROM whp.ftp_users WHERE username = ?");
            $stmt->execute([$username]);
            $user_exists = $stmt->fetch();
            
            if ($user_exists) {
                // Hash the new password
                $password_hash = password_hash($new_password, PASSWORD_DEFAULT);
                
                // Update FTP user password
                $stmt = $pdo->prepare("UPDATE whp.ftp_users SET password = ?, modified = NOW() WHERE username = ?");
                $result = $stmt->execute([$password_hash, $username]);
                
                if ($result) {
                    return true;
                } else {
                    error_log("Failed to update FTP password for user: $username");
                    return false;
                }
            }
            
            return true; // User doesn't have FTP access, nothing to update
            
        } catch (Exception $e) {
            error_log("Exception updating FTP password for user $username: " . $e->getMessage());
            return false;
        }
    }

    /**
     * Set up FTP quota for a user
     */
    private function setupFTPQuota($username, $bytes_limit) {
        try {
            require_once('/docker/whp/web/libs/mysqlmgmt.php');
            $MySQLMgmt = new mysqlmgmt();
            $pdo = $MySQLMgmt->getMySQLConnection();
            
            if (!$pdo) {
                error_log("Failed to connect to MySQL for FTP quota setup: $username");
                return false;
            }
            
            // Insert or update FTP quota record
            $stmt = $pdo->prepare("INSERT INTO whp.ftp_quotas (username, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) 
                                  VALUES (?, ?, ?, ?, 1000, 1000, 2000) 
                                  ON DUPLICATE KEY UPDATE 
                                  bytes_in_avail = VALUES(bytes_in_avail), 
                                  bytes_out_avail = VALUES(bytes_out_avail), 
                                  bytes_xfer_avail = VALUES(bytes_xfer_avail),
                                  files_in_avail = VALUES(files_in_avail),
                                  files_out_avail = VALUES(files_out_avail),
                                  files_xfer_avail = VALUES(files_xfer_avail)");
            
            $result = $stmt->execute([$username, $bytes_limit, $bytes_limit, $bytes_limit * 2]);
            
            if ($result) {
                return true;
            } else {
                error_log("Failed to set up FTP quota for user: $username");
                return false;
            }
            
        } catch (Exception $e) {
            error_log("Exception setting up FTP quota for user $username: " . $e->getMessage());
            return false;
        }
    }

    /**
     * Encrypt password using AES-256-GCM if encryption key exists
     */
    private function encryptPassword($password) {
        $keyFile = '/etc/whp/encryption.key';
        
        if (!file_exists($keyFile)) {
            // Fallback to base64 if no encryption key
            return base64_encode($password);
        }
        
        try {
            $key = base64_decode(trim(file_get_contents($keyFile)));
            $iv = random_bytes(12); // 96-bit IV for GCM
            $encrypted = openssl_encrypt($password, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
            
            if ($encrypted === false) {
                throw new Exception("Encryption failed");
            }
            
            // Combine IV + tag + encrypted data and base64 encode
            return base64_encode($iv . $tag . $encrypted);
        } catch (Exception $e) {
            error_log("Password encryption failed: " . $e->getMessage());
            throw new Exception("Password encryption failed: " . $e->getMessage());
        }
    }
    
    /**
     * Decrypt password using AES-256-GCM if encrypted, otherwise base64 decode
     */
    private function decryptPassword($encryptedPassword) {
        $keyFile = '/etc/whp/encryption.key';
        
        if (!file_exists($keyFile)) {
            // Fallback to base64 decode for legacy passwords
            return base64_decode($encryptedPassword);
        }
        
        try {
            $key = base64_decode(trim(file_get_contents($keyFile)));
            $data = base64_decode($encryptedPassword);
            
            // Check if this looks like encrypted data (IV + tag + data = at least 28 bytes)
            if (strlen($data) < 28) {
                // Probably legacy base64 encoded password
                return base64_decode($encryptedPassword);
            }
            
            $iv = substr($data, 0, 12);
            $tag = substr($data, 12, 16);
            $encrypted = substr($data, 28);
            
            $decrypted = openssl_decrypt($encrypted, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
            
            if ($decrypted === false) {
                throw new Exception("Decryption failed - password may be corrupted");
            }
            
            return $decrypted;
        } catch (Exception $e) {
            error_log("Password decryption failed for encrypted data: " . $e->getMessage());
            throw new Exception("Password decryption failed: " . $e->getMessage());
        }
    }

    /**
     * Store user password in the database for MySQL authentication
     */
    private function storeUserPassword($username, $password) {
        try {
            require_once('/docker/whp/web/libs/mysqlmgmt.php');
            $MySQLMgmt = new mysqlmgmt();
            $pdo = $MySQLMgmt->getMySQLConnection();
            
            if (!$pdo) {
                error_log("Failed to connect to MySQL to store password for user: $username");
                return false;
            }
            
            // Hash the system password for storage
            $password_hash = password_hash($password, PASSWORD_DEFAULT);
            
            // Encrypt the MySQL password for storage
            $mysql_password_encrypted = $this->encryptPassword($password);
            
            // Insert or update the password record
            $stmt = $pdo->prepare("INSERT INTO whp.user_passwords (username, password_hash, mysql_password) 
                                  VALUES (?, ?, ?) 
                                  ON DUPLICATE KEY UPDATE 
                                  password_hash = VALUES(password_hash), 
                                  mysql_password = VALUES(mysql_password)");
            $stmt->execute([$username, $password_hash, $mysql_password_encrypted]);
            
            return true;
        } catch (Exception $e) {
            error_log("Failed to store password for user $username: " . $e->getMessage());
            return false;
        }
    }

    /**
     * Get user's MySQL password from the database
     */
    public function getUserMySQLPassword($username) {
        try {
            require_once('/docker/whp/web/libs/mysqlmgmt.php');
            $MySQLMgmt = new mysqlmgmt();
            $pdo = $MySQLMgmt->getMySQLConnection();
            
            if (!$pdo) {
                return false;
            }
            
            $stmt = $pdo->prepare("SELECT mysql_password FROM whp.user_passwords WHERE username = ?");
            $stmt->execute([$username]);
            $result = $stmt->fetch();
            
            if ($result) {
                // Decrypt the MySQL password
                return $this->decryptPassword($result['mysql_password']);
            }
            
            return false;
        } catch (Exception $e) {
            error_log("Failed to get MySQL password for user $username: " . $e->getMessage());
            return false;
        }
    }

    /**
     * Get PostgreSQL password for user
     */
    public function getUserPostgreSQLPassword($username) {
        try {
            require_once('/docker/whp/web/libs/mysqlmgmt.php');
            $MySQLMgmt = new mysqlmgmt();
            $pdo = $MySQLMgmt->getMySQLConnection();
            
            if (!$pdo) {
                return false;
            }
            
            $stmt = $pdo->prepare("SELECT postgresql_password FROM whp.user_passwords WHERE username = ?");
            $stmt->execute([$username]);
            $result = $stmt->fetch();
            
            if ($result && $result['postgresql_password']) {
                // Decrypt the PostgreSQL password
                return $this->decryptPassword($result['postgresql_password']);
            }
            
            return false;
        } catch (Exception $e) {
            error_log("Failed to get PostgreSQL password for user $username: " . $e->getMessage());
            return false;
        }
    }

    /**
     * Set PostgreSQL password for user
     */
    public function setUserPostgreSQLPassword($username, $password) {
        try {
            require_once('/docker/whp/web/libs/mysqlmgmt.php');
            $MySQLMgmt = new mysqlmgmt();
            $pdo = $MySQLMgmt->getMySQLConnection();
            
            if (!$pdo) {
                return false;
            }
            
            // Encrypt the PostgreSQL password for storage
            $postgresql_password_encrypted = $this->encryptPassword($password);
            
            // Insert or update the password record
            $stmt = $pdo->prepare("INSERT INTO whp.user_passwords (username, password_hash, mysql_password, postgresql_password) 
                                  VALUES (?, '', '', ?) 
                                  ON DUPLICATE KEY UPDATE 
                                  postgresql_password = VALUES(postgresql_password)");
            $stmt->execute([$username, $postgresql_password_encrypted]);
            
            return true;
        } catch (Exception $e) {
            error_log("Failed to set PostgreSQL password for user $username: " . $e->getMessage());
            return false;
        }
    }

    public function create_new_user( $username, $password, $skip_default_resources = false ) {
        // Defence-in-depth: validate username before any shell-out, even if
        // pages/user-management.php (the typical caller) skips validation.
        $username = $this->validate_username_or_throw($username);

        // Check if user exists
        $id = shell_exec('/usr/bin/id -u ' . escapeshellarg($username));
        if ( is_numeric( $id ) ) {
            $response = array("status" => "1", "msg"=> "User Exists");
            return $response;
        }
        // User does not exist create the home dir and user
        $user_dir = $this->base_dir . $username;
        if ( is_dir( $user_dir ) ) {
            $response = array("status"=> "1", "msg"=> "User's directory Already Exists");
            return $response;
        }

        // Create user home directory
        // Note: Domain-specific directories are created when domains are added
        mkdir($user_dir, 0755);

        ini_set("max_execution_time","60");
        ini_set("max_input_time","60");

        // Find the next available UID above 1000
        $next_uid = 1001;
        $existing_uids = [];
        $passwd_content = file_get_contents('/etc/passwd');
        $lines = explode("\n", $passwd_content);
        foreach ($lines as $line) {
            if (empty(trim($line))) continue;
            $parts = explode(':', $line);
            if (count($parts) >= 3) {
                $existing_uids[] = (int)$parts[2];
            }
        }

        // Find the next available UID starting from 1001
        while (in_array($next_uid, $existing_uids)) {
            $next_uid++;
        }

        // Create user with specific UID above 1000
        shell_exec(sprintf(
            '/usr/sbin/useradd -u %s -M -d %s -c %s %s',
            escapeshellarg((string)(int)$next_uid),
            escapeshellarg($user_dir),
            escapeshellarg('Webhosting Panel Website User'),
            escapeshellarg($username)
        ));
        $id = shell_exec('/usr/bin/id -u ' . escapeshellarg($username));

        // Set home directory ownership to root:root and not writable by user
        shell_exec('/usr/bin/chown root:root ' . escapeshellarg($user_dir));
        shell_exec('/usr/bin/chmod 755 ' . escapeshellarg($user_dir));

        // Create a README.txt in the home directory
        $readme = "Welcome to your hosting account!\n\nYour base home directory is not writable for security reasons.\n\nTo store files, use the 'userfiles' directory inside your home:\n  /docker/users/$username/userfiles\n\nEach site you add will create a domain directory that is writable by you.\n";
        file_put_contents("$user_dir/README.txt", $readme);
        shell_exec('/usr/bin/chown root:root ' . escapeshellarg("$user_dir/README.txt"));
        shell_exec('/usr/bin/chmod 644 ' . escapeshellarg("$user_dir/README.txt"));

        // Create a userfiles directory for user storage
        $userfiles_dir = "$user_dir/userfiles";
        if (!is_dir($userfiles_dir)) {
            mkdir($userfiles_dir, 0755);
            // $username is already validated; safe to embed in user:group form.
            shell_exec(sprintf(
                '/usr/bin/chown %s %s',
                escapeshellarg($username . ':' . $username),
                escapeshellarg($userfiles_dir)
            ));
            shell_exec('/usr/bin/chmod 755 ' . escapeshellarg($userfiles_dir));
        }

        shell_exec("/usr/bin/echo " . escapeshellarg("$username:$password") . "|/usr/sbin/chpasswd");
        
        // Store the password in our database for MySQL authentication
        $this->storeUserPassword($username, $password);
        
        // Create MySQL user for the new system user
        require_once('/docker/whp/web/libs/mysqlmgmt.php');
        $MySQLMgmt = new mysqlmgmt();
        $mysql_result = $MySQLMgmt->createMySQLUser($username, $password);
        
        if ($mysql_result['status'] !== '0') {
            // Log the MySQL user creation failure but don't fail the entire user creation
            error_log("Failed to create MySQL user for {$username}: " . $mysql_result['msg']);
        }
        
        // Create default resource allowances in database (unless skipped)
        if (!$skip_default_resources) {
            $allowances_result = $MySQLMgmt->updateClientAllowances($username, 0.5, 512, null, 1000, 0);
            if ($allowances_result['status'] !== '0') {
                error_log("Failed to create default allowances for {$username}: " . $allowances_result['msg']);
            }
            
            // Set filesystem quota for the new user
            $this->setUserQuota($username, 1000); // Default 1GB quota
        }
        
        // Set up FTP access for the new user
        // Note: With PAM authentication, system users automatically get FTP access
        // No additional database setup required for FTP with PAM
        error_log("User {$username} created - FTP access available via PAM authentication");

        // Create the default daily backup schedule (site / userfiles / mysql /
        // postgres). Wrapped in try/catch so any backup-side failure logs but
        // doesn't roll back user creation — the user account is the source of
        // truth and must always succeed if we got this far.
        try {
            require_once(__DIR__ . '/BackupDefaults.php');
            $bdMysql = new mysqlmgmt();
            $bdPdo = $bdMysql->getMySQLConnection();
            if ($bdPdo) {
                $bdPdo->query("USE whp");
                \WHPBackup\BackupDefaults::createForUser($bdPdo, $username);
            }
        } catch (\Throwable $e) {
            error_log("BackupDefaults failed for {$username}: " . $e->getMessage());
        }

        $response = array("status"=> "0", "msg"=> "User Created", "uid"=> $id );
        return $response;
    }

    public function delete_user( $username ) {
        // Defence-in-depth: validate before any shell-out.
        $username = $this->validate_username_or_throw($username);

        // Check if user is protected
        if ($this->is_protected_user($username)) {
            $response = array("status"=> "1","msg"=> "Cannot delete protected system user: $username");
            return $response;
        }

        $id = shell_exec('/usr/bin/id -u ' . escapeshellarg($username));
        $id = (int) $id;
        if ($id < 1001) {
            $response = array("status"=> "1","msg"=> "Attempt to remove System User failed");
            return $response;
        }

        // Tear down Valkey (no-op if not enabled). See:
        // docs/superpowers/specs/2026-04-20-customer-valkey-design.md
        try {
            require_once('/docker/whp/web/libs/valkey-mgmt.php');
            global $db;
            $vk = new ValkeyMgr($db);
            $appStmt = $db->prepare("SELECT sc.container_id FROM whp.site_containers sc JOIN whp.sites s ON s.id = sc.site_id WHERE s.username = ?");
            $appStmt->execute([$username]);
            $apps = $appStmt->fetchAll(PDO::FETCH_COLUMN) ?: [];
            $vk->disable($username, $apps);
        } catch (Throwable $e) {
            error_log("delete_user: valkey disable failed (continuing): " . $e->getMessage());
        }

        // Remove user quota first
        $this->removeUserQuota($username);
        
        // Note: With PAM authentication, FTP access is automatically removed when user is deleted
        // No additional database cleanup required for FTP with PAM
        
        // Delete MySQL user before deleting system user
        require_once('/docker/whp/web/libs/mysqlmgmt.php');
        $MySQLMgmt = new mysqlmgmt();
        $mysql_result = $MySQLMgmt->deleteMySQLUser($username);
        
        if ($mysql_result['status'] !== '0') {
            // Log the MySQL user deletion failure but don't fail the entire user deletion
            error_log("Failed to delete MySQL user for {$username}: " . $mysql_result['msg']);
        }
        
        // Remove client allowances from database
        try {
            $pdo = $MySQLMgmt->getMySQLConnection();
            if ($pdo) {
                $stmt = $pdo->prepare("DELETE FROM whp.client_allowances WHERE username = ?");
                $stmt->execute([$username]);
                
                $stmt = $pdo->prepare("DELETE FROM whp.user_passwords WHERE username = ?");
                $stmt->execute([$username]);
            }
        } catch (Exception $e) {
            error_log("Failed to remove database records for user $username: " . $e->getMessage());
        }
        
        shell_exec('/usr/sbin/userdel ' . escapeshellarg($username));
        // Mail spool path is constructed from validated $username; escape anyway.
        shell_exec('/usr/bin/rm ' . escapeshellarg('/var/spool/mail/' . $username));

        $user_dir = $this->base_dir . $username;
        // Containment check: refuse to rm -rf anything that resolves outside
        // $this->base_dir (e.g. /docker/users/). This prevents catastrophic
        // deletes if a future caller passes an unexpected $user_dir.
        try {
            $this->assert_user_dir_contained($user_dir);
            shell_exec('rm -rf ' . escapeshellarg($user_dir));
        } catch (\RuntimeException $e) {
            error_log("delete_user: refusing rm -rf outside base_dir: " . $e->getMessage());
        }
        return array("status" => "0", "msg"=> "User Deleted if existed");
    }

    public function change_password( $username, $new_password ) {
        // 'root' is the only protected user we'll allow here; all others must
        // satisfy the strict pattern. Skip validation only for the literal
        // 'root' to preserve existing behaviour.
        if ($username !== 'root') {
            $username = $this->validate_username_or_throw($username);
        }

        // Check if user is protected (allow root password changes but warn)
        if ($this->is_protected_user($username) && $username !== 'root') {
            $response = array("status" => "1", "msg" => "Cannot change password for protected system user: $username");
            return $response;
        }

        $id = shell_exec('/usr/bin/id -u ' . escapeshellarg($username));
        if ( !is_numeric( $id ) ) {
            $response = array("status" => "1", "msg" => "User does not exist");
            return $response;
        }
        
        // Change system password
        shell_exec("/usr/bin/echo " . escapeshellarg("$username:$new_password") . "|/usr/sbin/chpasswd");
        
        // Update stored password in database
        $this->storeUserPassword($username, $new_password);
        
        // Change MySQL password
        require_once('/docker/whp/web/libs/mysqlmgmt.php');
        $MySQLMgmt = new mysqlmgmt();
        $mysql_result = $MySQLMgmt->changeMySQLPassword($username, $new_password);
        
        if ($mysql_result['status'] !== '0') {
            // Log the MySQL password change failure but don't fail the entire operation
            error_log("Failed to change MySQL password for {$username}: " . $mysql_result['msg']);
        }
        
        // Note: With PAM authentication, FTP password is automatically updated with system password
        // No additional database update required for FTP with PAM
        
        return array("status" => "0", "msg" => "Password changed successfully");
    }

    public function list_users() {
        $users = array();
        $passwd_content = file_get_contents('/etc/passwd');
        $lines = explode("\n", $passwd_content);
        
        foreach ($lines as $line) {
            if (empty(trim($line))) continue;
            
            $parts = explode(':', $line);
            if (count($parts) >= 7) {
                $username = $parts[0];
                $users[] = array(
                    'username' => $username,
                    'uid' => (int)$parts[2],
                    'gid' => (int)$parts[3],
                    'home' => $parts[5],
                    'shell' => $parts[6],
                    'protected' => $this->is_protected_user($username)
                );
            }
        }
        
        // Sort by UID
        usort($users, function($a, $b) {
            return $a['uid'] - $b['uid'];
        });
        
        return $users;
    }

    /**
     * Check if a user exists on the system
     */
    public function user_exists($username) {
        $id = shell_exec("/usr/bin/id -u " . escapeshellarg($username) . " 2>/dev/null");
        return is_numeric(trim($id));
    }

}