#!/usr/bin/env php
<?php

declare(strict_types=1);


mysqli_report(MYSQLI_REPORT_OFF); // Disable mysqli exceptions
define('SOCKET_PATH', '/var/run/jabali/agent.sock');
define('PID_FILE', '/var/run/jabali/agent.pid');
define('LOG_FILE', '/var/log/jabali/agent.log');

// Email/Mail constants
define('VMAIL_UID', 5000);
define('VMAIL_GID', 5000);
define('POSTFIX_VIRTUAL_DOMAINS', '/etc/postfix/virtual_mailbox_domains');
define('POSTFIX_VIRTUAL_MAILBOXES', '/etc/postfix/virtual_mailbox_maps');
define('POSTFIX_VIRTUAL_ALIASES', '/etc/postfix/virtual_alias_maps');
define('DKIM_KEYS_DIR', '/etc/opendkim/keys');

// DNSSEC constants
define('DNSSEC_KEYS_DIR', '/etc/bind/keys');
define('DNSSEC_ALGORITHM', 'ECDSAP256SHA256'); // Algorithm 13 - recommended for modern DNSSEC

// Jabali nginx include locations
define('JABALI_NGINX_DIR', '/etc/nginx/jabali');
define('JABALI_NGINX_INCLUDES', '/etc/nginx/jabali/includes');
define('JABALI_WAF_INCLUDE', '/etc/nginx/jabali/includes/waf.conf');
define('JABALI_GEO_INCLUDE', '/etc/nginx/jabali/includes/geo.conf');
define('JABALI_WAF_RULES', '/etc/nginx/jabali/modsecurity.conf');
define('JABALI_GEO_HTTP_CONF', '/etc/nginx/conf.d/jabali-geoip.conf');

$protectedUsers = ['root', 'www-data', 'mysql', 'postgres', 'nobody', 'ubuntu', 'debian'];
$allowedServices = ['nginx', 'apache2', 'php-fpm', 'php8.4-fpm', 'php8.3-fpm', 'mysql', 'mariadb', 'postfix', 'dovecot', 'bind9', 'named'];

function logger(string $message, string $level = 'INFO'): void
{
    $timestamp = date('Y-m-d H:i:s');
    $logMessage = "[$timestamp] [$level] $message\n";
    file_put_contents(LOG_FILE, $logMessage, FILE_APPEND);
}

function validateUsername(string $username): bool
{
    return preg_match('/^[a-z][a-z0-9_]{0,31}$/', $username) === 1;
}

function validateDomain(string $domain): bool
{
    return preg_match('/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i', $domain) === 1;
}

function isProtectedUser(string $username): bool
{
    global $protectedUsers;
    return in_array($username, $protectedUsers, true);
}

function sanitizePath(string $path): string
{
    // Remove null bytes and normalize
    $path = str_replace("\0", '', $path);
    // Remove .. to prevent directory traversal
    $parts = explode('/', $path);
    $safe = [];
    foreach ($parts as $part) {
        if ($part === '..') continue;
        if ($part === '.' || $part === '') continue;
        $safe[] = $part;
    }
    return implode('/', $safe);
}

function validateUserPath(string $username, string $path): ?string
{
    $homeDir = rtrim("/home/$username", '/');
    $homePrefix = $homeDir . '/';
    $sanitized = sanitizePath($path);
    $fullPath = $homeDir . '/' . $sanitized;
    
    // Resolve to real path and verify it's within home directory
    $realPath = realpath($fullPath);
    if ($realPath === false) {
        // Path doesn't exist yet, check parent
        $parentPath = dirname($fullPath);
        $realParent = realpath($parentPath);
        if ($realParent === false || ($realParent !== $homeDir && !str_starts_with($realParent, $homePrefix))) {
            return null;
        }
        return $fullPath;
    }
    
    if ($realPath !== $homeDir && !str_starts_with($realPath, $homePrefix)) {
        return null; // Path escapes home directory
    }
    
    return $realPath;
}

/**
 * Get list of domains owned by a user.
 */
function getUserDomains(string $username): array
{
    $homeDir = "/home/$username";
    $domains = [];

    // Check .domains file first
    $domainsFile = "$homeDir/.domains";
    if (file_exists($domainsFile)) {
        $domainsData = json_decode(file_get_contents($domainsFile), true) ?: [];
        $domains = array_keys($domainsData);
    }

    // Also check domains directory
    $domainsDir = "$homeDir/domains";
    if (is_dir($domainsDir)) {
        foreach (scandir($domainsDir) as $d) {
            if ($d !== '.' && $d !== '..' && is_dir("$domainsDir/$d")) {
                if (!in_array($d, $domains)) {
                    $domains[] = $d;
                }
            }
        }
    }

    return $domains;
}

/**
 * Check if a domain belongs to a user.
 */
function validateUserDomain(string $username, string $domain): bool
{
    // Sanitize domain name - no path traversal
    if (!validateDomain($domain) || strpos($domain, '..') !== false || strpos($domain, '/') !== false) {
        return false;
    }

    $userDomains = getUserDomains($username);
    return in_array($domain, $userDomains);
}

/**
 * Validate and sanitize MySQL users.sql file.
 * Only allows CREATE USER and GRANT statements for users matching username_ prefix.
 * Returns sanitized SQL or null if invalid.
 */
function validateMysqlUsersFile(string $username, string $filePath): ?string
{
    if (!file_exists($filePath)) {
        return null;
    }

    $content = file_get_contents($filePath);
    $lines = explode("\n", $content);
    $sanitized = [];
    $prefix = $username . '_';
    $prefixPattern = preg_quote($prefix, '/');

    // System databases that should never be granted access to
    $systemDatabases = ['mysql', 'information_schema', 'performance_schema', 'sys'];

    foreach ($lines as $line) {
        $trimmed = trim($line);

        // Allow comments and empty lines
        if (empty($trimmed) || strpos($trimmed, '--') === 0 || strpos($trimmed, '#') === 0) {
            $sanitized[] = $line;
            continue;
        }

        // Allow FLUSH PRIVILEGES
        if (preg_match('/^FLUSH\s+PRIVILEGES/i', $trimmed)) {
            $sanitized[] = $line;
            continue;
        }

        // Validate DROP USER IF EXISTS - must match prefix
        if (preg_match('/^DROP\s+USER\s+IF\s+EXISTS\s+[\'"`]?(' . $prefixPattern . '[a-zA-Z0-9_]+)[\'"`]?@/i', $trimmed, $matches)) {
            $sanitized[] = $line;
            continue;
        }

        // Validate CREATE USER - must match prefix
        if (preg_match('/^CREATE\s+USER\s+[`\'"]?(' . $prefixPattern . '[a-zA-Z0-9_]+)[`\'"]?@/i', $trimmed, $matches)) {
            $sanitized[] = $line;
            continue;
        }

        // Validate GRANT statements
        if (preg_match('/^GRANT\s+/i', $trimmed)) {
            // Check for global grants (ON *.*)
            if (preg_match('/\bON\s+\*\.\*/i', $trimmed)) {
                // Only allow USAGE on *.* (which grants no privileges)
                if (!preg_match('/^GRANT\s+USAGE\s+ON\s+\*\.\*/i', $trimmed)) {
                    logger( "Blocked global grant in users.sql: $trimmed");
                    continue; // Skip dangerous global grants
                }
            }

            // Check for grants on system databases
            $blocked = false;
            foreach ($systemDatabases as $sysDb) {
                if (preg_match('/\bON\s+[`\'"]?' . preg_quote($sysDb, '/') . '[`\'"]?\./i', $trimmed)) {
                    logger( "Blocked system database grant in users.sql: $trimmed");
                    $blocked = true;
                    break;
                }
            }
            if ($blocked) continue;

            // SECURITY: Grant must satisfy BOTH conditions:
            // 1. The database must match username_% pattern (user's own databases only)
            // 2. The TO user must match username_ prefix

            // Check if grant is on user's own databases (username_% or username\_%)
            $dbPattern = preg_quote($username, '/') . '[_\\\\]+[%a-zA-Z0-9_]*';
            $isOwnDatabase = preg_match('/\bON\s+[`\'"]?' . $dbPattern . '/i', $trimmed);

            // Check if the TO user matches the prefix
            $isOwnUser = preg_match('/\bTO\s+[`\'"]?' . $prefixPattern . '/i', $trimmed);

            // Allow GRANT USAGE ON *.* (no actual privileges) if TO user matches prefix
            if (preg_match('/^GRANT\s+USAGE\s+ON\s+\*\.\*/i', $trimmed) && $isOwnUser) {
                $sanitized[] = $line;
                continue;
            }

            // For all other grants, database MUST be user's own
            if ($isOwnDatabase && $isOwnUser) {
                $sanitized[] = $line;
                continue;
            }

            // Block any other grants
            logger("Blocked unauthorized grant in users.sql: $trimmed");
            continue;
        }

        // Block any other statements (potential injection)
        logger( "Blocked unknown statement in users.sql: $trimmed");
    }

    return implode("\n", $sanitized);
}

/**
 * Sanitize a database dump file by removing dangerous constructs.
 * Modifies the file in place and returns true if successful.
 */
function sanitizeDatabaseDump(string $username, string $filePath): bool
{
    $isGzipped = preg_match('/\.gz$/i', $filePath);
    $prefix = $username . '_';

    // Dangerous patterns to remove/block
    $dangerousPatterns = [
        // DEFINER clauses (stored procedures, triggers, views run as definer)
        '/DEFINER\s*=\s*[`\'"][^`\'"]+[`\'"]\s*@\s*[`\'"][^`\'"]+[`\'"]/i' => '',
        // SET GLOBAL statements
        '/^\s*SET\s+GLOBAL\s+/im' => '-- BLOCKED: SET GLOBAL ',
        // GRANT statements embedded in dumps
        '/^\s*GRANT\s+/im' => '-- BLOCKED: GRANT ',
        // REVOKE statements
        '/^\s*REVOKE\s+/im' => '-- BLOCKED: REVOKE ',
        // CREATE USER statements
        '/^\s*CREATE\s+USER\s+/im' => '-- BLOCKED: CREATE USER ',
        // DROP USER statements
        '/^\s*DROP\s+USER\s+/im' => '-- BLOCKED: DROP USER ',
        // LOAD DATA INFILE (file reading)
        '/LOAD\s+DATA\s+(LOCAL\s+)?INFILE/i' => '-- BLOCKED: LOAD DATA INFILE',
        // SELECT INTO OUTFILE/DUMPFILE (file writing)
        '/SELECT\s+.*\s+INTO\s+(OUTFILE|DUMPFILE)/i' => '-- BLOCKED: SELECT INTO OUTFILE',
        // INSTALL/UNINSTALL PLUGIN
        '/^\s*(INSTALL|UNINSTALL)\s+PLUGIN/im' => '-- BLOCKED: PLUGIN ',
    ];

    try {
        if ($isGzipped) {
            $content = gzdecode(file_get_contents($filePath));
            if ($content === false) {
                return false;
            }
        } else {
            $content = file_get_contents($filePath);
        }

        // Apply sanitization
        $modified = false;
        foreach ($dangerousPatterns as $pattern => $replacement) {
            $newContent = preg_replace($pattern, $replacement, $content);
            if ($newContent !== $content) {
                $modified = true;
                $content = $newContent;
                logger( "Sanitized dangerous pattern in database dump: $filePath");
            }
        }

        // Check for USE statements pointing to other users' databases
        if (preg_match_all('/^\s*USE\s+[`\'"]?([a-zA-Z0-9_]+)[`\'"]?\s*;/im', $content, $matches)) {
            foreach ($matches[1] as $dbName) {
                if (strpos($dbName, $prefix) !== 0) {
                    // USE statement for a database not owned by this user
                    $content = preg_replace(
                        '/^\s*USE\s+[`\'"]?' . preg_quote($dbName, '/') . '[`\'"]?\s*;/im',
                        "-- BLOCKED: USE $dbName (not owned by user)",
                        $content
                    );
                    $modified = true;
                    logger( "Blocked USE statement for foreign database: $dbName");
                }
            }
        }

        if ($modified) {
            if ($isGzipped) {
                file_put_contents($filePath, gzencode($content));
            } else {
                file_put_contents($filePath, $content);
            }
        }

        return true;
    } catch (Exception $e) {
        logger( "Failed to sanitize database dump: " . $e->getMessage());
        return false;
    }
}

/**
 * Check a directory for dangerous symlinks that point outside allowed paths.
 * Returns array of dangerous symlink paths found.
 */
function findDangerousSymlinks(string $directory, string $allowedBase): array
{
    $dangerous = [];

    if (!is_dir($directory)) {
        return $dangerous;
    }

    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS),
        RecursiveIteratorIterator::SELF_FIRST
    );

    foreach ($iterator as $file) {
        if (is_link($file->getPathname())) {
            $target = readlink($file->getPathname());
            if ($target === false) {
                continue;
            }

            // Resolve absolute path of symlink target
            if ($target[0] !== '/') {
                $target = dirname($file->getPathname()) . '/' . $target;
            }
            $realTarget = realpath($target);

            // If target doesn't exist or is outside allowed base, it's dangerous
            if ($realTarget === false || strpos($realTarget, $allowedBase) !== 0) {
                $dangerous[] = $file->getPathname();
                logger( "Found dangerous symlink: {$file->getPathname()} -> $target");
            }
        }
    }

    return $dangerous;
}

/**
 * Remove dangerous symlinks from a directory.
 */
function removeDangerousSymlinks(string $directory, string $allowedBase): int
{
    $dangerous = findDangerousSymlinks($directory, $allowedBase);
    $removed = 0;

    foreach ($dangerous as $symlink) {
        if (unlink($symlink)) {
            $removed++;
            logger( "Removed dangerous symlink: $symlink");
        }
    }

    return $removed;
}

function handleAction(array $request): array
{
    $action = $request['action'] ?? '';
    $params = $request['params'] ?? $request;
    
    return match ($action) {
        'ping' => ['success' => true, 'message' => 'pong', 'version' => '1.0.0'],
        'user.create' => createUser($params),
        'user.delete' => deleteUser($params),
        'user.exists' => userExists($params),
        'ufw.status' => ufwStatus($params),
        'ufw.list_rules' => ufwListRules($params),
        'ufw.enable' => ufwEnable($params),
        'ufw.disable' => ufwDisable($params),
        'ufw.allow_port' => ufwAllowPort($params),
        'ufw.deny_port' => ufwDenyPort($params),
        'ufw.allow_ip' => ufwAllowIp($params),
        'ufw.deny_ip' => ufwDenyIp($params),
        'ufw.delete_rule' => ufwDeleteRule($params),
        'ufw.set_default' => ufwSetDefault($params),
        'ufw.limit_port' => ufwLimitPort($params),
        'ufw.reset' => ufwReset($params),
        'ufw.reload' => ufwReload($params),
        'ufw.allow_service' => ufwAllowService($params),
        'user.password' => setUserPassword($params),
        'user.exists' => userExists($params),
        'ufw.status' => ufwStatus($params),
        'ufw.list_rules' => ufwListRules($params),
        'ufw.enable' => ufwEnable($params),
        'ufw.disable' => ufwDisable($params),
        'ufw.allow_port' => ufwAllowPort($params),
        'ufw.deny_port' => ufwDenyPort($params),
        'ufw.allow_ip' => ufwAllowIp($params),
        'ufw.deny_ip' => ufwDenyIp($params),
        'ufw.delete_rule' => ufwDeleteRule($params),
        'ufw.set_default' => ufwSetDefault($params),
        'ufw.limit_port' => ufwLimitPort($params),
        'ufw.reset' => ufwReset($params),
        'ufw.reload' => ufwReload($params),
        'ufw.allow_service' => ufwAllowService($params),
        'domain.create' => domainCreate($params),
        'domain.alias_add' => domainAliasAdd($params),
        'domain.alias_remove' => domainAliasRemove($params),
        'domain.ensure_error_pages' => domainEnsureErrorPages($params),
        'domain.delete' => domainDelete($params),
        'domain.list' => domainList($params),
        'domain.toggle' => domainToggle($params),
        'domain.set_redirects' => domainSetRedirects($params),
        'domain.set_hotlink_protection' => domainSetHotlinkProtection($params),
        'domain.set_directory_index' => domainSetDirectoryIndex($params),
        'domain.list_protected_dirs' => domainListProtectedDirs($params),
        'domain.add_protected_dir' => domainAddProtectedDir($params),
        'domain.remove_protected_dir' => domainRemoveProtectedDir($params),
        'domain.add_protected_dir_user' => domainAddProtectedDirUser($params),
        'domain.remove_protected_dir_user' => domainRemoveProtectedDirUser($params),
        'php.getSettings' => phpGetSettings($params),
        'php.setSettings' => phpSetSettings($params),
        'php.update_pool_limits' => phpUpdatePoolLimits($params),
        'php.update_all_pool_limits' => phpUpdateAllPoolLimits($params),
        'wp.install' => wpInstall($params),
        'wp.list' => wpList($params),
        'wp.delete' => wpDelete($params),
        'wp.auto_login' => wpAutoLogin($params),
        'wp.update' => wpUpdate($params),
        'wp.scan' => wpScan($params),
        'wp.import' => wpImport($params),
        'wp.cache_enable' => wpCacheEnable($params),
        'wp.cache_disable' => wpCacheDisable($params),
        'wp.cache_flush' => wpCacheFlush($params),
        'wp.cache_status' => wpCacheStatus($params),
        'wp.toggle_debug' => wpToggleDebug($params),
        'wp.toggle_auto_update' => wpToggleAutoUpdate($params),
        'wp.create_staging' => wpCreateStaging($params),
        'wp.push_staging' => wpPushStaging($params),
        'wp.page_cache_enable' => wpPageCacheEnable($params),
        'wp.page_cache_disable' => wpPageCacheDisable($params),
        'wp.page_cache_purge' => wpPageCachePurge($params),
        'wp.page_cache_status' => wpPageCacheStatus($params),
        'ssh.list_keys' => sshListKeys($params),
        'ssh.add_key' => sshAddKey($params),
        'ssh.delete_key' => sshDeleteKey($params),
        'ssh.enable_shell' => sshEnableShell($params),
        'ssh.disable_shell' => sshDisableShell($params),
        'ssh.shell_status' => sshGetShellStatus($params),
        'file.list' => fileList($params),
        'file.read' => fileRead($params),
        'file.write' => fileWrite($params),
        'file.delete' => fileDelete($params),
        'file.mkdir' => fileMkdir($params),
        'file.rename' => fileRename($params),
        'file.move' => fileMove($params),
        'file.copy' => fileCopy($params),
        'file.upload' => fileUpload($params),
        'file.upload_temp' => fileUploadTemp($params),
        'file.download' => fileDownload($params),
        'file.exists' => fileExists($params),
        'file.info' => fileInfo($params),
        'file.extract' => fileExtract($params),
        'file.chmod' => fileChmod($params),
        'file.chown' => fileChown($params),
        'file.trash' => fileTrash($params),
        'file.restore' => fileRestore($params),
        'file.empty_trash' => fileEmptyTrash($params),
        'file.list_trash' => fileListTrash($params),
        'image.optimize' => imageOptimize($params),
        'mysql.list_databases' => mysqlListDatabases($params),
        'mysql.create_database' => mysqlCreateDatabase($params),
        'mysql.delete_database' => mysqlDeleteDatabase($params),
        'mysql.list_users' => mysqlListUsers($params),
        'mysql.create_user' => mysqlCreateUser($params),
        'mysql.delete_user' => mysqlDeleteUser($params),
        'mysql.change_password' => mysqlChangePassword($params),
        'mysql.grant_privileges' => mysqlGrantPrivileges($params),
        'mysql.revoke_privileges' => mysqlRevokePrivileges($params),
        'mysql.get_privileges' => mysqlGetPrivileges($params),
        'mysql.create_master_user' => mysqlCreateMasterUser($params),
        'mysql.import_database' => mysqlImportDatabase($params),
        'mysql.export_database' => mysqlExportDatabase($params),
        'postgres.list_databases' => postgresListDatabases($params),
        'postgres.list_users' => postgresListUsers($params),
        'postgres.create_database' => postgresCreateDatabase($params),
        'postgres.delete_database' => postgresDeleteDatabase($params),
        'postgres.create_user' => postgresCreateUser($params),
        'postgres.delete_user' => postgresDeleteUser($params),
        'postgres.change_password' => postgresChangePassword($params),
        'postgres.grant_privileges' => postgresGrantPrivileges($params),
        'service.restart' => restartService($params),
        'service.reload' => reloadService($params),
        'service.status' => getServiceStatus($params),
        'dns.create_zone' => dnsCreateZone($params),
        'dns.sync_zone' => dnsSyncZone($params),
        'dns.delete_zone' => dnsDeleteZone($params),
        'dns.reload' => dnsReload($params),
        'dns.enable_dnssec' => dnsEnableDnssec($params),
        'dns.disable_dnssec' => dnsDisableDnssec($params),
        'dns.get_dnssec_status' => dnsGetDnssecStatus($params),
        'dns.get_ds_records' => dnsGetDsRecords($params),
        'php.install' => phpInstall($params),
        'php.uninstall' => phpUninstall($params),
        'php.set_default' => phpSetDefaultVersion($params),
        'php.restart_fpm' => phpRestartFpm($params),
        'php.reload_fpm' => phpReloadFpm($params),
        'php.restart_all_fpm' => phpRestartAllFpm($params),
        'php.reload_all_fpm' => phpReloadAllFpm($params),
        'php.list_versions' => phpListVersions($params),
        'php.install_wp_modules' => phpInstallWordPressModules($params),
        'ssh.generate_key' => sshGenerateKey($params),
        'git.generate_key' => gitGenerateKey($params),
        'git.deploy' => gitDeploy($params),
        'rspamd.user_settings' => rspamdUserSettings($params),
        'usage.bandwidth_total' => usageBandwidthTotal($params),
        'usage.user_resources' => usageUserResources($params),
        'server.set_hostname' => setHostname($params),
        'server.set_upload_limits' => setUploadLimits($params),
        'server.update_bind' => updateBindConfig($params),
        'server.info' => getServerInfo($params),
        'server.create_zone' => createServerZone($params),
        'updates.list' => updatesList($params),
        'updates.run' => updatesRun($params),
        'waf.apply' => wafApplySettings($params),
        'waf.audit_log' => wafAuditLogList($params),
        'geo.apply_rules' => geoApplyRules($params),
        'geo.update_database' => geoUpdateDatabase($params),
        'geo.upload_database' => geoUploadDatabase($params),
        'database.persist_tuning' => databasePersistTuning($params),
        'database.get_variables' => databaseGetVariables($params),
        'database.set_global' => databaseSetGlobal($params),
        'server.export_config' => serverExportConfig($params),
        'server.import_config' => serverImportConfig($params),
        'server.get_resolvers' => serverGetResolvers($params),
        'server.set_resolvers' => serverSetResolvers($params),
        'php.install' => phpInstall($params),
        'php.uninstall' => phpUninstall($params),
        'php.set_default' => phpSetDefault($params),
        'php.restart_fpm' => phpRestartFpm($params),
        'php.reload_fpm' => phpReloadFpm($params),
        'php.restart_all_fpm' => phpRestartAllFpm($params),
        'php.reload_all_fpm' => phpReloadAllFpm($params),
        'php.list_versions' => phpListVersions($params),
        'php.install_wp_modules' => phpInstallWordPressModules($params),
        'ssh.generate_key' => sshGenerateKey($params),
        'server.set_hostname' => setHostname($params),
        'server.set_upload_limits' => setUploadLimits($params),
        'server.update_bind' => updateBindConfig($params),
        'server.info' => getServerInfo($params),
        'server.create_zone' => createServerZone($params),
        'nginx.enable_compression' => nginxEnableCompression($params),
        'nginx.get_compression_status' => nginxGetCompressionStatus($params),
        // Email operations
        'email.enable_domain' => emailEnableDomain($params),
        'email.disable_domain' => emailDisableDomain($params),
        'email.generate_dkim' => emailGenerateDkim($params),
        'email.domain_info' => emailGetDomainInfo($params),
        'email.mailbox_create' => emailMailboxCreate($params),
        'email.mailbox_delete' => emailMailboxDelete($params),
        'email.mailbox_change_password' => emailMailboxChangePassword($params),
        'email.mailbox_set_quota' => emailMailboxSetQuota($params),
        'email.mailbox_quota_usage' => emailMailboxGetQuotaUsage($params),
        'email.mailbox_toggle' => emailMailboxToggle($params),
        'email.sync_virtual_users' => emailSyncVirtualUsers($params),
        'email.reload_services' => emailReloadServices($params),
        'email.forwarder_create' => emailForwarderCreate($params),
        'email.forwarder_delete' => emailForwarderDelete($params),
        'email.forwarder_update' => emailForwarderUpdate($params),
        'email.forwarder_toggle' => emailForwarderToggle($params),
        'email.catchall_update' => emailCatchallUpdate($params),
        'email.sync_maps' => emailSyncMaps($params),
        'email.get_logs' => emailGetLogs($params),
        'email.autoresponder_set' => emailAutoresponderSet($params),
        'email.autoresponder_toggle' => emailAutoresponderToggle($params),
        'email.autoresponder_delete' => emailAutoresponderDelete($params),
        'email.hash_password' => emailHashPassword($params),
        // Mail queue operations
        'mail.queue_list' => mailQueueList($params),
        'mail.queue_retry' => mailQueueRetry($params),
        'mail.queue_delete' => mailQueueDelete($params),
        'service.list' => serviceList($params),
        'service.start' => serviceStart($params),
        'service.stop' => serviceStop($params),
        'service.restart' => serviceRestart($params),
        'service.enable' => serviceEnable($params),
        'service.disable' => serviceDisable($params),
        // Server Import operations
        'import.discover' => importDiscover($params),
        'import.start' => importStart($params),
        // SSL Certificate operations
        'ssl.check' => sslCheck($params),
        'ssl.issue' => sslIssue($params),
        'ssl.install' => sslInstall($params),
        'ssl.renew' => sslRenew($params),
        'ssl.generate_self_signed' => sslGenerateSelfSigned($params),
        'ssl.delete' => sslDelete($params),
        // Backup operations
        'backup.create' => backupCreate($params),
        'backup.create_server' => backupCreateServer($params),
        'backup.incremental_direct' => backupServerIncrementalDirect($params),
        'backup.restore' => backupRestore($params),
        'backup.list' => backupList($params),
        'backup.delete' => backupDelete($params),
        'backup.delete_server' => backupDeleteServer($params),
        'backup.verify' => backupVerify($params),
        'backup.get_info' => backupGetInfo($params),
        'backup.upload_remote' => backupUploadRemote($params),
        'backup.download_remote' => backupDownloadRemote($params),
        'backup.list_remote' => backupListRemote($params),
        'backup.delete_remote' => backupDeleteRemote($params),
        'backup.test_destination' => backupTestDestination($params),
        'backup.download_user_archive' => backupDownloadUserArchive($params),
        // cPanel migration operations
        'cpanel.analyze_backup' => cpanelAnalyzeBackup($params),
        'cpanel.restore_backup' => cpanelRestoreBackup($params),
        'cpanel.fix_backup_permissions' => cpanelFixBackupPermissions($params),
        // WHM migration operations
        'whm.download_backup_scp' => whmDownloadBackupScp($params),
        // Jabali system SSH key operations
        'jabali_ssh.get_public_key' => jabaliSshGetPublicKey($params),
        'jabali_ssh.get_private_key' => jabaliSshGetPrivateKey($params),
        'jabali_ssh.ensure_exists' => jabaliSshEnsureExists($params),
        'jabali_ssh.add_to_authorized_keys' => jabaliSshAddToAuthorizedKeys($params),
        // Fail2ban operations
        'fail2ban.status' => fail2banStatus($params),
        'fail2ban.status_light' => fail2banStatusLight($params),
        'fail2ban.install' => fail2banInstall($params),
        'fail2ban.start' => fail2banStart($params),
        'fail2ban.stop' => fail2banStop($params),
        'fail2ban.restart' => fail2banRestart($params),
        'fail2ban.save_settings' => fail2banSaveSettings($params),
        'fail2ban.unban_ip' => fail2banUnbanIp($params),
        'fail2ban.ban_ip' => fail2banBanIp($params),
        'fail2ban.list_jails' => fail2banListJails($params),
        'fail2ban.enable_jail' => fail2banEnableJail($params),
        'fail2ban.disable_jail' => fail2banDisableJail($params),
        'fail2ban.logs' => fail2banLogs($params),
        // ClamAV operations
        'clamav.status' => clamavStatus($params),
        'clamav.status_light' => clamavStatusLight($params),
        'clamav.install' => clamavInstall($params),
        'clamav.start' => clamavStart($params),
        'clamav.stop' => clamavStop($params),
        'clamav.update_signatures' => clamavUpdateSignatures($params),
        'clamav.scan' => clamavScan($params),
        'clamav.realtime_start' => clamavRealtimeStart($params),
        'clamav.realtime_stop' => clamavRealtimeStop($params),
        'clamav.realtime_enable' => clamavRealtimeEnable($params),
        'clamav.realtime_disable' => clamavRealtimeDisable($params),
        'clamav.delete_quarantined' => clamavDeleteQuarantined($params),
        'clamav.clear_threats' => clamavClearThreats($params),
        'clamav.set_light_mode' => clamavSetLightMode($params),
        'clamav.set_full_mode' => clamavSetFullMode($params),
        'clamav.force_update_signatures' => clamavForceUpdateSignatures($params),
        'ssh.get_settings' => sshGetSettings($params),
        'ssh.save_settings' => sshSaveSettings($params),
        // Cron job operations
        'cron.list' => cronList($params),
        'cron.create' => cronCreate($params),
        'cron.delete' => cronDelete($params),
        'cron.toggle' => cronToggle($params),
        'cron.run' => cronRun($params),
        'cron.wp_setup' => cronWordPressSetup($params),
        // Server metrics operations
        'metrics.overview' => metricsOverview($params),
        'metrics.cpu' => metricsCpu($params),
        'metrics.memory' => metricsMemory($params),
        'metrics.disk' => metricsDisk($params),
        'metrics.network' => metricsNetwork($params),
        'metrics.processes' => metricsProcesses($params),
        'system.kill_process' => systemKillProcess($params),
        // Disk quota operations
        'quota.status' => quotaStatus($params),
        'quota.enable' => quotaEnable($params),
        'quota.set' => quotaSet($params),
        'quota.get' => quotaGet($params),
        'quota.report' => quotaReport($params),
        // IP address management
        'ip.list' => ipList($params),
        'ip.add' => ipAdd($params),
        'ip.remove' => ipRemove($params),
        'ip.info' => ipInfo($params),
        // Security scanner tools
        'scanner.install' => scannerInstall($params),
        'scanner.uninstall' => scannerUninstall($params),
        'scanner.status' => scannerStatus($params),
        'scanner.run_lynis' => scannerRunLynis($params),
        'scanner.run_nikto' => scannerRunNikto($params),
        'scanner.start_lynis' => scannerStartLynis($params),
        'scanner.get_scan_status' => scannerGetScanStatus($params),
        // Log analysis
        'logs.tail' => logsTail($params),
        'logs.goaccess' => logsGoaccess($params),
        // Redis ACL management
        'redis.create_user' => redisCreateUser($params),
        'redis.delete_user' => redisDeleteUser($params),
        'redis.user_exists' => redisUserExists($params),
        'redis.change_password' => redisChangePassword($params),
        'redis.migrate_users' => redisMigrateUsers($params),
        default => ['success' => false, 'error' => "Unknown action: $action"],
    };
}

function wafAuditLogList(array $params): array
{
    $limit = (int) ($params['limit'] ?? 200);
    if ($limit <= 0) {
        $limit = 200;
    }

    $logPath = '/var/log/nginx/modsec_audit.log';
    if (!file_exists($logPath)) {
        return ['success' => true, 'entries' => []];
    }

    $lines = [];
    exec('tail -n 5000 ' . escapeshellarg($logPath) . ' 2>/dev/null', $lines);

    $entries = [];
    $current = [
        'timestamp' => null,
        'remote_ip' => null,
        'host' => null,
        'uri' => null,
    ];
    $contexts = [];
    $hits = [];
    $blocks = [];

    foreach ($lines as $line) {
        if (preg_match('/^---[A-Za-z0-9]+---A--$/', $line)) {
            $current = [
                'timestamp' => null,
                'remote_ip' => null,
                'host' => null,
                'uri' => null,
            ];
            continue;
        }

        if (preg_match('/^\[(\d{2}\/[A-Za-z]{3}\/\d{4}:\d{2}:\d{2}:\d{2}) ([+-]\d{4})\]\s+\d+\.\d+\s+([0-9a-fA-F:.]+)/', $line, $matches)) {
            $date = DateTime::createFromFormat('d/M/Y:H:i:s O', $matches[1] . ' ' . $matches[2]);
            if ($date instanceof DateTime) {
                $current['timestamp'] = $date->getTimestamp();
            }
            $current['remote_ip'] = $matches[3];
            continue;
        }

        if (preg_match('/^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH)\s+([^ ]+)\s+HTTP/i', $line, $matches)) {
            $current['uri'] = $matches[2];
            continue;
        }

        if (preg_match('/^\s*host:\s*(.+)$/i', $line, $matches)) {
            $current['host'] = trim($matches[1]);
            continue;
        }

        if (!str_contains($line, 'ModSecurity:')) {
            continue;
        }

        $uniqueId = null;
        if (preg_match('/\\[unique_id "([^"]+)"\\]/', $line, $matches)) {
            $uniqueId = $matches[1];
        }

        if ($uniqueId !== null && !isset($contexts[$uniqueId])) {
            $contexts[$uniqueId] = [
                'timestamp' => $current['timestamp'],
                'remote_ip' => $current['remote_ip'],
                'host' => $current['host'],
                'uri' => $current['uri'],
            ];
        }

        $entry = [
            'timestamp' => $current['timestamp'],
            'remote_ip' => $current['remote_ip'],
            'host' => $current['host'],
            'uri' => $current['uri'],
            'rule_id' => null,
            'message' => null,
            'severity' => null,
            'unique_id' => $uniqueId,
        ];

        if ($uniqueId !== null && isset($contexts[$uniqueId])) {
            $entry = array_merge($entry, $contexts[$uniqueId]);
        }

        if (preg_match('/\\[id "([0-9]+)"\\]/', $line, $matches)) {
            $entry['rule_id'] = $matches[1];
        }
        if (preg_match('/\\[msg "([^"]+)"\\]/', $line, $matches)) {
            $entry['message'] = $matches[1];
        }
        if (preg_match('/\\[severity "([^"]+)"\\]/', $line, $matches)) {
            $entry['severity'] = $matches[1];
        }
        if (preg_match('/\\[uri "([^"]+)"\\]/', $line, $matches)) {
            $loggedUri = $matches[1];
            $currentUri = (string) ($entry['uri'] ?? '');
            if ($currentUri === '' || (!str_contains($currentUri, '?') && $loggedUri !== '')) {
                $entry['uri'] = $loggedUri;
            }
        }
        if (preg_match('/\\[hostname "([^"]+)"\\]/', $line, $matches)) {
            $loggedHost = $matches[1];
            $currentHost = (string) ($entry['host'] ?? '');
            $remoteIp = (string) ($entry['remote_ip'] ?? '');
            $shouldOverrideHost = $currentHost === ''
                || $currentHost === $remoteIp
                || $currentHost === '127.0.0.1'
                || $currentHost === '::1'
                || $currentHost === 'localhost';

            if ($shouldOverrideHost && $loggedHost !== '') {
                $entry['host'] = $loggedHost;
            }
        }

        if (str_contains($line, 'Access denied')) {
            if ($uniqueId !== null) {
                $blocks[$uniqueId] = $entry;
            } else {
                $entries[] = $entry;
            }
            continue;
        }

        if (str_contains($line, 'Warning.')) {
            if ($uniqueId !== null) {
                $hits[$uniqueId][] = $entry;
            } else {
                $entries[] = $entry;
            }
        }
    }

    foreach ($blocks as $uniqueId => $blockEntry) {
        if (!empty($hits[$uniqueId])) {
            foreach ($hits[$uniqueId] as $hitEntry) {
                $hitEntry['blocked'] = true;
                $entries[] = $hitEntry;
            }
            continue;
        }

        $blockEntry['blocked'] = true;
        $entries[] = $blockEntry;
    }

    $entries = array_reverse($entries);
    if (count($entries) > $limit) {
        $entries = array_slice($entries, 0, $limit);
    }

    return ['success' => true, 'entries' => $entries];
}

// ============ USER MANAGEMENT ============

function createUser(array $params): array
{
    $username = $params['username'] ?? '';
    $password = $params['password'] ?? null;
    
    logger("Creating user: $username");
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username format'];
    }
    
    if (isProtectedUser($username)) {
        return ['success' => false, 'error' => 'Cannot create protected system user'];
    }
    
    exec("id " . escapeshellarg($username) . " 2>/dev/null", $output, $exitCode);
    if ($exitCode === 0) {
        return ['success' => false, 'error' => 'User already exists'];
    }
    
    $homeDir = "/home/$username";

    // Create user with nologin shell (SFTP-only by default)
    $cmd = sprintf('useradd -m -d %s -s /usr/sbin/nologin %s 2>&1',
        escapeshellarg($homeDir),
        escapeshellarg($username)
    );
    exec($cmd, $output, $exitCode);
    
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'Failed to create user: ' . implode("\n", $output)];
    }
    
    if ($password) {
        $cmd = sprintf('echo %s:%s | chpasswd 2>&1',
            escapeshellarg($username),
            escapeshellarg($password)
        );
        exec($cmd);
    }
    
    // Remove symlinks that cause issues
    @unlink("$homeDir/.face.icon");
    @unlink("$homeDir/.face");
    
    // Create standard directories (NO ACLs for www-data!)
    $dirs = ['domains', 'logs', 'tmp', 'ssl', 'backups'];
    foreach ($dirs as $dir) {
        $path = "$homeDir/$dir";
        if (!is_dir($path)) {
            mkdir($path, 0755, true);
        }
        chown($path, $username);
        chgrp($path, $username);
    }
    
    // Set up for secure SFTP chroot
    exec("usermod -aG sftpusers " . escapeshellarg($username));

    // Chroot requires root ownership of home directory
    // Use user's group with 750 for complete isolation between users
    chown($homeDir, "root");
    chgrp($homeDir, $username);  // User's own group - only this user can access
    chmod($homeDir, 0750);       // root=rwx, user's group=r-x, others=none

    // Create PHP-FPM pool for the user (so it's ready when they create domains)
    // Don't reload FPM here - caller is responsible for reloading after all operations complete
    $fpmResult = createFpmPool($username, false);
    $fpmPoolCreated = (bool) ($fpmResult['pool_created'] ?? false);
    $fpmReloadRequired = $fpmPoolCreated && (bool) ($fpmResult['needs_reload'] ?? false);

    // Create Redis ACL user for isolated caching
    $redisPassword = bin2hex(random_bytes(16)); // 32 char password
    $redisResult = redisCreateUser(['username' => $username, 'password' => $redisPassword]);

    if ($redisResult['success']) {
        // Store Redis credentials in user's home directory
        $redisCredFile = "{$homeDir}/.redis_credentials";
        $credContent = "REDIS_USER=jabali_{$username}\n" .
                       "REDIS_PASS={$redisPassword}\n" .
                       "REDIS_PREFIX={$username}:\n";
        file_put_contents($redisCredFile, $credContent);
        chmod($redisCredFile, 0600);
        chown($redisCredFile, $username);
        chgrp($redisCredFile, $username);
        logger("Created Redis ACL user for $username");
    } else {
        logger("Warning: Failed to create Redis user for $username: " . ($redisResult['error'] ?? 'Unknown error'));
    }

    logger("Created user $username with home directory $homeDir");

    return [
        'success' => true,
        'message' => "User $username created successfully",
        'home_directory' => $homeDir,
        'redis_user' => $redisResult['success'] ? "jabali_{$username}" : null,
        'fpm_pool_created' => $fpmPoolCreated,
        'fpm_reload_required' => $fpmReloadRequired,
    ];
}

function deleteUser(array $params): array
{
    $username = $params['username'] ?? '';
    $removeHome = $params['remove_home'] ?? false;
    $domains = $params['domains'] ?? []; // List of user's domains to clean up
    $steps = [];

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username format'];
    }

    if (isProtectedUser($username)) {
        return ['success' => false, 'error' => 'Cannot delete protected system user'];
    }

    // Check if user exists
    exec("id " . escapeshellarg($username) . " 2>/dev/null", $idOutput, $idExit);
    if ($idExit !== 0) {
        return ['success' => false, 'error' => 'User does not exist'];
    }

    $homeDir = "/home/$username";

    // Get domains from .domains file if not provided
    if (empty($domains)) {
        $domainsFile = "$homeDir/.domains";
        if (file_exists($domainsFile)) {
            $domainsData = json_decode(file_get_contents($domainsFile), true) ?: [];
            $domains = array_keys($domainsData);
        }
    }

    $domainConfigRemoved = false;
    $domainsDirExists = is_dir("$homeDir/domains");

    // Clean up domain-related files for each domain
    foreach ($domains as $domain) {
        if (!validateDomain($domain)) {
            continue;
        }

        $domainTouched = false;

        // Remove nginx vhost configs (with .conf extension)
        $nginxAvailable = "/etc/nginx/sites-available/{$domain}.conf";
        $nginxEnabled = "/etc/nginx/sites-enabled/{$domain}.conf";
        // Also try without .conf for backwards compatibility
        $nginxAvailableOld = "/etc/nginx/sites-available/$domain";
        $nginxEnabledOld = "/etc/nginx/sites-enabled/$domain";

        foreach ([$nginxEnabled, $nginxEnabledOld] as $file) {
            if (file_exists($file) || is_link($file)) {
                @unlink($file);
                logger("Removed nginx symlink: $file");
                $domainTouched = true;
            }
        }
        foreach ([$nginxAvailable, $nginxAvailableOld] as $file) {
            if (file_exists($file)) {
                @unlink($file);
                logger("Removed nginx config: $file");
                $domainTouched = true;
            }
        }

        // Remove DNS zone file
        $zoneFile = "/etc/bind/zones/db.$domain";
        if (file_exists($zoneFile)) {
            @unlink($zoneFile);
            logger("Removed DNS zone: $zoneFile");
            $domainTouched = true;

            // Remove from named.conf.local
            $namedConf = '/etc/bind/named.conf.local';
            if (file_exists($namedConf)) {
                $content = file_get_contents($namedConf);
                // Remove zone block for this domain (use [\s\S]*?\n\} to match nested braces)
                $pattern = '/\n?zone\s+"' . preg_quote($domain, '/') . '"\s*\{[\s\S]*?\n\};\n?/';
                $newContent = preg_replace($pattern, "\n", $content);
                if ($newContent !== $content) {
                    file_put_contents($namedConf, $newContent);
                    logger("Removed zone from named.conf.local: $domain");
                }
            }
        }

        // Remove mail directories (both in home and /var/vmail)
        $mailDir = "$homeDir/mail/$domain";
        if (is_dir($mailDir)) {
            exec("rm -rf " . escapeshellarg($mailDir));
            logger("Removed mail directory: $mailDir");
            $domainTouched = true;
        }
        $vmailDir = "/var/vmail/$domain";
        if (is_dir($vmailDir)) {
            exec("rm -rf " . escapeshellarg($vmailDir));
            logger("Removed vmail directory: $vmailDir");
            $domainTouched = true;
        }

        // Remove from Postfix virtual_mailbox_domains
        $vdomainsFile = POSTFIX_VIRTUAL_DOMAINS;
        if (file_exists($vdomainsFile)) {
            $content = file_get_contents($vdomainsFile);
            $lines = explode("\n", $content);
            $lines = array_filter($lines, fn($line) => trim($line) !== $domain);
            file_put_contents($vdomainsFile, implode("\n", $lines));
        }

        // Remove mailboxes from Postfix virtual_mailbox_maps
        $vmailboxFile = POSTFIX_VIRTUAL_MAILBOXES;
        if (file_exists($vmailboxFile)) {
            $content = file_get_contents($vmailboxFile);
            $lines = explode("\n", $content);
            $lines = array_filter($lines, fn($line) => !str_contains($line, "@$domain"));
            file_put_contents($vmailboxFile, implode("\n", $lines));
        }

        // Remove from Postfix virtual_alias_maps
        $valiasFile = POSTFIX_VIRTUAL_ALIASES;
        if (file_exists($valiasFile)) {
            $content = file_get_contents($valiasFile);
            $lines = explode("\n", $content);
            $lines = array_filter($lines, fn($line) => !str_contains($line, "@$domain"));
            file_put_contents($valiasFile, implode("\n", $lines));
        }

        // Remove SSL certificates (live, archive, and renewal)
        $certPath = "/etc/letsencrypt/live/$domain";
        $certArchive = "/etc/letsencrypt/archive/$domain";
        $certRenewal = "/etc/letsencrypt/renewal/$domain.conf";
        if (is_dir($certPath)) {
            exec("rm -rf " . escapeshellarg($certPath));
            logger("Removed SSL certificate: $certPath");
            $domainTouched = true;
        }
        if (is_dir($certArchive)) {
            exec("rm -rf " . escapeshellarg($certArchive));
            logger("Removed SSL archive: $certArchive");
            $domainTouched = true;
        }
        if (file_exists($certRenewal)) {
            @unlink($certRenewal);
            logger("Removed SSL renewal config: $certRenewal");
            $domainTouched = true;
        }

        if ($domainTouched) {
            $domainConfigRemoved = true;
            $steps[] = "$domain config files removed";
        }
    }

    // Delete MySQL databases and users belonging to this user
    $dbPrefix = $username . '_';
    $mysqli = getMysqlConnection();
    if ($mysqli) {
        $dbDeletedCount = 0;
        $dbUserDeletedCount = 0;

        // Get all databases belonging to this user
        $result = $mysqli->query("SHOW DATABASES LIKE '{$mysqli->real_escape_string($dbPrefix)}%'");
        if ($result) {
            while ($row = $result->fetch_row()) {
                $dbName = $row[0];
                // Double-check it starts with username_
                if (strpos($dbName, $dbPrefix) === 0) {
                    $mysqli->query("DROP DATABASE IF EXISTS `{$mysqli->real_escape_string($dbName)}`");
                    logger("Deleted MySQL database: $dbName");
                    $dbDeletedCount++;
                }
            }
            $result->free();
        }

        // Get all MySQL users belonging to this user
        $result = $mysqli->query("SELECT User, Host FROM mysql.user WHERE User LIKE '{$mysqli->real_escape_string($dbPrefix)}%'");
        if ($result) {
            while ($row = $result->fetch_assoc()) {
                $dbUser = $row['User'];
                $dbHost = $row['Host'];
                // Double-check it starts with username_
                if (strpos($dbUser, $dbPrefix) === 0) {
                    $mysqli->query("DROP USER IF EXISTS '{$mysqli->real_escape_string($dbUser)}'@'{$mysqli->real_escape_string($dbHost)}'");
                    logger("Deleted MySQL user: $dbUser@$dbHost");
                    $dbUserDeletedCount++;
                }
            }
            $result->free();
        }
        $mysqli->query("FLUSH PRIVILEGES");
        $mysqli->close();

        if ($dbDeletedCount > 0) {
            $steps[] = "MySQL databases removed ({$dbDeletedCount})";
        }
        if ($dbUserDeletedCount > 0) {
            $steps[] = "MySQL users removed ({$dbUserDeletedCount})";
        }
    }

    // Remove PHP-FPM pool config
    $fpmRemovedCount = 0;
    foreach (glob("/etc/php/*/fpm/pool.d/$username.conf") as $poolConf) {
        if (@unlink($poolConf)) {
            logger("Removed PHP-FPM pool: $poolConf");
            $fpmRemovedCount++;
        }
    }
    if ($fpmRemovedCount > 0) {
        $steps[] = 'PHP-FPM pool removed';
        $domainConfigRemoved = true;
    }

    // Delete user with --force to ignore warnings about mail spool
    // Don't use -r since home directory is owned by root for chroot
    $cmd = sprintf('userdel --force %s 2>&1', escapeshellarg($username));
    $userdelOutput = [];
    exec($cmd, $userdelOutput, $userdelExit);

    // Verify user was actually deleted (userdel may return non-zero for warnings)
    exec("id " . escapeshellarg($username) . " 2>/dev/null", $checkOutput, $checkExit);
    if ($checkExit === 0) {
        // User still exists - deletion actually failed
        return ['success' => false, 'error' => 'Failed to delete user: ' . implode("\n", $userdelOutput)];
    }

    $steps[] = 'User removed from SSH';
    $steps[] = 'Unix user removed from the server';

    // Delete Redis ACL user (and all their cached keys)
    $redisResult = redisDeleteUser(['username' => $username]);
    if (!$redisResult['success']) {
        logger("Warning: Failed to delete Redis user for $username: " . ($redisResult['error'] ?? 'Unknown error'));
    } else {
        logger("Deleted Redis ACL user for $username");
        $steps[] = 'Redis ACL user removed';
    }

    // Manually remove home directory if requested (since it's owned by root)
    if ($removeHome && is_dir($homeDir)) {
        exec(sprintf('rm -rf %s 2>&1', escapeshellarg($homeDir)), $rmOutput, $rmExit);
        if ($rmExit !== 0) {
            logger("Warning: Failed to remove home directory for $username");
        } else {
            $steps[] = "User's data directory removed";
            if ($domainsDirExists) {
                $steps[] = "User's domains directory removed";
            }
        }
    }

    // Reload services
    exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_DOMAINS) . ' 2>/dev/null');
    exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_MAILBOXES) . ' 2>/dev/null');
    exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_ALIASES) . ' 2>/dev/null');
    exec('systemctl reload nginx 2>/dev/null');
    exec('rndc reload 2>/dev/null');
    exec('systemctl reload php*-fpm 2>/dev/null');

    if ($domainConfigRemoved) {
        $steps[] = "User's config files deleted";
    }

    logger("Deleted user $username" . ($removeHome ? " with home directory" : "") . " and cleaned up " . count($domains) . " domain(s)");

    return ['success' => true, 'message' => "User $username deleted successfully", 'steps' => $steps];
}

function setUserPassword(array $params): array
{
    $username = $params['username'] ?? '';
    $password = $params['password'] ?? '';
    
    if (!validateUsername($username) || empty($password)) {
        return ['success' => false, 'error' => 'Invalid username or password'];
    }
    
    exec("id " . escapeshellarg($username) . " 2>/dev/null", $output, $exitCode);
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'User does not exist'];
    }
    
    $cmd = sprintf('echo %s:%s | chpasswd 2>&1', escapeshellarg($username), escapeshellarg($password));
    exec($cmd, $output, $exitCode);
    
    return $exitCode === 0
        ? ['success' => true, 'message' => 'Password updated']
        : ['success' => false, 'error' => 'Failed to set password'];
}

function userExists(array $params): array
{
    $username = $params['username'] ?? '';
    
    if (!validateUsername($username)) {
        return ['success' => true, 'exists' => false];
    }
    
    exec("id " . escapeshellarg($username) . " 2>/dev/null", $output, $exitCode);
    
    return ['success' => true, 'exists' => $exitCode === 0];
}


// ============ PHP-FPM POOL MANAGEMENT ============

function getFpmSocketPath(string $username): string
{
    $phpVersion = '8.4';
    return "/run/php/php{$phpVersion}-fpm-{$username}.sock";
}

function generateNginxVhost(string $domain, string $publicHtml, string $logs, string $fpmSocket): string
{
    $config = <<<'NGINXCONF'
server {
    listen 80;
    listen [::]:80;
    server_name DOMAIN_PLACEHOLDER www.DOMAIN_PLACEHOLDER;
    root DOCROOT_PLACEHOLDER;

    include /etc/nginx/jabali/includes/waf.conf;
    include /etc/nginx/jabali/includes/geo.conf;

    # Allow ACME challenge for SSL certificate issuance/renewal
    location /.well-known/acme-challenge/ {
        try_files $uri =404;
    }

    # Redirect all other HTTP traffic to HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name DOMAIN_PLACEHOLDER www.DOMAIN_PLACEHOLDER;
    root DOCROOT_PLACEHOLDER;

    include /etc/nginx/jabali/includes/waf.conf;
    include /etc/nginx/jabali/includes/geo.conf;

    # Symlink protection - prevent following symlinks outside document root
    disable_symlinks if_not_owner from=$document_root;

    ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
    ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;

    index index.php index.html;
    client_max_body_size 50M;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
    
    location ~ \.php$ {
        fastcgi_pass unix:SOCKET_PLACEHOLDER;
        fastcgi_next_upstream error timeout invalid_header http_500 http_503;
        fastcgi_next_upstream_tries 2;
        fastcgi_next_upstream_timeout 5s;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # GoAccess statistics reports
    location /stats/ {
        alias STATS_PLACEHOLDER/;
        index report.html;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }

    access_log LOGS_PLACEHOLDER/access.log combined;
    error_log LOGS_PLACEHOLDER/error.log;
}
NGINXCONF;

    // Stats directory lives under the document root for web access
    $stats = rtrim($publicHtml, '/') . '/stats';

    $config = str_replace('DOMAIN_PLACEHOLDER', $domain, $config);
    $config = str_replace('DOCROOT_PLACEHOLDER', $publicHtml, $config);
    $config = str_replace('SOCKET_PLACEHOLDER', $fpmSocket, $config);
    $config = str_replace('LOGS_PLACEHOLDER', $logs, $config);
    $config = str_replace('STATS_PLACEHOLDER', $stats, $config);

    return $config;
}
function createFpmPool(string $username, bool $reload = true): array
{
    $phpVersion = '8.4';
    $poolFile = "/etc/php/{$phpVersion}/fpm/pool.d/{$username}.conf";

    // Check if pool already exists
    if (file_exists($poolFile)) {
        return [
            'success' => true,
            'message' => 'Pool already exists',
            'socket' => getFpmSocketPath($username),
            'pool_created' => false,
        ];
    }
    
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }
    
    $userHome = $userInfo['dir'];
    
    // Create required directories
    $dirs = ["{$userHome}/tmp", "{$userHome}/logs"];
    foreach ($dirs as $dir) {
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
            chown($dir, $username);
            chgrp($dir, $username);
        }
    }

    // Default PHP settings - can be overridden via admin settings
    $memoryLimit = '512M';
    $uploadMaxFilesize = '64M';
    $postMaxSize = '64M';
    $maxExecutionTime = '300';
    $maxInputTime = '300';
    $maxInputVars = '3000';

    // Resource limits - configurable via admin settings
    $pmMaxChildren = (int)($params['pm_max_children'] ?? 5);
    $pmStartServers = max(1, (int)($pmMaxChildren / 5));
    $pmMinSpareServers = max(1, (int)($pmMaxChildren / 5));
    $pmMaxSpareServers = max(2, (int)($pmMaxChildren / 2));
    $pmMaxRequests = (int)($params['pm_max_requests'] ?? 200);
    $rlimitFiles = (int)($params['rlimit_files'] ?? 1024);
    $processPriority = (int)($params['process_priority'] ?? 0);
    $requestTerminateTimeout = (int)($params['request_terminate_timeout'] ?? 300);

    $poolConfig = "[{$username}]
user = {$username}
group = {$username}

listen = /run/php/php{$phpVersion}-fpm-{$username}.sock
listen.owner = {$username}
listen.group = www-data
listen.mode = 0660

; Process manager settings
pm = dynamic
pm.max_children = {$pmMaxChildren}
pm.start_servers = {$pmStartServers}
pm.min_spare_servers = {$pmMinSpareServers}
pm.max_spare_servers = {$pmMaxSpareServers}
pm.max_requests = {$pmMaxRequests}

; Resource limits
rlimit_files = {$rlimitFiles}
process.priority = {$processPriority}
request_terminate_timeout = {$requestTerminateTimeout}s
; slowlog disabled by default to avoid startup failures when logs dir missing
; request_slowlog_timeout = 30s
; slowlog = {$userHome}/logs/php-slow.log

chdir = /

; PHP Settings (defaults)
php_admin_value[memory_limit] = {$memoryLimit}
php_admin_value[upload_max_filesize] = {$uploadMaxFilesize}
php_admin_value[post_max_size] = {$postMaxSize}
php_admin_value[max_execution_time] = {$maxExecutionTime}
php_admin_value[max_input_time] = {$maxInputTime}
php_admin_value[max_input_vars] = {$maxInputVars}

; Security
php_admin_value[open_basedir] = {$userHome}/:/tmp/:/usr/share/php/
php_admin_value[upload_tmp_dir] = {$userHome}/tmp
php_admin_value[session.save_path] = {$userHome}/tmp
php_admin_value[sys_temp_dir] = {$userHome}/tmp
php_admin_value[disable_functions] = symlink,link,exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec

; Logging
php_admin_flag[log_errors] = on
php_admin_value[error_log] = {$userHome}/logs/php-error.log

security.limit_extensions = .php
";
    if (file_put_contents($poolFile, $poolConfig) === false) {
        return [
            'success' => false,
            'error' => 'Failed to create pool configuration',
            'pool_created' => false,
        ];
    }

    // Reload PHP-FPM if requested (default behavior for normal operations)
    // Pass reload=false during migrations to avoid unnecessary reloads during batches
    if ($reload) {
        exec("systemctl reload php{$phpVersion}-fpm 2>&1", $output, $code);
        if ($code !== 0) {
            logger("Warning: PHP-FPM reload failed: " . implode("\n", $output));
        }
    }

    return [
        'success' => true,
        'socket' => getFpmSocketPath($username),
        'needs_reload' => !$reload,
        'pool_created' => true,
    ];
}

function deleteFpmPool(string $username): array
{
    $phpVersion = '8.4';
    $poolFile = "/etc/php/{$phpVersion}/fpm/pool.d/{$username}.conf";

    if (file_exists($poolFile)) {
        unlink($poolFile);
        exec("(sleep 1 && systemctl reload php{$phpVersion}-fpm) > /dev/null 2>&1 &");
    }

    return ['success' => true];
}

/**
 * Update FPM pool limits for a specific user
 */
function phpUpdatePoolLimits(array $params): array
{
    $username = $params['username'] ?? '';
    $phpVersion = '8.4';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $poolFile = "/etc/php/{$phpVersion}/fpm/pool.d/{$username}.conf";
    if (!file_exists($poolFile)) {
        return ['success' => false, 'error' => 'Pool configuration not found'];
    }

    $userHome = "/home/{$username}";

    // Ensure logs directory exists (for error logs)
    $logsDir = "{$userHome}/logs";
    if (!is_dir($logsDir)) {
        mkdir($logsDir, 0755, true);
        chown($logsDir, $username);
        chgrp($logsDir, $username);
    }

    // Get limits from params with defaults
    $pmMaxChildren = (int)($params['pm_max_children'] ?? 5);
    $pmStartServers = max(1, (int)($pmMaxChildren / 5));
    $pmMinSpareServers = max(1, (int)($pmMaxChildren / 5));
    $pmMaxSpareServers = max(2, (int)($pmMaxChildren / 2));
    $pmMaxRequests = (int)($params['pm_max_requests'] ?? 200);
    $rlimitFiles = (int)($params['rlimit_files'] ?? 1024);
    $processPriority = (int)($params['process_priority'] ?? 0);
    $requestTerminateTimeout = (int)($params['request_terminate_timeout'] ?? 300);

    // PHP settings
    $memoryLimit = $params['memory_limit'] ?? '512M';
    $uploadMaxFilesize = $params['upload_max_filesize'] ?? '64M';
    $postMaxSize = $params['post_max_size'] ?? '64M';
    $maxExecutionTime = $params['max_execution_time'] ?? '300';
    $maxInputTime = $params['max_input_time'] ?? '300';
    $maxInputVars = $params['max_input_vars'] ?? '3000';

    $poolConfig = "[{$username}]
user = {$username}
group = {$username}

listen = /run/php/php{$phpVersion}-fpm-{$username}.sock
listen.owner = {$username}
listen.group = www-data
listen.mode = 0660

; Process manager settings
pm = dynamic
pm.max_children = {$pmMaxChildren}
pm.start_servers = {$pmStartServers}
pm.min_spare_servers = {$pmMinSpareServers}
pm.max_spare_servers = {$pmMaxSpareServers}
pm.max_requests = {$pmMaxRequests}

; Resource limits
rlimit_files = {$rlimitFiles}
process.priority = {$processPriority}
request_terminate_timeout = {$requestTerminateTimeout}s
; slowlog disabled by default to avoid startup failures when logs dir missing
; request_slowlog_timeout = 30s
; slowlog = {$userHome}/logs/php-slow.log

chdir = /

; PHP Settings
php_admin_value[memory_limit] = {$memoryLimit}
php_admin_value[upload_max_filesize] = {$uploadMaxFilesize}
php_admin_value[post_max_size] = {$postMaxSize}
php_admin_value[max_execution_time] = {$maxExecutionTime}
php_admin_value[max_input_time] = {$maxInputTime}
php_admin_value[max_input_vars] = {$maxInputVars}

; Security
php_admin_value[open_basedir] = {$userHome}/:/tmp/:/usr/share/php/
php_admin_value[upload_tmp_dir] = {$userHome}/tmp
php_admin_value[session.save_path] = {$userHome}/tmp
php_admin_value[sys_temp_dir] = {$userHome}/tmp
php_admin_value[disable_functions] = symlink,link,exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec

; Logging
php_admin_flag[log_errors] = on
php_admin_value[error_log] = {$userHome}/logs/php-error.log

security.limit_extensions = .php
";

    if (file_put_contents($poolFile, $poolConfig) === false) {
        return ['success' => false, 'error' => 'Failed to update pool configuration'];
    }

    return ['success' => true];
}

/**
 * Update FPM pool limits for all users
 */
function phpUpdateAllPoolLimits(array $params): array
{
    $phpVersion = '8.4';
    $poolDir = "/etc/php/{$phpVersion}/fpm/pool.d";

    $pools = glob("{$poolDir}/*.conf");
    $updated = [];
    $errors = [];

    foreach ($pools as $poolFile) {
        $username = basename($poolFile, '.conf');

        // Skip www.conf (default pool)
        if ($username === 'www') {
            continue;
        }

        // Verify user exists
        exec("id " . escapeshellarg($username) . " 2>/dev/null", $output, $exitCode);
        if ($exitCode !== 0) {
            continue;
        }

        $result = phpUpdatePoolLimits(array_merge($params, ['username' => $username]));
        if ($result['success']) {
            $updated[] = $username;
        } else {
            $errors[$username] = $result['error'];
        }
    }

    // Reload PHP-FPM after all updates
    exec("(sleep 2 && systemctl reload php{$phpVersion}-fpm) > /dev/null 2>&1 &");

    return [
        'success' => true,
        'updated' => $updated,
        'errors' => $errors,
    ];
}


// ============ DOMAIN MANAGEMENT ============

function createDomain(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';
    
    if (!validateUsername($username) || !validateDomain($domain)) {
        return ['success' => false, 'error' => 'Invalid username or domain format'];
    }
    
    exec("id " . escapeshellarg($username) . " 2>/dev/null", $output, $exitCode);
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'User does not exist'];
    }
    
    $homeDir = "/home/$username";
    $domainDir = "$homeDir/domains/$domain";
    $publicDir = "$domainDir/public_html";
    
    if (is_dir($domainDir)) {
        return ['success' => false, 'error' => 'Domain directory already exists'];
    }
    
    if (!mkdir($publicDir, 0755, true)) {
        return ['success' => false, 'error' => 'Failed to create domain directory'];
    }
    
    // Set ownership to user (NO www-data access)
    exec(sprintf('chown -R %s:%s %s', escapeshellarg($username), escapeshellarg($username), escapeshellarg($domainDir)));
    
    // Create default index.html
    $indexContent = "<!DOCTYPE html>\n<html>\n<head><title>Welcome to $domain</title></head>\n<body><h1>Welcome to $domain</h1></body>\n</html>";
    file_put_contents("$publicDir/index.html", $indexContent);
    chown("$publicDir/index.html", $username);
    chgrp("$publicDir/index.html", $username);
    
    logger("Created domain $domain for user $username");
    
    return [
        'success' => true,
        'message' => "Domain $domain created",
        'domain_path' => $domainDir,
        'public_path' => $publicDir,
    ];
}

function deleteDomain(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';
    
    if (!validateUsername($username) || !validateDomain($domain)) {
        return ['success' => false, 'error' => 'Invalid username or domain format'];
    }
    
    $domainDir = "/home/$username/domains/$domain";
    
    if (!is_dir($domainDir)) {
        return ['success' => false, 'error' => 'Domain directory does not exist'];
    }
    
    exec(sprintf('rm -rf %s 2>&1', escapeshellarg($domainDir)), $output, $exitCode);
    
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'Failed to delete domain'];
    }
    
    logger("Deleted domain $domain for user $username");
    
    return ['success' => true, 'message' => "Domain $domain deleted"];
}

// ============ FILE OPERATIONS ============

function fileList(array $params): array
{
    $username = $params['username'] ?? '';
    $path = $params['path'] ?? '';
    $showHidden = (bool) ($params['show_hidden'] ?? false);

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $fullPath = validateUserPath($username, $path);
    if ($fullPath === null) {
        return ['success' => false, 'error' => 'Invalid path'];
    }

    if (!is_dir($fullPath)) {
        return ['success' => false, 'error' => 'Directory not found'];
    }

    $items = [];
    $entries = @scandir($fullPath);

    if ($entries === false) {
        return ['success' => false, 'error' => 'Cannot read directory'];
    }

    foreach ($entries as $entry) {
        if ($entry === '.' || $entry === '..') continue;
        // Hide hidden files unless show_hidden is true (always hide .trash)
        if (str_starts_with($entry, '.') && (!$showHidden || $entry === '.trash')) continue;
        
        $itemPath = "$fullPath/$entry";
        $stat = @stat($itemPath);
        
        if ($stat === false) continue;
        
        // Skip symlinks
        if (is_link($itemPath)) continue;
        
        $items[] = [
            'name' => $entry,
            'path' => ltrim(str_replace("/home/$username", '', $itemPath), '/'),
            'is_dir' => is_dir($itemPath),
            'size' => is_file($itemPath) ? $stat['size'] : null,
            'modified' => $stat['mtime'],
            'permissions' => substr(sprintf('%o', $stat['mode']), -4),
        ];
    }
    
    // Sort: directories first, then alphabetically
    usort($items, function ($a, $b) {
        if ($a['is_dir'] !== $b['is_dir']) {
            return $b['is_dir'] <=> $a['is_dir'];
        }
        return strcasecmp($a['name'], $b['name']);
    });
    
    return ['success' => true, 'items' => $items, 'path' => $path];
}

function fileRead(array $params): array
{
    $username = $params['username'] ?? '';
    $path = $params['path'] ?? '';
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    $fullPath = validateUserPath($username, $path);
    if ($fullPath === null || !is_file($fullPath)) {
        return ['success' => false, 'error' => 'File not found'];
    }
    
    // Limit file size for reading
    $maxSize = 50 * 1024 * 1024; // 5MB
    if (filesize($fullPath) > $maxSize) {
        return ['success' => false, 'error' => 'File too large to read'];
    }
    
    $content = @file_get_contents($fullPath);
    if ($content === false) {
        return ['success' => false, 'error' => 'Cannot read file'];
    }
    
    return [
        'success' => true,
        'content' => base64_encode($content),
        'encoding' => 'base64',
        'size' => strlen($content),
    ];
}

function fileWrite(array $params): array
{
    $username = $params['username'] ?? '';
    $path = $params['path'] ?? '';
    $content = $params['content'] ?? '';
    $encoding = $params['encoding'] ?? 'plain';
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    $fullPath = validateUserPath($username, $path);
    if ($fullPath === null) {
        return ['success' => false, 'error' => 'Invalid path'];
    }
    
    // Decode content if base64
    if ($encoding === 'base64') {
        $content = base64_decode($content, true);
        if ($content === false) {
            return ['success' => false, 'error' => 'Invalid base64 content'];
        }
    }
    
    // Ensure parent directory exists
    $parentDir = dirname($fullPath);
    if (!is_dir($parentDir)) {
        return ['success' => false, 'error' => 'Parent directory does not exist'];
    }
    
    if (@file_put_contents($fullPath, $content) === false) {
        return ['success' => false, 'error' => 'Cannot write file'];
    }
    
    chown($fullPath, $username);
    chgrp($fullPath, $username);
    chmod($fullPath, 0644);
    
    logger("File written: $fullPath for user $username");
    
    return ['success' => true, 'message' => 'File saved', 'size' => strlen($content)];
}

function fileDelete(array $params): array
{
    $username = $params['username'] ?? '';
    $path = $params['path'] ?? '';
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    $fullPath = validateUserPath($username, $path);
    if ($fullPath === null) {
        return ['success' => false, 'error' => 'Invalid path'];
    }
    
    // Don't allow deleting the home directory itself or critical folders
    $homeDir = "/home/$username";
    $protected = [$homeDir, "$homeDir/domains", "$homeDir/logs", "$homeDir/ssl", "$homeDir/backups", "$homeDir/tmp"];
    if (in_array($fullPath, $protected)) {
        return ['success' => false, 'error' => 'Cannot delete protected directory'];
    }
    
    if (is_dir($fullPath)) {
        exec(sprintf('rm -rf %s 2>&1', escapeshellarg($fullPath)), $output, $exitCode);
    } else {
        $exitCode = @unlink($fullPath) ? 0 : 1;
    }
    
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'Cannot delete'];
    }
    
    logger("Deleted: $fullPath for user $username");
    
    return ['success' => true, 'message' => 'Deleted successfully'];
}

function fileMkdir(array $params): array
{
    $username = $params['username'] ?? '';
    $path = $params['path'] ?? '';
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    $fullPath = validateUserPath($username, $path);
    if ($fullPath === null) {
        return ['success' => false, 'error' => 'Invalid path'];
    }
    
    if (file_exists($fullPath)) {
        return ['success' => false, 'error' => 'Path already exists'];
    }
    
    if (!@mkdir($fullPath, 0755, true)) {
        return ['success' => false, 'error' => 'Cannot create directory'];
    }
    
    chown($fullPath, $username);
    chgrp($fullPath, $username);
    
    logger("Directory created: $fullPath for user $username");
    
    return ['success' => true, 'message' => 'Directory created'];
}

function fileRename(array $params): array
{
    $username = $params['username'] ?? '';
    $path = $params['path'] ?? '';
    $newName = $params['new_name'] ?? '';
    
    if (!validateUsername($username) || empty($newName)) {
        return ['success' => false, 'error' => 'Invalid parameters'];
    }
    
    // Validate new name doesn't contain path separators
    if (str_contains($newName, '/') || str_contains($newName, '..')) {
        return ['success' => false, 'error' => 'Invalid new name'];
    }
    
    $fullPath = validateUserPath($username, $path);
    if ($fullPath === null || !file_exists($fullPath)) {
        return ['success' => false, 'error' => 'File not found'];
    }
    
    $newPath = dirname($fullPath) . '/' . $newName;
    
    if (file_exists($newPath)) {
        return ['success' => false, 'error' => 'Target already exists'];
    }
    
    if (!@rename($fullPath, $newPath)) {
        return ['success' => false, 'error' => 'Cannot rename'];
    }
    
    logger("Renamed: $fullPath to $newPath for user $username");
    
    return ['success' => true, 'message' => 'Renamed successfully'];
}

function fileMove(array $params): array
{
    $username = $params['username'] ?? '';
    $path = $params['path'] ?? '';
    $destination = $params['destination'] ?? '';
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    $fullPath = validateUserPath($username, $path);
    $destPath = validateUserPath($username, $destination);
    
    if ($fullPath === null || $destPath === null) {
        return ['success' => false, 'error' => 'Invalid path'];
    }
    
    if (!file_exists($fullPath)) {
        return ['success' => false, 'error' => 'Source not found'];
    }
    
    // If destination is a directory, move into it
    if (is_dir($destPath)) {
        $destPath = $destPath . '/' . basename($fullPath);
    }
    
    if (file_exists($destPath)) {
        return ['success' => false, 'error' => 'Destination already exists'];
    }
    
    if (!@rename($fullPath, $destPath)) {
        return ['success' => false, 'error' => 'Cannot move'];
    }
    
    logger("Moved: $fullPath to $destPath for user $username");
    
    return ['success' => true, 'message' => 'Moved successfully'];
}

function fileCopy(array $params): array
{
    $username = $params['username'] ?? '';
    $path = $params['path'] ?? '';
    $destination = $params['destination'] ?? '';
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    $fullPath = validateUserPath($username, $path);
    $destPath = validateUserPath($username, $destination);
    
    if ($fullPath === null || $destPath === null) {
        return ['success' => false, 'error' => 'Invalid path'];
    }
    
    if (!file_exists($fullPath)) {
        return ['success' => false, 'error' => 'Source not found'];
    }
    
    if (is_dir($destPath)) {
        $destPath = $destPath . '/' . basename($fullPath);
    }
    
    if (file_exists($destPath)) {
        return ['success' => false, 'error' => 'Destination already exists'];
    }
    
    if (is_dir($fullPath)) {
        exec(sprintf('cp -r %s %s 2>&1', escapeshellarg($fullPath), escapeshellarg($destPath)), $output, $exitCode);
        if ($exitCode !== 0) {
            return ['success' => false, 'error' => 'Cannot copy directory'];
        }
        exec(sprintf('chown -R %s:%s %s', escapeshellarg($username), escapeshellarg($username), escapeshellarg($destPath)));
    } else {
        if (!@copy($fullPath, $destPath)) {
            return ['success' => false, 'error' => 'Cannot copy file'];
        }
        chown($destPath, $username);
        chgrp($destPath, $username);
    }
    
    logger("Copied: $fullPath to $destPath for user $username");
    
    return ['success' => true, 'message' => 'Copied successfully'];
}

function fileUpload(array $params): array
{
    $username = $params['username'] ?? '';
    $path = $params['path'] ?? '';
    $filename = $params['filename'] ?? '';
    $content = $params['content'] ?? '';
    
    if (!validateUsername($username) || empty($filename)) {
        return ['success' => false, 'error' => 'Invalid parameters'];
    }
    
    // Sanitize filename
    $filename = basename($filename);
    $filename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $filename);
    
    if (empty($filename)) {
        return ['success' => false, 'error' => 'Invalid filename'];
    }
    
    $dirPath = validateUserPath($username, $path);
    if ($dirPath === null || !is_dir($dirPath)) {
        return ['success' => false, 'error' => 'Invalid directory'];
    }
    
    $fullPath = "$dirPath/$filename";
    
    // Decode base64 content
    $decoded = base64_decode($content, true);
    if ($decoded === false) {
        return ['success' => false, 'error' => 'Invalid file content'];
    }
    
    // Limit upload size (50MB)
    if (strlen($decoded) > 50 * 1024 * 1024) {
        return ['success' => false, 'error' => 'File too large'];
    }
    
    if (@file_put_contents($fullPath, $decoded) === false) {
        return ['success' => false, 'error' => 'Cannot save file'];
    }
    
    chown($fullPath, $username);
    chgrp($fullPath, $username);
    chmod($fullPath, 0644);
    
    logger("Uploaded: $fullPath for user $username (" . strlen($decoded) . " bytes)");

    return ['success' => true, 'message' => 'File uploaded', 'path' => ltrim(str_replace("/home/$username", '', $fullPath), '/')];
}

/**
 * Upload large files by moving from temp location.
 * This avoids JSON encoding issues with large binary content.
 */
function fileUploadTemp(array $params): array
{
    $username = $params['username'] ?? '';
    $path = $params['path'] ?? '';
    $filename = $params['filename'] ?? '';
    $tempPath = $params['temp_path'] ?? '';

    if (!validateUsername($username) || empty($filename) || empty($tempPath)) {
        return ['success' => false, 'error' => 'Invalid parameters'];
    }

    // Validate temp path is in allowed temp directory
    $allowedTempDir = '/tmp/jabali-uploads/';
    $realTempPath = realpath($tempPath);
    $allowedPrefix = rtrim($allowedTempDir, '/') . '/';
    if ($realTempPath === false || !str_starts_with($realTempPath, $allowedPrefix)) {
        logger("Invalid temp path: $tempPath (real: $realTempPath)", 'ERROR');
        return ['success' => false, 'error' => 'Invalid temp file path'];
    }

    if (!file_exists($realTempPath)) {
        return ['success' => false, 'error' => 'Temp file not found'];
    }

    // Sanitize filename
    $filename = basename($filename);
    $filename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $filename);

    if (empty($filename)) {
        @unlink($realTempPath);
        return ['success' => false, 'error' => 'Invalid filename'];
    }

    $dirPath = validateUserPath($username, $path);
    if ($dirPath === null || !is_dir($dirPath)) {
        @unlink($realTempPath);
        return ['success' => false, 'error' => 'Invalid directory'];
    }

    $fullPath = "$dirPath/$filename";

    // Get file size for logging
    $fileSize = filesize($realTempPath);

    // Limit upload size (500MB for temp uploads)
    if ($fileSize > 500 * 1024 * 1024) {
        @unlink($realTempPath);
        return ['success' => false, 'error' => 'File too large (max 500MB)'];
    }

    // Move temp file to destination
    if (!@rename($realTempPath, $fullPath)) {
        // If rename fails (cross-device), try copy+delete
        if (!@copy($realTempPath, $fullPath)) {
            @unlink($realTempPath);
            return ['success' => false, 'error' => 'Cannot move file to destination'];
        }
        @unlink($realTempPath);
    }

    chown($fullPath, $username);
    chgrp($fullPath, $username);
    chmod($fullPath, 0644);

    logger("Uploaded (temp): $fullPath for user $username ($fileSize bytes)");

    return ['success' => true, 'message' => 'File uploaded', 'path' => ltrim(str_replace("/home/$username", '', $fullPath), '/')];
}

function fileDownload(array $params): array
{
    return fileRead($params); // Same as read, just different context
}

function fileExists(array $params): array
{
    $username = $params['username'] ?? '';
    $path = $params['path'] ?? '';
    
    if (!validateUsername($username)) {
        return ['success' => true, 'exists' => false];
    }
    
    $fullPath = validateUserPath($username, $path);
    
    return ['success' => true, 'exists' => $fullPath !== null && file_exists($fullPath)];
}

function fileInfo(array $params): array
{
    $username = $params['username'] ?? '';
    $path = $params['path'] ?? '';
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    $fullPath = validateUserPath($username, $path);
    if ($fullPath === null || !file_exists($fullPath)) {
        return ['success' => false, 'error' => 'File not found'];
    }
    
    $stat = stat($fullPath);
    
    return [
        'success' => true,
        'info' => [
            'name' => basename($fullPath),
            'path' => $path,
            'is_dir' => is_dir($fullPath),
            'is_file' => is_file($fullPath),
            'size' => $stat['size'],
            'modified' => $stat['mtime'],
            'permissions' => substr(sprintf('%o', $stat['mode']), -4),
            'owner' => posix_getpwuid($stat['uid'])['name'] ?? $stat['uid'],
            'group' => posix_getgrgid($stat['gid'])['name'] ?? $stat['gid'],
        ],
    ];
}

function fileExtract(array $params): array
{
    $username = $params['username'] ?? '';
    $path = $params['path'] ?? '';
    
    if (!validateUsername($username) || isProtectedUser($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    $fullPath = validateUserPath($username, $path);
    if ($fullPath === null) {
        return ['success' => false, 'error' => 'Invalid path'];
    }
    
    if (!file_exists($fullPath)) {
        return ['success' => false, 'error' => 'File not found'];
    }
    
    $ext = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION));
    $filename = basename($fullPath);
    $destDir = dirname($fullPath);
    
    $output = [];
    $returnCode = 0;
    
    // Handle .tar.gz, .tar.bz2, .tar.xz
    if (preg_match('/\.tar\.(gz|bz2|xz)$/i', $filename)) {
        $flag = match(strtolower(pathinfo($filename, PATHINFO_EXTENSION))) {
            'gz' => 'z',
            'bz2' => 'j',
            'xz' => 'J',
            default => ''
        };
        exec("cd " . escapeshellarg($destDir) . " && tar -x{$flag}f " . escapeshellarg($fullPath) . " 2>&1", $output, $returnCode);
    } else {
        switch ($ext) {
            case 'zip':
                exec("cd " . escapeshellarg($destDir) . " && unzip -o " . escapeshellarg($fullPath) . " 2>&1", $output, $returnCode);
                break;
            case 'gz':
                exec("cd " . escapeshellarg($destDir) . " && pigz -dk " . escapeshellarg($fullPath) . " 2>&1", $output, $returnCode);
                break;
            case 'tgz':
                exec("cd " . escapeshellarg($destDir) . " && tar -I pigz -xf " . escapeshellarg($fullPath) . " 2>&1", $output, $returnCode);
                break;
            case 'tar':
                exec("cd " . escapeshellarg($destDir) . " && tar -xf " . escapeshellarg($fullPath) . " 2>&1", $output, $returnCode);
                break;
            case 'bz2':
                exec("cd " . escapeshellarg($destDir) . " && bunzip2 -k " . escapeshellarg($fullPath) . " 2>&1", $output, $returnCode);
                break;
            case 'xz':
                exec("cd " . escapeshellarg($destDir) . " && unxz -k " . escapeshellarg($fullPath) . " 2>&1", $output, $returnCode);
                break;
            case 'rar':
                exec("cd " . escapeshellarg($destDir) . " && unrar x -o+ " . escapeshellarg($fullPath) . " 2>&1", $output, $returnCode);
                break;
            case '7z':
                exec("cd " . escapeshellarg($destDir) . " && 7z x -y " . escapeshellarg($fullPath) . " 2>&1", $output, $returnCode);
                break;
            default:
                return ['success' => false, 'error' => 'Unsupported archive format'];
        }
    }

    if ($returnCode !== 0) {
        return ['success' => false, 'error' => 'Extract failed: ' . implode("\n", $output)];
    }

    // Fix ownership of extracted files
    exec("chown -R " . escapeshellarg($username) . ":" . escapeshellarg($username) . " " . escapeshellarg($destDir));

    logger("Extracted: $fullPath for user $username");

    return ['success' => true, 'message' => 'Archive extracted successfully'];
}

function fileChmod(array $params): array
{
    $username = $params['username'] ?? '';
    $path = $params['path'] ?? '';
    $mode = $params['mode'] ?? '';

    if (!validateUsername($username) || isProtectedUser($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $fullPath = validateUserPath($username, $path);
    if ($fullPath === null) {
        return ['success' => false, 'error' => 'Invalid path'];
    }

    if (!file_exists($fullPath)) {
        return ['success' => false, 'error' => 'File not found'];
    }

    // Validate mode - must be octal string like "755" or "0755"
    $mode = ltrim($mode, '0');
    if (!preg_match('/^[0-7]{3,4}$/', $mode)) {
        return ['success' => false, 'error' => 'Invalid permission mode'];
    }

    $octalMode = octdec($mode);

    // Safety: Don't allow setuid/setgid bits for regular users
    $octalMode = $octalMode & 0777;

    if (!@chmod($fullPath, $octalMode)) {
        return ['success' => false, 'error' => 'Failed to change permissions'];
    }

    logger("Chmod: $fullPath to $mode for user $username");

    return ['success' => true, 'message' => 'Permissions changed successfully', 'mode' => sprintf('%o', $octalMode)];
}

function fileChown(array $params): array
{
    $path = $params['path'] ?? '';
    $owner = $params['owner'] ?? '';
    $group = $params['group'] ?? '';

    if (empty($path)) {
        return ['success' => false, 'error' => 'Path is required'];
    }

    if (empty($owner) && empty($group)) {
        return ['success' => false, 'error' => 'Owner or group is required'];
    }

    // Security: Only allow chown in specific directories
    $allowedPrefixes = [
        '/var/backups/jabali/',
        '/home/',
    ];

    $realPath = realpath($path);
    if ($realPath === false) {
        return ['success' => false, 'error' => 'Path does not exist'];
    }

    $allowed = false;
    foreach ($allowedPrefixes as $prefix) {
        if (strpos($realPath, $prefix) === 0) {
            $allowed = true;
            break;
        }
    }

    if (!$allowed) {
        logger("Security: Blocked chown attempt on $realPath", 'WARNING');
        return ['success' => false, 'error' => 'Path not in allowed directories'];
    }

    // Change owner
    if (!empty($owner)) {
        if (!@chown($realPath, $owner)) {
            return ['success' => false, 'error' => 'Failed to change owner'];
        }
    }

    // Change group
    if (!empty($group)) {
        if (!@chgrp($realPath, $group)) {
            return ['success' => false, 'error' => 'Failed to change group'];
        }
    }

    logger("Chown: $realPath to $owner:$group");

    return ['success' => true, 'message' => 'Ownership changed successfully'];
}

function fileTrash(array $params): array
{
    $username = $params['username'] ?? '';
    $path = $params['path'] ?? '';

    if (!validateUsername($username) || isProtectedUser($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $fullPath = validateUserPath($username, $path);
    if ($fullPath === null) {
        return ['success' => false, 'error' => 'Invalid path'];
    }

    if (!file_exists($fullPath)) {
        return ['success' => false, 'error' => 'File not found'];
    }

    // Create trash directory if it doesn't exist
    $trashDir = "/home/$username/.trash";
    if (!is_dir($trashDir)) {
        mkdir($trashDir, 0755, true);
        chown($trashDir, $username);
        chgrp($trashDir, $username);
    }

    // Generate unique name to avoid conflicts
    $basename = basename($fullPath);
    $timestamp = date('Y-m-d_His');
    $trashName = "{$basename}.{$timestamp}";
    $trashPath = "$trashDir/$trashName";

    // Store original path in metadata file for potential restore
    $metaFile = "$trashPath.meta";

    if (!@rename($fullPath, $trashPath)) {
        return ['success' => false, 'error' => 'Failed to move to trash'];
    }

    // Save metadata
    file_put_contents($metaFile, json_encode([
        'original_path' => str_replace("/home/$username/", '', $path),
        'trashed_at' => time(),
        'name' => $basename,
    ]));
    chown($metaFile, $username);
    chgrp($metaFile, $username);

    logger("Trashed: $fullPath to $trashPath for user $username");

    return ['success' => true, 'message' => 'Moved to trash', 'trash_path' => ".trash/$trashName"];
}

function fileRestore(array $params): array
{
    $username = $params['username'] ?? '';
    $trashName = $params['trash_name'] ?? '';

    if (!validateUsername($username) || isProtectedUser($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $trashDir = "/home/$username/.trash";
    $trashPath = "$trashDir/$trashName";
    $metaFile = "$trashPath.meta";

    if (!file_exists($trashPath)) {
        return ['success' => false, 'error' => 'Item not found in trash'];
    }

    // Try to get original path from metadata
    $originalPath = null;
    if (file_exists($metaFile)) {
        $meta = json_decode(file_get_contents($metaFile), true);
        $originalPath = $meta['original_path'] ?? null;
    }

    if (!$originalPath) {
        // Fall back to home directory with original name (strip timestamp)
        $basename = preg_replace('/\.\d{4}-\d{2}-\d{2}_\d{6}$/', '', $trashName);
        $originalPath = $basename;
    }

    $fullDestPath = "/home/$username/$originalPath";

    // If destination exists, add suffix
    if (file_exists($fullDestPath)) {
        $pathInfo = pathinfo($fullDestPath);
        $counter = 1;
        do {
            $newName = $pathInfo['filename'] . "_restored_$counter";
            if (isset($pathInfo['extension'])) {
                $newName .= '.' . $pathInfo['extension'];
            }
            $fullDestPath = $pathInfo['dirname'] . '/' . $newName;
            $counter++;
        } while (file_exists($fullDestPath));
    }

    // Ensure destination directory exists
    $destDir = dirname($fullDestPath);
    if (!is_dir($destDir)) {
        mkdir($destDir, 0755, true);
        chown($destDir, $username);
        chgrp($destDir, $username);
    }

    if (!@rename($trashPath, $fullDestPath)) {
        return ['success' => false, 'error' => 'Failed to restore from trash'];
    }

    // Remove metadata file
    @unlink($metaFile);

    logger("Restored: $trashPath to $fullDestPath for user $username");

    return ['success' => true, 'message' => 'Restored from trash', 'restored_path' => str_replace("/home/$username/", '', $fullDestPath)];
}

function fileEmptyTrash(array $params): array
{
    $username = $params['username'] ?? '';

    if (!validateUsername($username) || isProtectedUser($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $trashDir = "/home/$username/.trash";

    if (!is_dir($trashDir)) {
        return ['success' => true, 'message' => 'Trash is already empty', 'deleted' => 0];
    }

    $deleted = 0;
    $items = scandir($trashDir);
    foreach ($items as $item) {
        if ($item === '.' || $item === '..') continue;
        $itemPath = "$trashDir/$item";
        if (is_dir($itemPath)) {
            exec("rm -rf " . escapeshellarg($itemPath));
        } else {
            @unlink($itemPath);
        }
        $deleted++;
    }

    logger("Emptied trash for user $username ($deleted items)");

    return ['success' => true, 'message' => 'Trash emptied', 'deleted' => $deleted];
}

function fileListTrash(array $params): array
{
    $username = $params['username'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $trashDir = "/home/$username/.trash";

    if (!is_dir($trashDir)) {
        return ['success' => true, 'items' => []];
    }

    $items = [];
    $files = scandir($trashDir);
    foreach ($files as $file) {
        if ($file === '.' || $file === '..' || str_ends_with($file, '.meta')) continue;

        $fullPath = "$trashDir/$file";
        $stat = @stat($fullPath);
        if (!$stat) continue;

        $meta = null;
        $metaFile = "$fullPath.meta";
        if (file_exists($metaFile)) {
            $meta = json_decode(file_get_contents($metaFile), true);
        }

        $items[] = [
            'trash_name' => $file,
            'name' => $meta['name'] ?? preg_replace('/\.\d{4}-\d{2}-\d{2}_\d{6}$/', '', $file),
            'original_path' => $meta['original_path'] ?? null,
            'trashed_at' => $meta['trashed_at'] ?? $stat['mtime'],
            'is_dir' => is_dir($fullPath),
            'size' => is_dir($fullPath) ? 0 : $stat['size'],
        ];
    }

    // Sort by trashed_at descending (most recent first)
    usort($items, fn($a, $b) => $b['trashed_at'] <=> $a['trashed_at']);

    return ['success' => true, 'items' => $items];
}

function mysqlCreateMasterUser(array $params): array
{
    $username = $params["username"] ?? "";
    
    if (!validateUsername($username)) {
        return ["success" => false, "error" => "Invalid username"];
    }
    
    $conn = getMysqlConnection();
    if (!$conn) {
        return ["success" => false, "error" => "Cannot connect to MySQL"];
    }
    
    // Create master user with pattern like: user1_admin
    $masterUser = $username . "_admin";
    $password = bin2hex(random_bytes(16));
    
    // Drop if exists
    $conn->query("DROP USER IF EXISTS '{$masterUser}'@'localhost'");
    
    // Create user
    $escapedPassword = $conn->real_escape_string($password);
    if (!$conn->query("CREATE USER '{$masterUser}'@'localhost' IDENTIFIED BY '{$escapedPassword}'")) {
        $conn->close();
        return ["success" => false, "error" => "Failed to create master user: " . $conn->error];
    }
    
    // Grant privileges to all user's databases using wildcard
    $pattern = $username . "\\_%";
    if (!$conn->query("GRANT ALL PRIVILEGES ON `{$pattern}`.* TO '{$masterUser}'@'localhost'")) {
        $conn->close();
        return ["success" => false, "error" => "Failed to grant privileges: " . $conn->error];
    }
    
    $conn->query("FLUSH PRIVILEGES");
    $conn->close();
    
    return [
        "success" => true,
        "master_user" => $masterUser,
        "password" => $password,
        "message" => "Master MySQL user created with access to all {$username}_* databases"
    ];
}

function mysqlImportDatabase(array $params): array
{
    $username = $params["username"] ?? "";
    $database = $params["database"] ?? "";
    $sqlFile = $params["sql_file"] ?? "";

    if (!validateUsername($username)) {
        return ["success" => false, "error" => "Invalid username"];
    }

    if (!file_exists($sqlFile) || !is_readable($sqlFile)) {
        return ["success" => false, "error" => "SQL file not found or not readable"];
    }

    // Ensure database belongs to user
    $prefix = $username . "_";
    if (strpos($database, $prefix) !== 0) {
        return ["success" => false, "error" => "Database does not belong to user"];
    }

    // Get MySQL root credentials
    $mysqlRoot = getMysqlRootCredentials();
    if (!$mysqlRoot) {
        return ["success" => false, "error" => "Cannot get MySQL credentials"];
    }

    // Detect file type by extension
    $extension = strtolower(pathinfo($sqlFile, PATHINFO_EXTENSION));
    $filename = strtolower(basename($sqlFile));

    // Import using mysql command with socket authentication
    // Redirect stderr to a temp file to avoid mixing with stdin
    $errFile = tempnam('/tmp', 'mysql_err_');

    if ($extension === 'gz' || str_ends_with($filename, '.sql.gz')) {
        // Gzipped SQL file - decompress and pipe to mysql
        $cmd = sprintf(
            'pigz -dc %s | mysql --defaults-file=/etc/mysql/debian.cnf %s 2>%s',
            escapeshellarg($sqlFile),
            escapeshellarg($database),
            escapeshellarg($errFile)
        );
    } elseif ($extension === 'zip') {
        // ZIP file - extract first file and pipe to mysql
        $cmd = sprintf(
            'unzip -p %s | mysql --defaults-file=/etc/mysql/debian.cnf %s 2>%s',
            escapeshellarg($sqlFile),
            escapeshellarg($database),
            escapeshellarg($errFile)
        );
    } else {
        // Plain SQL file
        $cmd = sprintf(
            'mysql --defaults-file=/etc/mysql/debian.cnf %s < %s 2>%s',
            escapeshellarg($database),
            escapeshellarg($sqlFile),
            escapeshellarg($errFile)
        );
    }

    exec($cmd, $output, $returnCode);

    // Read stderr if command failed
    if ($returnCode !== 0) {
        $stderr = file_get_contents($errFile);
        @unlink($errFile);
        return ["success" => false, "error" => "Import failed: " . trim($stderr)];
    }
    @unlink($errFile);

    logger("Database imported: $database from $sqlFile for user $username");

    return ["success" => true, "message" => "Database imported successfully"];
}

function mysqlExportDatabase(array $params): array
{
    $username = $params["username"] ?? "";
    $database = $params["database"] ?? "";
    $outputFile = $params["output_file"] ?? "";
    $compress = $params["compress"] ?? "gz"; // "gz", "zip", or "none"

    if (!validateUsername($username)) {
        return ["success" => false, "error" => "Invalid username"];
    }

    // Ensure database belongs to user
    $prefix = $username . "_";
    if (strpos($database, $prefix) !== 0) {
        return ["success" => false, "error" => "Database does not belong to user"];
    }

    // Get MySQL root credentials
    $mysqlRoot = getMysqlRootCredentials();
    if (!$mysqlRoot) {
        return ["success" => false, "error" => "Cannot get MySQL credentials"];
    }

    // Ensure output directory exists
    $outputDir = dirname($outputFile);
    if (!is_dir($outputDir)) {
        mkdir($outputDir, 0755, true);
    }

    // Export using mysqldump with socket authentication
    $errFile = tempnam('/tmp', 'mysqldump_err_');

    if ($compress === "gz") {
        // Pipe through pigz (parallel gzip) for compression
        $cmd = sprintf(
            'mysqldump --defaults-file=/etc/mysql/debian.cnf --single-transaction --routines --triggers %s 2>%s | pigz > %s',
            escapeshellarg($database),
            escapeshellarg($errFile),
            escapeshellarg($outputFile)
        );
    } elseif ($compress === "zip") {
        // Export to temp SQL file, then zip
        $tempSql = tempnam('/tmp', 'mysqldump_') . '.sql';
        $cmd = sprintf(
            'mysqldump --defaults-file=/etc/mysql/debian.cnf --single-transaction --routines --triggers %s > %s 2>%s && zip -j %s %s && rm -f %s',
            escapeshellarg($database),
            escapeshellarg($tempSql),
            escapeshellarg($errFile),
            escapeshellarg($outputFile),
            escapeshellarg($tempSql),
            escapeshellarg($tempSql)
        );
    } else {
        // No compression
        $cmd = sprintf(
            'mysqldump --defaults-file=/etc/mysql/debian.cnf --single-transaction --routines --triggers %s > %s 2>%s',
            escapeshellarg($database),
            escapeshellarg($outputFile),
            escapeshellarg($errFile)
        );
    }

    exec($cmd, $output, $returnCode);

    if ($returnCode !== 0) {
        $stderr = file_get_contents($errFile);
        @unlink($errFile);
        return ["success" => false, "error" => "Export failed: " . trim($stderr)];
    }
    @unlink($errFile);

    // Set ownership to user
    chown($outputFile, $username);
    chgrp($outputFile, $username);

    logger("Database exported: $database to $outputFile (compress: $compress) for user $username");

    return ["success" => true, "output_file" => $outputFile, "message" => "Database exported successfully"];
}

// ============ SERVICE MANAGEMENT ============

function shouldReloadService(string $service): bool
{
    $normalized = $service;
    if (str_ends_with($normalized, '.service')) {
        $normalized = substr($normalized, 0, -strlen('.service'));
    }

    if ($normalized === 'nginx') {
        return true;
    }

    return preg_match('/^php(\d+\.\d+)?-fpm$/', $normalized) === 1;
}

function restartService(array $params): array
{
    global $allowedServices;
    $service = $params['service'] ?? '';
    
    if (!in_array($service, $allowedServices, true)) {
        return ['success' => false, 'error' => "Service not allowed: $service"];
    }
    
    $action = shouldReloadService($service) ? 'reload' : 'restart';
    exec("systemctl {$action} " . escapeshellarg($service) . " 2>&1", $output, $exitCode);

    return $exitCode === 0
        ? ['success' => true, 'message' => "Service $service " . ($action === 'reload' ? 'reloaded' : 'restarted')]
        : ['success' => false, 'error' => 'Failed to ' . $action . ' service'];
}

function reloadService(array $params): array
{
    global $allowedServices;
    $service = $params['service'] ?? '';
    
    if (!in_array($service, $allowedServices, true)) {
        return ['success' => false, 'error' => "Service not allowed: $service"];
    }
    
    exec("systemctl reload " . escapeshellarg($service) . " 2>&1", $output, $exitCode);
    
    return $exitCode === 0
        ? ['success' => true, 'message' => "Service $service reloaded"]
        : ['success' => false, 'error' => 'Failed to reload service'];
}

function getServiceStatus(array $params): array
{
    global $allowedServices;
    $service = $params['service'] ?? '';
    
    if (!in_array($service, $allowedServices, true)) {
        return ['success' => false, 'error' => "Service not allowed: $service"];
    }
    
    exec("systemctl is-active " . escapeshellarg($service) . " 2>&1", $output, $exitCode);
    $status = trim($output[0] ?? 'unknown');
    
    return ['success' => true, 'service' => $service, 'status' => $status, 'running' => $status === 'active'];
}

/**
 * Enable gzip compression in nginx for all text-based content
 * This provides ~400-500KB savings on typical WordPress sites
 */
function nginxEnableCompression(array $params): array
{
    $nginxConf = '/etc/nginx/nginx.conf';

    if (!file_exists($nginxConf)) {
        return ['success' => false, 'error' => 'nginx.conf not found'];
    }

    $content = file_get_contents($nginxConf);
    $modified = false;

    // Check if gzip_types is already configured (not commented)
    if (preg_match('/^\s*gzip_types\s/m', $content)) {
        return ['success' => true, 'message' => 'Compression already enabled', 'already_enabled' => true];
    }

    // Uncomment and configure gzip settings (patterns account for tab indentation)
    $replacements = [
        '/^[ \t]*# gzip_vary on;/m' => "\tgzip_vary on;",
        '/^[ \t]*# gzip_proxied any;/m' => "\tgzip_proxied any;",
        '/^[ \t]*# gzip_comp_level 6;/m' => "\tgzip_comp_level 6;",
        '/^[ \t]*# gzip_buffers 16 8k;/m' => "\tgzip_buffers 16 8k;",
        '/^[ \t]*# gzip_http_version 1.1;/m' => "\tgzip_http_version 1.1;",
        '/^[ \t]*# gzip_types .*/m' => "\tgzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml application/xml+rss application/x-javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype font/woff font/woff2 image/svg+xml image/x-icon;",
    ];

    foreach ($replacements as $pattern => $replacement) {
        $newContent = preg_replace($pattern, $replacement, $content);
        if ($newContent !== $content) {
            $content = $newContent;
            $modified = true;
        }
    }

    // Add gzip_min_length if gzip_types was added and gzip_min_length doesn't exist
    if ($modified && strpos($content, 'gzip_min_length') === false) {
        $content = preg_replace(
            '/(gzip_types[^;]+;)/',
            "$1\n\tgzip_min_length 256;",
            $content
        );
    }

    // Write back if modified
    if ($modified) {
        // Test configuration before applying
        file_put_contents($nginxConf, $content);
        exec('nginx -t 2>&1', $testOutput, $testCode);

        if ($testCode !== 0) {
            // Restore original
            exec("git -C /etc/nginx checkout nginx.conf 2>/dev/null");
            return ['success' => false, 'error' => 'nginx configuration test failed: ' . implode("\n", $testOutput)];
        }

        // Reload nginx
        exec('systemctl reload nginx 2>&1', $output, $exitCode);

        if ($exitCode !== 0) {
            return ['success' => false, 'error' => 'Failed to reload nginx'];
        }

        return ['success' => true, 'message' => 'Compression enabled successfully'];
    }

    return ['success' => true, 'message' => 'Compression settings already optimal', 'already_enabled' => true];
}

/**
 * Get current nginx compression status
 */
function nginxGetCompressionStatus(array $params): array
{
    $nginxConf = '/etc/nginx/nginx.conf';

    if (!file_exists($nginxConf)) {
        return ['success' => false, 'error' => 'nginx.conf not found'];
    }

    $content = file_get_contents($nginxConf);

    $settings = [
        'gzip' => (bool)preg_match('/^\s*gzip\s+on\s*;/m', $content),
        'gzip_vary' => (bool)preg_match('/^\s*gzip_vary\s+on\s*;/m', $content),
        'gzip_proxied' => (bool)preg_match('/^\s*gzip_proxied\s/m', $content),
        'gzip_comp_level' => null,
        'gzip_types' => (bool)preg_match('/^\s*gzip_types\s/m', $content),
        'gzip_min_length' => null,
    ];

    // Extract compression level
    if (preg_match('/^\s*gzip_comp_level\s+(\d+)\s*;/m', $content, $matches)) {
        $settings['gzip_comp_level'] = (int)$matches[1];
    }

    // Extract min length
    if (preg_match('/^\s*gzip_min_length\s+(\d+)\s*;/m', $content, $matches)) {
        $settings['gzip_min_length'] = (int)$matches[1];
    }

    // Determine if fully optimized
    $optimized = $settings['gzip'] && $settings['gzip_vary'] && $settings['gzip_types'] && $settings['gzip_comp_level'];

    return [
        'success' => true,
        'enabled' => $settings['gzip'],
        'optimized' => $optimized,
        'settings' => $settings,
    ];
}

function ensureJabaliNginxIncludeFiles(): void
{
    if (!is_dir(JABALI_NGINX_INCLUDES)) {
        @mkdir(JABALI_NGINX_INCLUDES, 0755, true);
    }

    ensureWafUnicodeMapFile();
    ensureWafMainConfig();

    $modSecurityAvailable = isModSecurityModuleAvailable();
    $baseConfig = findWafBaseConfig();
    $shouldDisableWaf = $baseConfig === null || !$modSecurityAvailable;

    if (!file_exists(JABALI_WAF_INCLUDE)) {
        $content = "# Managed by Jabali\n";
        if (!$modSecurityAvailable) {
            $content .= "# ModSecurity module not available in nginx.\n";
        } elseif ($shouldDisableWaf) {
            $content .= "modsecurity off;\n";
        }
        file_put_contents(JABALI_WAF_INCLUDE, $content);
    } elseif ($shouldDisableWaf) {
        $current = file_get_contents(JABALI_WAF_INCLUDE);
        if ($current === false || strpos($current, 'modsecurity_rules_file') !== false || strpos($current, 'modsecurity on;') !== false || strpos($current, 'modsecurity off;') !== false) {
            if (!$modSecurityAvailable) {
                file_put_contents(JABALI_WAF_INCLUDE, "# Managed by Jabali\n# ModSecurity module not available in nginx.\n");
            } else {
                file_put_contents(JABALI_WAF_INCLUDE, "# Managed by Jabali\nmodsecurity off;\n");
            }
        }
    }

    if (!file_exists(JABALI_GEO_INCLUDE)) {
        file_put_contents(JABALI_GEO_INCLUDE, "# Managed by Jabali\n");
    }
}

function ensureWafMainConfig(): void
{
    $path = '/etc/nginx/modsec/main.conf';
    $dir = dirname($path);

    if (!is_dir($dir)) {
        @mkdir($dir, 0755, true);
    }

    $needsRewrite = !file_exists($path);
    if (!$needsRewrite) {
        $content = file_get_contents($path);
        if ($content === false || stripos($content, 'IncludeOptional') !== false || stripos($content, 'owasp-crs.load') !== false) {
            $needsRewrite = true;
        }
    }

    if (!$needsRewrite) {
        return;
    }

    $lines = ['Include /etc/modsecurity/modsecurity.conf'];

    if (file_exists('/etc/modsecurity/crs/crs-setup.conf')) {
        $lines[] = 'Include /etc/modsecurity/crs/crs-setup.conf';
    } elseif (file_exists('/usr/share/modsecurity-crs/crs-setup.conf')) {
        $lines[] = 'Include /usr/share/modsecurity-crs/crs-setup.conf';
    }

    if (file_exists('/etc/modsecurity/crs/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf')) {
        $lines[] = 'Include /etc/modsecurity/crs/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf';
    }

    if (is_dir('/usr/share/modsecurity-crs/rules')) {
        $lines[] = 'Include /usr/share/modsecurity-crs/rules/*.conf';
    }

    if (file_exists('/etc/modsecurity/crs/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf')) {
        $lines[] = 'Include /etc/modsecurity/crs/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf';
    }

    file_put_contents($path, implode("\n", $lines) . "\n");
}

function ensureWafUnicodeMapFile(): void
{
    $target = '/etc/modsecurity/unicode.mapping';
    if (file_exists($target)) {
        return;
    }

    $sources = [
        '/usr/share/modsecurity-crs/util/unicode.mapping',
        '/usr/share/modsecurity-crs/unicode.mapping',
        '/usr/share/modsecurity/unicode.mapping',
        '/etc/nginx/unicode.mapping',
        '/usr/share/nginx/docs/modsecurity/unicode.mapping',
    ];

    foreach ($sources as $source) {
        if (!file_exists($source)) {
            continue;
        }

        if (!is_dir('/etc/modsecurity')) {
            @mkdir('/etc/modsecurity', 0755, true);
        }

        if (@copy($source, $target)) {
            break;
        }
    }
}

function ensureNginxServerIncludes(array $includeLines): array
{
    $files = glob('/etc/nginx/sites-enabled/*.conf') ?: [];
    $updated = 0;

    foreach ($files as $file) {
        $content = file_get_contents($file);
        if ($content === false) {
            continue;
        }

        $original = $content;
        foreach ($includeLines as $line) {
            if (strpos($content, $line) !== false) {
                continue;
            }

            $content = preg_replace('/(server_name[^\n]*\n)/', "$1    {$line}\n", $content);
        }

        if ($content !== $original) {
            file_put_contents($file, $content);
            $updated++;
        }
    }

    return [
        'files' => count($files),
        'updated' => $updated,
    ];
}

function nginxTestAndReload(): array
{
    exec('nginx -t 2>&1', $testOutput, $testCode);
    if ($testCode !== 0) {
        return ['success' => false, 'error' => 'nginx configuration test failed: ' . implode("\n", $testOutput)];
    }

    exec('systemctl reload nginx 2>&1', $output, $exitCode);
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'Failed to reload nginx'];
    }

    return ['success' => true];
}

function findWafBaseConfig(): ?string
{
    $paths = [
        '/etc/nginx/modsec/main.conf',
        '/etc/nginx/modsecurity.conf',
        '/etc/modsecurity/modsecurity.conf',
        '/etc/modsecurity/modsecurity.conf-recommended',
    ];

    foreach ($paths as $path) {
        if (file_exists($path) && isWafBaseConfigUsable($path)) {
            return $path;
        }
    }

    return null;
}

function findWafCoreConfig(): ?string
{
    $paths = [
        '/etc/modsecurity/modsecurity.conf',
        '/etc/modsecurity/modsecurity.conf-recommended',
        '/etc/nginx/modsecurity.conf',
    ];

    foreach ($paths as $path) {
        if (file_exists($path) && isWafBaseConfigUsable($path)) {
            return $path;
        }
    }

    return null;
}

function buildWafCrsIncludeLines(): array
{
    $lines = [];

    if (file_exists('/etc/modsecurity/crs/crs-setup.conf')) {
        $lines[] = 'Include /etc/modsecurity/crs/crs-setup.conf';
    } elseif (file_exists('/usr/share/modsecurity-crs/crs-setup.conf')) {
        $lines[] = 'Include /usr/share/modsecurity-crs/crs-setup.conf';
    }

    if (file_exists('/etc/modsecurity/crs/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf')) {
        $lines[] = 'Include /etc/modsecurity/crs/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf';
    }

    if (is_dir('/etc/modsecurity/crs/rules')) {
        $lines[] = 'Include /etc/modsecurity/crs/rules/*.conf';
    } elseif (is_dir('/usr/share/modsecurity-crs/rules')) {
        $lines[] = 'Include /usr/share/modsecurity-crs/rules/*.conf';
    }

    if (file_exists('/etc/modsecurity/crs/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf')) {
        $lines[] = 'Include /etc/modsecurity/crs/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf';
    }

    return $lines;
}

function isModSecurityModuleAvailable(): bool
{
    $output = [];
    exec('nginx -V 2>&1', $output);
    $info = implode("\n", $output);

    if (stripos($info, 'modsecurity') !== false) {
        return true;
    }

    foreach (glob('/etc/nginx/modules-enabled/*.conf') ?: [] as $file) {
        $content = file_get_contents($file);
        if ($content !== false && stripos($content, 'modsecurity') !== false) {
            return true;
        }
    }

    return false;
}

function isWafBaseConfigUsable(string $path): bool
{
    if (!is_readable($path)) {
        return false;
    }

    $content = file_get_contents($path);
    if ($content === false) {
        return false;
    }

    if (stripos($content, 'IncludeOptional') !== false) {
        return false;
    }

    if (preg_match_all('/^\s*Include\s+("?)([^"\s]+)\1/m', $content, $matches)) {
        foreach ($matches[2] as $includePath) {
            if ($includePath === '/etc/modsecurity/modsecurity.conf' && !file_exists($includePath)) {
                return false;
            }
        }
    }

    if (preg_match_all('/^\s*SecUnicodeMapFile\s+([^\s]+)\s*/m', $content, $matches)) {
        $baseDir = dirname($path);
        foreach ($matches[1] as $mapPath) {
            $candidates = [];
            if (str_starts_with($mapPath, '/')) {
                $candidates[] = $mapPath;
            } else {
                $candidates[] = $baseDir . '/' . $mapPath;
                $candidates[] = '/etc/modsecurity/' . $mapPath;
            }

            $found = false;
            foreach ($candidates as $candidate) {
                if (file_exists($candidate)) {
                    $found = true;
                    break;
                }
            }

            if (!$found) {
                return false;
            }
        }
    }

    return true;
}

function wafApplySettings(array $params): array
{
    $enabled = !empty($params['enabled']);
    $paranoia = (int) ($params['paranoia'] ?? 1);
    $paranoia = max(1, min(4, $paranoia));
    $auditLog = !empty($params['audit_log']);
    $whitelistRules = $params['whitelist_rules'] ?? [];

    ensureJabaliNginxIncludeFiles();

    $prevInclude = file_exists(JABALI_WAF_INCLUDE) ? file_get_contents(JABALI_WAF_INCLUDE) : null;
    $prevRules = file_exists(JABALI_WAF_RULES) ? file_get_contents(JABALI_WAF_RULES) : null;
    $modSecurityAvailable = isModSecurityModuleAvailable();

    if ($enabled) {
        if (!$modSecurityAvailable) {
            file_put_contents(JABALI_WAF_INCLUDE, "# Managed by Jabali\n# ModSecurity module not available in nginx.\n");
            return ['success' => false, 'error' => 'ModSecurity module not available in nginx'];
        }

        ensureWafUnicodeMapFile();
        $coreConfig = findWafCoreConfig();
        if (!$coreConfig) {
            file_put_contents(JABALI_WAF_INCLUDE, "# Managed by Jabali\nmodsecurity off;\n");
            return ['success' => false, 'error' => 'ModSecurity base configuration not found'];
        }

        $rules = [
            '# Managed by Jabali',
            'Include "' . $coreConfig . '"',
            'SecRuleEngine On',
            'SecAuditEngine ' . ($auditLog ? 'On' : 'Off'),
            'SecAuditLog /var/log/nginx/modsec_audit.log',
            'SecAction "id:900000,phase:1,t:none,pass,setvar:tx.paranoia_level=' . $paranoia . '"',
            'SecAction "id:900110,phase:1,t:none,pass,setvar:tx.executing_paranoia_level=' . $paranoia . '"',
        ];

        $whitelistLines = buildWafWhitelistRules($whitelistRules);
        if (!empty($whitelistLines)) {
            $rules = array_merge($rules, $whitelistLines);
        }

        $crsIncludes = buildWafCrsIncludeLines();
        if (!empty($crsIncludes)) {
            $rules = array_merge($rules, $crsIncludes);
        }

        file_put_contents(JABALI_WAF_RULES, implode("\n", $rules) . "\n");

        $include = [
            '# Managed by Jabali',
            'modsecurity on;',
            'modsecurity_rules_file ' . JABALI_WAF_RULES . ';',
        ];

        file_put_contents(JABALI_WAF_INCLUDE, implode("\n", $include) . "\n");
    } else {
        if ($modSecurityAvailable) {
            file_put_contents(JABALI_WAF_INCLUDE, "# Managed by Jabali\nmodsecurity off;\n");
        } else {
            file_put_contents(JABALI_WAF_INCLUDE, "# Managed by Jabali\n# ModSecurity module not available in nginx.\n");
        }
    }

    ensureNginxServerIncludes([
        'include ' . JABALI_WAF_INCLUDE . ';',
    ]);

    $reload = nginxTestAndReload();
    if (!($reload['success'] ?? false)) {
        if ($prevInclude === null) {
            @unlink(JABALI_WAF_INCLUDE);
        } else {
            file_put_contents(JABALI_WAF_INCLUDE, $prevInclude);
        }

        if ($prevRules === null) {
            @unlink(JABALI_WAF_RULES);
        } else {
            file_put_contents(JABALI_WAF_RULES, $prevRules);
        }

        return $reload;
    }

    return ['success' => true, 'enabled' => $enabled, 'paranoia' => $paranoia, 'audit_log' => $auditLog];
}

function buildWafWhitelistRules(array $rules): array
{
    $lines = [];
    $ruleBaseId = 120000;
    $index = 0;

    $matchMap = [
        'ip' => ['REMOTE_ADDR', '@ipMatch'],
        'uri_exact' => ['REQUEST_URI', '@streq'],
        'uri_prefix' => ['REQUEST_URI', '@beginsWith'],
        'host' => ['REQUEST_HEADERS:Host', '@streq'],
    ];

    foreach ($rules as $rule) {
        if (!is_array($rule)) {
            continue;
        }

        $matchType = (string) ($rule['match_type'] ?? '');
        $matchValue = trim((string) ($rule['match_value'] ?? ''));
        $idsRaw = (string) ($rule['rule_ids'] ?? '');

        if ($matchValue === '' || $idsRaw === '') {
            continue;
        }

        if (!isset($matchMap[$matchType])) {
            continue;
        }

        $ids = preg_split('/[,\s]+/', $idsRaw, -1, PREG_SPLIT_NO_EMPTY) ?: [];
        $ids = array_values(array_filter(array_map('trim', $ids), function ($id) {
            return ctype_digit($id);
        }));

        if (empty($ids)) {
            continue;
        }

        [$variable, $operator] = $matchMap[$matchType];
        $ruleId = $ruleBaseId + $index;
        $index++;

        $ctlParts = [];
        foreach ($ids as $id) {
            $ctlParts[] = 'ctl:ruleRemoveById=' . $id;
        }

        $value = str_replace('"', '\\"', $matchValue);
        $lines[] = sprintf(
            'SecRule %s "%s %s" "id:%d,phase:1,pass,nolog,%s"',
            $variable,
            $operator,
            $value,
            $ruleId,
            implode(',', $ctlParts)
        );
    }

    if (!empty($lines)) {
        array_unshift($lines, '# Whitelist rules (managed by Jabali)');
    }

    return $lines;
}

function geoUpdateDatabase(array $params): array
{
    $accountId = trim((string) ($params['account_id'] ?? ''));
    $licenseKey = trim((string) ($params['license_key'] ?? ''));
    $editionIdsRaw = $params['edition_ids'] ?? 'GeoLite2-Country';
    $useExisting = !empty($params['use_existing']);

    $toolError = ensureGeoIpUpdateTool();
    if ($toolError !== null) {
        return ['success' => false, 'error' => $toolError];
    }

    if (!$useExisting && ($accountId === '' || $licenseKey === '')) {
        return ['success' => false, 'error' => 'MaxMind Account ID and License Key are required'];
    }

    $editionIds = [];
    if (is_array($editionIdsRaw)) {
        $editionIds = $editionIdsRaw;
    } else {
        $editionIds = preg_split('/[,\s]+/', (string) $editionIdsRaw, -1, PREG_SPLIT_NO_EMPTY) ?: [];
    }

    $editionIds = array_values(array_filter(array_map('trim', $editionIds)));
    if (empty($editionIds)) {
        $editionIds = ['GeoLite2-Country'];
    }

    $configLines = [
        '# Managed by Jabali',
        'AccountID ' . $accountId,
        'LicenseKey ' . $licenseKey,
        'EditionIDs ' . implode(' ', $editionIds),
        'DatabaseDirectory /usr/share/GeoIP',
    ];
    $config = implode("\n", $configLines) . "\n";

    if (!is_dir('/usr/share/GeoIP')) {
        @mkdir('/usr/share/GeoIP', 0755, true);
    }

    $configPaths = [
        '/etc/GeoIP.conf',
        '/etc/geoipupdate/GeoIP.conf',
    ];

    foreach ($configPaths as $path) {
        $dir = dirname($path);
        if (!is_dir($dir)) {
            @mkdir($dir, 0755, true);
        }

        if (!$useExisting) {
            file_put_contents($path, $config);
            @chmod($path, 0600);
        } elseif (!file_exists($path)) {
            continue;
        }
    }

    exec('geoipupdate -v 2>&1', $output, $code);
    $outputText = trim(implode("\n", $output));
    if ($code !== 0) {
        return [
            'success' => false,
            'error' => $outputText !== '' ? $outputText : 'geoipupdate failed',
        ];
    }

    $paths = [];
    foreach ($editionIds as $edition) {
        $paths[] = '/usr/share/GeoIP/' . $edition . '.mmdb';
        $paths[] = '/usr/local/share/GeoIP/' . $edition . '.mmdb';
    }

    foreach ($paths as $path) {
        if (file_exists($path)) {
            return ['success' => true, 'path' => $path];
        }
    }

    return ['success' => false, 'error' => 'GeoIP database not found after update'];
}

function geoUploadDatabase(array $params): array
{
    $edition = trim((string) ($params['edition'] ?? 'GeoLite2-Country'));
    $content = (string) ($params['content'] ?? '');

    if ($content === '') {
        return ['success' => false, 'error' => 'No database content provided'];
    }

    if (!preg_match('/^[A-Za-z0-9._-]+$/', $edition)) {
        return ['success' => false, 'error' => 'Invalid edition name'];
    }

    $decoded = base64_decode($content, true);
    if ($decoded === false) {
        return ['success' => false, 'error' => 'Invalid database content'];
    }

    $targetDir = '/usr/share/GeoIP';
    if (!is_dir($targetDir)) {
        @mkdir($targetDir, 0755, true);
    }

    $target = $targetDir . '/' . $edition . '.mmdb';
    if (file_put_contents($target, $decoded) === false) {
        return ['success' => false, 'error' => 'Failed to write GeoIP database'];
    }

    @chmod($target, 0644);

    return ['success' => true, 'path' => $target];
}

function ensureGeoIpUpdateTool(): ?string
{
    if (toolExists('geoipupdate')) {
        return null;
    }

    $error = installGeoIpUpdateBinary();
    if ($error !== null) {
        return $error;
    }

    if (!toolExists('geoipupdate')) {
        return 'geoipupdate is not installed';
    }

    return null;
}

function installGeoIpUpdateBinary(): ?string
{
    $arch = php_uname('m');
    $archMap = [
        'x86_64' => 'amd64',
        'amd64' => 'amd64',
        'aarch64' => 'arm64',
        'arm64' => 'arm64',
    ];
    $archToken = $archMap[$arch] ?? $arch;

    $apiUrl = 'https://api.github.com/repos/maxmind/geoipupdate/releases/latest';
    $metadata = @shell_exec('curl -fsSL ' . escapeshellarg($apiUrl) . ' 2>/dev/null');
    if (!$metadata) {
        $metadata = @shell_exec('wget -qO- ' . escapeshellarg($apiUrl) . ' 2>/dev/null');
    }

    if (!$metadata) {
        return 'Failed to download geoipupdate release metadata';
    }

    $data = json_decode($metadata, true);
    if (!is_array($data)) {
        return 'Invalid geoipupdate release metadata';
    }

    $downloadUrl = null;
    foreach (($data['assets'] ?? []) as $asset) {
        $name = strtolower((string) ($asset['name'] ?? ''));
        $url = (string) ($asset['browser_download_url'] ?? '');
        if ($name === '' || $url === '') {
            continue;
        }
        if (strpos($name, 'linux') === false) {
            continue;
        }
        if (strpos($name, $archToken) === false) {
            if (!($archToken === 'amd64' && strpos($name, 'x86_64') !== false)) {
                continue;
            }
        }
        if (!str_ends_with($name, '.tar.gz') && !str_ends_with($name, '.tgz')) {
            continue;
        }
        $downloadUrl = $url;
        break;
    }

    if (!$downloadUrl) {
        return 'No suitable geoipupdate binary found for ' . $arch;
    }

    $tmpDir = sys_get_temp_dir() . '/jabali-geoipupdate-' . bin2hex(random_bytes(4));
    @mkdir($tmpDir, 0755, true);
    $archive = $tmpDir . '/geoipupdate.tgz';

    $downloadCmd = toolExists('curl')
        ? 'curl -fsSL ' . escapeshellarg($downloadUrl) . ' -o ' . escapeshellarg($archive)
        : 'wget -qO ' . escapeshellarg($archive) . ' ' . escapeshellarg($downloadUrl);

    exec($downloadCmd . ' 2>&1', $output, $code);
    if ($code !== 0) {
        return 'Failed to download geoipupdate binary';
    }

    exec('tar -xzf ' . escapeshellarg($archive) . ' -C ' . escapeshellarg($tmpDir) . ' 2>&1', $output, $code);
    if ($code !== 0) {
        return 'Failed to extract geoipupdate archive';
    }

    $binary = null;
    $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($tmpDir, FilesystemIterator::SKIP_DOTS));
    foreach ($iterator as $file) {
        if ($file->isFile() && $file->getFilename() === 'geoipupdate') {
            $binary = $file->getPathname();
            break;
        }
    }

    if (!$binary) {
        return 'geoipupdate binary not found in archive';
    }

    exec('install -m 0755 ' . escapeshellarg($binary) . ' /usr/local/bin/geoipupdate 2>&1', $output, $code);
    if ($code !== 0) {
        return 'Failed to install geoipupdate';
    }

    return null;
}

function ensureGeoIpModuleEnabled(): ?string
{
    $modulePaths = [
        '/usr/lib/nginx/modules/ngx_http_geoip2_module.so',
        '/usr/share/nginx/modules/ngx_http_geoip2_module.so',
    ];

    $modulePath = null;
    foreach ($modulePaths as $path) {
        if (file_exists($path)) {
            $modulePath = $path;
            break;
        }
    }

    if (!$modulePath) {
        return 'nginx geoip2 module not installed';
    }

    $modulesEnabledDir = '/etc/nginx/modules-enabled';
    if (!is_dir($modulesEnabledDir)) {
        return 'nginx modules-enabled directory not found';
    }

    $alreadyEnabled = false;
    foreach (glob($modulesEnabledDir . '/*.conf') ?: [] as $file) {
        $contents = file_get_contents($file);
        if ($contents !== false && strpos($contents, 'geoip2_module') !== false) {
            $alreadyEnabled = true;
            break;
        }
    }

    if ($alreadyEnabled) {
        return null;
    }

    $loadLine = 'load_module ' . $modulePath . ';';
    $target = $modulesEnabledDir . '/50-jabali-geoip2.conf';
    file_put_contents($target, $loadLine . "\n");

    return null;
}

function geoApplyRules(array $params): array
{
    $rules = $params['rules'] ?? [];
    $activeRules = array_values(array_filter($rules, function ($rule) {
        return !isset($rule['is_active']) || !empty($rule['is_active']);
    }));

    $allow = array_values(array_filter($activeRules, fn ($rule) => ($rule['action'] ?? '') === 'allow'));
    $block = array_values(array_filter($activeRules, fn ($rule) => ($rule['action'] ?? '') === 'block'));

    ensureJabaliNginxIncludeFiles();

    $prevGeoHttp = file_exists(JABALI_GEO_HTTP_CONF) ? file_get_contents(JABALI_GEO_HTTP_CONF) : null;
    $prevGeoInclude = file_exists(JABALI_GEO_INCLUDE) ? file_get_contents(JABALI_GEO_INCLUDE) : null;

    if (empty($allow) && empty($block)) {
        file_put_contents(JABALI_GEO_HTTP_CONF, "# Managed by Jabali\n# No geo rules enabled\n");
        file_put_contents(JABALI_GEO_INCLUDE, "# Managed by Jabali\n");
        ensureNginxServerIncludes([
            'include ' . JABALI_GEO_INCLUDE . ';',
        ]);

        $reload = nginxTestAndReload();
        if (!($reload['success'] ?? false)) {
            return $reload;
        }

        return ['success' => true, 'rules' => 0];
    }

    $mmdbPaths = [
        '/usr/share/GeoIP/GeoLite2-Country.mmdb',
        '/usr/local/share/GeoIP/GeoLite2-Country.mmdb',
    ];
    $mmdb = null;
    foreach ($mmdbPaths as $path) {
        if (file_exists($path)) {
            $mmdb = $path;
            break;
        }
    }

    if (!$mmdb) {
        $update = geoUpdateDatabase([
            'use_existing' => true,
            'edition_ids' => 'GeoLite2-Country',
        ]);
        if (!empty($update['success'])) {
            $mmdb = $update['path'] ?? null;
        }
    }

    if (!$mmdb) {
        return ['success' => false, 'error' => 'GeoIP database not found. Update the GeoIP database in the panel.'];
    }

    $geoModule = ensureGeoIpModuleEnabled();
    if ($geoModule !== null) {
        return ['success' => false, 'error' => $geoModule];
    }

    $countryVar = '$jabali_geo_country_code';
    $mapName = !empty($allow) ? '$jabali_geo_allow' : '$jabali_geo_block';
    $mapLines = [
        "map {$countryVar} {$mapName} {",
        '    default 0;',
    ];

    $ruleset = !empty($allow) ? $allow : $block;
    foreach ($ruleset as $rule) {
        $code = strtoupper(trim($rule['country_code'] ?? ''));
        if ($code === '') {
            continue;
        }

        $mapLines[] = "    {$code} 1;";
    }
    $mapLines[] = '}';

    $httpConf = [
        '# Managed by Jabali',
        'real_ip_header X-Forwarded-For;',
        'real_ip_recursive on;',
        'set_real_ip_from 127.0.0.1;',
        'set_real_ip_from ::1;',
        '',
        "geoip2 {$mmdb} {",
        "    {$countryVar} country iso_code;",
        '}',
        '',
        ...$mapLines,
    ];
    file_put_contents(JABALI_GEO_HTTP_CONF, implode("\n", $httpConf) . "\n");

    if (!empty($allow)) {
        $geoInclude = "# Managed by Jabali\nif ({$mapName} = 0) { return 403; }\n";
    } else {
        $geoInclude = "# Managed by Jabali\nif ({$mapName} = 1) { return 403; }\n";
    }

    file_put_contents(JABALI_GEO_INCLUDE, $geoInclude);

    ensureNginxServerIncludes([
        'include ' . JABALI_GEO_INCLUDE . ';',
    ]);

    $reload = nginxTestAndReload();
    if (!($reload['success'] ?? false)) {
        if ($prevGeoHttp === null) {
            @unlink(JABALI_GEO_HTTP_CONF);
        } else {
            file_put_contents(JABALI_GEO_HTTP_CONF, $prevGeoHttp);
        }

        if ($prevGeoInclude === null) {
            @unlink(JABALI_GEO_INCLUDE);
        } else {
            file_put_contents(JABALI_GEO_INCLUDE, $prevGeoInclude);
        }

        return $reload;
    }

    return ['success' => true, 'rules' => count($ruleset)];
}

function databasePersistTuning(array $params): array
{
    $name = $params['name'] ?? '';
    $value = $params['value'] ?? '';

    if (!preg_match('/^[a-zA-Z0-9_]+$/', $name)) {
        return ['success' => false, 'error' => 'Invalid variable name'];
    }

    $configDir = '/etc/mysql/mariadb.conf.d';
    if (!is_dir($configDir)) {
        $configDir = '/etc/mysql/conf.d';
    }

    if (!is_dir($configDir)) {
        return ['success' => false, 'error' => 'MySQL configuration directory not found'];
    }

    $file = $configDir . '/90-jabali-tuning.cnf';
    $lines = file_exists($file) ? file($file, FILE_IGNORE_NEW_LINES) : [];

    if (empty($lines)) {
        $lines = ['# Managed by Jabali', '[mysqld]'];
    }

    $hasSection = false;
    $found = false;
    foreach ($lines as $index => $line) {
        if (trim($line) === '[mysqld]') {
            $hasSection = true;
        }

        if (preg_match('/^\s*' . preg_quote($name, '/') . '\s*=/i', $line)) {
            $lines[$index] = $name . ' = ' . $value;
            $found = true;
        }
    }

    if (!$hasSection) {
        $lines[] = '[mysqld]';
    }

    if (!$found) {
        $lines[] = $name . ' = ' . $value;
    }

    file_put_contents($file, implode("\n", $lines) . "\n");

    return ['success' => true, 'message' => 'Configuration persisted'];
}

// ============ MAIN ============

function main(): void
{
    @mkdir(dirname(SOCKET_PATH), 0755, true);
    @mkdir(dirname(LOG_FILE), 0755, true);
    
    if (file_exists(SOCKET_PATH)) {
        unlink(SOCKET_PATH);
    }
    

// SSH Key Management Functions
function sshListKeys(array $params): array
{
    $username = $params['username'] ?? '';
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }
    
    $sshDir = $userInfo['dir'] . '/.ssh';
    $authKeysFile = $sshDir . '/authorized_keys';
    
    if (!file_exists($authKeysFile)) {
        return ['success' => true, 'keys' => []];
    }
    
    $keys = [];
    $lines = file($authKeysFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    
    foreach ($lines as $index => $line) {
        $line = trim($line);
        if (empty($line) || strpos($line, '#') === 0) {
            continue;
        }
        
        // Parse SSH key: type base64 comment
        $parts = preg_split('/\s+/', $line, 3);
        if (count($parts) < 2) {
            continue;
        }
        
        $type = $parts[0];
        $keyData = $parts[1];
        $comment = $parts[2] ?? 'Key ' . ($index + 1);
        
        // Generate fingerprint
        $tempFile = tempnam(sys_get_temp_dir(), 'sshkey');
        file_put_contents($tempFile, $line);
        exec("ssh-keygen -lf " . escapeshellarg($tempFile) . " 2>&1", $fpOutput);
        @unlink($tempFile);
        
        $fingerprint = isset($fpOutput[0]) ? preg_replace('/^\d+\s+/', '', $fpOutput[0]) : substr($keyData, 0, 20) . '...';
        
        $keys[] = [
            'id' => md5($line),
            'name' => $comment,
            'type' => $type,
            'fingerprint' => $fingerprint,
            'key' => substr($keyData, 0, 30) . '...',
        ];
    }
    
    return ['success' => true, 'keys' => $keys];
}

function sshAddKey(array $params): array
{
    $username = $params['username'] ?? '';
    $name = $params['name'] ?? '';
    $publicKey = $params['public_key'] ?? '';
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    if (empty($publicKey)) {
        return ['success' => false, 'error' => 'Public key is required'];
    }
    
    // Validate key format
    $publicKey = trim($publicKey);
    if (!preg_match('/^(ssh-rsa|ssh-ed25519|ssh-dss|ecdsa-sha2-\S+)\s+[A-Za-z0-9+\/=]+/', $publicKey)) {
        return ['success' => false, 'error' => 'Invalid SSH public key format'];
    }
    
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }
    
    $uid = $userInfo['uid'];
    $gid = $userInfo['gid'];
    $sshDir = $userInfo['dir'] . '/.ssh';
    $authKeysFile = $sshDir . '/authorized_keys';
    
    // Create .ssh directory if it doesn't exist
    if (!is_dir($sshDir)) {
        mkdir($sshDir, 0700, true);
        chown($sshDir, $uid);
        chgrp($sshDir, $gid);
    }
    
    // Add comment if not present
    if (!preg_match('/\s+\S+$/', $publicKey) || preg_match('/==$/', $publicKey)) {
        $publicKey .= ' ' . $name;
    }
    
    // Check if key already exists
    if (file_exists($authKeysFile)) {
        $existingKeys = file_get_contents($authKeysFile);
        $keyParts = preg_split('/\s+/', $publicKey);
        if (count($keyParts) >= 2 && strpos($existingKeys, $keyParts[1]) !== false) {
            return ['success' => false, 'error' => 'This key already exists'];
        }
    }
    
    // Append key to authorized_keys
    $result = file_put_contents($authKeysFile, $publicKey . "\n", FILE_APPEND | LOCK_EX);
    
    if ($result === false) {
        return ['success' => false, 'error' => 'Failed to write authorized_keys file'];
    }
    
    // Set proper permissions
    chmod($authKeysFile, 0600);
    chown($authKeysFile, $uid);
    chgrp($authKeysFile, $gid);
    
    logger("SSH key added for user $username: $name");
    
    return ['success' => true, 'message' => 'SSH key added successfully'];
}

function sshDeleteKey(array $params): array
{
    $username = $params['username'] ?? '';
    $keyId = $params['key_id'] ?? '';
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    if (empty($keyId)) {
        return ['success' => false, 'error' => 'Key ID is required'];
    }
    
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }
    
    $uid = $userInfo['uid'];
    $gid = $userInfo['gid'];
    $authKeysFile = $userInfo['dir'] . '/.ssh/authorized_keys';
    
    if (!file_exists($authKeysFile)) {
        return ['success' => false, 'error' => 'No SSH keys found'];
    }
    
    $lines = file($authKeysFile, FILE_IGNORE_NEW_LINES);
    $newLines = [];
    $found = false;
    
    foreach ($lines as $line) {
        if (md5(trim($line)) === $keyId) {
            $found = true;
            continue; // Skip this key
        }
        $newLines[] = $line;
    }
    
    if (!$found) {
        return ['success' => false, 'error' => 'Key not found'];
    }
    
    // Write back
    file_put_contents($authKeysFile, implode("\n", $newLines) . (count($newLines) > 0 ? "\n" : ""));
    chmod($authKeysFile, 0600);
    chown($authKeysFile, $uid);
    chgrp($authKeysFile, $gid);
    
    logger("SSH key deleted for user $username: $keyId");
    
    return ['success' => true, 'message' => 'SSH key deleted successfully'];
}

    $socket = socket_create(AF_UNIX, SOCK_STREAM, 0);
    if ($socket === false) {
        logger("Failed to create socket", 'ERROR');
        exit(1);
    }
    
    if (socket_bind($socket, SOCKET_PATH) === false) {
        logger("Failed to bind socket", 'ERROR');
        exit(1);
    }
    
    if (socket_listen($socket, 5) === false) {
        logger("Failed to listen", 'ERROR');
        exit(1);
    }
    
    chmod(SOCKET_PATH, 0660);
    chown(SOCKET_PATH, 'root');
    chgrp(SOCKET_PATH, 'www-data');
    
    file_put_contents(PID_FILE, getmypid());
    
    logger("Jabali Agent started on " . SOCKET_PATH);
    
    pcntl_signal(SIGTERM, function () use ($socket) {
        logger("Shutting down...");
        socket_close($socket);
        @unlink(SOCKET_PATH);
        @unlink(PID_FILE);
        exit(0);
    });
    
    pcntl_signal(SIGINT, function () use ($socket) {
        logger("Shutting down...");
        socket_close($socket);
        @unlink(SOCKET_PATH);
        @unlink(PID_FILE);
        exit(0);
    });
    
    // Make socket non-blocking for signal handling
    socket_set_nonblock($socket);
    
    while (true) {
        pcntl_signal_dispatch();
        
        $client = @socket_accept($socket);
        if ($client === false) {
            usleep(50000); // 50ms delay
            continue;
        }
        
        // Set short timeout for reading
        socket_set_option($client, SOL_SOCKET, SO_RCVTIMEO, ['sec' => 30, 'usec' => 0]);
        
        $data = '';
        while (($chunk = @socket_read($client, 65536)) !== false && $chunk !== '') {
            $data .= $chunk;
            // Check if we have complete JSON
            if (strlen($chunk) < 65536) {
                break;
            }
        }
        
        if (!empty($data)) {
            $request = json_decode($data, true);

            if (json_last_error() !== JSON_ERROR_NONE) {
                $response = ['success' => false, 'error' => 'Invalid JSON: ' . json_last_error_msg()];
            } else {
                try {
                    $response = handleAction($request);
                } catch (Throwable $e) {
                    logger("[ERROR] Exception in handleAction: " . $e->getMessage());
                    $response = ['success' => false, 'error' => 'Internal error: ' . $e->getMessage()];
                }
            }

            socket_write($client, json_encode($response));
        }
        
        socket_close($client);
    }
}


// ============ MYSQL MANAGEMENT ============

function getMysqlConnection(): ?mysqli
{
    $socket = "/var/run/mysqld/mysqld.sock";

    // Try socket connection as root
    $conn = @new mysqli("localhost", "root", "", "", 0, $socket);
    if ($conn->connect_error) {
        // Try TCP
        $conn = @new mysqli("127.0.0.1", "root", "");
        if ($conn->connect_error) {
            return null;
        }
    }
    return $conn;
}

function getMysqlRootCredentials(): ?array
{
    // Try to read from debian.cnf first (Debian/Ubuntu)
    $debianCnf = '/etc/mysql/debian.cnf';
    if (file_exists($debianCnf)) {
        $content = file_get_contents($debianCnf);
        if (preg_match('/user\s*=\s*(\S+)/', $content, $userMatch) &&
            preg_match('/password\s*=\s*(\S+)/', $content, $passMatch)) {
            return ['user' => $userMatch[1], 'password' => $passMatch[1]];
        }
    }

    // Try root with no password (socket auth)
    return ['user' => 'root', 'password' => ''];
}

function mysqlListDatabases(array $params): array
{
    $username = $params["username"] ?? "";

    $conn = getMysqlConnection();
    if (!$conn) {
        return ["success" => false, "error" => "Cannot connect to MySQL"];
    }

    $databases = [];
    $prefix = $username . "_";

    // Get database sizes
    $sizeQuery = "SELECT table_schema AS db_name,
                         SUM(data_length + index_length) AS size_bytes
                  FROM information_schema.tables
                  GROUP BY table_schema";
    $sizeResult = $conn->query($sizeQuery);
    $dbSizes = [];
    if ($sizeResult) {
        while ($row = $sizeResult->fetch_assoc()) {
            $dbSizes[$row['db_name']] = (int)($row['size_bytes'] ?? 0);
        }
    }

    $result = $conn->query("SHOW DATABASES");
    while ($row = $result->fetch_array()) {
        $dbName = $row[0];
        if (strpos($dbName, $prefix) === 0 || $username === "admin") {
            $sizeBytes = $dbSizes[$dbName] ?? 0;
            $databases[] = [
                "name" => $dbName,
                "size_bytes" => $sizeBytes,
                "size_human" => formatBytes($sizeBytes),
            ];
        }
    }

    $conn->close();
    return ["success" => true, "databases" => $databases];
}

function mysqlCreateDatabase(array $params): array
{
    $username = $params["username"] ?? "";
    $database = $params["database"] ?? "";
    
    if (!validateUsername($username)) {
        return ["success" => false, "error" => "Invalid username"];
    }
    
    // Check if already prefixed
    $prefix = $username . "_";
    $cleanDb = preg_replace("/[^a-zA-Z0-9_]/", "", $database);
    if (strpos($cleanDb, $prefix) === 0) {
        $dbName = $cleanDb;
    } else {
        $dbName = $prefix . $cleanDb;
    }
    
    if (strlen($dbName) > 64) {
        return ["success" => false, "error" => "Database name too long"];
    }
    
    $conn = getMysqlConnection();
    if (!$conn) {
        return ["success" => false, "error" => "Cannot connect to MySQL"];
    }
    
    $dbName = $conn->real_escape_string($dbName);
    
    if (!$conn->query("CREATE DATABASE IF NOT EXISTS `$dbName` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")) {
        $error = $conn->error;
        $conn->close();
        return ["success" => false, "error" => "Failed to create database: $error"];
    }
    
    $conn->close();
    logger("Created MySQL database: $dbName for user $username");
    return ["success" => true, "message" => "Database created", "database" => $dbName];
}

function mysqlDeleteDatabase(array $params): array
{
    $username = $params["username"] ?? "";
    $database = $params["database"] ?? "";
    
    if (!validateUsername($username)) {
        return ["success" => false, "error" => "Invalid username"];
    }
    
    $prefix = $username . "_";
    if (strpos($database, $prefix) !== 0 && $username !== "admin") {
        return ["success" => false, "error" => "Access denied"];
    }
    
    $conn = getMysqlConnection();
    if (!$conn) {
        return ["success" => false, "error" => "Cannot connect to MySQL"];
    }
    
    $dbName = $conn->real_escape_string($database);
    
    if (!$conn->query("DROP DATABASE IF EXISTS `$dbName`")) {
        $error = $conn->error;
        $conn->close();
        return ["success" => false, "error" => "Failed to delete database: $error"];
    }
    
    $conn->close();
    logger("Deleted MySQL database: $dbName for user $username");
    return ["success" => true, "message" => "Database deleted"];
}

function mysqlListUsers(array $params): array
{
    $username = $params["username"] ?? "";
    
    $conn = getMysqlConnection();
    if (!$conn) {
        return ["success" => false, "error" => "Cannot connect to MySQL"];
    }
    
    $users = [];
    $prefix = $username . "_";
    
    $result = $conn->query("SELECT User, Host FROM mysql.user WHERE User != 'root' AND User != '' AND User NOT LIKE 'mysql.%'");
    while ($row = $result->fetch_assoc()) {
        if (strpos($row["User"], $prefix) === 0 || $username === "admin") {
            $users[] = ["user" => $row["User"], "host" => $row["Host"]];
        }
    }
    
    $conn->close();
    return ["success" => true, "users" => $users];
}

function mysqlCreateUser(array $params): array
{
    $username = $params["username"] ?? "";
    $dbUser = $params["db_user"] ?? "";
    $password = $params["password"] ?? "";
    $host = $params["host"] ?? "localhost";
    
    if (!validateUsername($username)) {
        return ["success" => false, "error" => "Invalid username"];
    }
    
    if (empty($password) || strlen($password) < 8) {
        return ["success" => false, "error" => "Password must be at least 8 characters"];
    }
    
    // Check if already prefixed
    $prefix = $username . "_";
    $cleanDbUser = preg_replace("/[^a-zA-Z0-9_]/", "", $dbUser);
    if (strpos($cleanDbUser, $prefix) === 0) {
        $dbUserName = $cleanDbUser;
    } else {
        $dbUserName = $prefix . $cleanDbUser;
    }
    
    if (strlen($dbUserName) > 32) {
        return ["success" => false, "error" => "Username too long"];
    }
    
    $conn = getMysqlConnection();
    if (!$conn) {
        return ["success" => false, "error" => "Cannot connect to MySQL"];
    }
    
    $dbUserName = $conn->real_escape_string($dbUserName);
    $host = $conn->real_escape_string($host);
    $password = $conn->real_escape_string($password);
    
    if (!$conn->query("CREATE USER '$dbUserName'@'$host' IDENTIFIED BY '$password'")) {
        $error = $conn->error;
        $conn->close();
        return ["success" => false, "error" => "Failed to create user: $error"];
    }
    
    $conn->close();
    logger("Created MySQL user: $dbUserName@$host for user $username");
    return ["success" => true, "message" => "User created", "db_user" => $dbUserName];
}

function mysqlDeleteUser(array $params): array
{
    $username = $params["username"] ?? "";
    $dbUser = $params["db_user"] ?? "";
    $host = $params["host"] ?? "localhost";
    
    if (!validateUsername($username)) {
        return ["success" => false, "error" => "Invalid username"];
    }
    
    $prefix = $username . "_";
    if (strpos($dbUser, $prefix) !== 0 && $username !== "admin") {
        return ["success" => false, "error" => "Access denied"];
    }
    
    $conn = getMysqlConnection();
    if (!$conn) {
        return ["success" => false, "error" => "Cannot connect to MySQL"];
    }
    
    $dbUser = $conn->real_escape_string($dbUser);
    $host = $conn->real_escape_string($host);
    
    if (!$conn->query("DROP USER IF EXISTS '$dbUser'@'$host'")) {
        $error = $conn->error;
        $conn->close();
        return ["success" => false, "error" => "Failed to delete user: $error"];
    }
    
    $conn->close();
    logger("Deleted MySQL user: $dbUser@$host for user $username");
    return ["success" => true, "message" => "User deleted"];
}

function mysqlChangePassword(array $params): array
{
    $username = $params["username"] ?? "";
    $dbUser = $params["db_user"] ?? "";
    $password = $params["password"] ?? "";
    $host = $params["host"] ?? "localhost";
    
    if (!validateUsername($username)) {
        return ["success" => false, "error" => "Invalid username"];
    }
    
    $prefix = $username . "_";
    if (strpos($dbUser, $prefix) !== 0 && $username !== "admin") {
        return ["success" => false, "error" => "Access denied"];
    }
    
    if (empty($password) || strlen($password) < 8) {
        return ["success" => false, "error" => "Password must be at least 8 characters"];
    }
    
    $conn = getMysqlConnection();
    if (!$conn) {
        return ["success" => false, "error" => "Cannot connect to MySQL"];
    }
    
    $dbUser = $conn->real_escape_string($dbUser);
    $host = $conn->real_escape_string($host);
    $password = $conn->real_escape_string($password);
    
    if (!$conn->query("ALTER USER '$dbUser'@'$host' IDENTIFIED BY '$password'")) {
        $error = $conn->error;
        $conn->close();
        return ["success" => false, "error" => "Failed to change password: $error"];
    }
    
    $conn->query("FLUSH PRIVILEGES");
    $conn->close();
    logger("Changed password for MySQL user: $dbUser@$host");
    return ["success" => true, "message" => "Password changed"];
}

function mysqlGrantPrivileges(array $params): array
{
    $username = $params["username"] ?? "";
    $dbUser = $params["db_user"] ?? "";
    $database = $params["database"] ?? "";
    $privileges = $params["privileges"] ?? ["ALL"];
    $host = $params["host"] ?? "localhost";
    
    if (!validateUsername($username)) {
        return ["success" => false, "error" => "Invalid username"];
    }
    
    $prefix = $username . "_";
    if ($username !== "admin") {
        if (strpos($dbUser, $prefix) !== 0) {
            return ["success" => false, "error" => "Access denied to user"];
        }
        if (strpos($database, $prefix) !== 0 && $database !== "*") {
            return ["success" => false, "error" => "Access denied to database"];
        }
    }
    
    $conn = getMysqlConnection();
    if (!$conn) {
        return ["success" => false, "error" => "Cannot connect to MySQL"];
    }
    
    $dbUser = $conn->real_escape_string($dbUser);
    $host = $conn->real_escape_string($host);
    $database = $conn->real_escape_string($database);
    
    $allowedPrivs = ["ALL", "SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "INDEX", "ALTER", "EXECUTE", "CREATE VIEW", "SHOW VIEW"];
    $privList = [];
    foreach ($privileges as $priv) {
        $priv = strtoupper(trim($priv));
        if (in_array($priv, $allowedPrivs)) {
            $privList[] = $priv;
        }
    }
    
    if (empty($privList)) {
        $privList = ["ALL"];
    }
    
    $privString = implode(", ", $privList);
    $dbTarget = $database === "*" ? "*.*" : "`$database`.*";
    
    if (!$conn->query("GRANT $privString ON $dbTarget TO '$dbUser'@'$host'")) {
        $error = $conn->error;
        $conn->close();
        return ["success" => false, "error" => "Failed to grant privileges: $error"];
    }
    
    $conn->query("FLUSH PRIVILEGES");
    $conn->close();
    logger("Granted $privString on $database to $dbUser@$host");
    return ["success" => true, "message" => "Privileges granted"];
}

function mysqlRevokePrivileges(array $params): array
{
    $username = $params["username"] ?? "";
    $dbUser = $params["db_user"] ?? "";
    $database = $params["database"] ?? "";
    $host = $params["host"] ?? "localhost";
    
    if (!validateUsername($username)) {
        return ["success" => false, "error" => "Invalid username"];
    }
    
    $prefix = $username . "_";
    if ($username !== "admin") {
        if (strpos($dbUser, $prefix) !== 0) {
            return ["success" => false, "error" => "Access denied to user"];
        }
    }
    
    $conn = getMysqlConnection();
    if (!$conn) {
        return ["success" => false, "error" => "Cannot connect to MySQL"];
    }
    
    $dbUser = $conn->real_escape_string($dbUser);
    $host = $conn->real_escape_string($host);
    $database = $conn->real_escape_string($database);
    
    $dbTarget = $database === "*" ? "*.*" : "`$database`.*";
    
    if (!$conn->query("REVOKE ALL PRIVILEGES ON $dbTarget FROM '$dbUser'@'$host'")) {
        $error = $conn->error;
        $conn->close();
        return ["success" => false, "error" => "Failed to revoke privileges: $error"];
    }
    
    $conn->query("FLUSH PRIVILEGES");
    $conn->close();
    logger("Revoked privileges on $database from $dbUser@$host");
    return ["success" => true, "message" => "Privileges revoked"];
}

function mysqlGetPrivileges(array $params): array
{
    $username = $params["username"] ?? "";
    $dbUser = $params["db_user"] ?? "";
    $host = $params["host"] ?? "localhost";
    
    if (!validateUsername($username)) {
        return ["success" => false, "error" => "Invalid username"];

// Signal handling for graceful shutdown
$shutdown = false;

pcntl_async_signals(true);
pcntl_signal(SIGTERM, function() use (&$shutdown) {
    global $socket;
    $shutdown = true;
    if ($socket) {
        socket_close($socket);
    }
    exit(0);
});
pcntl_signal(SIGINT, function() use (&$shutdown) {
    global $socket;
    $shutdown = true;
    if ($socket) {
        socket_close($socket);
    }
    exit(0);
});

while (!$shutdown) {
    // Set socket timeout so it doesn't block forever
    socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, ['sec' => 1, 'usec' => 0]);
    
    $client = @socket_accept($socket);
    
    if ($client === false) {
        // Timeout or error - check if we should shutdown
        if ($shutdown) break;
        continue;
    }
    
    handleConnection($client);
}

socket_close($socket);
unlink($socketPath);
    }
    
    $conn = getMysqlConnection();
    if (!$conn) {
        return ["success" => false, "error" => "Cannot connect to MySQL"];
    }
    
    $dbUser = $conn->real_escape_string($dbUser);
    $host = $conn->real_escape_string($host);
    
    $rawPrivileges = [];
    $parsedPrivileges = [];
    
    $result = $conn->query("SHOW GRANTS FOR '$dbUser'@'$host'");
    
    if ($result) {
        while ($row = $result->fetch_array()) {
            $grant = $row[0];
            $rawPrivileges[] = $grant;
            
            // Parse: GRANT SELECT, INSERT ON `db`.* TO user
            // Or: GRANT ALL PRIVILEGES ON `db`.* TO user
            if (preg_match('/GRANT\s+(.+?)\s+ON\s+[`"\']*([^`"\'\.\s]+)[`"\']*\.\*\s+TO/i', $grant, $matches)) {
                $privsStr = trim($matches[1]);
                $db = trim($matches[2], "`\'\"");
                
                if ($db !== "*") {
                    $privList = [];
                    if (stripos($privsStr, "ALL PRIVILEGES") !== false) {
                        $privList = ["ALL PRIVILEGES"];
                    } elseif (stripos($privsStr, "ALL") === 0) {
                        $privList = ["ALL PRIVILEGES"];
                    } else {
                        $parts = explode(",", $privsStr);
                        foreach ($parts as $p) {
                            $p = trim($p);
                            if (!empty($p) && stripos($p, "GRANT OPTION") === false) {
                                $privList[] = strtoupper($p);
                            }
                        }
                    }
                    
                    if (!empty($privList)) {
                        $parsedPrivileges[] = [
                            "database" => $db,
                            "privileges" => $privList
                        ];
                    }
                }
            }
        }
    }
    
    $conn->close();
    return ["success" => true, "privileges" => $rawPrivileges, "parsed" => $parsedPrivileges];
}

main();

// Domain Management Functions

function domainCreate(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    // Validate domain format
    $domain = strtolower(trim($domain));
    if (!preg_match('/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*\.[a-z]{2,}$/', $domain)) {
        return ['success' => false, 'error' => 'Invalid domain format'];
    }
    
    // Check if domain already exists
    $vhostFile = "/etc/nginx/sites-available/{$domain}.conf";
    if (file_exists($vhostFile)) {
        return ['success' => false, 'error' => 'Domain already exists'];
    }
    
    // Get user info
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }
    
    $userHome = $userInfo['dir'];
    $uid = $userInfo['uid'];
    $gid = $userInfo['gid'];

    ensureJabaliNginxIncludeFiles();

    // Create domain directories
    $domainRoot = "{$userHome}/domains/{$domain}";
    $publicHtml = "{$domainRoot}/public_html";
    $logs = "{$domainRoot}/logs";
    
    $dirs = [$domainRoot, $publicHtml, $logs];
    foreach ($dirs as $dir) {
        if (!is_dir($dir)) {
            if (!mkdir($dir, 0755, true)) {
                return ['success' => false, 'error' => "Failed to create directory: {$dir}"];
            }
            chown($dir, $uid);
            chgrp($dir, $gid);
        }
    }
    
    // Create default index.html
    $indexContent = '<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Welcome to ' . $domain . '</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: system-ui, sans-serif; min-height: 100vh; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
        .container { text-align: center; padding: 40px; background: rgba(255,255,255,0.1); border-radius: 16px; }
        h1 { font-size: 2.5rem; margin-bottom: 16px; }
        p { font-size: 1.1rem; opacity: 0.9; }
        .domain { display: inline-block; margin-top: 20px; padding: 8px 16px; background: rgba(255,255,255,0.2); border-radius: 8px; font-family: monospace; }
    </style>
</head>
<body>
    <div class="container">
        <h1>Welcome!</h1>
        <p>Your website is ready. Upload your files to get started.</p>
        <span class="domain">' . $domain . '</span>
    </div>
</body>
</html>';
    
    $indexFile = "{$publicHtml}/index.html";
    if (!file_exists($indexFile)) {
        file_put_contents($indexFile, $indexContent);
        chown($indexFile, $uid);
        chgrp($indexFile, $gid);
    }
    
    // Ensure FPM pool exists for this user (don't reload - caller handles that)
    createFpmPool($username, false);
    $fpmSocket = getFpmSocketPath($username);
    
    // Create Nginx virtual host configuration
    $vhostContent = generateNginxVhost($domain, $publicHtml, $logs, $fpmSocket);
    
    if (file_put_contents($vhostFile, $vhostContent) === false) {
        return ['success' => false, 'error' => 'Failed to create virtual host configuration'];
    }
    
    // Enable the site (create symlink)
    $enableCmd = "ln -sf " . escapeshellarg($vhostFile) . " /etc/nginx/sites-enabled/" . escapeshellarg("{$domain}.conf") . " 2>&1";
    exec($enableCmd, $output, $returnCode);
    if ($returnCode !== 0) {
        unlink($vhostFile);
        return ['success' => false, 'error' => 'Failed to enable site: ' . implode("\n", $output)];
    }
    
    // Set ACLs for Nginx to access files
    exec("setfacl -m u:www-data:x " . escapeshellarg($userHome));
    exec("setfacl -m u:www-data:x " . escapeshellarg("{$userHome}/domains"));
    exec("setfacl -m u:www-data:rx " . escapeshellarg($domainRoot));
    exec("setfacl -R -m u:www-data:rx " . escapeshellarg($publicHtml));
    exec("setfacl -R -m u:www-data:rwx " . escapeshellarg($logs));
    exec("setfacl -R -d -m u:www-data:rx " . escapeshellarg($publicHtml));
    exec("setfacl -R -d -m u:www-data:rwx " . escapeshellarg($logs));
    
    // Reload Nginx
    exec("nginx -t 2>&1", $testOutput, $testCode);
    if ($testCode !== 0) {
        unlink($vhostFile);
        unlink("/etc/nginx/sites-enabled/{$domain}.conf");
        return ['success' => false, 'error' => 'Nginx config test failed: ' . implode("\n", $testOutput)];
    }
    
    exec("systemctl reload nginx 2>&1", $reloadOutput, $reloadCode);
    if ($reloadCode !== 0) {
        return ['success' => false, 'error' => 'Site created but failed to reload Nginx: ' . implode("\n", $reloadOutput)];
    }
    
    // Store domain in user's domain list
    $domainListFile = "{$userHome}/.domains";
    $domains = [];
    if (file_exists($domainListFile)) {
        $domains = json_decode(file_get_contents($domainListFile), true) ?: [];
    }
    $domains[$domain] = [
        'created' => date('Y-m-d H:i:s'),
        'document_root' => $publicHtml,
        'ssl' => false
    ];
    file_put_contents($domainListFile, json_encode($domains, JSON_PRETTY_PRINT));
    chown($domainListFile, $uid);
    chgrp($domainListFile, $gid);
    
    return [
        'success' => true,
        'domain' => $domain,
        'document_root' => $publicHtml,
        'message' => "Domain {$domain} created successfully"
    ];
}

function updateVhostServerNames(string $vhostFile, callable $mutator): array
{
    if (!file_exists($vhostFile)) {
        return ['success' => false, 'error' => 'Domain configuration not found'];
    }

    $original = file_get_contents($vhostFile);
    if ($original === false) {
        return ['success' => false, 'error' => 'Failed to read virtual host configuration'];
    }

    $updated = preg_replace_callback('/server_name\\s+([^;]+);/i', function ($matches) use ($mutator) {
        $names = preg_split('/\\s+/', trim($matches[1]));
        $names = array_values(array_filter($names));
        $names = $mutator($names);
        $names = array_values(array_unique($names));

        return '    server_name ' . implode(' ', $names) . ';';
    }, $original, -1, $count);

    if ($count === 0 || $updated === null) {
        return ['success' => false, 'error' => 'Failed to update server_name entries'];
    }

    if (file_put_contents($vhostFile, $updated) === false) {
        return ['success' => false, 'error' => 'Failed to write virtual host configuration'];
    }

    exec("nginx -t 2>&1", $testOutput, $testCode);
    if ($testCode !== 0) {
        // rollback
        file_put_contents($vhostFile, $original);
        return ['success' => false, 'error' => 'Nginx config test failed: ' . implode("\n", $testOutput)];
    }

    exec("systemctl reload nginx 2>&1", $reloadOutput, $reloadCode);
    if ($reloadCode !== 0) {
        return ['success' => false, 'error' => 'Failed to reload Nginx: ' . implode("\n", $reloadOutput)];
    }

    return ['success' => true];
}

function databaseGetVariables(array $params): array
{
    $names = $params['names'] ?? [];
    if (!is_array($names) || $names === []) {
        return ['success' => false, 'error' => 'No variables requested'];
    }

    $safeNames = [];
    foreach ($names as $name) {
        if (preg_match('/^[a-zA-Z0-9_]+$/', (string) $name)) {
            $safeNames[] = $name;
        }
    }

    if ($safeNames === []) {
        return ['success' => false, 'error' => 'No valid variable names'];
    }

    $inList = implode("','", array_map(fn($name) => str_replace("'", "\\'", (string) $name), $safeNames));
    $query = "SHOW VARIABLES WHERE Variable_name IN ('{$inList}')";
    $command = 'mysql --batch --skip-column-names -e ' . escapeshellarg($query) . ' 2>&1';

    exec($command, $output, $code);

    if ($code !== 0) {
        return ['success' => false, 'error' => implode("\n", $output)];
    }

    $variables = [];
    foreach ($output as $line) {
        $line = trim($line);
        if ($line === '') {
            continue;
        }
        $parts = explode("\t", $line, 2);
        $name = $parts[0] ?? null;
        if ($name === null || $name === '') {
            continue;
        }
        $variables[] = [
            'name' => $name,
            'value' => $parts[1] ?? '',
        ];
    }

    return ['success' => true, 'variables' => $variables];
}

function databaseSetGlobal(array $params): array
{
    $name = $params['name'] ?? '';
    $value = (string) ($params['value'] ?? '');

    if (!preg_match('/^[a-zA-Z0-9_]+$/', $name)) {
        return ['success' => false, 'error' => 'Invalid variable name'];
    }

    $escapedValue = addslashes($value);
    $query = "SET GLOBAL {$name} = '{$escapedValue}'";
    $command = 'mysql -e ' . escapeshellarg($query) . ' 2>&1';

    exec($command, $output, $code);

    if ($code !== 0) {
        return ['success' => false, 'error' => implode("\n", $output)];
    }

    return ['success' => true];
}

function domainAliasAdd(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = strtolower(trim($params['domain'] ?? ''));
    $alias = strtolower(trim($params['alias'] ?? ''));

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    if (!validateDomain($domain) || !validateDomain($alias)) {
        return ['success' => false, 'error' => 'Invalid domain format'];
    }

    if ($alias === $domain || $alias === "www.{$domain}") {
        return ['success' => false, 'error' => 'Alias cannot match the primary domain'];
    }

    $vhostFile = "/etc/nginx/sites-available/{$domain}.conf";

    foreach (glob('/etc/nginx/sites-available/*.conf') as $file) {
        if (!is_readable($file)) {
            continue;
        }
        $content = file_get_contents($file);
        if ($content === false) {
            continue;
        }
        if (preg_match('/server_name\\s+[^;]*\\b' . preg_quote($alias, '/') . '\\b/i', $content) ||
            preg_match('/server_name\\s+[^;]*\\b' . preg_quote("www.{$alias}", '/') . '\\b/i', $content)) {
            if ($file !== $vhostFile) {
                return ['success' => false, 'error' => 'Alias already exists on another domain'];
            }
        }
    }

    $result = updateVhostServerNames($vhostFile, function (array $names) use ($alias) {
        if (!in_array($alias, $names, true)) {
            $names[] = $alias;
        }
        $wwwAlias = "www.{$alias}";
        if (!in_array($wwwAlias, $names, true)) {
            $names[] = $wwwAlias;
        }

        return $names;
    });

    if (!($result['success'] ?? false)) {
        return $result;
    }

    return ['success' => true, 'message' => "Alias {$alias} added to {$domain}"];
}

function domainAliasRemove(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = strtolower(trim($params['domain'] ?? ''));
    $alias = strtolower(trim($params['alias'] ?? ''));

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    if (!validateDomain($domain) || !validateDomain($alias)) {
        return ['success' => false, 'error' => 'Invalid domain format'];
    }

    $vhostFile = "/etc/nginx/sites-available/{$domain}.conf";

    $result = updateVhostServerNames($vhostFile, function (array $names) use ($alias) {
        $wwwAlias = "www.{$alias}";
        return array_values(array_filter($names, function ($name) use ($alias, $wwwAlias) {
            return $name !== $alias && $name !== $wwwAlias;
        }));
    });

    if (!($result['success'] ?? false)) {
        return $result;
    }

    return ['success' => true, 'message' => "Alias {$alias} removed from {$domain}"];
}

function domainEnsureErrorPages(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = strtolower(trim($params['domain'] ?? ''));

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    if (!validateDomain($domain)) {
        return ['success' => false, 'error' => 'Invalid domain format'];
    }

    $vhostFile = "/etc/nginx/sites-available/{$domain}.conf";
    if (!file_exists($vhostFile)) {
        return ['success' => false, 'error' => 'Domain configuration not found'];
    }

    $content = file_get_contents($vhostFile);
    if ($content === false) {
        return ['success' => false, 'error' => 'Failed to read virtual host configuration'];
    }

    if (strpos($content, 'error_page 404') !== false) {
        return ['success' => true, 'message' => 'Error page directives already configured'];
    }

    $snippet = <<<NGINX
    # Custom error pages (optional)
    error_page 404 /404.html;
    error_page 500 502 503 504 /500.html;
    error_page 503 /503.html;

    location = /404.html { internal; }
    location = /500.html { internal; }
    location = /503.html { internal; }
NGINX;

    $updated = preg_replace('/(client_max_body_size\\s+[^;]+;)/', "$1\\n\\n{$snippet}", $content, 1, $count);
    if ($count === 0 || $updated === null) {
        return ['success' => false, 'error' => 'Failed to inject error page directives'];
    }

    if (file_put_contents($vhostFile, $updated) === false) {
        return ['success' => false, 'error' => 'Failed to write virtual host configuration'];
    }

    exec("nginx -t 2>&1", $testOutput, $testCode);
    if ($testCode !== 0) {
        file_put_contents($vhostFile, $content);
        return ['success' => false, 'error' => 'Nginx config test failed: ' . implode("\n", $testOutput)];
    }

    exec("systemctl reload nginx 2>&1", $reloadOutput, $reloadCode);
    if ($reloadCode !== 0) {
        return ['success' => false, 'error' => 'Failed to reload Nginx: ' . implode("\n", $reloadOutput)];
    }

    return ['success' => true, 'message' => 'Error pages enabled'];
}

function domainDelete(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';
    $deleteFiles = $params['delete_files'] ?? false;
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    $domain = strtolower(trim($domain));
    
    // Verify ownership
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }
    
    $userHome = $userInfo['dir'];
    $domainListFile = "{$userHome}/.domains";
    
    // Check if user owns this domain
    $domains = [];
    if (file_exists($domainListFile)) {
        $domains = json_decode(file_get_contents($domainListFile), true) ?: [];
    }
    
    // Admin can delete any domain, regular users only their own
    if ($username !== 'admin' && !isset($domains[$domain])) {
        return ['success' => false, 'error' => 'Domain not found or access denied'];
    }
    
    // Disable and remove the site
    $vhostFile = "/etc/nginx/sites-available/{$domain}.conf";
    
    if (file_exists("/etc/nginx/sites-enabled/{$domain}.conf")) {
        exec("rm -f /etc/nginx/sites-enabled/" . escapeshellarg("{$domain}.conf") . " 2>&1", $output, $returnCode);
    }
    
    if (file_exists($vhostFile)) {
        unlink($vhostFile);
    }

    // Reload Nginx
    exec("nginx -t && systemctl reload nginx 2>&1");

    // Delete DNS zone file and remove from named.conf.local
    $zoneFile = "/etc/bind/zones/db.{$domain}";
    if (file_exists($zoneFile)) {
        unlink($zoneFile);
        logger("Deleted DNS zone file for {$domain}");
    }

    $namedConf = '/etc/bind/named.conf.local';
    if (file_exists($namedConf)) {
        $content = file_get_contents($namedConf);
        // Use [\s\S]*? to match any chars including newlines (handles nested braces like allow-transfer { none; })
        $pattern = '/\n?zone\s+"' . preg_quote($domain, '/') . '"\s*\{[\s\S]*?\n\};\n?/';
        $newContent = preg_replace($pattern, "\n", $content);
        if ($newContent !== $content) {
            file_put_contents($namedConf, trim($newContent) . "\n");
            exec('systemctl reload bind9 2>&1 || systemctl reload named 2>&1');
            logger("Removed DNS zone entry and reloaded BIND for {$domain}");
        }
    }

    // Delete domain files if requested
    if ($deleteFiles) {
        $domainRoot = "{$userHome}/domains/{$domain}";
        if (is_dir($domainRoot)) {
            exec("rm -rf " . escapeshellarg($domainRoot));
        }
    }
    
    // Remove from domain list
    unset($domains[$domain]);
    file_put_contents($domainListFile, json_encode($domains, JSON_PRETTY_PRINT));
    
    return [
        'success' => true,
        'message' => "Domain {$domain} deleted successfully"
    ];
}

function domainList(array $params): array
{
    $username = $params['username'] ?? '';
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }
    
    $userHome = $userInfo['dir'];
    $domainListFile = "{$userHome}/.domains";
    
    $domains = [];
    if (file_exists($domainListFile)) {
        $domains = json_decode(file_get_contents($domainListFile), true) ?: [];
    }
    
    // Enrich with additional info
    $result = [];
    foreach ($domains as $domain => $info) {
        $docRoot = $info['document_root'] ?? "{$userHome}/domains/{$domain}/public_html";
        $result[] = [
            'domain' => $domain,
            'document_root' => $docRoot,
            'created' => $info['created'] ?? 'Unknown',
            'ssl' => $info['ssl'] ?? false,
            'enabled' => file_exists("/etc/nginx/sites-enabled/{$domain}.conf")
        ];
    }
    
    return ['success' => true, 'domains' => $result];
}

function domainToggle(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';
    $enable = $params['enable'] ?? true;
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    $domain = strtolower(trim($domain));
    $vhostFile = "/etc/nginx/sites-available/{$domain}.conf";
    
    if (!file_exists($vhostFile)) {
        return ['success' => false, 'error' => 'Domain configuration not found'];
    }
    
    if ($enable) {
        exec("ln -sf /etc/nginx/sites-available/" . escapeshellarg("{$domain}.conf") . " /etc/nginx/sites-enabled/" . escapeshellarg("{$domain}.conf") . " 2>&1", $output, $returnCode);
        $action = 'enabled';
    } else {
        exec("rm -f /etc/nginx/sites-enabled/" . escapeshellarg("{$domain}.conf") . " 2>&1", $output, $returnCode);
        $action = 'disabled';
    }
    
    exec("nginx -t && systemctl reload nginx 2>&1");

    return ['success' => true, 'message' => "Domain {$domain} {$action}"];
}

function domainSetRedirects(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';
    $redirects = $params['redirects'] ?? [];

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $domain = strtolower(trim($domain));
    $vhostFile = "/etc/nginx/sites-available/{$domain}.conf";

    if (!file_exists($vhostFile)) {
        return ['success' => false, 'error' => 'Domain configuration not found'];
    }

    $vhostContent = file_get_contents($vhostFile);

    // Remove any existing Jabali redirect markers and content (from ALL server blocks)
    $vhostContent = preg_replace('/\n\s*# JABALI_REDIRECTS_START.*?# JABALI_REDIRECTS_END\n/s', "\n", $vhostContent);

    // Check if there's a domain-wide redirect
    $domainWideRedirect = null;
    $pageRedirects = [];

    foreach ($redirects as $redirect) {
        $source = $redirect['source'] ?? '';
        $destination = $redirect['destination'] ?? '';
        $type = $redirect['type'] ?? '301';
        $wildcard = $redirect['wildcard'] ?? false;

        if (empty($source) || empty($destination)) {
            continue;
        }

        // Sanitize destination URL
        $destination = filter_var($destination, FILTER_SANITIZE_URL);
        $type = in_array($type, ['301', '302']) ? $type : '301';

        if ($source === '/*' || $source === '*' || $source === '/') {
            // Domain-wide redirect
            $domainWideRedirect = [
                'destination' => $destination,
                'type' => $type,
            ];
        } else {
            // Sanitize source path
            $source = preg_replace('/[^a-zA-Z0-9\/_\-\.\*]/', '', $source);
            $pageRedirects[] = [
                'source' => $source,
                'destination' => $destination,
                'type' => $type,
                'wildcard' => $wildcard,
            ];
        }
    }

    // Build redirect configuration
    $redirectConfig = "\n    # JABALI_REDIRECTS_START\n";

    if ($domainWideRedirect) {
        // For domain-wide redirect, use return at server level (before location blocks)
        $redirectConfig .= "    # Domain-wide redirect - all requests go to: {$domainWideRedirect['destination']}\n";
        $redirectConfig .= "    return {$domainWideRedirect['type']} {$domainWideRedirect['destination']}\$request_uri;\n";
    } else {
        // Add page-specific redirects using rewrite rules (works before location matching)
        foreach ($pageRedirects as $redirect) {
            $source = $redirect['source'];
            $destination = $redirect['destination'];
            $type = $redirect['type'];
            $wildcard = $redirect['wildcard'];

            $redirectConfig .= "    # Redirect: {$source}\n";
            if ($wildcard) {
                // Wildcard: match path and everything after
                $escapedSource = preg_quote($source, '/');
                $redirectConfig .= "    rewrite ^{$escapedSource}(.*)\$ {$destination}\$1 permanent;\n";
            } else {
                // Exact match
                $escapedSource = preg_quote($source, '/');
                $flag = $type === '301' ? 'permanent' : 'redirect';
                $redirectConfig .= "    rewrite ^{$escapedSource}\$ {$destination} {$flag};\n";
            }
        }
    }

    $redirectConfig .= "    # JABALI_REDIRECTS_END\n";

    // Insert redirect config after EVERY server_name line (both HTTP and HTTPS blocks)
    if (!empty($redirects)) {
        $pattern = '/(server_name\s+' . preg_quote($domain, '/') . '[^;]*;)/';
        $vhostContent = preg_replace(
            $pattern,
            "$1\n{$redirectConfig}",
            $vhostContent
        );
    }

    // Write updated vhost
    file_put_contents($vhostFile, $vhostContent);

    // Test and reload nginx
    exec("nginx -t 2>&1", $testOutput, $testCode);
    if ($testCode !== 0) {
        // Restore original file on failure
        return ['success' => false, 'error' => 'Nginx configuration test failed: ' . implode("\n", $testOutput)];
    }

    exec("systemctl reload nginx 2>&1", $reloadOutput, $reloadCode);

    return [
        'success' => $reloadCode === 0,
        'message' => 'Redirects updated successfully',
        'redirects_count' => count($redirects),
    ];
}

function domainSetHotlinkProtection(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';
    $enabled = $params['enabled'] ?? false;
    $allowedDomains = $params['allowed_domains'] ?? [];
    $blockBlankReferrer = $params['block_blank_referrer'] ?? true;
    $protectedExtensions = $params['protected_extensions'] ?? ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'mp4', 'mp3', 'pdf'];
    $redirectUrl = $params['redirect_url'] ?? null;

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $domain = strtolower(trim($domain));
    $vhostFile = "/etc/nginx/sites-available/{$domain}.conf";

    if (!file_exists($vhostFile)) {
        return ['success' => false, 'error' => 'Domain configuration not found'];
    }

    $vhostContent = file_get_contents($vhostFile);

    // Remove any existing hotlink protection markers
    $vhostContent = preg_replace('/\n\s*# JABALI_HOTLINK_START.*?# JABALI_HOTLINK_END\n/s', "\n", $vhostContent);

    if ($enabled && !empty($protectedExtensions)) {
        // Build the hotlink protection config
        $extensionsPattern = implode('|', array_map('preg_quote', $protectedExtensions));

        // Build valid referers list using nginx valid_referers syntax
        // server_names matches the server's own names from server_name directive
        // Use regex patterns (~pattern) to match domains in the referer URL
        $validReferers = ['server_names'];

        // Add the domain itself (exact and with subdomains)
        // Referer format: https://domain.com/path or https://sub.domain.com/path
        $escapedDomain = str_replace('.', '\.', $domain);
        $validReferers[] = "~{$escapedDomain}";

        // Add user-specified allowed domains
        foreach ($allowedDomains as $allowedDomain) {
            $allowedDomain = trim($allowedDomain);
            if (!empty($allowedDomain)) {
                $escapedAllowed = str_replace('.', '\.', $allowedDomain);
                $validReferers[] = "~{$escapedAllowed}";
            }
        }

        // Handle blank referrer
        if (!$blockBlankReferrer) {
            array_unshift($validReferers, 'none');
        }

        $validReferersStr = implode(' ', $validReferers);

        // Determine the action for invalid referrers
        if (!empty($redirectUrl)) {
            $action = "return 301 {$redirectUrl}";
        } else {
            $action = "return 403";
        }

        $hotlinkConfig = <<<NGINX

    # JABALI_HOTLINK_START
    location ~* \.({$extensionsPattern})$ {
        valid_referers {$validReferersStr};
        if (\$invalid_referer) {
            {$action};
        }
    }
    # JABALI_HOTLINK_END
NGINX;

        // Insert hotlink config before the access_log line in the HTTPS server block
        // We identify the HTTPS block by finding access_log that comes after listen 443
        if (preg_match('/listen\s+443/', $vhostContent)) {
            // Insert before the access_log line (which is typically near the end of the server block)
            $vhostContent = preg_replace(
                '/(\n\s*access_log\s+\/home\/)/',
                "{$hotlinkConfig}\n$1",
                $vhostContent,
                1 // Only replace first occurrence (in HTTPS block)
            );
        }
    }

    // Write updated vhost
    file_put_contents($vhostFile, $vhostContent);

    // Test and reload nginx
    exec("nginx -t 2>&1", $testOutput, $testCode);
    if ($testCode !== 0) {
        return ['success' => false, 'error' => 'Nginx configuration test failed: ' . implode("\n", $testOutput)];
    }

    exec("systemctl reload nginx 2>&1", $reloadOutput, $reloadCode);

    return [
        'success' => $reloadCode === 0,
        'message' => $enabled ? 'Hotlink protection enabled' : 'Hotlink protection disabled',
    ];
}

function domainSetDirectoryIndex(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';
    $directoryIndex = $params['directory_index'] ?? 'index.php index.html';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $domain = strtolower(trim($domain));
    $vhostFile = "/etc/nginx/sites-available/{$domain}.conf";

    if (!file_exists($vhostFile)) {
        return ['success' => false, 'error' => 'Domain configuration not found'];
    }

    // Validate and sanitize directory index value
    $validIndexFiles = ['index.php', 'index.html', 'index.htm'];
    $indexParts = preg_split('/\s+/', trim($directoryIndex));
    $sanitizedParts = [];
    foreach ($indexParts as $part) {
        if (in_array($part, $validIndexFiles)) {
            $sanitizedParts[] = $part;
        }
    }

    if (empty($sanitizedParts)) {
        $sanitizedParts = ['index.php', 'index.html'];
    }

    $newIndex = implode(' ', $sanitizedParts);

    $vhostContent = file_get_contents($vhostFile);

    // Replace the existing index directive
    $vhostContent = preg_replace(
        '/(\s+index\s+)[^;]+;/',
        "$1{$newIndex};",
        $vhostContent
    );

    // Write updated vhost
    file_put_contents($vhostFile, $vhostContent);

    // Test and reload nginx
    exec("nginx -t 2>&1", $testOutput, $testCode);
    if ($testCode !== 0) {
        return ['success' => false, 'error' => 'Nginx configuration test failed: ' . implode("\n", $testOutput)];
    }

    exec("systemctl reload nginx 2>&1", $reloadOutput, $reloadCode);

    return [
        'success' => $reloadCode === 0,
        'message' => 'Directory index updated',
        'directory_index' => $newIndex,
    ];
}

/**
 * List protected directories for a domain
 */
function domainListProtectedDirs(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $domain = strtolower(trim($domain));
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }

    $userHome = $userInfo['dir'];
    $protectedDir = "{$userHome}/domains/{$domain}/.protected";

    $directories = [];

    if (is_dir($protectedDir)) {
        $files = glob("{$protectedDir}/*.conf");
        foreach ($files as $confFile) {
            $config = parse_ini_file($confFile);
            if ($config === false) {
                continue;
            }

            $path = $config['path'] ?? '';
            $name = $config['name'] ?? 'Protected Area';
            $htpasswdFile = "{$protectedDir}/" . md5($path) . ".htpasswd";

            $users = [];
            if (file_exists($htpasswdFile)) {
                $lines = file($htpasswdFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
                foreach ($lines as $line) {
                    if (strpos($line, ':') !== false) {
                        [$authUser] = explode(':', $line, 2);
                        $users[] = $authUser;
                    }
                }
            }

            $directories[] = [
                'path' => $path,
                'name' => $name,
                'users' => $users,
                'users_count' => count($users),
            ];
        }
    }

    return [
        'success' => true,
        'directories' => $directories,
    ];
}

/**
 * Add protection to a directory
 */
function domainAddProtectedDir(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';
    $path = $params['path'] ?? '';
    $name = $params['name'] ?? 'Protected Area';
    $authUsername = $params['auth_username'] ?? '';
    $authPassword = $params['auth_password'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    if (empty($path) || empty($authUsername) || empty($authPassword)) {
        return ['success' => false, 'error' => 'Missing required parameters'];
    }

    // Validate path - must start with / and not contain ..
    $path = '/' . ltrim($path, '/');
    if (strpos($path, '..') !== false || !preg_match('#^/[a-zA-Z0-9/_-]*$#', $path)) {
        return ['success' => false, 'error' => 'Invalid path'];
    }

    // Validate auth username
    if (!preg_match('/^[a-zA-Z0-9_]+$/', $authUsername)) {
        return ['success' => false, 'error' => 'Invalid username format'];
    }

    $domain = strtolower(trim($domain));
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }

    $userHome = $userInfo['dir'];
    $uid = $userInfo['uid'];
    $gid = $userInfo['gid'];
    $domainRoot = "{$userHome}/domains/{$domain}";
    $protectedDir = "{$domainRoot}/.protected";

    if (!is_dir($domainRoot)) {
        return ['success' => false, 'error' => 'Domain not found'];
    }

    // Create .protected directory
    if (!is_dir($protectedDir)) {
        mkdir($protectedDir, 0750, true);
        chown($protectedDir, $uid);
        chgrp($protectedDir, $gid);
        // Set ACL for nginx to read
        exec("setfacl -m u:www-data:rx " . escapeshellarg($protectedDir));
    }

    $pathHash = md5($path);
    $confFile = "{$protectedDir}/{$pathHash}.conf";
    $htpasswdFile = "{$protectedDir}/{$pathHash}.htpasswd";

    // Check if already protected
    if (file_exists($confFile)) {
        return ['success' => false, 'error' => 'This directory is already protected'];
    }

    // Create config file
    $configContent = "path=\"{$path}\"\nname=\"{$name}\"\n";
    file_put_contents($confFile, $configContent);
    chown($confFile, $uid);
    chgrp($confFile, $gid);
    chmod($confFile, 0640);

    // Create htpasswd file with first user
    $hashedPassword = password_hash($authPassword, PASSWORD_BCRYPT);
    file_put_contents($htpasswdFile, "{$authUsername}:{$hashedPassword}\n");
    chown($htpasswdFile, $uid);
    chgrp($htpasswdFile, $gid);
    chmod($htpasswdFile, 0640);
    // Set ACL for nginx to read htpasswd
    exec("setfacl -m u:www-data:r " . escapeshellarg($htpasswdFile));

    // Update nginx configuration
    $result = updateNginxProtectedDirs($domain, $domainRoot);
    if (!$result['success']) {
        // Rollback
        unlink($confFile);
        unlink($htpasswdFile);
        return $result;
    }

    return [
        'success' => true,
        'message' => "Directory {$path} is now protected",
    ];
}

/**
 * Remove protection from a directory
 */
function domainRemoveProtectedDir(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';
    $path = $params['path'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $domain = strtolower(trim($domain));
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }

    $userHome = $userInfo['dir'];
    $domainRoot = "{$userHome}/domains/{$domain}";
    $protectedDir = "{$domainRoot}/.protected";

    $pathHash = md5($path);
    $confFile = "{$protectedDir}/{$pathHash}.conf";
    $htpasswdFile = "{$protectedDir}/{$pathHash}.htpasswd";

    if (!file_exists($confFile)) {
        return ['success' => false, 'error' => 'Protected directory not found'];
    }

    // Remove files
    if (file_exists($confFile)) {
        unlink($confFile);
    }
    if (file_exists($htpasswdFile)) {
        unlink($htpasswdFile);
    }

    // Update nginx configuration
    $result = updateNginxProtectedDirs($domain, $domainRoot);

    return [
        'success' => true,
        'message' => "Protection removed from {$path}",
    ];
}

/**
 * Add a user to a protected directory
 */
function domainAddProtectedDirUser(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';
    $path = $params['path'] ?? '';
    $authUsername = $params['auth_username'] ?? '';
    $authPassword = $params['auth_password'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    if (empty($authUsername) || empty($authPassword)) {
        return ['success' => false, 'error' => 'Username and password are required'];
    }

    if (!preg_match('/^[a-zA-Z0-9_]+$/', $authUsername)) {
        return ['success' => false, 'error' => 'Invalid username format'];
    }

    $domain = strtolower(trim($domain));
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }

    $userHome = $userInfo['dir'];
    $uid = $userInfo['uid'];
    $gid = $userInfo['gid'];
    $domainRoot = "{$userHome}/domains/{$domain}";
    $protectedDir = "{$domainRoot}/.protected";

    $pathHash = md5($path);
    $htpasswdFile = "{$protectedDir}/{$pathHash}.htpasswd";

    if (!file_exists($htpasswdFile)) {
        return ['success' => false, 'error' => 'Protected directory not found'];
    }

    // Check if user already exists
    $lines = file($htpasswdFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    foreach ($lines as $line) {
        if (strpos($line, ':') !== false) {
            [$existingUser] = explode(':', $line, 2);
            if ($existingUser === $authUsername) {
                return ['success' => false, 'error' => 'User already exists'];
            }
        }
    }

    // Add new user
    $hashedPassword = password_hash($authPassword, PASSWORD_BCRYPT);
    file_put_contents($htpasswdFile, "{$authUsername}:{$hashedPassword}\n", FILE_APPEND);
    chown($htpasswdFile, $uid);
    chgrp($htpasswdFile, $gid);

    return [
        'success' => true,
        'message' => "User {$authUsername} added",
    ];
}

/**
 * Remove a user from a protected directory
 */
function domainRemoveProtectedDirUser(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';
    $path = $params['path'] ?? '';
    $authUsername = $params['auth_username'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $domain = strtolower(trim($domain));
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }

    $userHome = $userInfo['dir'];
    $uid = $userInfo['uid'];
    $gid = $userInfo['gid'];
    $domainRoot = "{$userHome}/domains/{$domain}";
    $protectedDir = "{$domainRoot}/.protected";

    $pathHash = md5($path);
    $htpasswdFile = "{$protectedDir}/{$pathHash}.htpasswd";

    if (!file_exists($htpasswdFile)) {
        return ['success' => false, 'error' => 'Protected directory not found'];
    }

    // Remove user from htpasswd file
    $lines = file($htpasswdFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    $newLines = [];
    $found = false;

    foreach ($lines as $line) {
        if (strpos($line, ':') !== false) {
            [$existingUser] = explode(':', $line, 2);
            if ($existingUser === $authUsername) {
                $found = true;
                continue;
            }
        }
        $newLines[] = $line;
    }

    if (!$found) {
        return ['success' => false, 'error' => 'User not found'];
    }

    file_put_contents($htpasswdFile, implode("\n", $newLines) . (count($newLines) > 0 ? "\n" : ""));
    chown($htpasswdFile, $uid);
    chgrp($htpasswdFile, $gid);

    return [
        'success' => true,
        'message' => "User {$authUsername} removed",
    ];
}

/**
 * Update nginx configuration for protected directories
 */
function updateNginxProtectedDirs(string $domain, string $domainRoot): array
{
    $vhostFile = "/etc/nginx/sites-available/{$domain}.conf";
    if (!file_exists($vhostFile)) {
        return ['success' => false, 'error' => 'Domain nginx configuration not found'];
    }

    $protectedDir = "{$domainRoot}/.protected";
    $vhostContent = file_get_contents($vhostFile);

    // Remove existing protected directory blocks
    $vhostContent = preg_replace('/\n\s*# Protected directory:[^\n]*\n\s*location[^}]+auth_basic[^}]+\}\n?/s', '', $vhostContent);

    // Build new protected directory blocks
    $protectedBlocks = '';
    if (is_dir($protectedDir)) {
        $files = glob("{$protectedDir}/*.conf");
        foreach ($files as $confFile) {
            $config = parse_ini_file($confFile);
            if ($config === false) {
                continue;
            }

            $path = $config['path'] ?? '';
            $name = addslashes($config['name'] ?? 'Protected Area');
            $pathHash = md5($path);
            $htpasswdFile = "{$protectedDir}/{$pathHash}.htpasswd";

            if (!file_exists($htpasswdFile)) {
                continue;
            }

            // Escape path for nginx location
            $escapedPath = preg_quote($path, '/');

            $protectedBlocks .= <<<NGINX

    # Protected directory: {$path}
    location ^~ {$path} {
        auth_basic "{$name}";
        auth_basic_user_file {$htpasswdFile};
        try_files \$uri \$uri/ /index.php?\$query_string;
    }
NGINX;
        }
    }

    // Insert protected blocks before the deny hidden files location
    if (!empty($protectedBlocks)) {
        if (preg_match('/(location\s+~\s+\/\\\\\.)/s', $vhostContent, $matches)) {
            $vhostContent = str_replace($matches[0], $protectedBlocks . "\n\n    " . $matches[0], $vhostContent);
        }
    }

    // Write updated vhost
    file_put_contents($vhostFile, $vhostContent);

    // Test nginx configuration
    exec("nginx -t 2>&1", $testOutput, $testCode);
    if ($testCode !== 0) {
        return ['success' => false, 'error' => 'Nginx configuration test failed: ' . implode("\n", $testOutput)];
    }

    // Reload nginx
    exec("systemctl reload nginx 2>&1", $reloadOutput, $reloadCode);

    return [
        'success' => $reloadCode === 0,
        'error' => $reloadCode !== 0 ? 'Failed to reload nginx' : null,
    ];
}


main();

// ============ REDIS ACL MANAGEMENT ============

/**
 * Get Redis admin credentials from config file
 */
function getRedisAdminCredentials(): array
{
    $credFile = '/root/.jabali_redis_credentials';
    if (!file_exists($credFile)) {
        return ['error' => 'Redis credentials file not found'];
    }

    $content = file_get_contents($credFile);
    $password = '';

    if (preg_match('/REDIS_ADMIN_PASSWORD=(.+)/', $content, $matches)) {
        $password = trim($matches[1]);
    }

    if (empty($password)) {
        return ['error' => 'Redis admin password not found'];
    }

    return ['username' => 'jabali_admin', 'password' => $password];
}

/**
 * Execute Redis ACL command
 */
function redisAclCommand(string $command): array
{
    $creds = getRedisAdminCredentials();
    if (isset($creds['error'])) {
        return ['success' => false, 'error' => $creds['error']];
    }

    $escapedPassword = escapeshellarg($creds['password']);

    // Use echo to pipe the command to redis-cli to avoid shell argument parsing issues
    $escapedCommand = escapeshellarg($command);
    $cmd = "echo {$escapedCommand} | redis-cli --user {$creds['username']} -a {$escapedPassword} --no-auth-warning 2>&1";
    $output = [];
    $returnCode = 0;
    exec($cmd, $output, $returnCode);

    return [
        'success' => $returnCode === 0,
        'output' => implode("\n", $output),
        'return_code' => $returnCode
    ];
}

/**
 * Create a Redis ACL user for a Jabali user
 */
function redisCreateUser(array $params): array
{
    $username = $params['username'] ?? '';
    $password = $params['password'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    if (empty($password) || strlen($password) < 16) {
        return ['success' => false, 'error' => 'Password must be at least 16 characters'];
    }

    // Redis ACL user will be prefixed with 'jabali_'
    $redisUser = 'jabali_' . $username;

    // Check if user already exists
    $checkResult = redisAclCommand("ACL GETUSER {$redisUser}");
    if ($checkResult['success'] && strpos($checkResult['output'], 'ERR') === false) {
        return ['success' => false, 'error' => 'Redis user already exists'];
    }

    // Create user with key pattern restriction (only keys prefixed with username:)
    // Permissions: all commands except admin commands, restricted to keys matching pattern
    $keyPattern = $username . ':*';
    $aclRule = "ACL SETUSER {$redisUser} on >{$password} ~{$keyPattern} +@all -@admin -@dangerous";

    $result = redisAclCommand($aclRule);
    if (!$result['success'] || strpos($result['output'], 'ERR') !== false) {
        return ['success' => false, 'error' => 'Failed to create Redis user: ' . $result['output']];
    }

    // Save ACL to disk
    $saveResult = redisAclCommand("ACL SAVE");
    if (!$saveResult['success']) {
        // User was created but not saved - log warning but don't fail
        error_log("Warning: Redis ACL created but not saved to disk");
    }

    return [
        'success' => true,
        'message' => 'Redis user created',
        'redis_user' => $redisUser,
        'key_prefix' => $username . ':'
    ];
}

/**
 * Delete a Redis ACL user
 */
function redisDeleteUser(array $params): array
{
    $username = $params['username'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $redisUser = 'jabali_' . $username;

    // Check if user exists first
    $checkResult = redisAclCommand("ACL GETUSER {$redisUser}");
    if (!$checkResult['success'] || strpos($checkResult['output'], 'ERR') !== false) {
        return ['success' => false, 'error' => 'Redis user does not exist'];
    }

    // Delete all keys with user's prefix first
    $keyPattern = $username . ':*';
    $creds = getRedisAdminCredentials();
    if (!isset($creds['error'])) {
        $escapedPassword = escapeshellarg($creds['password']);
        exec("redis-cli --user {$creds['username']} -a {$escapedPassword} --no-auth-warning --scan --pattern " . escapeshellarg($keyPattern) . " | xargs -r redis-cli --user {$creds['username']} -a {$escapedPassword} --no-auth-warning DEL 2>/dev/null");
    }

    // Delete user
    $result = redisAclCommand("ACL DELUSER {$redisUser}");
    if (!$result['success'] || strpos($result['output'], 'ERR') !== false) {
        return ['success' => false, 'error' => 'Failed to delete Redis user: ' . $result['output']];
    }

    // Save ACL to disk
    redisAclCommand("ACL SAVE");

    return ['success' => true, 'message' => 'Redis user deleted'];
}

/**
 * Check if a Redis ACL user exists
 */
function redisUserExists(array $params): array
{
    $username = $params['username'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $redisUser = 'jabali_' . $username;

    $result = redisAclCommand("ACL GETUSER {$redisUser}");
    $exists = $result['success'] && strpos($result['output'], 'ERR') === false;

    return [
        'success' => true,
        'exists' => $exists,
        'redis_user' => $redisUser
    ];
}

/**
 * Change Redis user password
 */
function redisChangePassword(array $params): array
{
    $username = $params['username'] ?? '';
    $password = $params['password'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    if (empty($password) || strlen($password) < 16) {
        return ['success' => false, 'error' => 'Password must be at least 16 characters'];
    }

    $redisUser = 'jabali_' . $username;

    // Check if user exists
    $checkResult = redisAclCommand("ACL GETUSER {$redisUser}");
    if (!$checkResult['success'] || strpos($checkResult['output'], 'ERR') !== false) {
        return ['success' => false, 'error' => 'Redis user does not exist'];
    }

    // Reset passwords and set new one
    $result = redisAclCommand("ACL SETUSER {$redisUser} resetpass >{$password}");
    if (!$result['success'] || strpos($result['output'], 'ERR') !== false) {
        return ['success' => false, 'error' => 'Failed to change password: ' . $result['output']];
    }

    // Save ACL to disk
    redisAclCommand("ACL SAVE");

    return ['success' => true, 'message' => 'Redis password changed'];
}

/**
 * Migrate existing Jabali users to have Redis ACL users
 * Can migrate all users or a specific user
 */
function redisMigrateUsers(array $params): array
{
    $specificUser = $params['username'] ?? '';
    $migrated = [];
    $skipped = [];
    $failed = [];

    // Get list of users to migrate
    $users = [];
    if (!empty($specificUser)) {
        // Migrate specific user
        if (!validateUsername($specificUser)) {
            return ['success' => false, 'error' => 'Invalid username'];
        }
        $users = [$specificUser];
    } else {
        // Migrate all users in /home
        $homeDirs = glob('/home/*', GLOB_ONLYDIR);
        foreach ($homeDirs as $homeDir) {
            $username = basename($homeDir);
            // Skip system/protected users
            if (validateUsername($username) && !isProtectedUser($username)) {
                // Check if it's actually a Jabali user (has domains directory)
                if (is_dir("{$homeDir}/domains")) {
                    $users[] = $username;
                }
            }
        }
    }

    foreach ($users as $username) {
        $homeDir = "/home/{$username}";
        $redisCredFile = "{$homeDir}/.redis_credentials";

        // Check if user already has Redis credentials
        if (file_exists($redisCredFile)) {
            $skipped[] = ['username' => $username, 'reason' => 'Already has Redis credentials'];
            continue;
        }

        // Check if Redis user already exists
        $existsResult = redisUserExists(['username' => $username]);
        if ($existsResult['success'] && $existsResult['exists']) {
            $skipped[] = ['username' => $username, 'reason' => 'Redis user already exists'];
            continue;
        }

        // Create Redis ACL user
        $redisPassword = bin2hex(random_bytes(16)); // 32 char password
        $createResult = redisCreateUser(['username' => $username, 'password' => $redisPassword]);

        if (!$createResult['success']) {
            $failed[] = ['username' => $username, 'error' => $createResult['error'] ?? 'Unknown error'];
            continue;
        }

        // Store Redis credentials in user's home directory
        $credContent = "REDIS_USER=jabali_{$username}\n" .
                       "REDIS_PASS={$redisPassword}\n" .
                       "REDIS_PREFIX={$username}:\n";
        file_put_contents($redisCredFile, $credContent);
        chmod($redisCredFile, 0600);

        // Get user's UID/GID
        $userInfo = posix_getpwnam($username);
        if ($userInfo) {
            chown($redisCredFile, $userInfo['uid']);
            chgrp($redisCredFile, $userInfo['gid']);
        }

        $migrated[] = ['username' => $username, 'redis_user' => "jabali_{$username}"];
        logger("Migrated Redis ACL for user: {$username}");
    }

    return [
        'success' => true,
        'message' => sprintf('Migration complete: %d migrated, %d skipped, %d failed',
            count($migrated), count($skipped), count($failed)),
        'migrated' => $migrated,
        'skipped' => $skipped,
        'failed' => $failed,
    ];
}

// ============ WORDPRESS MANAGEMENT ============

function wpInstall(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';
    $siteTitle = $params['site_title'] ?? 'My WordPress Site';
    $adminUser = $params['admin_user'] ?? 'admin';
    $adminPassword = $params['admin_password'] ?? '';
    $adminEmail = $params['admin_email'] ?? '';
    $path = $params['path'] ?? ''; // Subdirectory, empty for root
    $useWww = $params['use_www'] ?? false;
    $language = $params['language'] ?? 'en_US';
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }
    
    $uid = $userInfo['uid'];
    $gid = $userInfo['gid'];
    $userHome = $userInfo['dir'];
    
    // Determine install path
    $docRoot = "{$userHome}/domains/{$domain}/public_html";
    $installPath = $path ? "{$docRoot}/{$path}" : $docRoot;
    
    if (!is_dir($docRoot)) {
        return ['success' => false, 'error' => 'Domain not found. Please create the domain first.'];
    }
    
    // Create subdirectory if needed
    if ($path && !is_dir($installPath)) {
        mkdir($installPath, 0755, true);
        chown($installPath, $uid);
        chgrp($installPath, $gid);
    }
    
    // Check if WordPress already installed
    if (file_exists("{$installPath}/wp-config.php")) {
        return ['success' => false, 'error' => 'WordPress is already installed at this location'];
    }
    
    // Generate database credentials
    $dbName = $username . '_wp' . substr(md5($domain . $path . time()), 0, 6);
    $dbUser = $dbName;
    $dbPass = bin2hex(random_bytes(12));
    
    // Truncate to MySQL limits
    $dbName = substr($dbName, 0, 64);
    $dbUser = substr($dbUser, 0, 32);
    
    // Create database and user
    $conn = getMysqlConnection();
    if (!$conn) {
        return ['success' => false, 'error' => 'Cannot connect to MySQL'];
    }
    
    $dbName = $conn->real_escape_string($dbName);
    $dbUser = $conn->real_escape_string($dbUser);
    
    // Create database
    if (!$conn->query("CREATE DATABASE IF NOT EXISTS `{$dbName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")) {
        $conn->close();
        return ['success' => false, 'error' => 'Failed to create database: ' . $conn->error];
    }
    
    // Create user and grant privileges
    $conn->query("DROP USER IF EXISTS '{$dbUser}'@'localhost'");
    if (!$conn->query("CREATE USER '{$dbUser}'@'localhost' IDENTIFIED BY '{$dbPass}'")) {
        $conn->close();
        return ['success' => false, 'error' => 'Failed to create database user: ' . $conn->error];
    }
    
    if (!$conn->query("GRANT ALL PRIVILEGES ON `{$dbName}`.* TO '{$dbUser}'@'localhost'")) {
        $conn->close();
        return ['success' => false, 'error' => 'Failed to grant privileges: ' . $conn->error];
    }
    
    $conn->query("FLUSH PRIVILEGES");
    $conn->close();
    
    // Download WordPress (always in English first for reliability)
    $cmd = "cd " . escapeshellarg($installPath) . " && sudo -u " . escapeshellarg($username) . " wp core download 2>&1";
    exec($cmd, $output, $returnCode);
    
    if ($returnCode !== 0) {
        return ['success' => false, 'error' => 'Failed to download WordPress: ' . implode("\n", $output)];
    }
    
    // Create wp-config.php
    $urlDomain = $useWww ? "www.{$domain}" : $domain;
    $siteUrl = "https://{$urlDomain}" . ($path ? "/{$path}" : "");
    $cmd = "cd " . escapeshellarg($installPath) . " && sudo -u " . escapeshellarg($username) . " wp config create " .
           "--dbname=" . escapeshellarg($dbName) . " " .
           "--dbuser=" . escapeshellarg($dbUser) . " " .
           "--dbpass=" . escapeshellarg($dbPass) . " " .
           "--dbhost=localhost " .
           "--dbcharset=utf8mb4 2>&1";
    exec($cmd, $output2, $returnCode2);
    
    if ($returnCode2 !== 0) {
        return ['success' => false, 'error' => 'Failed to create wp-config.php: ' . implode("\n", $output2)];
    }

    // Add Redis cache configuration
    $redisDb = abs(crc32($domain)) % 16;
    $redisUser = 'wp_' . substr(md5($domain), 0, 8);
    $redisPass = bin2hex(random_bytes(16));
    $prefixBase = $domain . ($path ? '_' . $path : '');
    $redisPrefix = 'jc_' . preg_replace('/[^a-z0-9]/', '_', strtolower($prefixBase)) . '_';

    // Create Redis user with ACL (if Redis ACL is available)
    $redisCredsFile = '/root/.jabali_redis_credentials';
    if (file_exists($redisCredsFile)) {
        // Get admin credentials
        $redisCreds = parse_ini_file($redisCredsFile);
        $adminPass = $redisCreds['REDIS_ADMIN_PASSWORD'] ?? null;

        if ($adminPass) {
            // Connect to Redis and create user
            $redis = new Redis();
            try {
                $redis->connect('127.0.0.1', 6379);
                $redis->auth(['user' => 'jabali_admin', 'pass' => $adminPass]);

                // Create user with access only to their database and key prefix
                // ACL: on (enabled), password, ~{prefix}* (key pattern), +@all (all commands)
                $aclRule = "on >{$redisPass} ~{$redisPrefix}* +@all";
                $redis->rawCommand('ACL', 'SETUSER', $redisUser, ...explode(' ', $aclRule));
                $redis->rawCommand('ACL', 'SAVE');
                $redis->close();
            } catch (Exception $e) {
                // Redis ACL not available or failed, will use without auth
                $redisUser = '';
                $redisPass = '';
            }
        }
    } else {
        // No Redis credentials file, skip authentication
        $redisUser = '';
        $redisPass = '';
    }

    $wpConfigFile = "{$installPath}/wp-config.php";
    $wpConfig = file_get_contents($wpConfigFile);

    // Add cache constants before "That's all, stop editing!"
    $cacheConstants = "\n/** Jabali Cache - Redis configuration */\n";
    $cacheConstants .= "define('JABALI_CACHE_DATABASE', {$redisDb});\n";
    $cacheConstants .= "define('JABALI_CACHE_PREFIX', '{$redisPrefix}');\n";
    if ($redisUser && $redisPass) {
        $cacheConstants .= "define('JABALI_CACHE_REDIS_USER', '{$redisUser}');\n";
        $cacheConstants .= "define('JABALI_CACHE_REDIS_PASS', '{$redisPass}');\n";
    }

    $wpConfig = str_replace(
        "/* That's all, stop editing!",
        $cacheConstants . "\n/* That's all, stop editing!",
        $wpConfig
    );
    file_put_contents($wpConfigFile, $wpConfig);
    chown($wpConfigFile, $uid);
    chgrp($wpConfigFile, $gid);

    // Install WordPress
    $cmd = "cd " . escapeshellarg($installPath) . " && sudo -u " . escapeshellarg($username) . " wp core install " .
           "--url=" . escapeshellarg($siteUrl) . " " .
           "--title=" . escapeshellarg($siteTitle) . " " .
           "--admin_user=" . escapeshellarg($adminUser) . " " .
           "--admin_password=" . escapeshellarg($adminPassword) . " " .
           "--admin_email=" . escapeshellarg($adminEmail) . " " .
           "--skip-email 2>&1";
    exec($cmd, $output3, $returnCode3);
    
    if ($returnCode3 !== 0) {
        return ['success' => false, 'error' => 'Failed to install WordPress: ' . implode("\n", $output3)];
    }
    
    // Install language pack if not English
    if ($language && $language !== 'en_US') {
        $cmd = "cd " . escapeshellarg($installPath) . " && sudo -u " . escapeshellarg($username) . " wp language core install " . escapeshellarg($language) . " 2>&1";
        exec($cmd, $langOutput, $langReturnCode);
        
        if ($langReturnCode === 0) {
            // Switch site language
            $cmd = "cd " . escapeshellarg($installPath) . " && sudo -u " . escapeshellarg($username) . " wp option update WPLANG " . escapeshellarg($language) . " 2>&1";
            exec($cmd);
        }
        // If language install fails, continue anyway - site will be in English
    }
    
    // Set proper permissions
    exec("chown -R " . escapeshellarg($username) . ":" . escapeshellarg($username) . " " . escapeshellarg($installPath));
    exec("find " . escapeshellarg($installPath) . " -type d -exec chmod 755 {} \\;");
    exec("find " . escapeshellarg($installPath) . " -type f -exec chmod 644 {} \\;");
    
    // Store installation info
    $wpListFile = "{$userHome}/.wordpress_sites";
    $wpSites = [];
    if (file_exists($wpListFile)) {
        $wpSites = json_decode(file_get_contents($wpListFile), true) ?: [];
    }
    
    $siteId = md5($domain . $path);
    
    // Get version and counts
    $wpVersion = trim((string)shell_exec("cd " . escapeshellarg($installPath) . " && sudo -u " . escapeshellarg($username) . " wp core version 2>/dev/null") ?: 'Unknown');
    $pluginCount = (int) trim((string)shell_exec("cd " . escapeshellarg($installPath) . " && sudo -u " . escapeshellarg($username) . " wp plugin list --format=count 2>/dev/null") ?: '0');
    $themeCount = (int) trim((string)shell_exec("cd " . escapeshellarg($installPath) . " && sudo -u " . escapeshellarg($username) . " wp theme list --format=count 2>/dev/null") ?: '0');
    
    $wpSites[$siteId] = [
        'domain' => $domain,
        'path' => $path,
        'install_path' => $installPath,
        'url' => $siteUrl,
        'admin_user' => $adminUser,
        'admin_email' => $adminEmail,
        'db_name' => $dbName,
        'db_user' => $dbUser,
        'installed_at' => date('Y-m-d H:i:s'),
        'version' => $wpVersion,
        'plugin_count' => $pluginCount,
        'theme_count' => $themeCount
    ];
    
    file_put_contents($wpListFile, json_encode($wpSites, JSON_PRETTY_PRINT));
    chown($wpListFile, $uid);
    chgrp($wpListFile, $gid);

    // Enable nginx page cache by default for all WordPress installations
    // This is server-level caching that works independently of WordPress caching plugins
    try {
        wpPageCacheEnable(['username' => $username, 'domain' => $domain, 'site_id' => $siteId]);
        logger("Enabled nginx page cache for WordPress site {$domain}");
    } catch (Exception $e) {
        // Page cache enable failed, but installation succeeded - continue
        logger("Warning: Failed to enable page cache for {$domain}: " . $e->getMessage());
    }

    return [
        'success' => true,
        'site_id' => $siteId,
        'url' => $siteUrl,
        'admin_url' => "{$siteUrl}/wp-admin/",
        'admin_user' => $adminUser,
        'admin_password' => $adminPassword,
        'db_name' => $dbName,
        'db_user' => $dbUser,
        'db_password' => $dbPass,
        'message' => 'WordPress installed successfully'
    ];
}

function wpList(array $params): array
{
    $username = $params['username'] ?? '';
    $refresh = $params['refresh'] ?? false;
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }
    
    $userHome = $userInfo['dir'];
    $wpListFile = "{$userHome}/.wordpress_sites";
    
    $wpSites = [];
    if (file_exists($wpListFile)) {
        $wpSites = json_decode(file_get_contents($wpListFile), true) ?: [];
    }
    
    $sites = [];
    foreach ($wpSites as $siteId => $site) {
        $installPath = $site['install_path'] ?? '';
        
        // Check if still exists
        if (!$installPath || !file_exists("{$installPath}/wp-config.php")) {
            continue;
        }
        
        // Check if Jabali Cache plugin is active
        $cacheEnabled = false;
        $activePluginsFile = "{$installPath}/wp-content/options-active_plugins.php";
        if (file_exists($activePluginsFile)) {
            // Check serialized active plugins
            $content = @file_get_contents($activePluginsFile);
            if ($content && strpos($content, 'jabali-cache') !== false) {
                $cacheEnabled = true;
            }
        }
        // Alternative: check if object-cache.php drop-in exists (indicates cache is active)
        if (!$cacheEnabled && file_exists("{$installPath}/wp-content/object-cache.php")) {
            $content = @file_get_contents("{$installPath}/wp-content/object-cache.php");
            if ($content && strpos($content, 'Jabali Cache') !== false) {
                $cacheEnabled = true;
            }
        }
        // Also check if plugin directory exists and is linked
        if (!$cacheEnabled && file_exists("{$installPath}/wp-content/plugins/jabali-cache/jabali-cache.php")) {
            // Plugin exists, check if active via wp-cli or database
            // For now, assume if drop-in doesn't exist but plugin does, cache is disabled
            $cacheEnabled = false;
        }

        // Check for warnings (conflicting plugins, security issues, etc.)
        $warnings = [];

        // Check for known conflicting caching plugins
        $conflictingPlugins = [
            'w3-total-cache', 'wp-super-cache', 'wp-fastest-cache', 'litespeed-cache',
            'wp-rocket', 'redis-cache', 'autoptimize', 'wp-optimize', 'hummingbird-performance',
            'sg-cachepress', 'breeze', 'cache-enabler', 'comet-cache', 'powered-cache', 'hyper-cache'
        ];

        $pluginsDir = "{$installPath}/wp-content/plugins";
        if (is_dir($pluginsDir)) {
            foreach ($conflictingPlugins as $conflictPlugin) {
                if (is_dir("{$pluginsDir}/{$conflictPlugin}")) {
                    $warnings[] = [
                        'type' => 'conflict',
                        'message' => "Conflicting cache plugin detected: {$conflictPlugin}",
                        'severity' => 'warning',
                    ];
                }
            }
        }

        // Check debug and auto-update status from wp-config.php
        $debugEnabled = false;
        $autoUpdate = $site['auto_update'] ?? false;
        $wpConfigContent = @file_get_contents("{$installPath}/wp-config.php");
        if ($wpConfigContent) {
            if (preg_match("/define\s*\(\s*['\"]WP_DEBUG['\"]\s*,\s*true\s*\)/i", $wpConfigContent)) {
                $debugEnabled = true;
            }
            if (preg_match("/define\s*\(\s*['\"]AUTOMATIC_UPDATER_DISABLED['\"]\s*,\s*false\s*\)/i", $wpConfigContent)) {
                $autoUpdate = true;
            } elseif (preg_match("/define\s*\(\s*['\"]WP_AUTO_UPDATE_CORE['\"]\s*,\s*true\s*\)/i", $wpConfigContent)) {
                $autoUpdate = true;
            }
        }

        $sites[] = [
            'id' => $siteId,
            'domain' => $site['domain'] ?? '',
            'path' => $site['path'] ?? '',
            'url' => $site['url'] ?? '',
            'admin_url' => ($site['url'] ?? '') . '/wp-admin/',
            'admin_user' => $site['admin_user'] ?? 'admin',
            'admin_email' => $site['admin_email'] ?? '',
            'version' => $site['version'] ?? 'Unknown',
            'update_available' => false,
            'plugin_count' => $site['plugin_count'] ?? 0,
            'theme_count' => $site['theme_count'] ?? 0,
            'installed_at' => $site['installed_at'] ?? '',
            'db_name' => $site['db_name'] ?? '',
            'cache_enabled' => $cacheEnabled,
            'debug_enabled' => $debugEnabled,
            'auto_update' => $autoUpdate,
            'is_staging' => $site['is_staging'] ?? false,
            'warnings' => $warnings,
        ];
    }

    return ['success' => true, 'sites' => $sites];
}

/**
 * Scan user's home folder for WordPress installations that aren't tracked yet
 */
function wpScan(array $params): array
{
    $username = $params['username'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }

    $userHome = $userInfo['dir'];
    $wpListFile = "{$userHome}/.wordpress_sites";

    // Get tracked sites
    $trackedSites = [];
    if (file_exists($wpListFile)) {
        $trackedSites = json_decode(file_get_contents($wpListFile), true) ?: [];
    }

    // Build list of already tracked paths
    $trackedPaths = [];
    foreach ($trackedSites as $site) {
        if (isset($site['install_path'])) {
            $trackedPaths[] = realpath($site['install_path']);
        }
    }

    // Scan for wp-config.php files in common locations
    $scanDirs = [
        $userHome,
        "{$userHome}/public_html",
        "{$userHome}/www",
        "{$userHome}/sites",
    ];

    // Also scan domain directories
    $domainDirs = glob("{$userHome}/domains/*/public_html");
    $scanDirs = array_merge($scanDirs, $domainDirs);

    $found = [];

    foreach ($scanDirs as $scanDir) {
        if (!is_dir($scanDir)) {
            continue;
        }

        // Find wp-config.php files (max depth 3)
        $cmd = sprintf(
            'find %s -maxdepth 3 -name "wp-config.php" -type f 2>/dev/null',
            escapeshellarg($scanDir)
        );
        $output = [];
        exec($cmd, $output);

        foreach ($output as $configFile) {
            $wpDir = dirname($configFile);
            $realPath = realpath($wpDir);

            // Skip if already tracked
            if (in_array($realPath, $trackedPaths)) {
                continue;
            }

            // Verify it's a valid WordPress installation
            if (!file_exists("{$wpDir}/wp-includes/version.php")) {
                continue;
            }

            // Get WordPress version
            $version = 'Unknown';
            $versionFile = "{$wpDir}/wp-includes/version.php";
            if (file_exists($versionFile)) {
                $content = file_get_contents($versionFile);
                if (preg_match('/\$wp_version\s*=\s*[\'"]([^\'"]+)[\'"]/i', $content, $m)) {
                    $version = $m[1];
                }
            }

            // Try to get site URL and DB info from wp-config.php
            $configContent = file_get_contents($configFile);
            $dbName = '';
            $dbUser = '';
            $tablePrefix = 'wp_';
            $siteUrl = '';

            if (preg_match('/define\s*\(\s*[\'"]DB_NAME[\'"]\s*,\s*[\'"]([^\'"]+)[\'"]\s*\)/i', $configContent, $m)) {
                $dbName = $m[1];
            }
            if (preg_match('/define\s*\(\s*[\'"]DB_USER[\'"]\s*,\s*[\'"]([^\'"]+)[\'"]\s*\)/i', $configContent, $m)) {
                $dbUser = $m[1];
            }
            if (preg_match('/\$table_prefix\s*=\s*[\'"]([^\'"]+)[\'"]/i', $configContent, $m)) {
                $tablePrefix = $m[1];
            }
            if (preg_match('/define\s*\(\s*[\'"]WP_SITEURL[\'"]\s*,\s*[\'"]([^\'"]+)[\'"]\s*\)/i', $configContent, $m)) {
                $siteUrl = $m[1];
            } elseif (preg_match('/define\s*\(\s*[\'"]WP_HOME[\'"]\s*,\s*[\'"]([^\'"]+)[\'"]\s*\)/i', $configContent, $m)) {
                $siteUrl = $m[1];
            }

            // Try to get site URL from database if not in config
            if (empty($siteUrl) && !empty($dbName)) {
                $siteUrl = wpGetSiteUrlFromDb($dbName, $tablePrefix);
            }

            // Determine relative path from home
            $relativePath = str_replace($userHome, '', $wpDir);
            $relativePath = ltrim($relativePath, '/');

            $found[] = [
                'path' => $wpDir,
                'relative_path' => $relativePath,
                'version' => $version,
                'site_url' => $siteUrl,
                'db_name' => $dbName,
                'db_user' => $dbUser,
                'table_prefix' => $tablePrefix,
            ];
        }
    }

    // Remove duplicates by path
    $uniqueFound = [];
    $seenPaths = [];
    foreach ($found as $site) {
        $realPath = realpath($site['path']);
        if (!in_array($realPath, $seenPaths)) {
            $seenPaths[] = $realPath;
            $uniqueFound[] = $site;
        }
    }

    return ['success' => true, 'found' => $uniqueFound, 'count' => count($uniqueFound)];
}

/**
 * Helper to get site URL from WordPress database
 */
function wpGetSiteUrlFromDb(string $dbName, string $tablePrefix): string
{
    try {
        $conn = getMysqlConnection();
        if (!$conn) {
            return '';
        }

        // Select the WordPress database
        if (!$conn->select_db($dbName)) {
            $conn->close();
            return '';
        }

        $table = $conn->real_escape_string($tablePrefix . 'options');
        $result = $conn->query("SELECT option_value FROM {$table} WHERE option_name = 'siteurl' LIMIT 1");
        if ($result && $row = $result->fetch_assoc()) {
            $siteUrl = $row['option_value'];
            $conn->close();
            return $siteUrl;
        }
        $conn->close();
    } catch (Throwable $e) {
        // Silently fail
    }
    return '';
}

/**
 * Import a found WordPress installation into tracking
 */
function wpImport(array $params): array
{
    $username = $params['username'] ?? '';
    $path = $params['path'] ?? '';
    $domain = $params['domain'] ?? '';
    $sitePath = $params['site_path'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }

    $userHome = $userInfo['dir'];

    // Validate the path is within user's home
    $realPath = realpath($path);
    if (!$realPath || strpos($realPath, $userHome) !== 0) {
        return ['success' => false, 'error' => 'Invalid path'];
    }

    // Check it's a valid WordPress installation
    if (!file_exists("{$realPath}/wp-config.php") || !file_exists("{$realPath}/wp-includes/version.php")) {
        return ['success' => false, 'error' => 'Not a valid WordPress installation'];
    }

    // Get WordPress version
    $version = 'Unknown';
    $versionFile = "{$realPath}/wp-includes/version.php";
    if (file_exists($versionFile)) {
        $content = file_get_contents($versionFile);
        if (preg_match('/\$wp_version\s*=\s*[\'"]([^\'"]+)[\'"]/i', $content, $m)) {
            $version = $m[1];
        }
    }

    // Get DB info from wp-config.php
    $configContent = file_get_contents("{$realPath}/wp-config.php");
    $dbName = '';
    $dbUser = '';
    $tablePrefix = 'wp_';

    if (preg_match('/define\s*\(\s*[\'"]DB_NAME[\'"]\s*,\s*[\'"]([^\'"]+)[\'"]\s*\)/i', $configContent, $m)) {
        $dbName = $m[1];
    }
    if (preg_match('/define\s*\(\s*[\'"]DB_USER[\'"]\s*,\s*[\'"]([^\'"]+)[\'"]\s*\)/i', $configContent, $m)) {
        $dbUser = $m[1];
    }
    if (preg_match('/\$table_prefix\s*=\s*[\'"]([^\'"]+)[\'"]/i', $configContent, $m)) {
        $tablePrefix = $m[1];
    }

    // Get site URL - check wp-config.php first, then fall back to database
    $siteUrl = '';
    if (!empty($domain)) {
        $siteUrl = 'https://' . $domain . ($sitePath ? '/' . ltrim($sitePath, '/') : '');
    } else {
        // First try WP_SITEURL from wp-config.php
        if (preg_match('/define\s*\(\s*[\'"]WP_SITEURL[\'"]\s*,\s*[\'"]([^\'"]+)[\'"]\s*\)/i', $configContent, $m)) {
            $siteUrl = $m[1];
        }
        // Then try WP_HOME from wp-config.php
        elseif (preg_match('/define\s*\(\s*[\'"]WP_HOME[\'"]\s*,\s*[\'"]([^\'"]+)[\'"]\s*\)/i', $configContent, $m)) {
            $siteUrl = $m[1];
        }
        // Fall back to database only if not found in wp-config.php
        if (empty($siteUrl) && !empty($dbName)) {
            $siteUrl = wpGetSiteUrlFromDb($dbName, $tablePrefix);
        }
    }

    // Load existing sites
    $wpListFile = "{$userHome}/.wordpress_sites";
    $wpSites = [];
    if (file_exists($wpListFile)) {
        $wpSites = json_decode(file_get_contents($wpListFile), true) ?: [];
    }

    // Generate unique site ID
    $siteId = 'wp_' . substr(md5($realPath . time()), 0, 12);

    // Add new site
    $wpSites[$siteId] = [
        'domain' => $domain ?: parse_url($siteUrl, PHP_URL_HOST) ?: 'unknown',
        'path' => $sitePath,
        'url' => $siteUrl,
        'admin_user' => 'admin',
        'admin_email' => '',
        'version' => $version,
        'install_path' => $realPath,
        'installed_at' => date('Y-m-d H:i:s'),
        'db_name' => $dbName,
        'db_user' => $dbUser,
        'db_prefix' => $tablePrefix,
        'imported' => true,
    ];

    // Save
    file_put_contents($wpListFile, json_encode($wpSites, JSON_PRETTY_PRINT));
    chown($wpListFile, $userInfo['uid']);
    chgrp($wpListFile, $userInfo['gid']);
    chmod($wpListFile, 0600);

    logger("Imported WordPress site at {$realPath} for user {$username}");

    // Enable nginx page cache by default for imported WordPress sites
    $importedDomain = $wpSites[$siteId]['domain'] ?? '';
    if ($importedDomain && $importedDomain !== 'unknown') {
        try {
            wpPageCacheEnable(['username' => $username, 'domain' => $importedDomain, 'site_id' => $siteId]);
            logger("Enabled nginx page cache for imported WordPress site {$importedDomain}");
        } catch (Exception $e) {
            // Page cache enable failed, but import succeeded - continue
            logger("Warning: Failed to enable page cache for {$importedDomain}: " . $e->getMessage());
        }
    }

    return [
        'success' => true,
        'site_id' => $siteId,
        'message' => 'WordPress site imported successfully',
    ];
}

/**
 * Enable Jabali Cache for a WordPress site
 */
function wpCacheEnable(array $params): array
{
    $username = $params['username'] ?? '';
    $siteId = $params['site_id'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }

    $userHome = $userInfo['dir'];
    $wpListFile = "{$userHome}/.wordpress_sites";

    $wpSites = [];
    if (file_exists($wpListFile)) {
        $wpSites = json_decode(file_get_contents($wpListFile), true) ?: [];
    }

    if (!isset($wpSites[$siteId])) {
        return ['success' => false, 'error' => 'WordPress site not found'];
    }

    $site = $wpSites[$siteId];
    $installPath = $site['install_path'] ?? '';
    $forceEnable = $params['force'] ?? false;

    if (!$installPath || !is_dir($installPath)) {
        return ['success' => false, 'error' => 'WordPress installation path not found'];
    }

    $wpContentDir = "{$installPath}/wp-content";
    if (!is_dir($wpContentDir)) {
        return ['success' => false, 'error' => 'wp-content directory not found'];
    }

    // List of known conflicting caching plugins
    $conflictingPlugins = [
        'w3-total-cache' => 'W3 Total Cache',
        'wp-super-cache' => 'WP Super Cache',
        'wp-fastest-cache' => 'WP Fastest Cache',
        'litespeed-cache' => 'LiteSpeed Cache',
        'wp-rocket' => 'WP Rocket',
        'redis-cache' => 'Redis Object Cache',
        'autoptimize' => 'Autoptimize',
        'wp-optimize' => 'WP-Optimize',
        'hummingbird-performance' => 'Hummingbird',
        'sg-cachepress' => 'SG Optimizer',
        'breeze' => 'Breeze',
        'cache-enabler' => 'Cache Enabler',
        'comet-cache' => 'Comet Cache',
        'powered-cache' => 'Powered Cache',
        'hyper-cache' => 'Hyper Cache',
    ];

    // Check for active conflicting plugins
    $foundConflicts = [];
    $cmd = "cd " . escapeshellarg($installPath) . " && sudo -u " . escapeshellarg($username) . " wp plugin list --status=active --format=json 2>/dev/null";
    exec($cmd, $output, $returnCode);

    if ($returnCode === 0 && !empty($output)) {
        $activePlugins = json_decode(implode('', $output), true);
        if (is_array($activePlugins)) {
            foreach ($activePlugins as $plugin) {
                $pluginName = $plugin['name'] ?? '';
                if (isset($conflictingPlugins[$pluginName])) {
                    $foundConflicts[] = [
                        'slug' => $pluginName,
                        'name' => $conflictingPlugins[$pluginName],
                    ];
                }
            }
        }
    }

    // If conflicts found and not forcing, return warning
    if (!empty($foundConflicts) && !$forceEnable) {
        return [
            'success' => false,
            'error' => 'Conflicting caching plugins detected',
            'conflicts' => $foundConflicts,
            'message' => 'Please disable the following plugins before enabling Jabali Cache: ' . implode(', ', array_column($foundConflicts, 'name')),
        ];
    }

    // Copy Jabali Cache plugin
    $pluginSrc = '/var/www/jabali/resources/wordpress/jabali-cache';
    $pluginDst = "{$wpContentDir}/plugins/jabali-cache";

    if (!is_dir($pluginSrc)) {
        return ['success' => false, 'error' => 'Jabali Cache plugin source not found'];
    }

    // Create plugin directory
    if (!is_dir($pluginDst)) {
        mkdir($pluginDst, 0755, true);
    }

    // Copy plugin files
    foreach (['jabali-cache.php', 'object-cache.php', 'readme.txt'] as $file) {
        if (file_exists("{$pluginSrc}/{$file}")) {
            copy("{$pluginSrc}/{$file}", "{$pluginDst}/{$file}");
        }
    }

    // Copy object-cache.php to wp-content
    copy("{$pluginSrc}/object-cache.php", "{$wpContentDir}/object-cache.php");

    // Read Redis credentials from user's home directory
    $redisCredFile = "{$userHome}/.redis_credentials";
    $redisUser = '';
    $redisPass = '';
    $redisPrefix = '';

    if (file_exists($redisCredFile)) {
        $credContent = file_get_contents($redisCredFile);
        if (preg_match('/REDIS_USER=(.+)/', $credContent, $m)) $redisUser = trim($m[1]);
        if (preg_match('/REDIS_PASS=(.+)/', $credContent, $m)) $redisPass = trim($m[1]);
        if (preg_match('/REDIS_PREFIX=(.+)/', $credContent, $m)) $redisPrefix = trim($m[1]);
    }

    // Fallback prefix if Redis credentials not found
    if (empty($redisPrefix)) {
        $redisPrefix = $username . ':';
    }

    // Add site-specific suffix to prefix for multiple WordPress sites
    $siteSuffix = substr(md5($installPath), 0, 8);
    $cachePrefix = $redisPrefix . $siteSuffix . '_';

    // Add cache constants to wp-config.php if not present
    $wpConfigPath = "{$installPath}/wp-config.php";
    if (file_exists($wpConfigPath)) {
        $wpConfig = file_get_contents($wpConfigPath);

        // Check if constants already exist
        if (strpos($wpConfig, 'JABALI_CACHE_PREFIX') === false) {
            $cacheConfig = "\n// Jabali Cache Configuration\n" .
                "define('JABALI_CACHE_HOST', '127.0.0.1');\n" .
                "define('JABALI_CACHE_PORT', 6379);\n" .
                "define('JABALI_CACHE_PREFIX', '{$cachePrefix}');\n";

            // Add Redis ACL credentials if available
            if (!empty($redisUser) && !empty($redisPass)) {
                $cacheConfig .= "define('JABALI_CACHE_REDIS_USER', '{$redisUser}');\n" .
                    "define('JABALI_CACHE_REDIS_PASS', '{$redisPass}');\n";
            }
            $cacheConfig .= "\n";

            // Insert after opening PHP tag
            $wpConfig = preg_replace('/^<\?php\s*/i', "<?php\n{$cacheConfig}", $wpConfig, 1);
            file_put_contents($wpConfigPath, $wpConfig);
        }
    }

    // Set ownership
    chown($pluginDst, $userInfo['uid']);
    chgrp($pluginDst, $userInfo['gid']);
    foreach (glob("{$pluginDst}/*") as $file) {
        chown($file, $userInfo['uid']);
        chgrp($file, $userInfo['gid']);
    }
    chown("{$wpContentDir}/object-cache.php", $userInfo['uid']);
    chgrp("{$wpContentDir}/object-cache.php", $userInfo['gid']);

    // Activate the plugin using PHP (WP-CLI has issues with this plugin)
    $phpScript = <<<'PHPSCRIPT'
<?php
define('WP_USE_THEMES', false);
require('wp-load.php');

// Add to active_plugins
$active = get_option('active_plugins', []);
$plugin = 'jabali-cache/jabali-cache.php';
if (!in_array($plugin, $active)) {
    $active[] = $plugin;
    update_option('active_plugins', $active);
}

// Load plugin and run activation logic
include_once(WP_PLUGIN_DIR . '/jabali-cache/jabali-cache.php');
$p = Jabali_Cache_Plugin::get_instance();
$p->enable_page_cache_on_activation();

// Ensure settings exist with page_cache enabled
$settings = get_option(Jabali_Cache_Plugin::OPTION_KEY);
if ($settings === false) {
    update_option(Jabali_Cache_Plugin::OPTION_KEY, ['page_cache' => true]);
}

echo 'OK';
PHPSCRIPT;

    $tempScript = tempnam('/tmp', 'jabali_activate_');
    file_put_contents($tempScript, $phpScript);
    chmod($tempScript, 0644);
    $cmd = "cd " . escapeshellarg($installPath) . " && sudo -u " . escapeshellarg($username) . " php " . escapeshellarg($tempScript) . " 2>&1";
    exec($cmd, $output, $returnCode);
    unlink($tempScript);
    $activationOk = ($returnCode === 0 && in_array('OK', $output));

    // Also enable nginx page cache directly (in case the plugin's API call fails)
    $domain = $site['domain'] ?? '';
    if (!empty($domain)) {
        wpPageCacheEnable(['username' => $username, 'domain' => $domain, 'site_id' => $siteId]);
    }

    // Update site record
    $wpSites[$siteId]['cache_enabled'] = true;
    $wpSites[$siteId]['cache_prefix'] = $cachePrefix;
    file_put_contents($wpListFile, json_encode($wpSites, JSON_PRETTY_PRINT));

    logger("Enabled Jabali Cache for WordPress site {$siteId} (user: {$username})");

    return [
        'success' => true,
        'message' => 'Jabali Cache enabled and activated',
        'cache_prefix' => $cachePrefix,
        'plugin_activated' => $activationOk,
    ];
}

/**
 * Disable Jabali Cache for a WordPress site
 */
function wpCacheDisable(array $params): array
{
    $username = $params['username'] ?? '';
    $siteId = $params['site_id'] ?? '';
    $removePlugin = $params['remove_plugin'] ?? false;
    $resetData = $params['reset_data'] ?? false;

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }

    $userHome = $userInfo['dir'];
    $wpListFile = "{$userHome}/.wordpress_sites";

    $wpSites = [];
    if (file_exists($wpListFile)) {
        $wpSites = json_decode(file_get_contents($wpListFile), true) ?: [];
    }

    if (!isset($wpSites[$siteId])) {
        return ['success' => false, 'error' => 'WordPress site not found'];
    }

    $site = $wpSites[$siteId];
    $installPath = $site['install_path'] ?? '';

    if (!$installPath || !is_dir($installPath)) {
        return ['success' => false, 'error' => 'WordPress installation path not found'];
    }

    $wpContentDir = "{$installPath}/wp-content";

    // Deactivate the plugin using PHP (WP-CLI has issues with this plugin)
    $phpScript = <<<'PHPSCRIPT'
<?php
define('WP_USE_THEMES', false);
require('wp-load.php');

// Load plugin first to run deactivation logic
if (file_exists(WP_PLUGIN_DIR . '/jabali-cache/jabali-cache.php')) {
    include_once(WP_PLUGIN_DIR . '/jabali-cache/jabali-cache.php');
    $p = Jabali_Cache_Plugin::get_instance();
    $p->disable_page_cache_on_deactivation();
    $p->disable_drop_in();
}

// Remove from active_plugins
$active = get_option('active_plugins', []);
$plugin = 'jabali-cache/jabali-cache.php';
$active = array_diff($active, [$plugin]);
update_option('active_plugins', array_values($active));

echo 'OK';
PHPSCRIPT;

    $tempScript = tempnam('/tmp', 'jabali_deactivate_');
    file_put_contents($tempScript, $phpScript);
    chmod($tempScript, 0644);
    $cmd = "cd " . escapeshellarg($installPath) . " && sudo -u " . escapeshellarg($username) . " php " . escapeshellarg($tempScript) . " 2>&1";
    exec($cmd, $output, $returnCode);
    unlink($tempScript);

    // Remove object-cache.php drop-in
    $objectCachePath = "{$wpContentDir}/object-cache.php";
    if (file_exists($objectCachePath)) {
        unlink($objectCachePath);
    }

    // Also disable nginx page cache directly (in case the plugin's API call fails)
    $domain = $site['domain'] ?? '';
    if (!empty($domain)) {
        wpPageCacheDisable(['username' => $username, 'domain' => $domain, 'site_id' => $siteId]);
    }

    // Remove plugin if requested
    if ($removePlugin) {
        $pluginDir = "{$wpContentDir}/plugins/jabali-cache";
        if (is_dir($pluginDir)) {
            foreach (glob("{$pluginDir}/*") as $file) {
                unlink($file);
            }
            rmdir($pluginDir);
        }

        // Delete plugin settings from database if requested
        if ($resetData) {
            $resetScript = <<<'PHPSCRIPT'
<?php
define('WP_USE_THEMES', false);
require('wp-load.php');

// Delete Jabali Cache settings
delete_option('jabali_cache_settings');

// Clean up any transients
global $wpdb;
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_jabali_cache%'");
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_jabali_cache%'");

echo 'OK';
PHPSCRIPT;

            $tempResetScript = tempnam('/tmp', 'jabali_reset_');
            file_put_contents($tempResetScript, $resetScript);
            chmod($tempResetScript, 0644);
            $cmd = "cd " . escapeshellarg($installPath) . " && sudo -u " . escapeshellarg($username) . " php " . escapeshellarg($tempResetScript) . " 2>&1";
            exec($cmd, $resetOutput, $resetCode);
            unlink($tempResetScript);
        }
    }

    // Update site record
    $wpSites[$siteId]['cache_enabled'] = false;
    file_put_contents($wpListFile, json_encode($wpSites, JSON_PRETTY_PRINT));

    logger("Disabled Jabali Cache for WordPress site {$siteId} (user: {$username})");

    return [
        'success' => true,
        'message' => 'Jabali Cache disabled',
    ];
}

/**
 * Flush WordPress object cache
 */
function wpCacheFlush(array $params): array
{
    $username = $params['username'] ?? '';
    $siteId = $params['site_id'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }

    $userHome = $userInfo['dir'];
    $wpListFile = "{$userHome}/.wordpress_sites";

    $wpSites = [];
    if (file_exists($wpListFile)) {
        $wpSites = json_decode(file_get_contents($wpListFile), true) ?: [];
    }

    if (!isset($wpSites[$siteId])) {
        return ['success' => false, 'error' => 'WordPress site not found'];
    }

    $site = $wpSites[$siteId];
    $installPath = $site['install_path'] ?? '';
    $cachePrefix = $site['cache_prefix'] ?? '';

    if (!$installPath || !is_dir($installPath)) {
        return ['success' => false, 'error' => 'WordPress installation path not found'];
    }

    // Use WP-CLI to flush cache
    $cmd = "cd " . escapeshellarg($installPath) . " && sudo -u " . escapeshellarg($username) . " wp cache flush 2>&1";
    exec($cmd, $output, $returnCode);

    if ($returnCode !== 0) {
        // Fallback: flush directly via Redis if WP-CLI fails
        if (!empty($cachePrefix)) {
            try {
                $redis = new Redis();
                $redis->connect('127.0.0.1', 6379, 1);
                $keys = $redis->keys($cachePrefix . '*');
                if (!empty($keys)) {
                    $redis->del($keys);
                }
                $redis->close();

                logger("Flushed Redis cache for WordPress site {$siteId} (prefix: {$cachePrefix})");

                return [
                    'success' => true,
                    'message' => 'Cache flushed via Redis',
                    'keys_deleted' => count($keys),
                ];
            } catch (Exception $e) {
                return ['success' => false, 'error' => 'Failed to flush cache: ' . $e->getMessage()];
            }
        }

        return ['success' => false, 'error' => 'Failed to flush cache: ' . implode("\n", $output)];
    }

    logger("Flushed WordPress cache for site {$siteId} (user: {$username})");

    return [
        'success' => true,
        'message' => 'Cache flushed successfully',
    ];
}

/**
 * Get WordPress cache status
 */
function wpCacheStatus(array $params): array
{
    $username = $params['username'] ?? '';
    $siteId = $params['site_id'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }

    $userHome = $userInfo['dir'];
    $wpListFile = "{$userHome}/.wordpress_sites";

    $wpSites = [];
    if (file_exists($wpListFile)) {
        $wpSites = json_decode(file_get_contents($wpListFile), true) ?: [];
    }

    if (!isset($wpSites[$siteId])) {
        return ['success' => false, 'error' => 'WordPress site not found'];
    }

    $site = $wpSites[$siteId];
    $installPath = $site['install_path'] ?? '';
    $cachePrefix = $site['cache_prefix'] ?? '';

    if (!$installPath || !is_dir($installPath)) {
        return ['success' => false, 'error' => 'WordPress installation path not found'];
    }

    $wpContentDir = "{$installPath}/wp-content";

    $status = [
        'enabled' => $site['cache_enabled'] ?? false,
        'drop_in_installed' => file_exists("{$wpContentDir}/object-cache.php"),
        'plugin_installed' => is_dir("{$wpContentDir}/plugins/jabali-cache"),
        'cache_prefix' => $cachePrefix,
        'redis_connected' => false,
        'cached_keys' => 0,
    ];

    // Check Redis connection and key count
    if (!empty($cachePrefix)) {
        try {
            $redis = new Redis();
            $redis->connect('127.0.0.1', 6379, 1);
            $status['redis_connected'] = true;
            $keys = $redis->keys($cachePrefix . '*');
            $status['cached_keys'] = is_array($keys) ? count($keys) : 0;
            $redis->close();
        } catch (Exception $e) {
            // Redis not available
        }
    }

    return [
        'success' => true,
        'status' => $status,
    ];
}

/**
 * Enable nginx page cache for a domain
 */
function wpPageCacheEnable(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';
    $siteId = $params['site_id'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    if (empty($domain)) {
        return ['success' => false, 'error' => 'Domain is required'];
    }

    // Validate domain format
    if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9-]*(\.[a-zA-Z0-9-]+)+$/', $domain)) {
        return ['success' => false, 'error' => 'Invalid domain format'];
    }

    $configFile = "/etc/nginx/sites-available/{$domain}.conf";
    if (!file_exists($configFile)) {
        return ['success' => false, 'error' => 'Nginx config not found for domain'];
    }

    $config = file_get_contents($configFile);

    $hasPageCache = strpos($config, 'fastcgi_cache JABALI') !== false;
    $hasHammerBypass = strpos($config, 'cache_reason "hammer"') !== false;

    // If cache is already enabled, ensure hammer bypass exists
    if ($hasPageCache && ! $hasHammerBypass) {
        $hammerRule = <<<'HAMMER'

    # Skip cache for hammer/stress test endpoints
    if ($request_uri ~* "/hammer|/io-hammer|/hammer-all") {
        set $skip_cache 1;
        set $cache_reason "hammer";
    }
HAMMER;

        $updated = preg_replace(
            '/\n\s*# Browser caching for static assets/',
            $hammerRule . "\n\n    # Browser caching for static assets",
            $config,
            1
        );

        if ($updated === null || $updated === $config) {
            $updated = preg_replace(
                '/(set \\$cache_reason \"\";)/',
                "$1{$hammerRule}",
                $config,
                1
            );
        }

        if ($updated && $updated !== $config) {
            copy($configFile, $configFile . '.bak');
            if (file_put_contents($configFile, $updated) === false) {
                return ['success' => false, 'error' => 'Failed to update nginx config'];
            }

            exec('nginx -t 2>&1', $output, $exitCode);
            if ($exitCode !== 0) {
                copy($configFile . '.bak', $configFile);
                return ['success' => false, 'error' => 'Nginx config test failed: ' . implode(' ', $output)];
            }

            exec('systemctl reload nginx 2>&1', $output, $exitCode);
            if ($exitCode !== 0) {
                return ['success' => false, 'error' => 'Failed to reload nginx'];
            }
        }

        return ['success' => true, 'message' => 'Page cache updated with hammer bypass'];
    }

    // Check if page cache is already enabled
    if ($hasPageCache) {
        return ['success' => true, 'message' => 'Page cache already enabled'];
    }

    // Create cache include block
    $cacheBlock = <<<'CACHE'

    # Jabali Page Cache v2.3
    set $skip_cache 0;
    set $cache_reason "";

    # Skip cache for POST requests
    if ($request_method = POST) {
        set $skip_cache 1;
        set $cache_reason "method";
    }

    # Normalize query strings - ignore marketing params for better cache hit rate
    # UTM params, click IDs, affiliate params are stripped from cache key
    set $normalized_args "";
    if ($args ~* "^(.*)(?:utm_source|utm_medium|utm_campaign|utm_term|utm_content|gclid|fbclid|msclkid|mc_cid|mc_eid|_ga|ref|affiliate)=") {
        set $normalized_args "";
    }
    if ($args !~* "(?:utm_source|utm_medium|utm_campaign|utm_term|utm_content|gclid|fbclid|msclkid|mc_cid|mc_eid|_ga|ref|affiliate)") {
        set $normalized_args $args;
    }

    # Skip cache for meaningful query strings (search, pagination, filters)
    if ($normalized_args ~* "s=|page=|filter|sort|order|product-page|add-to-cart") {
        set $skip_cache 1;
        set $cache_reason "query_string";
    }

    # Skip cache for logged-in users and recent commenters
    if ($http_cookie ~* "wordpress_logged_in|wp-postpass|woocommerce_cart_hash|woocommerce_items_in_cart") {
        set $skip_cache 1;
        set $cache_reason "logged_in";
    }

    # Skip cache for specific URLs (admin, login, API, cart, checkout, account)
    if ($request_uri ~* "/wp-admin/|/wp-login.php|/xmlrpc.php|/wp-cron.php|/wp-json/|/feed/|/cart/|/checkout/|/my-account/") {
        set $skip_cache 1;
        set $cache_reason "admin_url";
    }

    # Skip cache for hammer/stress test endpoints
    if ($request_uri ~* "/hammer|/io-hammer|/hammer-all") {
        set $skip_cache 1;
        set $cache_reason "hammer";
    }

    # Browser caching for static assets (1 year, immutable for versioned files)
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|webp|avif|mp4|webm|ogg|mp3|wav|pdf|zip)$ {
        expires 1y;
        add_header Cache-Control "public, max-age=31536000, immutable";
        access_log off;
    }

    # Dynamic content directories - don't cache 404s
    # Covers ALL plugins that generate CSS/JS/assets on-demand
    # (Spectra, Elementor, Autoptimize, WP Rocket, etc.)
    location ~* ^/wp-content/(uploads|cache)/ {
        try_files $uri @dynamic_asset_fallback;
        expires 1y;
        add_header Cache-Control "public, max-age=31536000";
        access_log off;
    }

    # Fallback for dynamically generated assets - pass to PHP for generation
    location @dynamic_asset_fallback {
        try_files /index.php =404;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root/index.php;
        fastcgi_pass unix:/run/php/php-fpm.sock;
        # Don't cache - may be 404 or trigger generation
        fastcgi_cache_bypass 1;
        fastcgi_no_cache 1;
    }

CACHE;

    // Add cache directives to PHP location block
    $phpCacheDirectives = <<<'PHPCACHE'

        # Page Cache
        fastcgi_cache JABALI;
        fastcgi_cache_valid 200 301 302 60m;
        fastcgi_cache_valid 404 1m;
        fastcgi_cache_bypass $skip_cache;
        fastcgi_no_cache $skip_cache;
        add_header X-Cache $upstream_cache_status;
        add_header X-Cache-Reason $cache_reason;
        # bfcache-compatible: no-store breaks back/forward cache
        add_header Cache-Control "public, no-cache";
PHPCACHE;

    // Insert cache block after root directive in HTTPS server block
    // Use preg_replace_callback to avoid $ and {} being interpreted as backreferences
    $config = preg_replace_callback(
        '/(listen 443 ssl;[^}]*?root [^;]+;)/s',
        function($matches) use ($cacheBlock) {
            return $matches[1] . $cacheBlock;
        },
        $config,
        1
    );

    // Insert cache directives after include fastcgi_params in PHP location
    $config = preg_replace_callback(
        '/(location\s+~\s+\\\\.php\$\s*\{[^}]*?include\s+fastcgi_params;)/s',
        function($matches) use ($phpCacheDirectives) {
            return $matches[1] . $phpCacheDirectives;
        },
        $config
    );

    // Backup original config
    copy($configFile, $configFile . '.bak');

    // Write new config
    if (file_put_contents($configFile, $config) === false) {
        return ['success' => false, 'error' => 'Failed to write nginx config'];
    }

    // Test nginx config
    exec('nginx -t 2>&1', $output, $exitCode);
    if ($exitCode !== 0) {
        // Restore backup
        copy($configFile . '.bak', $configFile);
        return ['success' => false, 'error' => 'Nginx config test failed: ' . implode(' ', $output)];
    }

    // Reload nginx
    exec('systemctl reload nginx 2>&1', $output, $exitCode);
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'Failed to reload nginx'];
    }

    // Also enable gzip compression in nginx.conf if not already enabled
    nginxEnableCompression([]);

    // Update WordPress site record if siteId provided
    if (!empty($siteId)) {
        $userInfo = posix_getpwnam($username);
        if ($userInfo) {
            $wpListFile = $userInfo['dir'] . '/.wordpress_sites';
            if (file_exists($wpListFile)) {
                $wpSites = json_decode(file_get_contents($wpListFile), true) ?: [];
                if (isset($wpSites[$siteId])) {
                    $wpSites[$siteId]['page_cache_enabled'] = true;
                    file_put_contents($wpListFile, json_encode($wpSites, JSON_PRETTY_PRINT));
                }
            }
        }
    }

    logger("Enabled page cache for domain: {$domain}");
    return ['success' => true, 'message' => 'Page cache enabled'];
}

/**
 * Disable nginx page cache for a domain
 */
function wpPageCacheDisable(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';
    $siteId = $params['site_id'] ?? '';

    if (empty($domain)) {
        return ['success' => false, 'error' => 'Domain is required'];
    }

    $configFile = "/etc/nginx/sites-available/{$domain}.conf";
    if (!file_exists($configFile)) {
        return ['success' => false, 'error' => 'Nginx config not found for domain'];
    }

    $config = file_get_contents($configFile);

    // Check if page cache is enabled
    if (strpos($config, 'fastcgi_cache JABALI') === false) {
        return ['success' => true, 'message' => 'Page cache not enabled'];
    }

    // Remove server-level cache block (v2.3 format)
    // From "# Jabali Page Cache" to just before "# Symlink protection" or "disable_symlinks" or "ssl_certificate"
    $config = preg_replace(
        '/\n\s*# Jabali Page Cache[^\n]*\n.*?(?=\n\s*(?:# Symlink protection|disable_symlinks|ssl_certificate))/s',
        "\n",
        $config
    );

    // Remove static asset location block if it was added by cache config
    // (Browser caching for static assets)
    $config = preg_replace(
        '/\n\s*# Browser caching for static assets[^\n]*\n\s*location ~\*.*?access_log off;\s*\}\s*\n/s',
        "\n",
        $config
    );

    // Remove dynamic content directories location block
    $config = preg_replace(
        '/\n\s*# Dynamic content directories[^\n]*\n.*?access_log off;\s*\}\s*\n/s',
        "\n",
        $config
    );

    // Remove fallback location block
    $config = preg_replace(
        '/\n\s*# Fallback for dynamically generated assets[^\n]*\n\s*location @dynamic_asset_fallback\s*\{.*?\}\s*\n/s',
        "\n",
        $config
    );

    // Remove cache directives from PHP block line by line
    // This handles all formats (v2.1, v2.2, v2.3)
    $cacheLines = [
        '/^\s*# Page Cache\s*$/m',
        '/^\s*# bfcache-compatible:.*$/m',
        '/^\s*fastcgi_cache\s+JABALI\s*;/m',
        '/^\s*fastcgi_cache_valid\s+[^;]+;/m',
        '/^\s*fastcgi_cache_bypass\s+\$skip_cache\s*;/m',
        '/^\s*fastcgi_no_cache\s+\$skip_cache\s*;/m',
        '/^\s*add_header\s+X-Cache\s+\$upstream_cache_status\s*;/m',
        '/^\s*add_header\s+X-Cache-Reason\s+\$cache_reason\s*;/m',
        '/^\s*add_header\s+X-Cache-Key\s+[^;]+;/m',
        '/^\s*add_header\s+Cache-Control\s+"public,\s*no-cache"\s*;/m',
    ];
    foreach ($cacheLines as $pattern) {
        $config = preg_replace($pattern, '', $config);
    }
    // Clean up multiple blank lines
    $config = preg_replace('/\n{3,}/', "\n\n", $config);

    // Backup original config
    copy($configFile, $configFile . '.bak');

    // Write new config
    if (file_put_contents($configFile, $config) === false) {
        return ['success' => false, 'error' => 'Failed to write nginx config'];
    }

    // Test nginx config
    exec('nginx -t 2>&1', $output, $exitCode);
    if ($exitCode !== 0) {
        // Restore backup
        copy($configFile . '.bak', $configFile);
        return ['success' => false, 'error' => 'Nginx config test failed: ' . implode(' ', $output)];
    }

    // Reload nginx
    exec('systemctl reload nginx 2>&1', $output, $exitCode);
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'Failed to reload nginx'];
    }

    // Update WordPress site record if siteId provided
    if (!empty($siteId) && !empty($username) && validateUsername($username)) {
        $userInfo = posix_getpwnam($username);
        if ($userInfo) {
            $wpListFile = $userInfo['dir'] . '/.wordpress_sites';
            if (file_exists($wpListFile)) {
                $wpSites = json_decode(file_get_contents($wpListFile), true) ?: [];
                if (isset($wpSites[$siteId])) {
                    $wpSites[$siteId]['page_cache_enabled'] = false;
                    file_put_contents($wpListFile, json_encode($wpSites, JSON_PRETTY_PRINT));
                }
            }
        }
    }

    logger("Disabled page cache for domain: {$domain}");
    return ['success' => true, 'message' => 'Page cache disabled'];
}

/**
 * Purge nginx page cache for a domain
 * Supports single path, array of paths, or full domain purge
 */
function wpPageCachePurge(array $params): array
{
    $domain = $params['domain'] ?? '';
    $path = $params['path'] ?? '';      // Optional: single path to purge
    $paths = $params['paths'] ?? [];    // Optional: array of paths to purge

    if (empty($domain)) {
        return ['success' => false, 'error' => 'Domain is required'];
    }

    $cacheDir = '/var/cache/nginx/fastcgi';
    if (!is_dir($cacheDir)) {
        return ['success' => false, 'error' => 'Cache directory not found'];
    }

    $purged = 0;

    // Combine single path with paths array if both provided
    $pathsToCheck = $paths;
    if (!empty($path)) {
        $pathsToCheck[] = $path;
    }
    $pathsToCheck = array_unique($pathsToCheck);

    // Generate cache key pattern to find cached files
    // The cache key is: $scheme$request_method$host$request_uri
    // Files are stored in levels=1:2 subdirectories based on MD5 hash

    if (empty($pathsToCheck)) {
        // Purge all cache for this domain
        // We need to scan all cache files and check the key inside
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($cacheDir, RecursiveDirectoryIterator::SKIP_DOTS),
            RecursiveIteratorIterator::CHILD_FIRST
        );

        foreach ($iterator as $file) {
            if ($file->isFile()) {
                $content = file_get_contents($file->getPathname(), false, null, 0, 2048);
                // Check if this cache file is for our domain
                if (strpos($content, $domain) !== false) {
                    if (unlink($file->getPathname())) {
                        $purged++;
                    }
                }
            }
        }
        logger("Purged all {$purged} cache files for domain: {$domain}");
    } else {
        // Purge specific paths - calculate the cache file location for each
        $schemes = ['https', 'http'];
        foreach ($pathsToCheck as $pathToPurge) {
            // Ensure path starts with /
            if (!str_starts_with($pathToPurge, '/')) {
                $pathToPurge = '/' . $pathToPurge;
            }

            foreach ($schemes as $scheme) {
                // Standard cache key
                $key = $scheme . 'GET' . $domain . $pathToPurge;
                $hash = md5($key);
                $cacheFile = $cacheDir . '/' . substr($hash, -1) . '/' . substr($hash, -3, 2) . '/' . $hash;

                if (file_exists($cacheFile)) {
                    if (unlink($cacheFile)) {
                        $purged++;
                    }
                }

                // Also try with trailing slash variants
                if (substr($pathToPurge, -1) !== '/') {
                    $keyWithSlash = $scheme . 'GET' . $domain . $pathToPurge . '/';
                    $hashWithSlash = md5($keyWithSlash);
                    $cacheFileWithSlash = $cacheDir . '/' . substr($hashWithSlash, -1) . '/' . substr($hashWithSlash, -3, 2) . '/' . $hashWithSlash;

                    if (file_exists($cacheFileWithSlash)) {
                        if (unlink($cacheFileWithSlash)) {
                            $purged++;
                        }
                    }
                }
            }
        }
        logger("Purged {$purged} cache files for " . count($pathsToCheck) . " paths on domain: {$domain}");
    }

    return [
        'success' => true,
        'message' => "Purged {$purged} cached files",
        'purged_count' => $purged,
        'paths_checked' => count($pathsToCheck) ?: 'all',
    ];
}

/**
 * Get page cache status for a domain
 */
function wpPageCacheStatus(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';
    $siteId = $params['site_id'] ?? '';

    if (empty($domain)) {
        return ['success' => false, 'error' => 'Domain is required'];
    }

    $configFile = "/etc/nginx/sites-available/{$domain}.conf";
    $enabled = false;

    if (file_exists($configFile)) {
        $config = file_get_contents($configFile);
        $enabled = strpos($config, 'fastcgi_cache JABALI') !== false;
    }

    // Count cached files for this domain
    $cachedFiles = 0;
    $cacheSize = 0;
    $cacheDir = '/var/cache/nginx/fastcgi';

    if (is_dir($cacheDir) && $enabled) {
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($cacheDir, RecursiveDirectoryIterator::SKIP_DOTS),
            RecursiveIteratorIterator::CHILD_FIRST
        );

        foreach ($iterator as $file) {
            if ($file->isFile()) {
                $content = file_get_contents($file->getPathname(), false, null, 0, 2048);
                if (strpos($content, $domain) !== false) {
                    $cachedFiles++;
                    $cacheSize += $file->getSize();
                }
            }
        }
    }

    return [
        'success' => true,
        'enabled' => $enabled,
        'cached_files' => $cachedFiles,
        'cache_size' => $cacheSize,
        'cache_size_human' => formatBytes($cacheSize),
    ];
}

function wpDelete(array $params): array
{
    $username = $params['username'] ?? '';
    $siteId = $params['site_id'] ?? '';
    $deleteFiles = $params['delete_files'] ?? true;
    $deleteDatabase = $params['delete_database'] ?? true;

    logger("wpDelete called: username={$username}, siteId={$siteId}, deleteFiles=" . ($deleteFiles ? 'true' : 'false') . ", deleteDatabase=" . ($deleteDatabase ? 'true' : 'false'));

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }
    
    $userHome = $userInfo['dir'];
    $wpListFile = "{$userHome}/.wordpress_sites";
    
    $wpSites = [];
    if (file_exists($wpListFile)) {
        $wpSites = json_decode(file_get_contents($wpListFile), true) ?: [];
    }
    
    if (!isset($wpSites[$siteId])) {
        return ['success' => false, 'error' => 'WordPress site not found'];
    }
    
    $site = $wpSites[$siteId];
    
    // Delete database if requested
    $dbDeleted = false;
    if ($deleteDatabase && !empty($site['db_name']) && !empty($site['db_user'])) {
        $conn = getMysqlConnection();
        if ($conn) {
            $dbName = $conn->real_escape_string($site['db_name']);
            $dbUser = $conn->real_escape_string($site['db_user']);

            if ($conn->query("DROP DATABASE IF EXISTS `{$dbName}`")) {
                $dbDeleted = true;
                logger("Dropped database: {$dbName}");
            } else {
                logger("Failed to drop database {$dbName}: " . $conn->error);
            }

            if ($conn->query("DROP USER IF EXISTS '{$dbUser}'@'localhost'")) {
                logger("Dropped user: {$dbUser}");
            } else {
                logger("Failed to drop user {$dbUser}: " . $conn->error);
            }

            $conn->query("FLUSH PRIVILEGES");
            $conn->close();
        } else {
            logger("Failed to connect to MySQL for database deletion");
        }
    }
    
    // Delete files if requested
    if ($deleteFiles && !empty($site['install_path'])) {
        $installPath = $site['install_path'];
        // Safety check - make sure it's within user's domain folder
        if (strpos($installPath, "{$userHome}/domains/") === 0) {
            // If it's the root of public_html, just delete WP files, not the folder
            if (preg_match('#/public_html$#', $installPath)) {
                exec("rm -rf " . escapeshellarg($installPath) . "/wp-* " . escapeshellarg($installPath) . "/license.txt " . escapeshellarg($installPath) . "/readme.html " . escapeshellarg($installPath) . "/xmlrpc.php " . escapeshellarg($installPath) . "/index.php");
            } else {
                exec("rm -rf " . escapeshellarg($installPath));
            }
        }
    }
    
    // Remove from list
    unset($wpSites[$siteId]);
    file_put_contents($wpListFile, json_encode($wpSites, JSON_PRETTY_PRINT));
    
    return ['success' => true, 'message' => 'WordPress site deleted successfully'];
}

function wpAutoLogin(array $params): array
{
    $username = $params['username'] ?? '';
    $siteId = $params['site_id'] ?? '';
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }
    
    $userHome = $userInfo['dir'];
    $wpListFile = "{$userHome}/.wordpress_sites";
    
    $wpSites = [];
    if (file_exists($wpListFile)) {
        $wpSites = json_decode(file_get_contents($wpListFile), true) ?: [];
    }
    
    if (!isset($wpSites[$siteId])) {
        return ['success' => false, 'error' => 'WordPress site not found'];
    }
    
    $site = $wpSites[$siteId];
    $installPath = $site['install_path'];
    $adminUser = $site['admin_user'];
    
    // Skip WP-CLI login package (slow) - use direct fallback method
    // Generate secure auto-login token
    $loginUrl = ''; // Always use fallback
    if (true) {
        // Fallback: Create a temporary auto-login file
        $token = bin2hex(random_bytes(32));
        $expiry = time() + 300; // 5 minute expiry
        
        $adminUrl = $site['url'] . '/wp-admin/';
        $autoLoginContent = '<?php
// Auto-login script - expires after one use or 5 minutes
$token = "' . $token . '";
$expiry = ' . $expiry . ';
$admin_user = "' . addslashes($adminUser) . '";
$admin_url = "' . addslashes($adminUrl) . '";

if (time() > $expiry || !isset($_GET["token"]) || $_GET["token"] !== $token) {
    @unlink(__FILE__);
    die("Login link expired or invalid.");
}

require_once(dirname(__FILE__) . "/wp-load.php");
$user = get_user_by("login", $admin_user);
if ($user) {
    wp_set_auth_cookie($user->ID, true);
    @unlink(__FILE__);
    wp_redirect($admin_url);
    exit;
}
@unlink(__FILE__);
die("User not found.");
';
        
        $autoLoginFile = "{$installPath}/jabali-auto-login-{$token}.php";
        
        // Write file as the user using sudo
        $tempFile = "/tmp/jabali-auto-login-{$token}.php";
        file_put_contents($tempFile, $autoLoginContent);
        exec("sudo -u " . escapeshellarg($username) . " cp " . escapeshellarg($tempFile) . " " . escapeshellarg($autoLoginFile));
        exec("sudo -u " . escapeshellarg($username) . " chmod 644 " . escapeshellarg($autoLoginFile));
        @unlink($tempFile);
        
        $loginUrl = $site['url'] . "/jabali-auto-login-{$token}.php?token={$token}";
    }
    
    return ['success' => true, 'login_url' => $loginUrl];
}

function wpUpdate(array $params): array
{
    $username = $params['username'] ?? '';
    $siteId = $params['site_id'] ?? '';
    $updateType = $params['type'] ?? 'core'; // core, plugins, themes, all
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }
    
    $userHome = $userInfo['dir'];
    $wpListFile = "{$userHome}/.wordpress_sites";
    
    $wpSites = [];
    if (file_exists($wpListFile)) {
        $wpSites = json_decode(file_get_contents($wpListFile), true) ?: [];
    }
    
    if (!isset($wpSites[$siteId])) {
        return ['success' => false, 'error' => 'WordPress site not found'];
    }
    
    $site = $wpSites[$siteId];
    $installPath = $site['install_path'];
    $results = [];
    
    if ($updateType === 'core' || $updateType === 'all') {
        exec("cd " . escapeshellarg($installPath) . " && sudo -u " . escapeshellarg($username) . " wp core update 2>&1", $output, $code);
        $results['core'] = $code === 0 ? 'Updated' : implode("\n", $output);
    }
    
    if ($updateType === 'plugins' || $updateType === 'all') {
        exec("cd " . escapeshellarg($installPath) . " && sudo -u " . escapeshellarg($username) . " wp plugin update --all 2>&1", $output2, $code2);
        $results['plugins'] = $code2 === 0 ? 'Updated' : implode("\n", $output2);
    }
    
    if ($updateType === 'themes' || $updateType === 'all') {
        exec("cd " . escapeshellarg($installPath) . " && sudo -u " . escapeshellarg($username) . " wp theme update --all 2>&1", $output3, $code3);
        $results['themes'] = $code3 === 0 ? 'Updated' : implode("\n", $output3);
    }
    
    // Update version in our records
    $newVersion = trim((string)shell_exec("cd " . escapeshellarg($installPath) . " && sudo -u " . escapeshellarg($username) . " wp core version 2>/dev/null") ?: 'Unknown');
    $wpSites[$siteId]['version'] = $newVersion;
    file_put_contents($wpListFile, json_encode($wpSites, JSON_PRETTY_PRINT));
    
    return ['success' => true, 'results' => $results, 'new_version' => $newVersion];
}

function wpToggleDebug(array $params): array
{
    $username = $params['username'] ?? '';
    $siteId = $params['site_id'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }

    $userHome = $userInfo['dir'];
    $wpListFile = "{$userHome}/.wordpress_sites";

    $wpSites = [];
    if (file_exists($wpListFile)) {
        $wpSites = json_decode(file_get_contents($wpListFile), true) ?: [];
    }

    if (!isset($wpSites[$siteId])) {
        return ['success' => false, 'error' => 'WordPress site not found'];
    }

    $site = $wpSites[$siteId];
    $wpConfigPath = $site['install_path'] . '/wp-config.php';

    if (!file_exists($wpConfigPath)) {
        return ['success' => false, 'error' => 'wp-config.php not found'];
    }

    $wpConfig = file_get_contents($wpConfigPath);

    // Check current debug status
    $currentDebug = false;
    if (preg_match("/define\s*\(\s*['\"]WP_DEBUG['\"]\s*,\s*(true|false)\s*\)/i", $wpConfig, $matches)) {
        $currentDebug = strtolower($matches[1]) === 'true';
    }

    $newDebug = !$currentDebug;
    $newDebugValue = $newDebug ? 'true' : 'false';

    // Update WP_DEBUG
    if (preg_match("/define\s*\(\s*['\"]WP_DEBUG['\"]\s*,\s*(true|false)\s*\)/i", $wpConfig)) {
        $wpConfig = preg_replace(
            "/define\s*\(\s*['\"]WP_DEBUG['\"]\s*,\s*(true|false)\s*\)/i",
            "define('WP_DEBUG', {$newDebugValue})",
            $wpConfig
        );
    } else {
        // Add WP_DEBUG before "That's all, stop editing!"
        $wpConfig = preg_replace(
            "/\/\*\s*That's all/",
            "define('WP_DEBUG', {$newDebugValue});\n\n/* That's all",
            $wpConfig
        );
    }

    // Add or update WP_DEBUG_LOG and WP_DEBUG_DISPLAY
    if ($newDebug) {
        if (!preg_match("/define\s*\(\s*['\"]WP_DEBUG_LOG['\"]/", $wpConfig)) {
            $wpConfig = preg_replace(
                "/define\s*\(\s*['\"]WP_DEBUG['\"]\s*,\s*true\s*\)/",
                "define('WP_DEBUG', true);\ndefine('WP_DEBUG_LOG', true);\ndefine('WP_DEBUG_DISPLAY', false)",
                $wpConfig
            );
        }
    }

    file_put_contents($wpConfigPath, $wpConfig);
    chown($wpConfigPath, $userInfo['uid']);
    chgrp($wpConfigPath, $userInfo['gid']);

    // Update site record
    $wpSites[$siteId]['debug_enabled'] = $newDebug;
    file_put_contents($wpListFile, json_encode($wpSites, JSON_PRETTY_PRINT));

    return ['success' => true, 'debug_enabled' => $newDebug];
}

function wpToggleAutoUpdate(array $params): array
{
    $username = $params['username'] ?? '';
    $siteId = $params['site_id'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }

    $userHome = $userInfo['dir'];
    $wpListFile = "{$userHome}/.wordpress_sites";

    $wpSites = [];
    if (file_exists($wpListFile)) {
        $wpSites = json_decode(file_get_contents($wpListFile), true) ?: [];
    }

    if (!isset($wpSites[$siteId])) {
        return ['success' => false, 'error' => 'WordPress site not found'];
    }

    $site = $wpSites[$siteId];
    $wpConfigPath = $site['install_path'] . '/wp-config.php';

    if (!file_exists($wpConfigPath)) {
        return ['success' => false, 'error' => 'wp-config.php not found'];
    }

    $wpConfig = file_get_contents($wpConfigPath);

    // Check current auto-update status
    $currentAutoUpdate = $site['auto_update'] ?? false;
    $newAutoUpdate = !$currentAutoUpdate;

    // WordPress auto-update constants
    $autoUpdateConstants = [
        'WP_AUTO_UPDATE_CORE' => $newAutoUpdate ? 'true' : 'false',
        'AUTOMATIC_UPDATER_DISABLED' => $newAutoUpdate ? 'false' : 'true',
    ];

    foreach ($autoUpdateConstants as $constant => $value) {
        if (preg_match("/define\s*\(\s*['\"]" . $constant . "['\"]\s*,/", $wpConfig)) {
            $wpConfig = preg_replace(
                "/define\s*\(\s*['\"]" . $constant . "['\"]\s*,\s*[^)]+\)/",
                "define('{$constant}', {$value})",
                $wpConfig
            );
        } else {
            // Add constant before "That's all, stop editing!"
            $wpConfig = preg_replace(
                "/\/\*\s*That's all/",
                "define('{$constant}', {$value});\n/* That's all",
                $wpConfig
            );
        }
    }

    file_put_contents($wpConfigPath, $wpConfig);
    chown($wpConfigPath, $userInfo['uid']);
    chgrp($wpConfigPath, $userInfo['gid']);

    // Enable/disable auto-updates via wp-cli
    $installPath = $site['install_path'];
    if ($newAutoUpdate) {
        exec("cd " . escapeshellarg($installPath) . " && sudo -u " . escapeshellarg($username) . " wp plugin auto-updates enable --all 2>&1");
        exec("cd " . escapeshellarg($installPath) . " && sudo -u " . escapeshellarg($username) . " wp theme auto-updates enable --all 2>&1");
    } else {
        exec("cd " . escapeshellarg($installPath) . " && sudo -u " . escapeshellarg($username) . " wp plugin auto-updates disable --all 2>&1");
        exec("cd " . escapeshellarg($installPath) . " && sudo -u " . escapeshellarg($username) . " wp theme auto-updates disable --all 2>&1");
    }

    // Update site record
    $wpSites[$siteId]['auto_update'] = $newAutoUpdate;
    file_put_contents($wpListFile, json_encode($wpSites, JSON_PRETTY_PRINT));

    return ['success' => true, 'auto_update' => $newAutoUpdate];
}

function wpCreateStaging(array $params): array
{
    $username = $params['username'] ?? '';
    $siteId = $params['site_id'] ?? '';
    $subdomain = $params['subdomain'] ?? 'staging';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }

    $userHome = $userInfo['dir'];
    $wpListFile = "{$userHome}/.wordpress_sites";

    $wpSites = [];
    if (file_exists($wpListFile)) {
        $wpSites = json_decode(file_get_contents($wpListFile), true) ?: [];
    }

    if (!isset($wpSites[$siteId])) {
        return ['success' => false, 'error' => 'WordPress site not found'];
    }

    $site = $wpSites[$siteId];
    $sourcePath = $site['install_path'];
    $sourceDomain = $site['domain'];

    // Create staging domain
    $stagingDomain = "{$subdomain}.{$sourceDomain}";
    $stagingPath = "{$userHome}/domains/{$stagingDomain}/public_html";

    // Check if staging already exists
    if (is_dir($stagingPath)) {
        return ['success' => false, 'error' => 'Staging site already exists at ' . $stagingDomain];
    }

    // Create staging directories
    $stagingDir = dirname($stagingPath);
    $stagingLogs = "{$userHome}/domains/{$stagingDomain}/logs";
    if (!is_dir($stagingDir)) {
        mkdir($stagingDir, 0755, true);
        chown($stagingDir, $userInfo['uid']);
        chgrp($stagingDir, $userInfo['gid']);
    }
    if (!is_dir($stagingLogs)) {
        mkdir($stagingLogs, 0755, true);
        chown($stagingLogs, $userInfo['uid']);
        chgrp($stagingLogs, $userInfo['gid']);
    }

    // Copy files
    exec("cp -r " . escapeshellarg($sourcePath) . " " . escapeshellarg($stagingPath) . " 2>&1", $output, $code);
    if ($code !== 0) {
        return ['success' => false, 'error' => 'Failed to copy files: ' . implode("\n", $output)];
    }

    // Fix ownership
    exec("chown -R {$userInfo['uid']}:{$userInfo['gid']} " . escapeshellarg($stagingPath));
    exec("chown -R {$userInfo['uid']}:{$userInfo['gid']} " . escapeshellarg($stagingLogs));

    // Ensure Nginx can access the staging files
    exec("setfacl -m u:www-data:x " . escapeshellarg($userHome));
    exec("setfacl -m u:www-data:x " . escapeshellarg("{$userHome}/domains"));
    exec("setfacl -m u:www-data:rx " . escapeshellarg($stagingDir));
    exec("setfacl -R -m u:www-data:rx " . escapeshellarg($stagingPath));
    exec("setfacl -R -m u:www-data:rwx " . escapeshellarg($stagingLogs));
    exec("setfacl -R -d -m u:www-data:rx " . escapeshellarg($stagingPath));
    exec("setfacl -R -d -m u:www-data:rwx " . escapeshellarg($stagingLogs));

    // Get source database credentials
    $wpConfigPath = $stagingPath . '/wp-config.php';
    $wpConfig = file_get_contents($wpConfigPath);

    preg_match("/define\s*\(\s*['\"]DB_NAME['\"]\s*,\s*['\"]([^'\"]+)['\"]\s*\)/", $wpConfig, $dbNameMatch);
    preg_match("/define\s*\(\s*['\"]DB_USER['\"]\s*,\s*['\"]([^'\"]+)['\"]\s*\)/", $wpConfig, $dbUserMatch);
    preg_match("/define\s*\(\s*['\"]DB_PASSWORD['\"]\s*,\s*['\"]([^'\"]+)['\"]\s*\)/", $wpConfig, $dbPassMatch);

    $sourceDbName = $dbNameMatch[1] ?? '';
    $sourceDbUser = $dbUserMatch[1] ?? '';
    $sourceDbPass = $dbPassMatch[1] ?? '';

    if (empty($sourceDbName)) {
        return ['success' => false, 'error' => 'Could not read source database name'];
    }

    // Create staging database and user
    $suffix = preg_replace('/[^a-zA-Z0-9_]+/', '_', $subdomain);
    $suffix = trim((string) $suffix, '_');
    if ($suffix === '') {
        $suffix = 'staging';
    }

    $stagingDbName = substr($sourceDbName . '_' . $suffix . '_stg', 0, 64);
    $stagingDbUser = substr($sourceDbUser . '_' . $suffix . '_stg', 0, 32);
    $stagingDbPass = bin2hex(random_bytes(12));

    // Export and import database
    $dumpFile = "/tmp/wp_staging_{$siteId}.sql";
    exec("mysqldump --defaults-file=/etc/mysql/debian.cnf " . escapeshellarg($sourceDbName) . " > " . escapeshellarg($dumpFile) . " 2>&1", $dumpOutput, $dumpCode);

    if ($dumpCode !== 0) {
        return ['success' => false, 'error' => 'Failed to export database'];
    }

    // Create staging database
    $createDbSql = "CREATE DATABASE IF NOT EXISTS `" . addslashes($stagingDbName) . "` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci";
    exec("mysql --defaults-file=/etc/mysql/debian.cnf -e " . escapeshellarg($createDbSql) . " 2>&1", $createOutput, $createCode);

    if ($createCode !== 0) {
        unlink($dumpFile);
        return ['success' => false, 'error' => 'Failed to create staging database'];
    }

    // Create staging database user and grant permissions
    $createUserSql = "CREATE USER IF NOT EXISTS '" . addslashes($stagingDbUser) . "'@'localhost' IDENTIFIED BY '" . addslashes($stagingDbPass) . "'";
    exec("mysql --defaults-file=/etc/mysql/debian.cnf -e " . escapeshellarg($createUserSql) . " 2>&1", $userOutput, $userCode);
    if ($userCode !== 0) {
        unlink($dumpFile);
        return ['success' => false, 'error' => 'Failed to create staging database user'];
    }

    $grantSql = "GRANT ALL PRIVILEGES ON `" . addslashes($stagingDbName) . "`.* TO '" . addslashes($stagingDbUser) . "'@'localhost'";
    exec("mysql --defaults-file=/etc/mysql/debian.cnf -e " . escapeshellarg($grantSql) . " 2>&1", $grantOutput, $grantCode);
    if ($grantCode !== 0) {
        unlink($dumpFile);
        return ['success' => false, 'error' => 'Failed to grant database privileges'];
    }

    // Import database
    exec("mysql --defaults-file=/etc/mysql/debian.cnf " . escapeshellarg($stagingDbName) . " < " . escapeshellarg($dumpFile) . " 2>&1", $importOutput, $importCode);
    unlink($dumpFile);

    if ($importCode !== 0) {
        return ['success' => false, 'error' => 'Failed to import database'];
    }

    // Update wp-config.php with new database credentials
    $wpConfig = preg_replace(
        "/define\s*\(\s*['\"]DB_NAME['\"]\s*,\s*['\"][^'\"]+['\"]\s*\)/",
        "define('DB_NAME', '{$stagingDbName}')",
        $wpConfig
    );
    $wpConfig = preg_replace(
        "/define\s*\(\s*['\"]DB_USER['\"]\s*,\s*['\"][^'\"]+['\"]\s*\)/",
        "define('DB_USER', '{$stagingDbUser}')",
        $wpConfig
    );
    $wpConfig = preg_replace(
        "/define\s*\(\s*['\"]DB_PASSWORD['\"]\s*,\s*['\"][^'\"]*['\"]\s*\)/",
        "define('DB_PASSWORD', '{$stagingDbPass}')",
        $wpConfig
    );
    file_put_contents($wpConfigPath, $wpConfig);

    // Update URLs in staging database
    $stagingUrl = "https://{$stagingDomain}";
    $sourceUrl = $site['url'];

    exec("cd " . escapeshellarg($stagingPath) . " && sudo -u " . escapeshellarg($username) . " wp search-replace " . escapeshellarg($sourceUrl) . " " . escapeshellarg($stagingUrl) . " --all-tables 2>&1");

    // Also replace without protocol
    exec("cd " . escapeshellarg($stagingPath) . " && sudo -u " . escapeshellarg($username) . " wp search-replace " . escapeshellarg($sourceDomain) . " " . escapeshellarg($stagingDomain) . " --all-tables 2>&1");

    // Create Nginx config for staging (includes HTTPS with snakeoil cert)
    createFpmPool($username, false);
    $fpmSocket = getFpmSocketPath($username);
    $nginxConf = generateNginxVhost($stagingDomain, $stagingPath, $stagingLogs, $fpmSocket);
    file_put_contents("/etc/nginx/sites-available/{$stagingDomain}.conf", $nginxConf);

    if (!file_exists("/etc/nginx/sites-enabled/{$stagingDomain}.conf")) {
        symlink("/etc/nginx/sites-available/{$stagingDomain}.conf", "/etc/nginx/sites-enabled/{$stagingDomain}.conf");
    }

    // Reload Nginx
    exec("systemctl reload nginx 2>&1");

    // Add staging site to WordPress sites list
    $stagingSiteId = 'staging_' . $siteId . '_' . time();
    $wpSites[$stagingSiteId] = [
        'id' => $stagingSiteId,
        'domain' => $stagingDomain,
        'path' => '',
        'url' => $stagingUrl,
        'install_path' => $stagingPath,
        'db_name' => $stagingDbName,
        'db_user' => $stagingDbUser,
        'version' => $site['version'] ?? 'Unknown',
        'is_staging' => true,
        'source_site_id' => $siteId,
        'created_at' => date('Y-m-d H:i:s'),
    ];

    file_put_contents($wpListFile, json_encode($wpSites, JSON_PRETTY_PRINT));

    return [
        'success' => true,
        'staging_url' => $stagingUrl,
        'staging_domain' => $stagingDomain,
        'staging_site_id' => $stagingSiteId,
    ];
}

function wpPushStaging(array $params): array
{
    $username = $params['username'] ?? '';
    $stagingSiteId = $params['staging_site_id'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }

    $userHome = $userInfo['dir'];
    $wpListFile = "{$userHome}/.wordpress_sites";

    $wpSites = [];
    if (file_exists($wpListFile)) {
        $wpSites = json_decode(file_get_contents($wpListFile), true) ?: [];
    }

    if (!isset($wpSites[$stagingSiteId])) {
        return ['success' => false, 'error' => 'Staging site not found'];
    }

    $stagingSite = $wpSites[$stagingSiteId];
    if (!($stagingSite['is_staging'] ?? false)) {
        return ['success' => false, 'error' => 'Site is not a staging environment'];
    }

    $sourceSiteId = $stagingSite['source_site_id'] ?? null;
    if (!$sourceSiteId || !isset($wpSites[$sourceSiteId])) {
        return ['success' => false, 'error' => 'Source site not found'];
    }

    $sourceSite = $wpSites[$sourceSiteId];

    $stagingPath = $stagingSite['install_path'] ?? '';
    $sourcePath = $sourceSite['install_path'] ?? '';

    if (!$stagingPath || !is_dir($stagingPath) || !$sourcePath || !is_dir($sourcePath)) {
        return ['success' => false, 'error' => 'Invalid site paths'];
    }

    // Sync files (exclude wp-config.php so production DB credentials remain)
    $rsyncCmd = "rsync -a --delete --exclude " . escapeshellarg('wp-config.php') . " "
        . escapeshellarg(rtrim($stagingPath, '/') . '/') . " "
        . escapeshellarg(rtrim($sourcePath, '/') . '/') . " 2>&1";
    exec($rsyncCmd, $rsyncOutput, $rsyncCode);
    if ($rsyncCode !== 0) {
        return ['success' => false, 'error' => 'Failed to sync files: ' . implode("\n", $rsyncOutput)];
    }

    // Read DB credentials
    $stagingConfig = @file_get_contents($stagingPath . '/wp-config.php');
    $sourceConfig = @file_get_contents($sourcePath . '/wp-config.php');
    if (!$stagingConfig || !$sourceConfig) {
        return ['success' => false, 'error' => 'Could not read wp-config.php'];
    }

    preg_match("/define\s*\(\s*['\"]DB_NAME['\"]\s*,\s*['\"]([^'\"]+)['\"]\s*\)/", $stagingConfig, $stagingDbNameMatch);
    preg_match("/define\s*\(\s*['\"]DB_NAME['\"]\s*,\s*['\"]([^'\"]+)['\"]\s*\)/", $sourceConfig, $sourceDbNameMatch);

    $stagingDbName = $stagingDbNameMatch[1] ?? '';
    $sourceDbName = $sourceDbNameMatch[1] ?? '';

    if ($stagingDbName === '' || $sourceDbName === '') {
        return ['success' => false, 'error' => 'Failed to read database names'];
    }

    // Export staging DB and import into production DB
    $dumpFile = "/tmp/wp_push_{$stagingSiteId}.sql";
    exec("mysqldump --defaults-file=/etc/mysql/debian.cnf " . escapeshellarg($stagingDbName) . " > " . escapeshellarg($dumpFile) . " 2>&1", $dumpOutput, $dumpCode);
    if ($dumpCode !== 0) {
        return ['success' => false, 'error' => 'Failed to export staging database'];
    }

    exec("mysql --defaults-file=/etc/mysql/debian.cnf " . escapeshellarg($sourceDbName) . " < " . escapeshellarg($dumpFile) . " 2>&1", $importOutput, $importCode);
    @unlink($dumpFile);

    if ($importCode !== 0) {
        return ['success' => false, 'error' => 'Failed to import staging database'];
    }

    // Update URLs in production database
    $stagingUrl = $stagingSite['url'] ?? '';
    $sourceUrl = $sourceSite['url'] ?? '';
    $stagingDomain = $stagingSite['domain'] ?? '';
    $sourceDomain = $sourceSite['domain'] ?? '';

    if ($stagingUrl && $sourceUrl) {
        exec("cd " . escapeshellarg($sourcePath) . " && sudo -u " . escapeshellarg($username) . " wp search-replace " . escapeshellarg($stagingUrl) . " " . escapeshellarg($sourceUrl) . " --all-tables 2>&1");
    }
    if ($stagingDomain && $sourceDomain) {
        exec("cd " . escapeshellarg($sourcePath) . " && sudo -u " . escapeshellarg($username) . " wp search-replace " . escapeshellarg($stagingDomain) . " " . escapeshellarg($sourceDomain) . " --all-tables 2>&1");
    }

    // Fix ownership
    exec("chown -R {$userInfo['uid']}:{$userInfo['gid']} " . escapeshellarg($sourcePath));

    return ['success' => true, 'message' => 'Staging changes pushed to production'];
}


// ============ PHP SETTINGS MANAGEMENT ============

function phpGetSettings(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }
    
    $userHome = $userInfo['dir'];
    $settingsFile = "{$userHome}/domains/{$domain}/.php-settings.json";
    
    // Default settings (same as createFpmPool defaults)
    $defaults = [
        'php_version' => '8.4',
        'memory_limit' => '512M',
        'upload_max_filesize' => '64M',
        'post_max_size' => '64M',
        'max_execution_time' => '300',
        'max_input_time' => '300',
        'max_input_vars' => '3000',
    ];
    
    // First try to read from JSON settings file
    if (file_exists($settingsFile)) {
        $settings = json_decode(file_get_contents($settingsFile), true) ?: [];
        $settings = array_merge($defaults, $settings);
        return ['success' => true, 'settings' => $settings];
    }
    
    // If no JSON, try to read from the actual FPM pool config
    $phpVersion = '8.4';
    $poolFile = "/etc/php/{$phpVersion}/fpm/pool.d/{$username}.conf";
    
    if (file_exists($poolFile)) {
        $poolContent = file_get_contents($poolFile);
        $settings = $defaults;
        
        // Parse settings from pool config
        if (preg_match('/php_admin_value\[memory_limit\]\s*=\s*(\S+)/', $poolContent, $m)) {
            $settings['memory_limit'] = $m[1];
        }
        if (preg_match('/php_admin_value\[upload_max_filesize\]\s*=\s*(\S+)/', $poolContent, $m)) {
            $settings['upload_max_filesize'] = $m[1];
        }
        if (preg_match('/php_admin_value\[post_max_size\]\s*=\s*(\S+)/', $poolContent, $m)) {
            $settings['post_max_size'] = $m[1];
        }
        if (preg_match('/php_admin_value\[max_execution_time\]\s*=\s*(\S+)/', $poolContent, $m)) {
            $settings['max_execution_time'] = $m[1];
        }
        if (preg_match('/php_admin_value\[max_input_time\]\s*=\s*(\S+)/', $poolContent, $m)) {
            $settings['max_input_time'] = $m[1];
        }
        if (preg_match('/php_admin_value\[max_input_vars\]\s*=\s*(\S+)/', $poolContent, $m)) {
            $settings['max_input_vars'] = $m[1];
        }
        
        // Save to JSON for future use
        $domainDir = "{$userHome}/domains/{$domain}";
        if (is_dir($domainDir)) {
            file_put_contents($settingsFile, json_encode($settings, JSON_PRETTY_PRINT));
            chown($settingsFile, $userInfo['uid']);
            chgrp($settingsFile, $userInfo['gid']);
        }
        
        return ['success' => true, 'settings' => $settings];
    }
    
    // Return defaults if nothing found
    return ['success' => true, 'settings' => $defaults];
}
function phpSetSettings(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }
    
    $userHome = $userInfo['dir'];
    $domainRoot = "{$userHome}/domains/{$domain}";
    
    if (!is_dir($domainRoot)) {
        return ['success' => false, 'error' => 'Domain not found'];
    }
    
    $phpVersion = $params['php_version'] ?? '8.4';
    $memoryLimit = $params['memory_limit'] ?? '256M';
    $uploadMaxFilesize = $params['upload_max_filesize'] ?? '64M';
    $postMaxSize = $params['post_max_size'] ?? '64M';
    $maxExecutionTime = $params['max_execution_time'] ?? '300';
    $maxInputTime = $params['max_input_time'] ?? '300';
    $maxInputVars = $params['max_input_vars'] ?? '3000';
    
    // Save settings to JSON file
    $settings = [
        'php_version' => $phpVersion,
        'memory_limit' => $memoryLimit,
        'upload_max_filesize' => $uploadMaxFilesize,
        'post_max_size' => $postMaxSize,
        'max_execution_time' => $maxExecutionTime,
        'max_input_time' => $maxInputTime,
        'max_input_vars' => $maxInputVars,
    ];
    
    $settingsFile = "{$domainRoot}/.php-settings.json";
    file_put_contents($settingsFile, json_encode($settings, JSON_PRETTY_PRINT));
    chown($settingsFile, $userInfo['uid']);
    chgrp($settingsFile, $userInfo['gid']);
    
    // Update or create PHP-FPM pool with ALL settings
    $fpmSocket = "/run/php/php{$phpVersion}-fpm-{$username}.sock";
    $poolFile = "/etc/php/{$phpVersion}/fpm/pool.d/{$username}.conf";
    
    // Always write full pool config with all settings
    $poolConfig = "[{$username}]
user = {$username}
group = {$username}

listen = {$fpmSocket}
listen.owner = {$username}
listen.group = www-data
listen.mode = 0660

pm = dynamic
pm.max_children = 5
pm.start_servers = 1
pm.min_spare_servers = 1
pm.max_spare_servers = 3
pm.max_requests = 200

chdir = /

; PHP Settings
php_admin_value[memory_limit] = {$memoryLimit}
php_admin_value[upload_max_filesize] = {$uploadMaxFilesize}
php_admin_value[post_max_size] = {$postMaxSize}
php_admin_value[max_execution_time] = {$maxExecutionTime}
php_admin_value[max_input_time] = {$maxInputTime}
php_admin_value[max_input_vars] = {$maxInputVars}

; Security
php_admin_value[open_basedir] = {$userHome}/:/tmp/:/usr/share/php/
php_admin_value[upload_tmp_dir] = {$userHome}/tmp
php_admin_value[session.save_path] = {$userHome}/tmp
php_admin_value[sys_temp_dir] = {$userHome}/tmp
php_admin_value[disable_functions] = symlink,link,exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec

; Logging
php_admin_flag[log_errors] = on
php_admin_value[error_log] = {$userHome}/logs/php-error.log

security.limit_extensions = .php
";
    
    file_put_contents($poolFile, $poolConfig);
    
    // Update Nginx config for this domain to use the correct PHP version
    $nginxConfig = "/etc/nginx/sites-available/{$domain}.conf";
    if (file_exists($nginxConfig)) {
        $nginxContent = file_get_contents($nginxConfig);
        $nginxContent = preg_replace(
            '/fastcgi_pass unix:\/run\/php\/php[\d.]+-fpm-' . preg_quote($username, '/') . '\.sock;/',
            "fastcgi_pass unix:{$fpmSocket};",
            $nginxContent
        );
        file_put_contents($nginxConfig, $nginxContent);
    }
    
    // Reload services in background to not interrupt the request
    exec("(sleep 1 && systemctl reload php{$phpVersion}-fpm && nginx -t && systemctl reload nginx) > /dev/null 2>&1 &");
    
    return ['success' => true, 'message' => 'PHP settings updated successfully'];
}


// DNS Management Functions



// ============ DNS MANAGEMENT ============
function dnsCreateZone(array $params): array {
    $domain = $params['domain'] ?? '';
    $records = $params['records'] ?? null;
    $ns1 = $params['ns1'] ?? 'ns1.example.com';
    $ns2 = $params['ns2'] ?? 'ns2.example.com';
    $adminEmail = $params['admin_email'] ?? 'admin.example.com';
    $defaultIp = $params['default_ip'] ?? '127.0.0.1';
    $defaultIpv6 = $params['default_ipv6'] ?? '';
    $defaultTtl = $params['default_ttl'] ?? 3600;

    if (empty($domain)) {
        return ['success' => false, 'error' => 'Invalid domain name'];
    }

    if (is_array($records) && $records !== []) {
        return dnsSyncZone([
            'domain' => $domain,
            'records' => $records,
            'ns1' => $ns1,
            'ns2' => $ns2,
            'admin_email' => $adminEmail,
            'default_ttl' => $defaultTtl,
        ]);
    }

    $zonesDir = '/etc/bind/zones';
    $zoneFile = "{$zonesDir}/db.{$domain}";

    if (!is_dir($zonesDir)) {
        mkdir($zonesDir, 0755, true);
    }

    $serial = date('Ymd') . '01';
    $zoneContent = "\$TTL {$defaultTtl}\n";
    $zoneContent .= "@   IN  SOA {$ns1}. {$adminEmail}. (\n";
    $zoneContent .= "            {$serial}   ; Serial\n";
    $zoneContent .= "            3600        ; Refresh\n";
    $zoneContent .= "            1800        ; Retry\n";
    $zoneContent .= "            604800      ; Expire\n";
    $zoneContent .= "            86400 )     ; Minimum TTL\n\n";
    $zoneContent .= "@       IN  NS      {$ns1}.\n";
    $zoneContent .= "@       IN  NS      {$ns2}.\n\n";
    $zoneContent .= "@       IN  A       {$defaultIp}\n";
    $zoneContent .= "www     IN  A       {$defaultIp}\n";
    $zoneContent .= "mail    IN  A       {$defaultIp}\n";
    if (!empty($defaultIpv6)) {
        $zoneContent .= "@       IN  AAAA    {$defaultIpv6}\n";
        $zoneContent .= "www     IN  AAAA    {$defaultIpv6}\n";
        $zoneContent .= "mail    IN  AAAA    {$defaultIpv6}\n";
    }
    $zoneContent .= "@       IN  MX  10  mail.{$domain}.\n";

    if (file_put_contents($zoneFile, $zoneContent) === false) {
        return ['success' => false, 'error' => 'Failed to create zone file'];
    }
    chmod($zoneFile, 0644);

    $namedConf = '/etc/bind/named.conf.local';
    $zoneEntry = "\nzone \"{$domain}\" {\n    type master;\n    file \"{$zoneFile}\";\n    allow-transfer { none; };\n};\n";

    $existingConf = file_get_contents($namedConf);
    if (strpos($existingConf, "zone \"{$domain}\"") === false) {
        file_put_contents($namedConf, $zoneEntry, FILE_APPEND);
    }

    exec('systemctl reload bind9 2>&1 || systemctl reload named 2>&1');
    logger("Created DNS zone for {$domain}");
    return ['success' => true, 'message' => "Zone created for {$domain}"];
}

function dnsSyncZone(array $params): array {
    $domain = $params['domain'] ?? '';
    $records = $params['records'] ?? [];
    $ns1 = $params['ns1'] ?? 'ns1.example.com';
    $ns2 = $params['ns2'] ?? 'ns2.example.com';
    $adminEmail = $params['admin_email'] ?? 'admin.example.com';
    $defaultTtl = $params['default_ttl'] ?? 3600;
    $defaultIpv4 = $params['default_ip'] ?? null;
    $defaultIpv6 = $params['default_ipv6'] ?? null;

    if (empty($domain)) return ['success' => false, 'error' => 'Domain required'];

    $zonesDir = '/etc/bind/zones';
    $zoneFile = "{$zonesDir}/db.{$domain}";
    $isNew = !file_exists($zoneFile);
    $domainName = rtrim($domain, '.');
    $ns1 = rtrim($ns1, '.');
    $ns2 = rtrim($ns2, '.');

    $recordMap = ['A' => [], 'AAAA' => []];

    foreach ($records as $r) {
        $type = strtoupper($r['type'] ?? 'A');
        if (!in_array($type, ['A', 'AAAA'], true)) continue;
        $name = trim($r['name'] ?? '@');
        if ($name === '' || $name === '@') {
            $fqdn = $domainName;
        } else {
            $fqdn = rtrim($name, '.');
            if (substr($fqdn, -strlen(".{$domainName}")) !== ".{$domainName}") {
                $fqdn .= ".{$domainName}";
            }
        }
        $recordMap[$type][$fqdn] = $r['content'] ?? '';
    }

    if (empty($defaultIpv4)) {
        $defaultIpv4 = $recordMap['A'][$domainName] ?? (!empty($recordMap['A']) ? reset($recordMap['A']) : null);
    }

    if (empty($defaultIpv6)) {
        $defaultIpv6 = $recordMap['AAAA'][$domainName] ?? (!empty($recordMap['AAAA']) ? reset($recordMap['AAAA']) : null);
    }

    // Ensure zones directory exists
    if (!is_dir($zonesDir)) {
        mkdir($zonesDir, 0755, true);
    }

    $serial = date('Ymd') . sprintf('%02d', (int)date('H') + 1);
    $zoneContent = "\$TTL {$defaultTtl}\n@   IN  SOA {$ns1}. {$adminEmail}. ({$serial} 3600 1800 604800 86400)\n";
    $zoneContent .= "@       IN  NS      {$ns1}.\n@       IN  NS      {$ns2}.\n";

    $glueNames = [];
    foreach (array_filter([$ns1, $ns2]) as $ns) {
        if ($ns === $domainName) {
            $label = '@';
        } elseif (substr($ns, -strlen(".{$domainName}")) === ".{$domainName}") {
            $label = substr($ns, 0, -strlen(".{$domainName}"));
        } else {
            continue;
        }

        $glueNames[$label] = $ns;
    }

    foreach ($glueNames as $label => $fqdn) {
        if ($label === '@') {
            continue;
        }

        if (!isset($recordMap['A'][$fqdn]) && !empty($defaultIpv4)) {
            $zoneContent .= "{$label}\tIN\tA\t{$defaultIpv4}\n";
        }

        if (!isset($recordMap['AAAA'][$fqdn]) && !empty($defaultIpv6)) {
            $zoneContent .= "{$label}\tIN\tAAAA\t{$defaultIpv6}\n";
        }
    }

    foreach ($records as $r) {
        $name = $r['name'] ?? '@';
        $type = $r['type'] ?? 'A';
        $content = $r['content'] ?? '';
        $priority = $r['priority'] ?? '';
        if ($type === 'NS') continue;
        // Use tab separator to handle long names like default._domainkey
        $line = "{$name}\tIN\t{$type}\t";
        if ($type === 'MX' && $priority) $line .= "{$priority}\t";
        if (in_array($type, ['CNAME', 'MX', 'NS']) && substr($content, -1) !== '.') $content .= '.';
        if ($type === 'TXT') {
            // Remove existing quotes if present
            $content = trim($content, '"');
            // Split long TXT records into 255-char chunks (BIND requirement)
            if (strlen($content) > 255) {
                $chunks = str_split($content, 255);
                $content = '( "' . implode('" "', $chunks) . '" )';
            } else {
                $content = "\"{$content}\"";
            }
        }
        $zoneContent .= $line . $content . "\n";
    }

    file_put_contents($zoneFile, $zoneContent);
    chmod($zoneFile, 0644);

    // Add zone to named.conf.local if new
    if ($isNew) {
        $namedConf = '/etc/bind/named.conf.local';
        $zoneEntry = "\nzone \"{$domain}\" {\n    type master;\n    file \"{$zoneFile}\";\n    allow-transfer { none; };\n};\n";
        $existingConf = file_get_contents($namedConf);
        if (strpos($existingConf, "zone \"{$domain}\"") === false) {
            file_put_contents($namedConf, $zoneEntry, FILE_APPEND);
        }
    }

    exec('systemctl reload bind9 2>&1 || systemctl reload named 2>&1');
    logger(($isNew ? "Created" : "Synced") . " DNS zone for {$domain}");
    return ['success' => true, 'message' => "Zone " . ($isNew ? "created" : "synced") . " for {$domain}"];
}

function dnsDeleteZone(array $params): array {
    $domain = $params['domain'] ?? '';
    if (empty($domain)) return ['success' => false, 'error' => 'Domain required'];

    $zoneFile = "/etc/bind/zones/db.{$domain}";
    if (file_exists($zoneFile)) unlink($zoneFile);

    $namedConf = '/etc/bind/named.conf.local';
    if (file_exists($namedConf)) {
        $content = file_get_contents($namedConf);
        // Use [\s\S]*? to match any chars including newlines (handles nested braces)
        $pattern = '/\n?zone\s+"' . preg_quote($domain, '/') . '"\s*\{[\s\S]*?\n\};\n?/';
        $content = preg_replace($pattern, "\n", $content);
        file_put_contents($namedConf, trim($content) . "\n");
    }

    exec('systemctl reload bind9 2>&1 || systemctl reload named 2>&1');
    logger("Deleted DNS zone for {$domain}");
    return ['success' => true, 'message' => "Zone deleted for {$domain}"];
}

function dnsReload(array $params): array {
    exec('systemctl reload bind9 2>&1 || systemctl reload named 2>&1', $output, $ret);
    return $ret === 0 ? ['success' => true, 'message' => 'BIND reloaded'] : ['success' => false, 'error' => implode("\n", $output)];
}

// ============ DNSSEC MANAGEMENT ============

/**
 * Enable DNSSEC for a zone
 * Generates KSK and ZSK keys, configures inline signing
 */
function dnsEnableDnssec(array $params): array {
    $domain = $params['domain'] ?? '';
    if (empty($domain)) {
        return ['success' => false, 'error' => 'Domain required'];
    }

    $zoneFile = "/etc/bind/zones/db.{$domain}";
    if (!file_exists($zoneFile)) {
        return ['success' => false, 'error' => 'Zone file not found. Create the zone first.'];
    }

    // Check if dnssec-keygen is available
    exec('which dnssec-keygen 2>/dev/null', $output, $ret);
    if ($ret !== 0) {
        return ['success' => false, 'error' => 'dnssec-keygen not found. Install bind9-dnssec-utils or bind-dnssec-utils.'];
    }

    // Create keys directory
    $keysDir = DNSSEC_KEYS_DIR;
    $domainKeysDir = "{$keysDir}/{$domain}";
    if (!is_dir($domainKeysDir)) {
        mkdir($domainKeysDir, 0750, true);
        chown($domainKeysDir, 'bind');
        chgrp($domainKeysDir, 'bind');
    }

    // Check if keys already exist
    $existingKeys = glob("{$domainKeysDir}/K{$domain}.+*");
    if (!empty($existingKeys)) {
        return ['success' => false, 'error' => 'DNSSEC keys already exist for this domain. Disable DNSSEC first to regenerate.'];
    }

    $algorithm = DNSSEC_ALGORITHM;

    // Generate KSK (Key Signing Key) - Algorithm 13, 256 bits for ECDSA
    $kskCmd = sprintf(
        'cd %s && dnssec-keygen -a %s -f KSK -n ZONE %s 2>&1',
        escapeshellarg($domainKeysDir),
        escapeshellarg($algorithm),
        escapeshellarg($domain)
    );
    exec($kskCmd, $kskOutput, $kskRet);
    if ($kskRet !== 0) {
        return ['success' => false, 'error' => 'Failed to generate KSK: ' . implode("\n", $kskOutput)];
    }

    // Generate ZSK (Zone Signing Key)
    $zskCmd = sprintf(
        'cd %s && dnssec-keygen -a %s -n ZONE %s 2>&1',
        escapeshellarg($domainKeysDir),
        escapeshellarg($algorithm),
        escapeshellarg($domain)
    );
    exec($zskCmd, $zskOutput, $zskRet);
    if ($zskRet !== 0) {
        return ['success' => false, 'error' => 'Failed to generate ZSK: ' . implode("\n", $zskOutput)];
    }

    // Set proper ownership on key files
    exec("chown -R bind:bind " . escapeshellarg($domainKeysDir));
    exec("chmod 640 {$domainKeysDir}/*.private");
    exec("chmod 644 {$domainKeysDir}/*.key");

    // Update named.conf.local to enable inline signing
    $namedConf = '/etc/bind/named.conf.local';
    $content = file_get_contents($namedConf);

    // Check if zone exists
    if (strpos($content, "zone \"{$domain}\"") === false) {
        return ['success' => false, 'error' => 'Zone not found in named.conf.local'];
    }

    // Remove existing zone entry and add new one with DNSSEC
    // Use [\s\S]*?\n\} to match nested braces like allow-transfer { none; }
    $pattern = '/zone\s+"' . preg_quote($domain, '/') . '"\s*\{[\s\S]*?\n\};/';
    $signedZoneFile = "/etc/bind/zones/db.{$domain}.signed";

    $newZoneEntry = "zone \"{$domain}\" {\n" .
        "    type master;\n" .
        "    file \"{$zoneFile}\";\n" .
        "    key-directory \"{$domainKeysDir}\";\n" .
        "    auto-dnssec maintain;\n" .
        "    inline-signing yes;\n" .
        "    allow-transfer { none; };\n" .
        "};";

    $content = preg_replace($pattern, $newZoneEntry, $content);
    file_put_contents($namedConf, $content);

    // Reload BIND to apply changes
    exec('systemctl reload bind9 2>&1 || systemctl reload named 2>&1', $reloadOutput, $reloadRet);
    if ($reloadRet !== 0) {
        logger("DNSSEC: BIND reload warning for {$domain}: " . implode("\n", $reloadOutput));
    }

    // Wait a moment for signing to begin
    sleep(2);

    // Force zone re-signing
    exec("rndc sign {$domain} 2>&1", $signOutput, $signRet);

    logger("DNSSEC enabled for {$domain}");
    return [
        'success' => true,
        'message' => "DNSSEC enabled for {$domain}. Zone signing in progress.",
        'note' => 'Please wait a few minutes for the zone to be signed, then retrieve the DS records to provide to your registrar.'
    ];
}

/**
 * Disable DNSSEC for a zone
 * Removes keys and disables inline signing
 */
function dnsDisableDnssec(array $params): array {
    $domain = $params['domain'] ?? '';
    if (empty($domain)) {
        return ['success' => false, 'error' => 'Domain required'];
    }

    $zoneFile = "/etc/bind/zones/db.{$domain}";
    $domainKeysDir = DNSSEC_KEYS_DIR . "/{$domain}";

    // Update named.conf.local to disable DNSSEC
    $namedConf = '/etc/bind/named.conf.local';
    $content = file_get_contents($namedConf);

    if (strpos($content, "zone \"{$domain}\"") === false) {
        return ['success' => false, 'error' => 'Zone not found in named.conf.local'];
    }

    // Replace zone entry with non-DNSSEC version
    // Use [\s\S]*?\n\} to match nested braces like allow-transfer { none; }
    $pattern = '/zone\s+"' . preg_quote($domain, '/') . '"\s*\{[\s\S]*?\n\};/';
    $newZoneEntry = "zone \"{$domain}\" {\n" .
        "    type master;\n" .
        "    file \"{$zoneFile}\";\n" .
        "    allow-transfer { none; };\n" .
        "};";

    $content = preg_replace($pattern, $newZoneEntry, $content);
    file_put_contents($namedConf, $content);

    // Remove keys directory
    if (is_dir($domainKeysDir)) {
        exec("rm -rf " . escapeshellarg($domainKeysDir));
    }

    // Remove signed zone file if exists
    $signedZoneFile = "{$zoneFile}.signed";
    if (file_exists($signedZoneFile)) {
        unlink($signedZoneFile);
    }

    // Remove journal files
    foreach (glob("{$zoneFile}*.jnl") as $jnlFile) {
        unlink($jnlFile);
    }
    foreach (glob("{$zoneFile}*.signed.jnl") as $jnlFile) {
        unlink($jnlFile);
    }

    // Reload BIND
    exec('systemctl reload bind9 2>&1 || systemctl reload named 2>&1');

    logger("DNSSEC disabled for {$domain}");
    return [
        'success' => true,
        'message' => "DNSSEC disabled for {$domain}. Remember to remove the DS records from your registrar."
    ];
}

/**
 * Get DNSSEC status for a zone
 */
function dnsGetDnssecStatus(array $params): array {
    $domain = $params['domain'] ?? '';
    if (empty($domain)) {
        return ['success' => false, 'error' => 'Domain required'];
    }

    $domainKeysDir = DNSSEC_KEYS_DIR . "/{$domain}";
    $zoneFile = "/etc/bind/zones/db.{$domain}";
    $signedZoneFile = "{$zoneFile}.signed";

    // Check if zone exists
    if (!file_exists($zoneFile)) {
        return [
            'success' => true,
            'enabled' => false,
            'status' => 'no_zone',
            'message' => 'Zone file not found'
        ];
    }

    // Check if keys exist
    $kskFiles = glob("{$domainKeysDir}/K{$domain}.+*.key");
    $hasKeys = !empty($kskFiles);

    // Check named.conf.local for inline-signing (use [\s\S]*? to match nested braces)
    $namedConf = file_get_contents('/etc/bind/named.conf.local');
    $hasInlineSigning = preg_match('/zone\s+"' . preg_quote($domain, '/') . '"\s*\{[\s\S]*?inline-signing\s+yes[\s\S]*?\n\};/', $namedConf);

    // Check if signed zone exists
    $hasSigned = file_exists($signedZoneFile);

    if (!$hasKeys && !$hasInlineSigning) {
        return [
            'success' => true,
            'enabled' => false,
            'status' => 'disabled',
            'message' => 'DNSSEC is not enabled for this zone'
        ];
    }

    // Get key info
    $keys = [];
    foreach (glob("{$domainKeysDir}/K{$domain}.+*.key") as $keyFile) {
        $keyContent = file_get_contents($keyFile);
        $keyName = basename($keyFile, '.key');

        // Parse key file to determine if KSK or ZSK
        $isKsk = strpos($keyContent, '257') !== false; // KSK has flags 257

        // Get key ID from filename (format: Kdomain.+algorithm+keyid.key)
        preg_match('/\+(\d+)\+(\d+)\.key$/', $keyFile, $matches);
        $algorithm = $matches[1] ?? 'unknown';
        $keyId = $matches[2] ?? 'unknown';

        $keys[] = [
            'type' => $isKsk ? 'KSK' : 'ZSK',
            'algorithm' => $algorithm,
            'key_id' => $keyId,
            'file' => basename($keyFile)
        ];
    }

    return [
        'success' => true,
        'enabled' => true,
        'status' => $hasSigned ? 'active' : 'pending',
        'message' => $hasSigned ? 'DNSSEC is active and zone is signed' : 'DNSSEC is enabled, zone signing in progress',
        'keys' => $keys,
        'has_signed_zone' => $hasSigned
    ];
}

/**
 * Get DS records for registrar
 */
function dnsGetDsRecords(array $params): array {
    $domain = $params['domain'] ?? '';
    if (empty($domain)) {
        return ['success' => false, 'error' => 'Domain required'];
    }

    $domainKeysDir = DNSSEC_KEYS_DIR . "/{$domain}";

    // Find KSK key file (flags 257)
    $kskFiles = glob("{$domainKeysDir}/K{$domain}.+*.key");
    $kskFile = null;

    foreach ($kskFiles as $keyFile) {
        $content = file_get_contents($keyFile);
        if (strpos($content, ' 257 ') !== false) {
            $kskFile = $keyFile;
            break;
        }
    }

    if (!$kskFile) {
        return ['success' => false, 'error' => 'KSK not found. Enable DNSSEC first.'];
    }

    // Check if dnssec-dsfromkey is available
    exec('which dnssec-dsfromkey 2>/dev/null', $output, $ret);
    if ($ret !== 0) {
        return ['success' => false, 'error' => 'dnssec-dsfromkey not found'];
    }

    // Generate DS records in different digest formats
    $dsRecords = [];

    // SHA-256 (digest type 2) - recommended
    exec("dnssec-dsfromkey -2 " . escapeshellarg($kskFile) . " 2>&1", $ds2Output, $ds2Ret);
    if ($ds2Ret === 0 && !empty($ds2Output)) {
        $dsRecords['sha256'] = [
            'digest_type' => 2,
            'digest_name' => 'SHA-256',
            'record' => trim($ds2Output[0])
        ];

        // Parse the DS record
        if (preg_match('/(\S+)\s+IN\s+DS\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)/', $ds2Output[0], $matches)) {
            $dsRecords['sha256']['parsed'] = [
                'domain' => $matches[1],
                'key_tag' => $matches[2],
                'algorithm' => $matches[3],
                'digest_type' => $matches[4],
                'digest' => $matches[5]
            ];
        }
    }

    // SHA-384 (digest type 4) - for ECDSA P-384
    exec("dnssec-dsfromkey -4 " . escapeshellarg($kskFile) . " 2>&1", $ds4Output, $ds4Ret);
    if ($ds4Ret === 0 && !empty($ds4Output)) {
        $dsRecords['sha384'] = [
            'digest_type' => 4,
            'digest_name' => 'SHA-384',
            'record' => trim($ds4Output[0])
        ];

        if (preg_match('/(\S+)\s+IN\s+DS\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)/', $ds4Output[0], $matches)) {
            $dsRecords['sha384']['parsed'] = [
                'domain' => $matches[1],
                'key_tag' => $matches[2],
                'algorithm' => $matches[3],
                'digest_type' => $matches[4],
                'digest' => $matches[5]
            ];
        }
    }

    if (empty($dsRecords)) {
        return ['success' => false, 'error' => 'Failed to generate DS records'];
    }

    // Get DNSKEY record for those who need it
    $dnskeyRecord = trim(file_get_contents($kskFile));

    return [
        'success' => true,
        'domain' => $domain,
        'ds_records' => $dsRecords,
        'dnskey' => $dnskeyRecord,
        'instructions' => 'Add the DS record (SHA-256 recommended) to your domain registrar to complete DNSSEC setup.'
    ];
}

// ============ EMAIL MANAGEMENT ============

function emailEnableDomain(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = strtolower(trim($params['domain'] ?? ''));

    if (empty($username) || !validateUsername($username)) {
        return ['success' => false, 'error' => 'Valid username required'];
    }

    if (empty($domain) || !preg_match('/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*\.[a-z]{2,}$/', $domain)) {
        return ['success' => false, 'error' => 'Invalid domain format'];
    }

    // Get user's UID/GID
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'System user not found'];
    }
    $uid = $userInfo['uid'];
    $gid = $userInfo['gid'];

    // Create mail directory in user's home folder
    $userMailDir = "/home/{$username}/mail";
    $domainDir = "{$userMailDir}/{$domain}";

    if (!is_dir($userMailDir)) {
        mkdir($userMailDir, 0750, true);
        chown($userMailDir, $uid);
        chgrp($userMailDir, $gid);
    }

    if (!is_dir($domainDir)) {
        mkdir($domainDir, 0750, true);
        chown($domainDir, $uid);
        chgrp($domainDir, $gid);
    }

    // Add domain to virtual_mailbox_domains
    $domainsFile = POSTFIX_VIRTUAL_DOMAINS;
    $domainsContent = file_exists($domainsFile) ? file_get_contents($domainsFile) : '';
    if (strpos($domainsContent, $domain) === false) {
        file_put_contents($domainsFile, trim($domainsContent) . "\n{$domain} OK\n");
        exec('postmap ' . escapeshellarg($domainsFile));
    }

    logger("Enabled email for domain: {$domain} (user: {$username})");
    return ['success' => true, 'message' => "Email enabled for {$domain}"];
}

function emailDisableDomain(array $params): array
{
    $domain = strtolower(trim($params['domain'] ?? ''));

    if (empty($domain)) {
        return ['success' => false, 'error' => 'Domain required'];
    }

    // Remove domain from virtual_mailbox_domains
    $domainsFile = POSTFIX_VIRTUAL_DOMAINS;
    if (file_exists($domainsFile)) {
        $content = file_get_contents($domainsFile);
        $content = preg_replace('/^' . preg_quote($domain, '/') . '\s+OK\s*$/m', '', $content);
        file_put_contents($domainsFile, trim($content) . "\n");
        exec('postmap ' . escapeshellarg($domainsFile));
    }

    logger("Disabled email for domain: {$domain}");
    return ['success' => true, 'message' => "Email disabled for {$domain}"];
}

function emailGenerateDkim(array $params): array
{
    $domain = strtolower(trim($params['domain'] ?? ''));
    $selector = $params['selector'] ?? 'default';

    if (empty($domain)) {
        return ['success' => false, 'error' => 'Domain required'];
    }

    $keyDir = DKIM_KEYS_DIR . '/' . $domain;
    if (!is_dir($keyDir)) {
        mkdir($keyDir, 0700, true);
    }

    $privateKey = "{$keyDir}/{$selector}.private";
    $publicKey = "{$keyDir}/{$selector}.txt";

    // Generate DKIM key pair
    $cmd = sprintf(
        'opendkim-genkey -s %s -d %s -D %s 2>&1',
        escapeshellarg($selector),
        escapeshellarg($domain),
        escapeshellarg($keyDir)
    );
    exec($cmd, $output, $exitCode);

    if ($exitCode !== 0 || !file_exists($privateKey)) {
        return ['success' => false, 'error' => 'Failed to generate DKIM keys: ' . implode("\n", $output)];
    }

    // Set ownership
    chown($keyDir, 'opendkim');
    chgrp($keyDir, 'opendkim');
    chmod($keyDir, 0750);
    chown($privateKey, 'opendkim');
    chgrp($privateKey, 'opendkim');
    chmod($privateKey, 0600);
    if (file_exists($publicKey)) {
        chown($publicKey, 'opendkim');
        chgrp($publicKey, 'opendkim');
    }

    // Read public key for DNS record
    $publicKeyContent = file_exists($publicKey) ? file_get_contents($publicKey) : '';

    // Extract all quoted strings and concatenate them
    preg_match_all('/"([^"]*)"/', $publicKeyContent, $allMatches);
    $fullRecord = implode('', $allMatches[1] ?? []);
    // Extract just the p= value (the public key)
    preg_match('/p=([A-Za-z0-9+\/=]+)/', $fullRecord, $matches);
    $publicKeyValue = $matches[1] ?? '';

    // Update OpenDKIM KeyTable
    $keyTableFile = '/etc/opendkim/KeyTable';
    $keyTableEntry = "{$selector}._domainkey.{$domain} {$domain}:{$selector}:{$keyDir}/{$selector}.private\n";
    $keyTableContent = file_exists($keyTableFile) ? file_get_contents($keyTableFile) : '';
    // Remove old entry for this domain if exists
    $keyTableContent = preg_replace("/^.*{$domain}:.*\$/m", '', $keyTableContent);
    $keyTableContent = trim($keyTableContent) . "\n" . $keyTableEntry;
    file_put_contents($keyTableFile, trim($keyTableContent) . "\n");

    // Update OpenDKIM SigningTable
    $signingTableFile = '/etc/opendkim/SigningTable';
    $signingTableEntry = "*@{$domain} {$selector}._domainkey.{$domain}\n";
    $signingTableContent = file_exists($signingTableFile) ? file_get_contents($signingTableFile) : '';
    // Remove old entry for this domain if exists
    $signingTableContent = preg_replace("/^\\*@{$domain}.*\$/m", '', $signingTableContent);
    $signingTableContent = trim($signingTableContent) . "\n" . $signingTableEntry;
    file_put_contents($signingTableFile, trim($signingTableContent) . "\n");

    // Reload OpenDKIM
    exec('systemctl reload opendkim 2>&1', $reloadOutput, $reloadRet);

    logger("Generated DKIM keys for {$domain} with selector {$selector}");

    return [
        'success' => true,
        'message' => "DKIM keys generated for {$domain}",
        'selector' => $selector,
        'public_key' => $publicKeyValue,
        'dns_record' => "{$selector}._domainkey.{$domain} IN TXT \"v=DKIM1; k=rsa; p={$publicKeyValue}\"",
    ];
}

function emailGetDomainInfo(array $params): array
{
    $domain = strtolower(trim($params['domain'] ?? ''));
    $username = $params['username'] ?? '';

    if (empty($domain)) {
        return ['success' => false, 'error' => 'Domain required'];
    }

    // Check user's mail directory for the domain
    $domainDir = !empty($username) ? "/home/{$username}/mail/{$domain}" : null;
    $enabled = $domainDir && is_dir($domainDir);

    // Get mailbox count
    $mailboxCount = 0;
    if ($enabled && is_dir($domainDir)) {
        $dirs = glob($domainDir . '/*', GLOB_ONLYDIR);
        $mailboxCount = count($dirs);
    }

    // Check for DKIM
    $dkimExists = file_exists(DKIM_KEYS_DIR . '/' . $domain . '/default.private');

    return [
        'success' => true,
        'domain' => $domain,
        'enabled' => $enabled,
        'mailbox_count' => $mailboxCount,
        'dkim_configured' => $dkimExists,
    ];
}

function emailMailboxCreate(array $params): array
{
    $username = $params['username'] ?? '';
    $email = strtolower(trim($params['email'] ?? ''));
    $password = $params['password'] ?? '';
    $quotaBytes = (int) ($params['quota_bytes'] ?? 1073741824);

    if (empty($username) || !validateUsername($username)) {
        return ['success' => false, 'error' => 'Valid username required'];
    }

    if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
        return ['success' => false, 'error' => 'Invalid email address'];
    }

    if (empty($password) || strlen($password) < 8) {
        return ['success' => false, 'error' => 'Password must be at least 8 characters'];
    }

    list($localPart, $domain) = explode('@', $email);

    // Get user's UID/GID
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'System user not found'];
    }
    $uid = $userInfo['uid'];
    $gid = $userInfo['gid'];

    // Check domain directory exists in user's home
    $userMailDir = "/home/{$username}/mail";
    $domainDir = "{$userMailDir}/{$domain}";
    if (!is_dir($domainDir)) {
        return ['success' => false, 'error' => 'Email is not enabled for this domain'];
    }

    // Create mailbox directory (Maildir format)
    $mailboxDir = "{$domainDir}/{$localPart}";
    $folderExists = is_dir($mailboxDir);

    // Create/ensure Maildir subdirectories exist
    foreach (['', '/cur', '/new', '/tmp'] as $subdir) {
        $dir = $mailboxDir . $subdir;
        if (!is_dir($dir)) {
            mkdir($dir, 0750, true);
        }
        chown($dir, $uid);
        chgrp($dir, $gid);
    }

    // Generate password hash using doveadm
    $hashCmd = sprintf('doveadm pw -s SHA512-CRYPT -p %s 2>&1', escapeshellarg($password));
    $passwordHash = trim(shell_exec($hashCmd));

    if (empty($passwordHash) || strpos($passwordHash, '{SHA512-CRYPT}') === false) {
        // Fallback to PHP hash if doveadm not available
        $passwordHash = '{SHA512-CRYPT}' . crypt($password, '$6$' . bin2hex(random_bytes(8)) . '$');
    }

    // Full maildir path for database storage (Dovecot will use this)
    $maildirPath = "/home/{$username}/mail/{$domain}/{$localPart}/";

    // Add to virtual_mailbox_maps (Postfix uses this for delivery)
    // Format: email transport:path (lmtp delivers to Dovecot which looks up path from DB)
    $mailboxesFile = POSTFIX_VIRTUAL_MAILBOXES;
    $mailboxesContent = file_exists($mailboxesFile) ? file_get_contents($mailboxesFile) : '';
    if (strpos($mailboxesContent, $email) === false) {
        // Store just the relative path for Postfix virtual transport
        file_put_contents($mailboxesFile, trim($mailboxesContent) . "\n{$email} {$domain}/{$localPart}/\n");
        exec('postmap ' . escapeshellarg($mailboxesFile));
    }

    $action = $folderExists ? 'Recreated' : 'Created';
    logger("{$action} mailbox: {$email} at {$maildirPath}");

    return [
        'success' => true,
        'message' => "Mailbox {$email} {$action}",
        'email' => $email,
        'password_hash' => $passwordHash,
        'maildir_path' => $maildirPath,
        'uid' => $uid,
        'gid' => $gid,
    ];
}

function emailMailboxDelete(array $params): array
{
    $email = strtolower(trim($params['email'] ?? ''));
    $deleteFiles = (bool) ($params['delete_files'] ?? false);
    $maildirPath = $params['maildir_path'] ?? '';
    $username = $params['username'] ?? '';

    if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
        return ['success' => false, 'error' => 'Invalid email address'];
    }

    list($localPart, $domain) = explode('@', $email);

    // Remove from virtual_mailbox_maps
    $mailboxesFile = POSTFIX_VIRTUAL_MAILBOXES;
    if (file_exists($mailboxesFile)) {
        $content = file_get_contents($mailboxesFile);
        $content = preg_replace('/^' . preg_quote($email, '/') . '\s+.*$/m', '', $content);
        file_put_contents($mailboxesFile, trim($content) . "\n");
        exec('postmap ' . escapeshellarg($mailboxesFile));
    }

    // Delete mailbox directory if requested
    if ($deleteFiles) {
        $mailboxDir = null;

        // Try the provided path first
        if (!empty($maildirPath) && strpos($maildirPath, '/home/') === 0) {
            $mailboxDir = rtrim($maildirPath, '/');
        }

        // If path doesn't exist or wasn't provided, try to find it
        if (empty($mailboxDir) || !is_dir($mailboxDir)) {
            // Try /home/{username}/mail/{domain}/{localPart}
            if (!empty($username)) {
                $tryPath = "/home/$username/mail/$domain/$localPart";
                if (is_dir($tryPath)) {
                    $mailboxDir = $tryPath;
                }
            }

            // Try to find in any user's home if username not provided
            if (empty($mailboxDir)) {
                $pattern = "/home/*/mail/$domain/$localPart";
                $matches = glob($pattern);
                if (!empty($matches) && is_dir($matches[0])) {
                    $mailboxDir = $matches[0];
                }
            }
        }

        // Delete if found
        if (!empty($mailboxDir) && is_dir($mailboxDir)) {
            exec('rm -rf ' . escapeshellarg($mailboxDir));
            logger("Deleted mailbox files: {$mailboxDir}");
        }
    }

    logger("Deleted mailbox: {$email}" . ($deleteFiles ? " with files" : ""));

    return ['success' => true, 'message' => "Mailbox {$email} deleted"];
}

function emailMailboxChangePassword(array $params): array
{
    $email = strtolower(trim($params['email'] ?? ''));
    $password = $params['password'] ?? '';

    if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
        return ['success' => false, 'error' => 'Invalid email address'];
    }

    if (empty($password) || strlen($password) < 8) {
        return ['success' => false, 'error' => 'Password must be at least 8 characters'];
    }

    // Generate password hash
    $hashCmd = sprintf('doveadm pw -s SHA512-CRYPT -p %s 2>&1', escapeshellarg($password));
    $passwordHash = trim(shell_exec($hashCmd));

    if (empty($passwordHash) || strpos($passwordHash, '{SHA512-CRYPT}') === false) {
        $passwordHash = '{SHA512-CRYPT}' . crypt($password, '$6$' . bin2hex(random_bytes(8)) . '$');
    }

    logger("Changed password for mailbox: {$email}");

    return [
        'success' => true,
        'message' => "Password changed for {$email}",
        'password_hash' => $passwordHash,
    ];
}

/**
 * Generate a password hash for email authentication (Dovecot SHA512-CRYPT format).
 */
function emailHashPassword(array $params): array
{
    $password = $params['password'] ?? '';

    if (empty($password)) {
        return ['success' => false, 'error' => 'Password is required'];
    }

    // Generate password hash using doveadm
    $hashCmd = sprintf('doveadm pw -s SHA512-CRYPT -p %s 2>&1', escapeshellarg($password));
    $passwordHash = trim(shell_exec($hashCmd));

    if (empty($passwordHash) || strpos($passwordHash, '{SHA512-CRYPT}') === false) {
        // Fallback to PHP hash if doveadm not available
        $passwordHash = '{SHA512-CRYPT}' . crypt($password, '$6$' . bin2hex(random_bytes(8)) . '$');
    }

    return [
        'success' => true,
        'password_hash' => $passwordHash,
    ];
}

function emailMailboxSetQuota(array $params): array
{
    $email = strtolower(trim($params['email'] ?? ''));
    $quotaBytes = (int) ($params['quota_bytes'] ?? 0);

    if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
        return ['success' => false, 'error' => 'Invalid email address'];
    }

    // Quota is managed in the panel database and enforced by Dovecot via SQL query
    // This function is a placeholder for any file-based quota management

    logger("Set quota for {$email}: {$quotaBytes} bytes");

    return [
        'success' => true,
        'message' => "Quota set for {$email}",
        'quota_bytes' => $quotaBytes,
    ];
}

function emailMailboxGetQuotaUsage(array $params): array
{
    $email = strtolower(trim($params['email'] ?? ''));
    $maildirPath = $params['maildir_path'] ?? '';

    if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
        return ['success' => false, 'error' => 'Invalid email address'];
    }

    // maildir_path is required - it contains the full path to the mailbox
    if (empty($maildirPath)) {
        return ['success' => false, 'error' => 'maildir_path parameter required'];
    }

    $mailboxDir = rtrim($maildirPath, '/');

    if (!is_dir($mailboxDir)) {
        return ['success' => false, 'error' => 'Mailbox not found'];
    }

    // Calculate directory size
    $sizeCmd = sprintf('du -sb %s 2>/dev/null | cut -f1', escapeshellarg($mailboxDir));
    $sizeBytes = (int) trim(shell_exec($sizeCmd));

    return [
        'success' => true,
        'email' => $email,
        'quota_used_bytes' => $sizeBytes,
    ];
}

function emailMailboxToggle(array $params): array
{
    $email = strtolower(trim($params['email'] ?? ''));
    $active = (bool) ($params['active'] ?? true);

    if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
        return ['success' => false, 'error' => 'Invalid email address'];
    }

    // Mailbox status is managed in the panel database
    // Dovecot SQL query checks is_active field

    logger("Toggled mailbox {$email}: " . ($active ? 'active' : 'inactive'));

    return [
        'success' => true,
        'message' => "Mailbox {$email} " . ($active ? 'activated' : 'deactivated'),
    ];
}

function emailSyncVirtualUsers(array $params): array
{
    $domain = strtolower(trim($params['domain'] ?? ''));

    if (empty($domain)) {
        return ['success' => false, 'error' => 'Domain required'];
    }

    // Rebuild postfix hash files
    exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_DOMAINS) . ' 2>&1');
    exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_MAILBOXES) . ' 2>&1');
    exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_ALIASES) . ' 2>&1');

    logger("Synced virtual users for domain: {$domain}");

    return ['success' => true, 'message' => "Virtual users synced for {$domain}"];
}

function emailSyncMaps(array $params): array
{
    $domains = $params['domains'] ?? [];
    $mailboxes = $params['mailboxes'] ?? [];
    $aliases = $params['aliases'] ?? [];

    $domainLines = ["# Managed by Jabali"];
    foreach ($domains as $domain) {
        $domain = strtolower(trim((string) $domain));
        if ($domain === '' || !validateDomain($domain)) {
            continue;
        }

        $domainLines[] = "{$domain} OK";
    }

    $mailboxLines = ["# Managed by Jabali"];
    foreach ($mailboxes as $mailbox) {
        $email = strtolower(trim((string) ($mailbox['email'] ?? '')));
        $path = trim((string) ($mailbox['path'] ?? ''));
        if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
            continue;
        }
        if ($path === '') {
            continue;
        }

        $mailboxLines[] = "{$email} {$path}";
    }

    $aliasLines = ["# Managed by Jabali"];
    foreach ($aliases as $alias) {
        $source = strtolower(trim((string) ($alias['source'] ?? '')));
        $destinations = $alias['destinations'] ?? [];
        if ($source === '') {
            continue;
        }

        $destinations = array_filter(array_map('trim', (array) $destinations), fn ($dest) => filter_var($dest, FILTER_VALIDATE_EMAIL));
        if (empty($destinations)) {
            continue;
        }

        $aliasLines[] = $source . ' ' . implode(', ', $destinations);
    }

    file_put_contents(POSTFIX_VIRTUAL_DOMAINS, implode("\n", $domainLines) . "\n");
    file_put_contents(POSTFIX_VIRTUAL_MAILBOXES, implode("\n", $mailboxLines) . "\n");
    file_put_contents(POSTFIX_VIRTUAL_ALIASES, implode("\n", $aliasLines) . "\n");

    exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_DOMAINS) . ' 2>&1');
    exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_MAILBOXES) . ' 2>&1');
    exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_ALIASES) . ' 2>&1');

    logger("Synced postfix maps (domains: " . count($domains) . ", mailboxes: " . count($mailboxes) . ", aliases: " . count($aliases) . ")");

    return [
        'success' => true,
        'domains' => count($domains),
        'mailboxes' => count($mailboxes),
        'aliases' => count($aliases),
    ];
}

function emailReloadServices(array $params): array
{
    $output = [];
    $success = true;

    // Reload Postfix
    exec('systemctl reload postfix 2>&1', $postfixOutput, $postfixExit);
    if ($postfixExit !== 0) {
        $success = false;
        $output[] = 'Postfix reload failed: ' . implode(' ', $postfixOutput);
    }

    // Reload Dovecot
    exec('systemctl reload dovecot 2>&1', $dovecotOutput, $dovecotExit);
    if ($dovecotExit !== 0) {
        $success = false;
        $output[] = 'Dovecot reload failed: ' . implode(' ', $dovecotOutput);
    }

    if ($success) {
        logger("Reloaded email services");
        return ['success' => true, 'message' => 'Email services reloaded'];
    }

    return ['success' => false, 'error' => implode('; ', $output)];
}

// ============ Email Forwarder Management ============

function emailForwarderCreate(array $params): array
{
    $username = $params['username'] ?? '';
    $email = $params['email'] ?? '';
    $destinations = $params['destinations'] ?? [];

    if (empty($username) || empty($email) || empty($destinations)) {
        return ['success' => false, 'error' => 'Missing required parameters'];
    }

    // Validate email format
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        return ['success' => false, 'error' => 'Invalid email address'];
    }

    // Validate destinations
    foreach ($destinations as $dest) {
        if (!filter_var($dest, FILTER_VALIDATE_EMAIL)) {
            return ['success' => false, 'error' => "Invalid destination email: $dest"];
        }
    }

    $virtualAliasFile = '/etc/postfix/virtual_aliases';
    $destinationsList = implode(', ', $destinations);

    // Read existing aliases
    $aliases = [];
    if (file_exists($virtualAliasFile)) {
        $lines = file($virtualAliasFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        foreach ($lines as $line) {
            $line = trim($line);
            if (empty($line) || $line[0] === '#') continue;
            $parts = preg_split('/\s+/', $line, 2);
            if (count($parts) === 2) {
                $aliases[$parts[0]] = $parts[1];
            }
        }
    }

    // Check if alias already exists
    if (isset($aliases[$email])) {
        return ['success' => false, 'error' => 'Forwarder already exists'];
    }

    // Add new alias
    $aliases[$email] = $destinationsList;

    // Write back to file
    $content = "# Virtual Aliases - Managed by Jabali Panel\n";
    foreach ($aliases as $from => $to) {
        $content .= "$from\t$to\n";
    }

    if (file_put_contents($virtualAliasFile, $content) === false) {
        return ['success' => false, 'error' => 'Failed to write virtual aliases file'];
    }

    // Rebuild alias database
    exec('postmap /etc/postfix/virtual_aliases 2>&1', $output, $exitCode);
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'Failed to rebuild alias database'];
    }

    // Reload Postfix
    exec('systemctl reload postfix 2>&1');

    logger("Created forwarder: $email -> $destinationsList");
    return ['success' => true];
}

function emailForwarderDelete(array $params): array
{
    $username = $params['username'] ?? '';
    $email = $params['email'] ?? '';

    if (empty($email)) {
        return ['success' => false, 'error' => 'Missing email parameter'];
    }

    $virtualAliasFile = '/etc/postfix/virtual_aliases';

    if (!file_exists($virtualAliasFile)) {
        return ['success' => false, 'error' => 'Virtual aliases file not found'];
    }

    // Read existing aliases
    $aliases = [];
    $lines = file($virtualAliasFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    foreach ($lines as $line) {
        $line = trim($line);
        if (empty($line) || $line[0] === '#') continue;
        $parts = preg_split('/\s+/', $line, 2);
        if (count($parts) === 2 && $parts[0] !== $email) {
            $aliases[$parts[0]] = $parts[1];
        }
    }

    // Write back to file
    $content = "# Virtual Aliases - Managed by Jabali Panel\n";
    foreach ($aliases as $from => $to) {
        $content .= "$from\t$to\n";
    }

    if (file_put_contents($virtualAliasFile, $content) === false) {
        return ['success' => false, 'error' => 'Failed to write virtual aliases file'];
    }

    // Rebuild alias database
    exec('postmap /etc/postfix/virtual_aliases 2>&1', $output, $exitCode);
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'Failed to rebuild alias database'];
    }

    // Reload Postfix
    exec('systemctl reload postfix 2>&1');

    logger("Deleted forwarder: $email");
    return ['success' => true];
}

function emailForwarderUpdate(array $params): array
{
    $username = $params['username'] ?? '';
    $email = $params['email'] ?? '';
    $destinations = $params['destinations'] ?? [];

    if (empty($email) || empty($destinations)) {
        return ['success' => false, 'error' => 'Missing required parameters'];
    }

    // Validate destinations
    foreach ($destinations as $dest) {
        if (!filter_var($dest, FILTER_VALIDATE_EMAIL)) {
            return ['success' => false, 'error' => "Invalid destination email: $dest"];
        }
    }

    $virtualAliasFile = '/etc/postfix/virtual_aliases';
    $destinationsList = implode(', ', $destinations);

    if (!file_exists($virtualAliasFile)) {
        return ['success' => false, 'error' => 'Virtual aliases file not found'];
    }

    // Read existing aliases
    $aliases = [];
    $found = false;
    $lines = file($virtualAliasFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    foreach ($lines as $line) {
        $line = trim($line);
        if (empty($line) || $line[0] === '#') continue;
        $parts = preg_split('/\s+/', $line, 2);
        if (count($parts) === 2) {
            if ($parts[0] === $email) {
                $aliases[$parts[0]] = $destinationsList;
                $found = true;
            } else {
                $aliases[$parts[0]] = $parts[1];
            }
        }
    }

    if (!$found) {
        return ['success' => false, 'error' => 'Forwarder not found'];
    }

    // Write back to file
    $content = "# Virtual Aliases - Managed by Jabali Panel\n";
    foreach ($aliases as $from => $to) {
        $content .= "$from\t$to\n";
    }

    if (file_put_contents($virtualAliasFile, $content) === false) {
        return ['success' => false, 'error' => 'Failed to write virtual aliases file'];
    }

    // Rebuild alias database
    exec('postmap /etc/postfix/virtual_aliases 2>&1', $output, $exitCode);
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'Failed to rebuild alias database'];
    }

    // Reload Postfix
    exec('systemctl reload postfix 2>&1');

    logger("Updated forwarder: $email -> $destinationsList");
    return ['success' => true];
}

function emailForwarderToggle(array $params): array
{
    $username = $params['username'] ?? '';
    $email = $params['email'] ?? '';
    $active = $params['active'] ?? true;

    if (empty($email)) {
        return ['success' => false, 'error' => 'Missing email parameter'];
    }

    $virtualAliasFile = '/etc/postfix/virtual_aliases';

    if (!file_exists($virtualAliasFile)) {
        return ['success' => false, 'error' => 'Virtual aliases file not found'];
    }

    // Read existing aliases and comments (for disabled ones)
    $content = "# Virtual Aliases - Managed by Jabali Panel\n";
    $found = false;
    $lines = file($virtualAliasFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

    foreach ($lines as $line) {
        $line = trim($line);
        if (empty($line)) continue;

        // Check for disabled forwarder (commented out)
        if (preg_match('/^#DISABLED:\s*(.+?)\s+(.+)$/', $line, $matches)) {
            $forwarderEmail = $matches[1];
            $destinations = $matches[2];
            if ($forwarderEmail === $email) {
                $found = true;
                if ($active) {
                    // Re-enable: remove comment
                    $content .= "$forwarderEmail\t$destinations\n";
                } else {
                    // Keep disabled
                    $content .= "$line\n";
                }
            } else {
                $content .= "$line\n";
            }
        } elseif ($line[0] === '#') {
            // Other comments - keep as-is
            $content .= "$line\n";
        } else {
            // Active forwarder
            $parts = preg_split('/\s+/', $line, 2);
            if (count($parts) === 2) {
                if ($parts[0] === $email) {
                    $found = true;
                    if ($active) {
                        // Keep active
                        $content .= "$line\n";
                    } else {
                        // Disable: comment out
                        $content .= "#DISABLED: $line\n";
                    }
                } else {
                    $content .= "$line\n";
                }
            }
        }
    }

    if (!$found) {
        return ['success' => false, 'error' => 'Forwarder not found'];
    }

    if (file_put_contents($virtualAliasFile, $content) === false) {
        return ['success' => false, 'error' => 'Failed to write virtual aliases file'];
    }

    // Rebuild alias database
    exec('postmap /etc/postfix/virtual_aliases 2>&1', $output, $exitCode);
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'Failed to rebuild alias database'];
    }

    // Reload Postfix
    exec('systemctl reload postfix 2>&1');

    $status = $active ? 'enabled' : 'disabled';
    logger("Forwarder $status: $email");
    return ['success' => true];
}

function emailCatchallUpdate(array $params): array
{
    $domain = $params['domain'] ?? '';
    $enabled = $params['enabled'] ?? false;
    $address = $params['address'] ?? '';

    if (empty($domain)) {
        return ['success' => false, 'error' => 'Missing domain parameter'];
    }

    $virtualAliasFile = '/etc/postfix/virtual_aliases';

    // Read existing content
    $content = "# Virtual Aliases - Managed by Jabali Panel\n";
    if (file_exists($virtualAliasFile)) {
        $lines = file($virtualAliasFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        foreach ($lines as $line) {
            $line = trim($line);
            if (empty($line)) continue;

            // Skip existing catch-all for this domain
            if (preg_match('/^@' . preg_quote($domain, '/') . '\s+/', $line) ||
                preg_match('/^#CATCHALL:\s*@' . preg_quote($domain, '/') . '\s+/', $line)) {
                continue;
            }

            $content .= "$line\n";
        }
    }

    // Add catch-all if enabled
    if ($enabled && !empty($address)) {
        $content .= "@{$domain}\t{$address}\n";
    }

    if (file_put_contents($virtualAliasFile, $content) === false) {
        return ['success' => false, 'error' => 'Failed to write virtual aliases file'];
    }

    // Rebuild alias database
    exec('postmap /etc/postfix/virtual_aliases 2>&1', $output, $exitCode);
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'Failed to rebuild alias database'];
    }

    // Reload Postfix
    exec('systemctl reload postfix 2>&1');

    $status = $enabled ? "enabled (-> $address)" : 'disabled';
    logger("Catch-all $status for domain: $domain");
    return ['success' => true];
}

/**
 * Set or update autoresponder/vacation message using Sieve
 */
function emailAutoresponderSet(array $params): array
{
    $email = $params['email'] ?? '';
    $subject = $params['subject'] ?? 'Out of Office';
    $message = $params['message'] ?? '';
    $startDate = $params['start_date'] ?? null;
    $endDate = $params['end_date'] ?? null;
    $active = $params['active'] ?? true;

    if (empty($email) || empty($message)) {
        return ['success' => false, 'error' => 'Missing email or message parameter'];
    }

    // Parse email to get domain and local part
    $parts = explode('@', $email);
    if (count($parts) !== 2) {
        return ['success' => false, 'error' => 'Invalid email address'];
    }
    $localPart = $parts[0];
    $domain = $parts[1];

    // Sieve directory for user
    $sieveDir = "/var/vmail/{$domain}/{$localPart}/sieve";
    $sieveFile = "{$sieveDir}/vacation.sieve";
    $sieveActive = "{$sieveDir}/.dovecot.sieve";

    // Create sieve directory if it doesn't exist
    if (!is_dir($sieveDir)) {
        mkdir($sieveDir, 0700, true);
        chown($sieveDir, 'vmail');
        chgrp($sieveDir, 'vmail');
    }

    // Build date conditions for Sieve
    $dateConditions = '';
    if ($startDate || $endDate) {
        $dateConditions = 'require "date";' . "\n" . 'require "relational";' . "\n";
    }

    // Escape message for Sieve (handle multi-line)
    $escapedMessage = str_replace(['\\', '"'], ['\\\\', '\\"'], $message);
    $escapedSubject = str_replace(['\\', '"'], ['\\\\', '\\"'], $subject);

    // Build Sieve script
    $sieveScript = <<<SIEVE
# Vacation/Autoresponder - Managed by Jabali Panel
require "vacation";
{$dateConditions}
SIEVE;

    // Add date conditions if specified
    $conditions = [];
    if ($startDate) {
        $sieveScript .= "\n";
        $conditions[] = 'currentdate :value "ge" "date" "' . $startDate . '"';
    }
    if ($endDate) {
        $conditions[] = 'currentdate :value "le" "date" "' . $endDate . '"';
    }

    if (!empty($conditions)) {
        $conditionStr = implode(",\n    ", $conditions);
        $sieveScript .= <<<SIEVE

if allof (
    {$conditionStr}
) {
    vacation :days 1 :subject "{$escapedSubject}" "{$escapedMessage}";
}
SIEVE;
    } else {
        $sieveScript .= <<<SIEVE

vacation :days 1 :subject "{$escapedSubject}" "{$escapedMessage}";
SIEVE;
    }

    // Write Sieve script
    if (file_put_contents($sieveFile, $sieveScript) === false) {
        return ['success' => false, 'error' => 'Failed to write Sieve script'];
    }
    chown($sieveFile, 'vmail');
    chgrp($sieveFile, 'vmail');
    chmod($sieveFile, 0600);

    // Compile Sieve script
    exec("sievec " . escapeshellarg($sieveFile) . " 2>&1", $output, $exitCode);
    if ($exitCode !== 0) {
        logger("Sieve compile warning: " . implode("\n", $output));
        // Don't fail - some warnings are OK
    }

    // Activate or deactivate
    if ($active) {
        // Create symlink to activate
        if (file_exists($sieveActive) || is_link($sieveActive)) {
            unlink($sieveActive);
        }
        symlink($sieveFile, $sieveActive);
        chown($sieveActive, 'vmail');
        chgrp($sieveActive, 'vmail');
    } else {
        // Remove symlink to deactivate
        if (file_exists($sieveActive) || is_link($sieveActive)) {
            unlink($sieveActive);
        }
    }

    logger("Autoresponder " . ($active ? 'enabled' : 'disabled') . " for: $email");
    return ['success' => true];
}

/**
 * Toggle autoresponder on/off
 */
function emailAutoresponderToggle(array $params): array
{
    $email = $params['email'] ?? '';
    $active = $params['active'] ?? false;

    if (empty($email)) {
        return ['success' => false, 'error' => 'Missing email parameter'];
    }

    $parts = explode('@', $email);
    if (count($parts) !== 2) {
        return ['success' => false, 'error' => 'Invalid email address'];
    }
    $localPart = $parts[0];
    $domain = $parts[1];

    $sieveDir = "/var/vmail/{$domain}/{$localPart}/sieve";
    $sieveFile = "{$sieveDir}/vacation.sieve";
    $sieveActive = "{$sieveDir}/.dovecot.sieve";

    if (!file_exists($sieveFile)) {
        return ['success' => false, 'error' => 'No autoresponder configured'];
    }

    if ($active) {
        // Activate
        if (file_exists($sieveActive) || is_link($sieveActive)) {
            unlink($sieveActive);
        }
        symlink($sieveFile, $sieveActive);
        chown($sieveActive, 'vmail');
        chgrp($sieveActive, 'vmail');
    } else {
        // Deactivate
        if (file_exists($sieveActive) || is_link($sieveActive)) {
            unlink($sieveActive);
        }
    }

    logger("Autoresponder " . ($active ? 'enabled' : 'disabled') . " for: $email");
    return ['success' => true];
}

/**
 * Delete autoresponder
 */
function emailAutoresponderDelete(array $params): array
{
    $email = $params['email'] ?? '';

    if (empty($email)) {
        return ['success' => false, 'error' => 'Missing email parameter'];
    }

    $parts = explode('@', $email);
    if (count($parts) !== 2) {
        return ['success' => false, 'error' => 'Invalid email address'];
    }
    $localPart = $parts[0];
    $domain = $parts[1];

    $sieveDir = "/var/vmail/{$domain}/{$localPart}/sieve";
    $sieveFile = "{$sieveDir}/vacation.sieve";
    $sieveCompiled = "{$sieveDir}/vacation.svbin";
    $sieveActive = "{$sieveDir}/.dovecot.sieve";

    // Remove active symlink
    if (file_exists($sieveActive) || is_link($sieveActive)) {
        unlink($sieveActive);
    }

    // Remove Sieve script and compiled version
    if (file_exists($sieveFile)) {
        unlink($sieveFile);
    }
    if (file_exists($sieveCompiled)) {
        unlink($sieveCompiled);
    }

    logger("Autoresponder deleted for: $email");
    return ['success' => true];
}

function emailGetLogs(array $params): array
{
    $username = $params['username'] ?? '';
    $limit = min($params['limit'] ?? 100, 500);

    // Get user's domains to filter logs
    $userDomains = [];
    if (!empty($username)) {
        // Read from database - we'll get domains from the mail log patterns
        // For now, parse logs without domain filter for the user
    }

    $logs = [];
    $mailLogFile = '/var/log/mail.log';
    $output = [];

    if (file_exists($mailLogFile)) {
        // Read last N lines of mail log
        exec("tail -n 1000 " . escapeshellarg($mailLogFile), $output);
    } else {
        // Fallback to journald when mail.log is not present (common on systemd systems)
        exec("journalctl -u postfix --no-pager -n 1000 -o short-iso 2>/dev/null", $output, $journalCode);
        if ($journalCode !== 0) {
            return ['success' => true, 'logs' => []];
        }
    }

    $currentMessage = null;
    $messageIndex = [];

    foreach ($output as $line) {
        // Parse Postfix log lines - support both traditional syslog and ISO 8601 timestamp formats
        // Traditional: Jan 17 10:30:45 server postfix/smtp[12345]: MSGID: to=<user@domain>, ...
        // ISO 8601:    2026-01-18T00:37:30.123456+00:00 server postfix/smtp[12345]: MSGID: ...

        $timestamp = null;
        $component = null;
        $queueId = null;
        $message = null;

        // Try ISO 8601 format first (modern systemd/journald)
        if (preg_match('/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[+-]\d{2}:\d{2})?)\s+\S+\s+postfix\/([\w\-\/]+)\[\d+\]:\s+(.+)$/', $line, $matches)) {
            $timestamp = strtotime($matches[1]);
            $component = $matches[2];
            $message = $matches[3];
        }
        // Try traditional syslog format
        elseif (preg_match('/^(\w+\s+\d+\s+\d+:\d+:\d+)\s+\S+\s+postfix\/([\w\-\/]+)\[\d+\]:\s+(.+)$/', $line, $matches)) {
            $timestamp = strtotime($matches[1] . ' ' . date('Y'));
            $component = $matches[2];
            $message = $matches[3];
        }

        if ($timestamp && $component && $message) {
            $queueId = null;
            $payload = $message;

            if (preg_match('/^([A-F0-9]{5,}):\s+(.+)$/', $message, $idMatch)) {
                $queueId = $idMatch[1];
                $payload = $idMatch[2];
            } elseif (preg_match('/^NOQUEUE:\s+(.+)$/', $message, $noQueueMatch)) {
                $queueId = 'NOQUEUE';
                $payload = $noQueueMatch[1];
            } else {
                continue;
            }

            // Initialize message entry
            $messageKey = $queueId . '-' . $timestamp;
            if (!isset($messageIndex[$messageKey])) {
                $messageIndex[$messageKey] = [
                    'timestamp' => $timestamp,
                    'queue_id' => $queueId,
                    'component' => $component,
                    'from' => null,
                    'to' => null,
                    'subject' => null,
                    'status' => $queueId === 'NOQUEUE' ? 'reject' : 'unknown',
                    'message' => '',
                ];
            }

            // Parse from
            if (preg_match('/from=<([^>]*)>/', $payload, $fromMatch)) {
                $messageIndex[$messageKey]['from'] = $fromMatch[1];
            }

            // Parse to
            if (preg_match('/to=<([^>]*)>/', $payload, $toMatch)) {
                $messageIndex[$messageKey]['to'] = $toMatch[1];
            }

            // Parse status
            if (preg_match('/status=(\w+)/', $payload, $statusMatch)) {
                $messageIndex[$messageKey]['status'] = $statusMatch[1];
                $messageIndex[$messageKey]['message'] = $payload;
            } else {
                $messageIndex[$messageKey]['message'] = $payload;
            }

            // Parse delay and relay info
            if (preg_match('/relay=([^,]+)/', $payload, $relayMatch)) {
                $messageIndex[$messageKey]['relay'] = $relayMatch[1];
            }
        }
    }

    // Convert to array and limit
    $logs = array_values($messageIndex);

    // Sort by timestamp descending
    usort($logs, fn($a, $b) => $b['timestamp'] <=> $a['timestamp']);

    // Limit results
    $logs = array_slice($logs, 0, $limit);

    return ['success' => true, 'logs' => $logs];
}

// ============ UFW Firewall Management ============

function ufwStatus(array $params): array
{
    $output = [];
    exec('ufw status verbose 2>&1', $output, $exitCode);
    
    $status = implode("\n", $output);
    $isActive = strpos($status, 'Status: active') !== false;
    
    // Parse default policies
    $defaultIncoming = 'deny';
    $defaultOutgoing = 'allow';
    if (preg_match('/Default: (\w+) \(incoming\), (\w+) \(outgoing\)/', $status, $matches)) {
        $defaultIncoming = $matches[1];
        $defaultOutgoing = $matches[2];
    }
    
    return [
        'success' => true,
        'active' => $isActive,
        'status_text' => $status,
        'default_incoming' => $defaultIncoming,
        'default_outgoing' => $defaultOutgoing,
    ];
}

function ufwListRules(array $params): array
{
    $output = [];
    exec('ufw status numbered 2>&1', $output, $exitCode);
    
    $rules = [];
    $statusLine = '';
    
    foreach ($output as $line) {
        if (strpos($line, 'Status:') !== false) {
            $statusLine = $line;
            continue;
        }
        
        // Parse numbered rules like: [ 1] 22/tcp                     ALLOW IN    Anywhere
        if (preg_match('/\[\s*(\d+)\]\s+(.+?)\s+(ALLOW|DENY|REJECT|LIMIT)\s+(IN|OUT)?\s*(.*)/', $line, $matches)) {
            $rules[] = [
                'number' => (int)$matches[1],
                'to' => trim($matches[2]),
                'action' => $matches[3],
                'direction' => $matches[4] ?: 'IN',
                'from' => trim($matches[5]),
            ];
        }
    }
    
    return [
        'success' => true,
        'rules' => $rules,
        'raw_output' => implode("\n", $output),
    ];
}

function ufwEnable(array $params): array
{
    exec('echo "y" | ufw enable 2>&1', $output, $exitCode);
    
    return [
        'success' => $exitCode === 0,
        'message' => implode("\n", $output),
    ];
}

function ufwDisable(array $params): array
{
    exec('ufw disable 2>&1', $output, $exitCode);
    
    return [
        'success' => $exitCode === 0,
        'message' => implode("\n", $output),
    ];
}

function ufwAllowPort(array $params): array
{
    $port = $params['port'] ?? '';
    $protocol = $params['protocol'] ?? ''; // tcp, udp, or empty for both
    $comment = $params['comment'] ?? '';
    
    if (empty($port)) {
        return ['success' => false, 'error' => 'Port is required'];
    }
    
    // Validate port
    if (!preg_match('/^\d+$/', $port) && !preg_match('/^\d+:\d+$/', $port)) {
        return ['success' => false, 'error' => 'Invalid port format'];
    }
    
    $rule = $port;
    if (!empty($protocol)) {
        $rule .= '/' . $protocol;
    }
    
    $cmd = "ufw allow $rule";
    if (!empty($comment)) {
        $cmd .= " comment " . escapeshellarg($comment);
    }
    
    exec($cmd . ' 2>&1', $output, $exitCode);
    
    return [
        'success' => $exitCode === 0,
        'message' => implode("\n", $output),
    ];
}

function ufwDenyPort(array $params): array
{
    $port = $params['port'] ?? '';
    $protocol = $params['protocol'] ?? '';
    $comment = $params['comment'] ?? '';
    
    if (empty($port)) {
        return ['success' => false, 'error' => 'Port is required'];
    }
    
    $rule = $port;
    if (!empty($protocol)) {
        $rule .= '/' . $protocol;
    }
    
    $cmd = "ufw deny $rule";
    if (!empty($comment)) {
        $cmd .= " comment " . escapeshellarg($comment);
    }
    
    exec($cmd . ' 2>&1', $output, $exitCode);
    
    return [
        'success' => $exitCode === 0,
        'message' => implode("\n", $output),
    ];
}

function ufwAllowIp(array $params): array
{
    $ip = $params['ip'] ?? '';
    $port = $params['port'] ?? '';
    $protocol = $params['protocol'] ?? '';
    $comment = $params['comment'] ?? '';
    
    if (empty($ip)) {
        return ['success' => false, 'error' => 'IP address is required'];
    }
    
    // Validate IP (simple check)
    if (!filter_var($ip, FILTER_VALIDATE_IP) && !preg_match('/^\d+\.\d+\.\d+\.\d+\/\d+$/', $ip)) {
        return ['success' => false, 'error' => 'Invalid IP address format'];
    }
    
    $cmd = "ufw allow from " . escapeshellarg($ip);
    
    if (!empty($port)) {
        $cmd .= " to any port $port";
        if (!empty($protocol)) {
            $cmd .= " proto $protocol";
        }
    }
    
    if (!empty($comment)) {
        $cmd .= " comment " . escapeshellarg($comment);
    }
    
    exec($cmd . ' 2>&1', $output, $exitCode);
    
    return [
        'success' => $exitCode === 0,
        'message' => implode("\n", $output),
    ];
}

function ufwDenyIp(array $params): array
{
    $ip = $params['ip'] ?? '';
    $port = $params['port'] ?? '';
    $protocol = $params['protocol'] ?? '';
    $comment = $params['comment'] ?? '';
    
    if (empty($ip)) {
        return ['success' => false, 'error' => 'IP address is required'];
    }
    
    if (!filter_var($ip, FILTER_VALIDATE_IP) && !preg_match('/^\d+\.\d+\.\d+\.\d+\/\d+$/', $ip)) {
        return ['success' => false, 'error' => 'Invalid IP address format'];
    }
    
    $cmd = "ufw deny from " . escapeshellarg($ip);
    
    if (!empty($port)) {
        $cmd .= " to any port $port";
        if (!empty($protocol)) {
            $cmd .= " proto $protocol";
        }
    }
    
    if (!empty($comment)) {
        $cmd .= " comment " . escapeshellarg($comment);
    }
    
    exec($cmd . ' 2>&1', $output, $exitCode);
    
    return [
        'success' => $exitCode === 0,
        'message' => implode("\n", $output),
    ];
}

function ufwDeleteRule(array $params): array
{
    $ruleNumber = $params['rule_number'] ?? '';
    
    if (empty($ruleNumber)) {
        return ['success' => false, 'error' => 'Rule number is required'];
    }
    
    exec('echo "y" | ufw delete ' . (int)$ruleNumber . ' 2>&1', $output, $exitCode);
    
    return [
        'success' => $exitCode === 0,
        'message' => implode("\n", $output),
    ];
}

function ufwSetDefault(array $params): array
{
    $direction = $params['direction'] ?? 'incoming'; // incoming or outgoing
    $policy = $params['policy'] ?? 'deny'; // allow, deny, reject
    
    if (!in_array($direction, ['incoming', 'outgoing'])) {
        return ['success' => false, 'error' => 'Direction must be incoming or outgoing'];
    }
    
    if (!in_array($policy, ['allow', 'deny', 'reject'])) {
        return ['success' => false, 'error' => 'Policy must be allow, deny, or reject'];
    }
    
    exec("ufw default $policy $direction 2>&1", $output, $exitCode);
    
    return [
        'success' => $exitCode === 0,
        'message' => implode("\n", $output),
    ];
}

function ufwLimitPort(array $params): array
{
    $port = $params['port'] ?? '';
    $protocol = $params['protocol'] ?? 'tcp';
    
    if (empty($port)) {
        return ['success' => false, 'error' => 'Port is required'];
    }
    
    $rule = $port;
    if (!empty($protocol)) {
        $rule .= '/' . $protocol;
    }
    
    exec("ufw limit $rule 2>&1", $output, $exitCode);
    
    return [
        'success' => $exitCode === 0,
        'message' => implode("\n", $output),
    ];
}

function ufwReset(array $params): array
{
    exec('echo "y" | ufw reset 2>&1', $output, $exitCode);
    
    return [
        'success' => $exitCode === 0,
        'message' => implode("\n", $output),
    ];
}

function ufwReload(array $params): array
{
    exec('ufw reload 2>&1', $output, $exitCode);
    
    return [
        'success' => $exitCode === 0,
        'message' => implode("\n", $output),
    ];
}

function ufwAllowService(array $params): array
{
    $service = $params['service'] ?? '';
    
    if (empty($service)) {
        return ['success' => false, 'error' => 'Service name is required'];
    }
    
    // Whitelist common services
    $allowedServices = ['ssh', 'http', 'https', 'ftp', 'smtp', 'pop3', 'imap', 'dns', 'mysql', 'postgresql'];
    if (!in_array(strtolower($service), $allowedServices) && !preg_match('/^[a-zA-Z0-9_-]+$/', $service)) {
        return ['success' => false, 'error' => 'Invalid service name'];
    }
    
    exec("ufw allow " . escapeshellarg($service) . " 2>&1", $output, $exitCode);
    
    return [
        'success' => $exitCode === 0,
        'message' => implode("\n", $output),
    ];
}

// ============ PHP VERSION MANAGEMENT ============

function phpInstall(array $params): array
{
    $version = $params['version'] ?? '';

    if (!preg_match('/^\d+\.\d+$/', $version)) {
        return ['success' => false, 'error' => 'Invalid PHP version format'];
    }

    // Check if dpkg/apt is locked
    exec("fuser /var/lib/dpkg/lock-frontend 2>/dev/null", $lockOutput, $lockCode);
    if ($lockCode === 0 && !empty($lockOutput)) {
        return ['success' => false, 'error' => 'Package manager is busy. Please wait for the current operation to complete.'];
    }

    logger("Installing PHP $version");

    if (!file_exists('/etc/apt/sources.list.d/php.list')) {
        exec('apt-get update 2>&1');
        exec('apt-get install -y apt-transport-https lsb-release ca-certificates curl 2>&1');
        exec('curl -sSL https://packages.sury.org/php/apt.gpg | gpg --dearmor -o /usr/share/keyrings/php-archive-keyring.gpg 2>&1');
        $release = trim(shell_exec('lsb_release -sc'));
        file_put_contents('/etc/apt/sources.list.d/php.list', 
            "deb [signed-by=/usr/share/keyrings/php-archive-keyring.gpg] https://packages.sury.org/php/ $release main\n");
        exec('apt-get update 2>&1');
    }
    
    // Core PHP packages + WordPress recommended modules (imagick, opcache, redis, soap, imap, exif)
    $packages = "php{$version}-fpm php{$version}-cli php{$version}-common php{$version}-mysql php{$version}-zip php{$version}-gd php{$version}-mbstring php{$version}-curl php{$version}-xml php{$version}-bcmath php{$version}-intl php{$version}-imagick php{$version}-opcache php{$version}-redis php{$version}-soap php{$version}-imap php{$version}-exif";

    exec("DEBIAN_FRONTEND=noninteractive apt-get install -y $packages 2>&1", $output, $exitCode);
    
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'Failed to install PHP: ' . implode("\n", $output)];
    }
    
    exec("systemctl enable php{$version}-fpm 2>&1");
    exec("systemctl start php{$version}-fpm 2>&1");
    
    return ['success' => true, 'version' => $version, 'message' => "PHP $version installed successfully"];
}

function phpUninstall(array $params): array
{
    $version = $params['version'] ?? '';

    if (!preg_match('/^\d+\.\d+$/', $version)) {
        return ['success' => false, 'error' => 'Invalid PHP version format'];
    }

    // Check if dpkg/apt is locked
    exec("fuser /var/lib/dpkg/lock-frontend 2>/dev/null", $lockOutput, $lockCode);
    if ($lockCode === 0 && !empty($lockOutput)) {
        return ['success' => false, 'error' => 'Package manager is busy. Please wait for the current operation to complete.'];
    }

    exec("systemctl stop php{$version}-fpm 2>/dev/null");
    exec("systemctl disable php{$version}-fpm 2>/dev/null");

    $output = [];
    // Use purge instead of remove to also delete config files
    exec("DEBIAN_FRONTEND=noninteractive apt-get purge -y php{$version}-* 2>&1", $output, $exitCode);

    if ($exitCode !== 0) {
        $errorOutput = implode("\n", $output);
        if (strpos($errorOutput, 'lock') !== false || strpos($errorOutput, 'Could not get lock') !== false) {
            return ['success' => false, 'error' => 'Package manager is busy. Please wait for the current operation to complete.'];
        }
        return ['success' => false, 'error' => "Failed to uninstall PHP $version: " . $errorOutput];
    }

    exec("DEBIAN_FRONTEND=noninteractive apt-get autoremove --purge -y 2>&1");

    // Clean up any remaining config directory
    $configDir = "/etc/php/{$version}";
    if (is_dir($configDir)) {
        exec("rm -rf " . escapeshellarg($configDir));
    }

    return ['success' => true, 'version' => $version, 'message' => "PHP $version uninstalled"];
}

function phpSetDefaultVersion(array $params): array
{
    $version = $params['version'] ?? '';
    
    if (!preg_match('/^\d+\.\d+$/', $version)) {
        return ['success' => false, 'error' => 'Invalid PHP version format'];
    }
    
    exec("update-alternatives --set php /usr/bin/php{$version} 2>&1", $output, $exitCode);
    
    return ['success' => $exitCode === 0, 'version' => $version, 'message' => "PHP $version set as default"];
}

function phpRestartFpm(array $params): array
{
    return phpReloadFpm($params);
}

function phpReloadFpm(array $params): array
{
    $version = $params['version'] ?? '';

    if (!preg_match('/^\d+\.\d+$/', $version)) {
        return ['success' => false, 'error' => 'Invalid PHP version format'];
    }

    // Check if PHP-FPM service exists
    $serviceFile = "/lib/systemd/system/php{$version}-fpm.service";
    if (!file_exists($serviceFile)) {
        return ['success' => false, 'error' => "PHP $version is not installed"];
    }

    exec("systemctl reload php{$version}-fpm 2>&1", $output, $exitCode);

    if ($exitCode !== 0) {
        $errorOutput = implode("\n", $output);
        return ['success' => false, 'error' => "Failed to reload PHP $version FPM: " . ($errorOutput ?: 'Service error')];
    }

    return ['success' => true, 'version' => $version, 'message' => "PHP $version FPM reloaded"];
}

function phpRestartAllFpm(array $params): array
{
    return phpReloadAllFpm($params);
}

function phpReloadAllFpm(array $params): array
{
    $background = $params['background'] ?? false;
    $delay = (int) ($params['delay'] ?? 0);

    // Find all installed PHP-FPM services
    $services = glob('/lib/systemd/system/php*-fpm.service');
    $versions = [];
    foreach ($services as $serviceFile) {
        if (preg_match('/php(\d+\.\d+)-fpm\.service$/', basename($serviceFile), $matches)) {
            $versions[] = $matches[1];
        }
    }

    if (empty($versions)) {
        return ['success' => false, 'error' => 'No PHP-FPM services found'];
    }

    // Background reload to avoid blocking the caller
    if ($background) {
        $delayCmd = $delay > 0 ? "sleep {$delay} && " : '';
        $cmd = "({$delayCmd}";

        foreach ($versions as $version) {
            $cmd .= "systemctl reload php{$version}-fpm 2>/dev/null; ";
        }
        $cmd .= ") > /dev/null 2>&1 &";

        exec($cmd);

        return [
            'success' => true,
            'background' => true,
            'delay' => $delay,
            'versions' => $versions,
            'message' => 'PHP-FPM reload scheduled' . ($delay > 0 ? " in {$delay} seconds" : ' in background'),
        ];
    }

    $reloaded = [];
    $failed = [];

    foreach ($versions as $version) {
        exec("systemctl reload php{$version}-fpm 2>&1", $output, $exitCode);

        if ($exitCode === 0) {
            $reloaded[] = $version;
        } else {
            $failed[] = $version;
        }
    }

    return [
        'success' => empty($failed),
        'reloaded' => $reloaded,
        'failed' => $failed,
        'message' => 'Reloaded PHP-FPM versions: ' . implode(', ', $reloaded) . (empty($failed) ? '' : '. Failed: ' . implode(', ', $failed)),
    ];
}

function phpListVersions(array $params): array
{
    $versions = [];

    foreach (glob('/etc/php/*', GLOB_ONLYDIR) as $dir) {
        $version = basename($dir);
        if (preg_match('/^\d+\.\d+$/', $version)) {
            // Check if PHP binary actually exists (not just config directory)
            $phpBinary = "/usr/bin/php{$version}";
            $fpmService = "/lib/systemd/system/php{$version}-fpm.service";

            if (!file_exists($phpBinary) && !file_exists($fpmService)) {
                // PHP was uninstalled but config dir remains - skip it
                continue;
            }

            $fpmStatus = trim(shell_exec("systemctl is-active php{$version}-fpm 2>/dev/null") ?? 'inactive');
            $versions[] = [
                'version' => $version,
                'fpm_status' => $fpmStatus ?: 'inactive',
            ];
        }
    }

    $defaultVersion = null;
    $phpV = shell_exec('php -v 2>/dev/null');
    if ($phpV && preg_match('/PHP (\d+\.\d+)/', $phpV, $matches)) {
        $defaultVersion = $matches[1];
    }

    return ['success' => true, 'versions' => $versions, 'default' => $defaultVersion];
}

function phpInstallWordPressModules(array $params): array
{
    $version = $params['version'] ?? null;

    // Get all installed PHP versions if no specific version provided
    $versions = [];
    if ($version) {
        if (!preg_match('/^\d+\.\d+$/', $version)) {
            return ['success' => false, 'error' => 'Invalid PHP version format'];
        }
        $versions[] = $version;
    } else {
        // Install for all installed PHP versions
        foreach (glob('/etc/php/*', GLOB_ONLYDIR) as $dir) {
            $ver = basename($dir);
            if (preg_match('/^\d+\.\d+$/', $ver)) {
                $versions[] = $ver;
            }
        }
    }

    if (empty($versions)) {
        return ['success' => false, 'error' => 'No PHP versions found'];
    }

    logger("Installing WordPress PHP modules for versions: " . implode(', ', $versions));

    // WordPress recommended modules
    $wpModules = ['imagick', 'opcache', 'redis', 'soap', 'imap', 'exif'];

    $installed = [];
    $failed = [];

    foreach ($versions as $ver) {
        $packages = [];
        foreach ($wpModules as $module) {
            $packages[] = "php{$ver}-{$module}";
        }

        $packageList = implode(' ', $packages);
        exec("apt-get install -y $packageList 2>&1", $output, $exitCode);

        if ($exitCode === 0) {
            $installed[] = $ver;
            // Reload PHP-FPM for this version
            exec("systemctl reload php{$ver}-fpm 2>/dev/null");
        } else {
            $failed[] = $ver;
        }
    }

    if (empty($failed)) {
        return [
            'success' => true,
            'message' => 'WordPress PHP modules installed for: ' . implode(', ', $installed),
            'modules' => $wpModules,
            'versions' => $installed
        ];
    } elseif (!empty($installed)) {
        return [
            'success' => true,
            'message' => 'Installed for: ' . implode(', ', $installed) . '. Failed for: ' . implode(', ', $failed),
            'modules' => $wpModules,
            'versions' => $installed,
            'failed' => $failed
        ];
    } else {
        return ['success' => false, 'error' => 'Failed to install modules for all versions'];
    }
}

// ============ SSH KEY GENERATION ============

function sshGenerateKey(array $params): array
{
    $name = $params['name'] ?? 'key';
    $type = $params['type'] ?? 'ed25519';
    $passphrase = $params['passphrase'] ?? '';
    
    $tempDir = sys_get_temp_dir() . '/jabali_ssh_' . uniqid();
    mkdir($tempDir, 0700, true);
    
    $keyFile = "$tempDir/key";
    $pubFile = "$keyFile.pub";
    $ppkFile = "$tempDir/key.ppk";
    
    try {
        $keyType = $type === 'ed25519' ? 'ed25519' : 'rsa';
        $bits = $type === 'rsa' ? '-b 4096' : '';
        $passOpt = $passphrase ? "-N " . escapeshellarg($passphrase) : "-N ''";
        
        $cmd = "ssh-keygen -t {$keyType} {$bits} -f " . escapeshellarg($keyFile) . " {$passOpt} -C " . escapeshellarg($name) . " 2>&1";
        exec($cmd, $output, $returnCode);
        
        if ($returnCode !== 0 || !file_exists($keyFile)) {
            return ['success' => false, 'error' => 'Failed to generate SSH key: ' . implode("\n", $output)];
        }
        
        $privateKey = file_get_contents($keyFile);
        $publicKey = file_get_contents($pubFile);
        
        // Get fingerprint
        exec("ssh-keygen -lf " . escapeshellarg($pubFile) . " 2>&1", $fpOutput);
        $fingerprint = $fpOutput[0] ?? '';
        
        // Convert to PPK format
        $ppkKey = null;
        if ($passphrase) {
            $ppkCmd = "puttygen " . escapeshellarg($keyFile) . " -o " . escapeshellarg($ppkFile) . " -O private -P --old-passphrase " . escapeshellarg($passphrase) . " --new-passphrase " . escapeshellarg($passphrase) . " 2>&1";
        } else {
            $ppkCmd = "puttygen " . escapeshellarg($keyFile) . " -o " . escapeshellarg($ppkFile) . " -O private 2>&1";
        }
        exec($ppkCmd, $ppkOutput, $ppkReturnCode);
        
        if (file_exists($ppkFile)) {
            $ppkKey = file_get_contents($ppkFile);
        }
        
        return [
            'success' => true,
            'name' => $name,
            'type' => $type,
            'private_key' => $privateKey,
            'public_key' => $publicKey,
            'ppk_key' => $ppkKey,
            'fingerprint' => $fingerprint,
        ];
    } finally {
        @unlink($keyFile);
        @unlink($pubFile);
        @unlink($ppkFile);
        @rmdir($tempDir);
    }
}

// ============ SSH SHELL ACCESS MANAGEMENT ============

function sshEnableShell(array $params): array
{
    $username = $params['username'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User does not exist'];
    }

    // Create user's home directory in jail
    $jailHome = "/var/jail/home/$username";
    if (!is_dir($jailHome)) {
        mkdir($jailHome, 0750, true);
        chown($jailHome, $username);
        chgrp($jailHome, $username);
    }

    // Add user to shellusers group and remove from sftpusers
    exec("usermod -aG shellusers " . escapeshellarg($username));
    exec("gpasswd -d " . escapeshellarg($username) . " sftpusers 2>/dev/null");

    // Change shell to bash (relative to chroot /var/jail, so /bin/bash)
    exec("usermod -s /bin/bash " . escapeshellarg($username));

    // Add user to jail's passwd/group
    $passwdLine = "$username:x:{$userInfo['uid']}:{$userInfo['gid']}::/home/$username:/bin/bash\n";
    file_put_contents('/var/jail/etc/passwd', $passwdLine, FILE_APPEND);

    $groupLine = "$username:x:{$userInfo['gid']}:\n";
    file_put_contents('/var/jail/etc/group', $groupLine, FILE_APPEND);

    // Bind mount user's actual home to jail home for file access
    $realHome = $userInfo['dir'];
    if (!is_mounted($jailHome)) {
        exec("mount --bind " . escapeshellarg($realHome) . " " . escapeshellarg($jailHome));

        // Add to fstab for persistence
        $fstabEntry = "$realHome $jailHome none bind 0 0\n";
        if (strpos(file_get_contents('/etc/fstab'), $jailHome) === false) {
            file_put_contents('/etc/fstab', $fstabEntry, FILE_APPEND);
        }
    }

    logger("Enabled shell access for user $username");

    return [
        'success' => true,
        'message' => "Shell access enabled for $username",
        'shell' => '/bin/bash',
    ];
}

function sshDisableShell(array $params): array
{
    $username = $params['username'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User does not exist'];
    }

    // Remove from shellusers and add to sftpusers
    exec("gpasswd -d " . escapeshellarg($username) . " shellusers 2>/dev/null");
    exec("usermod -aG sftpusers " . escapeshellarg($username));

    // Change shell to nologin
    exec("usermod -s /usr/sbin/nologin " . escapeshellarg($username));

    // Unmount bind mount if exists
    $jailHome = "/var/jail/home/$username";
    if (is_mounted($jailHome)) {
        exec("umount " . escapeshellarg($jailHome));

        // Remove from fstab
        $fstab = file_get_contents('/etc/fstab');
        $fstab = preg_replace('/.*' . preg_quote($jailHome, '/') . '.*\n/', '', $fstab);
        file_put_contents('/etc/fstab', $fstab);
    }

    // Remove from jail passwd/group
    if (file_exists('/var/jail/etc/passwd')) {
        $passwd = file_get_contents('/var/jail/etc/passwd');
        $passwd = preg_replace('/^' . preg_quote($username, '/') . ':.*\n/m', '', $passwd);
        file_put_contents('/var/jail/etc/passwd', $passwd);
    }
    if (file_exists('/var/jail/etc/group')) {
        $group = file_get_contents('/var/jail/etc/group');
        $group = preg_replace('/^' . preg_quote($username, '/') . ':.*\n/m', '', $group);
        file_put_contents('/var/jail/etc/group', $group);
    }

    logger("Disabled shell access for user $username");

    return [
        'success' => true,
        'message' => "Shell access disabled for $username (SFTP-only)",
        'shell' => '/usr/sbin/nologin',
    ];
}

function sshGetShellStatus(array $params): array
{
    $username = $params['username'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User does not exist'];
    }

    $shell = $userInfo['shell'];
    $hasShell = !in_array($shell, ['/usr/sbin/nologin', '/bin/false', '/sbin/nologin']);

    // Check group membership
    exec("groups " . escapeshellarg($username), $groupOutput);
    $groups = $groupOutput[0] ?? '';
    $inShellUsers = strpos($groups, 'shellusers') !== false;
    $inSftpUsers = strpos($groups, 'sftpusers') !== false;

    return [
        'success' => true,
        'username' => $username,
        'shell' => $shell,
        'shell_enabled' => $hasShell && $inShellUsers,
        'sftp_only' => !$hasShell || $inSftpUsers,
        'groups' => $groups,
    ];
}

function is_mounted(string $path): bool
{
    exec("mountpoint -q " . escapeshellarg($path), $output, $returnCode);
    return $returnCode === 0;
}

// ============ SERVER SETTINGS ============

function setHostname(array $params): array
{
    $hostname = $params['hostname'] ?? '';
    
    if (empty($hostname) || !preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/', $hostname)) {
        return ['success' => false, 'error' => 'Invalid hostname format'];
    }
    
    // Set hostname
    exec("hostnamectl set-hostname " . escapeshellarg($hostname) . " 2>&1", $output, $exitCode);
    
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'Failed to set hostname: ' . implode("\n", $output)];
    }
    
    // Update /etc/hosts
    $hosts = file_get_contents('/etc/hosts');
    $shortname = explode('.', $hostname)[0];
    
    // Update or add the 127.0.1.1 line
    if (preg_match('/^127\.0\.1\.1\s+.*/m', $hosts)) {
        $hosts = preg_replace('/^127\.0\.1\.1\s+.*/m', "127.0.1.1\t$hostname $shortname", $hosts);
    } else {
        $hosts .= "\n127.0.1.1\t$hostname $shortname\n";
    }
    file_put_contents('/etc/hosts', $hosts);
    
    return ['success' => true, 'hostname' => $hostname];
}

function setUploadLimits(array $params): array
{
    $sizeMb = (int) ($params['size_mb'] ?? 100);

    // Validate size (1-500 MB)
    if ($sizeMb < 1) $sizeMb = 1;
    if ($sizeMb > 500) $sizeMb = 500;

    $errors = [];

    // Update nginx global config
    $nginxConf = '/etc/nginx/nginx.conf';
    if (file_exists($nginxConf)) {
        $content = file_get_contents($nginxConf);
        $content = preg_replace('/client_max_body_size\s+\d+M;/', "client_max_body_size {$sizeMb}M;", $content);
        file_put_contents($nginxConf, $content);
    }

    // Update jabali site config
    $jabaliSites = glob('/etc/nginx/sites-available/*');
    foreach ($jabaliSites as $site) {
        $content = file_get_contents($site);
        if (strpos($content, 'client_max_body_size') !== false) {
            $content = preg_replace('/client_max_body_size\s+\d+M;/', "client_max_body_size {$sizeMb}M;", $content);
            file_put_contents($site, $content);
        }
    }

    // Update PHP-FPM pools
    $pools = glob('/etc/php/*/fpm/pool.d/*.conf');
    foreach ($pools as $pool) {
        $content = file_get_contents($pool);
        $content = preg_replace('/^php_admin_value\[upload_max_filesize\].*$/m', "php_admin_value[upload_max_filesize] = {$sizeMb}M", $content);
        $content = preg_replace('/^php_admin_value\[post_max_size\].*$/m', "php_admin_value[post_max_size] = {$sizeMb}M", $content);
        file_put_contents($pool, $content);
    }

    // Update PHP ini files
    $inis = glob('/etc/php/*/fpm/php.ini');
    foreach ($inis as $ini) {
        $content = file_get_contents($ini);
        $content = preg_replace('/^upload_max_filesize\s*=.*/m', "upload_max_filesize = {$sizeMb}M", $content);
        $content = preg_replace('/^post_max_size\s*=.*/m', "post_max_size = {$sizeMb}M", $content);
        file_put_contents($ini, $content);
    }

    // Reload services
    exec('systemctl reload nginx 2>&1', $output, $exitCode);
    if ($exitCode !== 0) {
        $errors[] = 'Failed to reload nginx';
    }

    exec('systemctl reload php*-fpm 2>&1', $output, $exitCode);
    if ($exitCode !== 0) {
        // Try specific version
        exec('systemctl reload php8.4-fpm 2>&1', $output, $exitCode);
    }

    logger("Upload limits set to {$sizeMb}MB");

    return [
        'success' => empty($errors),
        'size_mb' => $sizeMb,
        'errors' => $errors
    ];
}

function updateBindConfig(array $params): array
{
    $listenIp = $params['listen_ip'] ?? 'any';
    $forwarders = $params['forwarders'] ?? ['8.8.8.8', '8.8.4.4'];
    
    $forwardersList = implode("; ", array_map('trim', $forwarders)) . ";";
    
    $config = <<<CONF
options {
    directory "/var/cache/bind";
    
    listen-on { $listenIp; };
    listen-on-v6 { any; };
    
    forwarders {
        $forwardersList
    };
    
    dnssec-validation auto;
    
    allow-query { any; };
    allow-recursion { 127.0.0.1; ::1; localnets; };
    
    auth-nxdomain no;
};
CONF;

    $configFile = '/etc/bind/named.conf.options';
    
    // Backup existing config
    if (file_exists($configFile)) {
        copy($configFile, $configFile . '.bak.' . date('YmdHis'));
    }
    
    file_put_contents($configFile, $config);
    
    // Check config syntax
    exec('named-checkconf 2>&1', $checkOutput, $checkCode);
    
    if ($checkCode !== 0) {
        return ['success' => false, 'error' => 'Invalid BIND config: ' . implode("\n", $checkOutput)];
    }
    
    // Reload BIND
    exec('systemctl reload bind9 2>&1 || systemctl reload named 2>&1', $reloadOutput, $reloadCode);
    
    return ['success' => true, 'message' => 'BIND configuration updated'];
}

function getServerInfo(array $params): array
{
    $info = [];
    
    // Hostname
    $info['hostname'] = trim(shell_exec('hostname -f 2>/dev/null') ?: shell_exec('hostname') ?: 'unknown');
    
    // Server IP
    $info['server_ip'] = trim(shell_exec("hostname -I 2>/dev/null | awk '{print \$1}'") ?: '');
    
    // OS Info
    if (file_exists('/etc/os-release')) {
        $osRelease = parse_ini_file('/etc/os-release');
        $info['os_info'] = $osRelease['PRETTY_NAME'] ?? 'Linux';
    } else {
        $info['os_info'] = PHP_OS;
    }
    
    // Uptime
    if (file_exists('/proc/uptime')) {
        $uptime = (float) file_get_contents('/proc/uptime');
        $days = floor($uptime / 86400);
        $hours = floor(($uptime % 86400) / 3600);
        $info['uptime'] = "{$days} days, {$hours} hours";
    }
    
    // Load average
    $load = sys_getloadavg();
    $info['load_average'] = $load ? implode(', ', array_map(fn($v) => number_format($v, 2), $load)) : 'N/A';
    
    // Memory
    if (file_exists('/proc/meminfo')) {
        $meminfo = file_get_contents('/proc/meminfo');
        preg_match('/MemTotal:\s+(\d+)/', $meminfo, $total);
        preg_match('/MemAvailable:\s+(\d+)/', $meminfo, $available);
        $totalGb = round(($total[1] ?? 0) / 1024 / 1024, 1);
        $availGb = round(($available[1] ?? 0) / 1024 / 1024, 1);
        $info['memory'] = "{$availGb} GB free / {$totalGb} GB total";
    }
    
    // Disk
    $diskTotal = disk_total_space('/');
    $diskFree = disk_free_space('/');
    if ($diskTotal && $diskFree) {
        $totalGb = round($diskTotal / 1024 / 1024 / 1024, 1);
        $freeGb = round($diskFree / 1024 / 1024 / 1024, 1);
        $info['disk'] = "{$freeGb} GB free / {$totalGb} GB total";
    }
    
    return ['success' => true, 'info' => $info];
}

/**
 * Get DNS resolver configuration from /etc/resolv.conf
 */
function serverGetResolvers(array $params): array
{
    $resolvConf = '/etc/resolv.conf';

    if (!file_exists($resolvConf)) {
        return ['success' => false, 'error' => 'resolv.conf not found'];
    }

    $content = file_get_contents($resolvConf);
    $lines = explode("\n", $content);

    $nameservers = [];
    $searchDomains = [];
    $options = [];

    foreach ($lines as $line) {
        $line = trim($line);
        if (empty($line) || $line[0] === '#') continue;

        if (preg_match('/^nameserver\s+(\S+)/', $line, $m)) {
            $nameservers[] = $m[1];
        } elseif (preg_match('/^search\s+(.+)/', $line, $m)) {
            $searchDomains = preg_split('/\s+/', trim($m[1]));
        } elseif (preg_match('/^options\s+(.+)/', $line, $m)) {
            $options = preg_split('/\s+/', trim($m[1]));
        }
    }

    // Check if managed by systemd-resolved
    $isSystemdResolved = is_link($resolvConf) && strpos(readlink($resolvConf), 'systemd') !== false;

    return [
        'success' => true,
        'nameservers' => $nameservers,
        'search_domains' => $searchDomains,
        'options' => $options,
        'is_systemd_resolved' => $isSystemdResolved,
        'raw_content' => $content,
    ];
}

/**
 * Set DNS resolver configuration in /etc/resolv.conf
 */
function serverSetResolvers(array $params): array
{
    $nameservers = $params['nameservers'] ?? [];
    $searchDomains = $params['search_domains'] ?? [];

    if (empty($nameservers)) {
        return ['success' => false, 'error' => 'At least one nameserver is required'];
    }

    // Validate nameservers
    foreach ($nameservers as $ns) {
        if (!filter_var($ns, FILTER_VALIDATE_IP)) {
            return ['success' => false, 'error' => "Invalid IP address: $ns"];
        }
    }

    $resolvConf = '/etc/resolv.conf';

    $systemdResolvedActive = trim(shell_exec('systemctl is-active systemd-resolved 2>/dev/null') ?? '') === 'active';

    // Check if managed by systemd-resolved
    if ($systemdResolvedActive || (is_link($resolvConf) && strpos(readlink($resolvConf), 'systemd') !== false)) {
        // Prefer persistent config for systemd-resolved
        $confDir = '/etc/systemd/resolved.conf.d';
        if (!is_dir($confDir)) {
            @mkdir($confDir, 0755, true);
        }

        $dnsLine = 'DNS=' . implode(' ', $nameservers);
        $domainLine = '';
        if (!empty($searchDomains)) {
            $domainLine = 'Domains=' . implode(' ', $searchDomains);
        }

        $conf = "[Resolve]\n{$dnsLine}\n";
        if ($domainLine !== '') {
            $conf .= "{$domainLine}\n";
        }

        file_put_contents($confDir . '/jabali.conf', $conf);

        exec('systemctl restart systemd-resolved 2>&1', $output, $code);
        if ($code === 0) {
            exec('command -v resolvectl 2>/dev/null', $resolvectlCheck, $resolvectlCode);
            if ($resolvectlCode === 0) {
                $iface = trim(shell_exec("ip route show default 2>/dev/null | awk '{print \$5}' | head -1") ?? '');
                if ($iface === '') {
                    $iface = trim(shell_exec("ip -6 route show default 2>/dev/null | awk '{print \$5}' | head -1") ?? '');
                }

                if ($iface !== '') {
                    $nsArgs = implode(' ', array_map('escapeshellarg', $nameservers));
                    exec("resolvectl dns " . escapeshellarg($iface) . " $nsArgs 2>&1");
                    if (!empty($searchDomains)) {
                        $searchArgs = implode(' ', array_map('escapeshellarg', $searchDomains));
                        exec("resolvectl domain " . escapeshellarg($iface) . " $searchArgs 2>&1");
                    }
                    exec('resolvectl flush-caches 2>/dev/null');
                }
            }

            return ['success' => true, 'message' => 'DNS resolvers updated via systemd-resolved'];
        }

        // Fallback to resolvectl if restart fails
        exec('command -v resolvectl 2>/dev/null', $resolvectlCheck, $resolvectlCode);
        if ($resolvectlCode === 0) {
            $nsArgs = implode(' ', array_map('escapeshellarg', $nameservers));

            $iface = trim(shell_exec("ip route show default 2>/dev/null | awk '{print \$5}' | head -1") ?? '');
            if ($iface === '') {
                $iface = trim(shell_exec("ip -6 route show default 2>/dev/null | awk '{print \$5}' | head -1") ?? '');
            }

            $ifaces = [];
            if ($iface !== '') {
                $ifaces[] = $iface;
            }

            $ifaceList = trim(shell_exec("ip -o link show 2>/dev/null | awk -F': ' '{print \$2}'") ?? '');
            if ($ifaceList !== '') {
                foreach (preg_split('/\s+/', $ifaceList) as $name) {
                    $name = trim($name);
                    if ($name === '' || $name === 'lo') {
                        continue;
                    }
                    if (preg_match('/^(docker|veth|br-|virbr|vmnet)/', $name)) {
                        continue;
                    }
                    $ifaces[] = $name;
                }
            }

            $ifaces = array_values(array_unique($ifaces));

            $applied = false;
            $lastError = implode("\n", $output);
            foreach ($ifaces as $candidate) {
                exec("resolvectl dns " . escapeshellarg($candidate) . " $nsArgs 2>&1", $output, $code);
                if ($code === 0) {
                    $applied = true;
                    if (!empty($searchDomains)) {
                        $searchArgs = implode(' ', array_map('escapeshellarg', $searchDomains));
                        exec("resolvectl domain " . escapeshellarg($candidate) . " $searchArgs 2>&1");
                    }
                    break;
                }
                $lastError = implode("\n", $output);
            }

            if ($applied) {
                return ['success' => true, 'message' => 'DNS resolvers updated via systemd-resolved'];
            }

            if ($lastError !== '') {
                return ['success' => false, 'error' => $lastError];
            }
        }
    }

    // Remove immutable flag if present
    if (file_exists($resolvConf)) {
        exec('chattr -i /etc/resolv.conf 2>/dev/null');
    }

    // Build resolv.conf content
    $content = "# Generated by Jabali Panel\n";
    $content .= "# " . date('Y-m-d H:i:s') . "\n\n";

    if (!empty($searchDomains)) {
        $content .= "search " . implode(' ', $searchDomains) . "\n";
    }

    foreach ($nameservers as $ns) {
        $content .= "nameserver $ns\n";
    }

    // Backup existing file
    if (file_exists($resolvConf) && !is_link($resolvConf)) {
        copy($resolvConf, $resolvConf . '.backup');
    }

    // Remove symlink if exists and write directly
    if (is_link($resolvConf)) {
        unlink($resolvConf);
    }

    if (file_put_contents($resolvConf, $content) === false) {
        return ['success' => false, 'error' => 'Failed to write resolv.conf'];
    }

    // Make immutable to prevent overwriting by DHCP/networkmanager
    exec('chattr +i /etc/resolv.conf 2>&1', $output, $code);

    return ['success' => true, 'message' => 'DNS resolvers updated'];
}

/**
 * Export server configuration including nginx vhosts, DNS zones, SSL certs, and maildir.
 */
function serverExportConfig(array $params): array
{
    $outputPath = $params['output_path'] ?? '/tmp/jabali-config-export.tar.gz';
    $includeNginx = $params['include_nginx'] ?? true;
    $includeDns = $params['include_dns'] ?? true;
    $includeSsl = $params['include_ssl'] ?? true;
    $includeMaildir = $params['include_maildir'] ?? false;

    $tempDir = sys_get_temp_dir() . '/jabali-export-' . uniqid();

    mkdir($tempDir, 0755, true);

    $manifest = [
        'version' => '2.0',
        'exported_at' => date('c'),
        'hostname' => trim(shell_exec('hostname -f 2>/dev/null') ?: shell_exec('hostname') ?: ''),
        'server_ip' => trim(shell_exec("hostname -I | awk '{print \$1}'") ?? ''),
        'files' => [],
    ];

    // Export nginx vhost configurations
    if ($includeNginx) {
        mkdir("$tempDir/nginx", 0755, true);
        $nginxDirs = ['/etc/nginx/sites-available', '/etc/nginx/sites-enabled'];
        foreach ($nginxDirs as $nginxDir) {
            if (is_dir($nginxDir)) {
                $files = scandir($nginxDir);
                foreach ($files as $file) {
                    if ($file === '.' || $file === '..' || $file === 'default') continue;
                    $srcPath = "$nginxDir/$file";
                    if (is_file($srcPath) && !is_link($srcPath)) {
                        $destPath = "$tempDir/nginx/" . basename($nginxDir) . "_$file";
                        copy($srcPath, $destPath);
                        $manifest['files']['nginx'][] = [
                            'source' => $srcPath,
                            'name' => $file,
                            'dir' => basename($nginxDir),
                        ];
                    }
                }
            }
        }
    }

    // Export DNS zone files
    if ($includeDns) {
        mkdir("$tempDir/dns", 0755, true);
        $zonesDir = '/etc/bind/zones';
        if (is_dir($zonesDir)) {
            $files = scandir($zonesDir);
            foreach ($files as $file) {
                if ($file === '.' || $file === '..') continue;
                $srcPath = "$zonesDir/$file";
                if (is_file($srcPath)) {
                    copy($srcPath, "$tempDir/dns/$file");
                    $manifest['files']['dns'][] = [
                        'source' => $srcPath,
                        'name' => $file,
                    ];
                }
            }
        }

        // Export named.conf.local (zone definitions)
        if (file_exists('/etc/bind/named.conf.local')) {
            copy('/etc/bind/named.conf.local', "$tempDir/dns/named.conf.local");
            $manifest['files']['dns'][] = [
                'source' => '/etc/bind/named.conf.local',
                'name' => 'named.conf.local',
            ];
        }
    }

    // Export SSL certificates (both fullchain and privkey for full migration)
    if ($includeSsl) {
        mkdir("$tempDir/ssl", 0755, true);
        $sslDir = '/etc/letsencrypt/live';
        if (is_dir($sslDir)) {
            $domains = scandir($sslDir);
            foreach ($domains as $domain) {
                if ($domain === '.' || $domain === '..' || $domain === 'README') continue;
                $domainDir = "$sslDir/$domain";
                if (!is_dir($domainDir)) continue;

                mkdir("$tempDir/ssl/$domain", 0755, true);

                // Copy all certificate files
                foreach (['fullchain.pem', 'privkey.pem', 'cert.pem', 'chain.pem'] as $certFile) {
                    $certPath = "$domainDir/$certFile";
                    if (file_exists($certPath)) {
                        // Resolve symlink to get actual file
                        $realPath = realpath($certPath);
                        if ($realPath && file_exists($realPath)) {
                            copy($realPath, "$tempDir/ssl/$domain/$certFile");
                            $manifest['files']['ssl'][] = [
                                'domain' => $domain,
                                'file' => $certFile,
                            ];
                        }
                    }
                }
            }
        }
    }

    // Export maildir data for all users (can be very large)
    if ($includeMaildir) {
        mkdir("$tempDir/maildir", 0755, true);
        $vhostsDir = '/var/mail/vhosts';
        if (is_dir($vhostsDir)) {
            $domains = scandir($vhostsDir);
            foreach ($domains as $domain) {
                if ($domain === '.' || $domain === '..') continue;
                $domainPath = "$vhostsDir/$domain";
                if (!is_dir($domainPath)) continue;

                // Create tar.gz for each domain's mail
                $mailArchive = "$tempDir/maildir/{$domain}.tar.gz";
                exec("tar -I pigz -cf " . escapeshellarg($mailArchive) . " -C " . escapeshellarg($vhostsDir) . " " . escapeshellarg($domain) . " 2>&1", $output, $retval);

                if ($retval === 0) {
                    $manifest['files']['maildir'][] = [
                        'domain' => $domain,
                        'archive' => "{$domain}.tar.gz",
                        'size' => filesize($mailArchive),
                    ];
                }
            }
        }
    }

    // Write manifest
    file_put_contents("$tempDir/manifest.json", json_encode($manifest, JSON_PRETTY_PRINT));

    // Create tar.gz archive
    $parentDir = dirname($tempDir);
    $dirName = basename($tempDir);
    exec("cd " . escapeshellarg($parentDir) . " && tar -I pigz -cf " . escapeshellarg($outputPath) . " " . escapeshellarg($dirName) . " 2>&1", $output, $retval);

    // Cleanup temp directory
    exec("rm -rf " . escapeshellarg($tempDir));

    if ($retval !== 0) {
        return ['success' => false, 'error' => 'Failed to create export archive: ' . implode("\n", $output)];
    }

    $size = filesize($outputPath);

    return [
        'success' => true,
        'path' => $outputPath,
        'size' => $size,
        'manifest' => $manifest,
    ];
}

/**
 * Import server configuration from export archive.
 */
function serverImportConfig(array $params): array
{
    $archivePath = $params['archive_path'] ?? '';
    $importNginx = $params['import_nginx'] ?? true;
    $importDns = $params['import_dns'] ?? true;
    $importSsl = $params['import_ssl'] ?? true;
    $importMaildir = $params['import_maildir'] ?? false;
    $dryRun = $params['dry_run'] ?? false;

    if (!file_exists($archivePath)) {
        return ['success' => false, 'error' => 'Archive file not found'];
    }

    $tempDir = sys_get_temp_dir() . '/jabali-import-' . uniqid();
    mkdir($tempDir, 0755, true);

    // Extract archive
    exec("tar -I pigz -xf " . escapeshellarg($archivePath) . " -C " . escapeshellarg($tempDir) . " 2>&1", $output, $retval);

    if ($retval !== 0) {
        exec("rm -rf " . escapeshellarg($tempDir));
        return ['success' => false, 'error' => 'Failed to extract archive: ' . implode("\n", $output)];
    }

    // Find the extracted directory
    $extractedDirs = array_filter(scandir($tempDir), fn($d) => $d !== '.' && $d !== '..' && is_dir("$tempDir/$d"));
    if (empty($extractedDirs)) {
        exec("rm -rf " . escapeshellarg($tempDir));
        return ['success' => false, 'error' => 'Invalid archive structure'];
    }
    $extractedDir = "$tempDir/" . reset($extractedDirs);

    // Read manifest
    $manifestPath = "$extractedDir/manifest.json";
    if (!file_exists($manifestPath)) {
        exec("rm -rf " . escapeshellarg($tempDir));
        return ['success' => false, 'error' => 'Manifest not found in archive'];
    }

    $manifest = json_decode(file_get_contents($manifestPath), true);
    $imported = ['nginx' => [], 'dns' => [], 'ssl' => [], 'maildir' => []];
    $errors = [];

    // Import nginx configurations
    if ($importNginx && isset($manifest['files']['nginx'])) {
        foreach ($manifest['files']['nginx'] as $fileInfo) {
            $srcFile = "$extractedDir/nginx/{$fileInfo['dir']}_{$fileInfo['name']}";
            $destDir = $fileInfo['dir'] === 'sites-available' ? '/etc/nginx/sites-available' : '/etc/nginx/sites-enabled';
            $destFile = "$destDir/{$fileInfo['name']}";

            if (file_exists($srcFile)) {
                if (!$dryRun) {
                    if (!is_dir($destDir)) {
                        mkdir($destDir, 0755, true);
                    }
                    if (copy($srcFile, $destFile)) {
                        $imported['nginx'][] = $fileInfo['name'];
                    } else {
                        $errors[] = "Failed to copy nginx config: {$fileInfo['name']}";
                    }
                } else {
                    $imported['nginx'][] = $fileInfo['name'] . ' (dry-run)';
                }
            }
        }
    }

    // Import DNS zone files
    if ($importDns && isset($manifest['files']['dns'])) {
        $zonesDir = '/etc/bind/zones';
        if (!is_dir($zonesDir) && !$dryRun) {
            mkdir($zonesDir, 0755, true);
        }

        foreach ($manifest['files']['dns'] as $fileInfo) {
            $srcFile = "$extractedDir/dns/{$fileInfo['name']}";

            if (file_exists($srcFile)) {
                if ($fileInfo['name'] === 'named.conf.local') {
                    // Append zone definitions instead of replacing
                    if (!$dryRun) {
                        $content = file_get_contents($srcFile);
                        // Check if zones already exist before appending
                        $existingContent = file_exists('/etc/bind/named.conf.local')
                            ? file_get_contents('/etc/bind/named.conf.local')
                            : '';

                        // Only append zones that don't exist
                        preg_match_all('/zone\s+"([^"]+)"/', $content, $newZones);
                        preg_match_all('/zone\s+"([^"]+)"/', $existingContent, $existingZones);

                        $zonesToAdd = array_diff($newZones[1] ?? [], $existingZones[1] ?? []);
                        if (!empty($zonesToAdd)) {
                            $imported['dns'][] = 'named.conf.local (zones: ' . implode(', ', $zonesToAdd) . ')';
                        }
                    } else {
                        $imported['dns'][] = 'named.conf.local (dry-run)';
                    }
                } else {
                    $destFile = "$zonesDir/{$fileInfo['name']}";
                    if (!$dryRun) {
                        if (copy($srcFile, $destFile)) {
                            chown($destFile, 'bind');
                            chgrp($destFile, 'bind');
                            $imported['dns'][] = $fileInfo['name'];
                        } else {
                            $errors[] = "Failed to copy zone file: {$fileInfo['name']}";
                        }
                    } else {
                        $imported['dns'][] = $fileInfo['name'] . ' (dry-run)';
                    }
                }
            }
        }
    }

    // Import SSL certificates
    if ($importSsl && isset($manifest['files']['ssl'])) {
        $sslDir = '/etc/letsencrypt/live';
        $archiveDir = '/etc/letsencrypt/archive';

        foreach ($manifest['files']['ssl'] as $fileInfo) {
            $domain = $fileInfo['domain'];
            $file = $fileInfo['file'];
            $srcFile = "$extractedDir/ssl/$domain/$file";

            if (file_exists($srcFile)) {
                if (!$dryRun) {
                    // Create directories if needed
                    $domainLiveDir = "$sslDir/$domain";
                    $domainArchiveDir = "$archiveDir/$domain";

                    if (!is_dir($domainLiveDir)) {
                        mkdir($domainLiveDir, 0755, true);
                    }
                    if (!is_dir($domainArchiveDir)) {
                        mkdir($domainArchiveDir, 0755, true);
                    }

                    // Copy to archive with version number
                    $baseName = str_replace('.pem', '', $file);
                    $archiveFile = "$domainArchiveDir/{$baseName}1.pem";
                    $liveFile = "$domainLiveDir/$file";

                    if (copy($srcFile, $archiveFile)) {
                        chmod($archiveFile, 0644);
                        // Create symlink in live directory
                        if (file_exists($liveFile)) {
                            unlink($liveFile);
                        }
                        symlink($archiveFile, $liveFile);
                        $imported['ssl'][] = "$domain/$file";
                    } else {
                        $errors[] = "Failed to copy SSL cert: $domain/$file";
                    }
                } else {
                    $imported['ssl'][] = "$domain/$file (dry-run)";
                }
            }
        }
    }

    // Import maildir data
    if ($importMaildir && isset($manifest['files']['maildir'])) {
        $vhostsDir = '/var/mail/vhosts';
        if (!is_dir($vhostsDir) && !$dryRun) {
            mkdir($vhostsDir, 0755, true);
        }

        foreach ($manifest['files']['maildir'] as $mailInfo) {
            $domain = $mailInfo['domain'];
            $archive = $mailInfo['archive'];
            $srcArchive = "$extractedDir/maildir/$archive";

            if (file_exists($srcArchive)) {
                if (!$dryRun) {
                    exec("tar -I pigz -xf " . escapeshellarg($srcArchive) . " -C " . escapeshellarg($vhostsDir) . " 2>&1", $mailOutput, $mailRet);
                    if ($mailRet === 0) {
                        // Fix permissions
                        exec("chown -R vmail:vmail " . escapeshellarg("$vhostsDir/$domain"));
                        $imported['maildir'][] = $domain;
                    } else {
                        $errors[] = "Failed to extract maildir for $domain: " . implode("\n", $mailOutput);
                    }
                } else {
                    $imported['maildir'][] = "$domain (dry-run)";
                }
            }
        }
    }

    // Cleanup temp directory
    exec("rm -rf " . escapeshellarg($tempDir));

    // Reload services if not dry run
    if (!$dryRun) {
        if (!empty($imported['nginx'])) {
            exec("nginx -t 2>&1", $testOutput, $testRet);
            if ($testRet === 0) {
                exec("systemctl reload nginx 2>&1");
            } else {
                $errors[] = "Nginx config test failed: " . implode("\n", $testOutput);
            }
        }

        if (!empty($imported['dns'])) {
            exec("systemctl reload named 2>/dev/null || systemctl reload bind9 2>/dev/null");
        }

        if (!empty($imported['maildir'])) {
            exec("systemctl reload dovecot 2>/dev/null");
        }
    }

    return [
        'success' => empty($errors),
        'imported' => $imported,
        'errors' => $errors,
        'manifest' => $manifest,
        'dry_run' => $dryRun,
    ];
}

function createServerZone(array $params): array
{
    $hostname = $params['hostname'] ?? '';
    $ns1 = $params['ns1'] ?? '';
    $ns1_ip = $params['ns1_ip'] ?? '';
    $ns2 = $params['ns2'] ?? '';
    $ns2_ip = $params['ns2_ip'] ?? '';
    $admin_email = $params['admin_email'] ?? 'admin.example.com';
    $server_ip = $params['server_ip'] ?? '';
    $server_ipv6 = $params['server_ipv6'] ?? '';
    $ttl = $params['ttl'] ?? '3600';
    
    $parts = explode('.', $hostname);
    if (count($parts) < 2) {
        return ['success' => false, 'error' => 'Invalid hostname format'];
    }
    
    $baseDomain = implode('.', array_slice($parts, -2));
    $serial = date('Ymd') . '01';
    $zonesDir = '/etc/bind/zones';
    
    if (!is_dir($zonesDir)) {
        mkdir($zonesDir, 0755, true);
    }
    
    // Create zone data file
    $zoneContent = "\$TTL {$ttl}\n";
    $zoneContent .= "@   IN  SOA {$ns1}. {$admin_email}. (\n";
    $zoneContent .= "            {$serial}   ; Serial\n";
    $zoneContent .= "            3600        ; Refresh\n";
    $zoneContent .= "            1800        ; Retry\n";
    $zoneContent .= "            604800      ; Expire\n";
    $zoneContent .= "            86400 )     ; Minimum TTL\n\n";
    $zoneContent .= "; Nameservers\n";
    $zoneContent .= "@       IN  NS      {$ns1}.\n";
    $zoneContent .= "@       IN  NS      {$ns2}.\n\n";
    $zoneContent .= "; Nameserver A records\n";
    
    if (str_ends_with($ns1, '.' . $baseDomain) && !empty($ns1_ip)) {
        $ns1Short = str_replace('.' . $baseDomain, '', $ns1);
        $zoneContent .= "{$ns1Short}    IN  A       {$ns1_ip}\n";
    }
    
    if (str_ends_with($ns2, '.' . $baseDomain) && !empty($ns2_ip)) {
        $ns2Short = str_replace('.' . $baseDomain, '', $ns2);
        $zoneContent .= "{$ns2Short}    IN  A       {$ns2_ip}\n";
    }
    
    if (!empty($server_ip)) {
        $zoneContent .= "\n; Server\n";
        $hostShort = str_replace('.' . $baseDomain, '', $hostname);
        if ($hostShort !== $hostname) {
            $zoneContent .= "{$hostShort}    IN  A       {$server_ip}\n";
        }
        $zoneContent .= "@       IN  A       {$server_ip}\n";
    }

    if (!empty($server_ipv6)) {
        $zoneContent .= "\n; Server IPv6\n";
        $hostShort = str_replace('.' . $baseDomain, '', $hostname);
        if ($hostShort !== $hostname) {
            $zoneContent .= "{$hostShort}    IN  AAAA    {$server_ipv6}\n";
        }
        $zoneContent .= "@       IN  AAAA    {$server_ipv6}\n";
    }
    
    $zoneFile = "{$zonesDir}/db.{$baseDomain}";
    file_put_contents($zoneFile, $zoneContent);
    
    // Create zone config file
    $zoneConf = "{$zonesDir}/{$baseDomain}.conf";
    $confContent = "zone \"{$baseDomain}\" {\n";
    $confContent .= "    type master;\n";
    $confContent .= "    file \"{$zoneFile}\";\n";
    $confContent .= "    allow-transfer { none; };\n";
    $confContent .= "};\n";
    file_put_contents($zoneConf, $confContent);
    
    // Add to zones.conf if not already included
    $masterConf = "{$zonesDir}/zones.conf";
    $masterContent = file_exists($masterConf) ? file_get_contents($masterConf) : "// Panel managed zones\n";
    $includeLine = "include \"{$zoneConf}\";";
    
    if (strpos($masterContent, $includeLine) === false) {
        file_put_contents($masterConf, $masterContent . $includeLine . "\n");
    }
    
    // Check config
    exec('named-checkconf 2>&1', $checkOutput, $checkCode);
    if ($checkCode !== 0) {
        return ['success' => false, 'error' => 'BIND config error: ' . implode("\n", $checkOutput)];
    }
    
    // Check zone
    exec("named-checkzone {$baseDomain} {$zoneFile} 2>&1", $zoneOutput, $zoneCode);
    if ($zoneCode !== 0) {
        return ['success' => false, 'error' => 'Zone error: ' . implode("\n", $zoneOutput)];
    }
    
    // Reload BIND
    exec('systemctl reload bind9 2>&1 || systemctl reload named 2>&1');
    
    return ['success' => true, 'zone' => $baseDomain, 'file' => $zoneFile];
}

// ============ SERVICE MANAGEMENT ============

/**
 * List status of specified services
 */
function serviceList(array $params): array
{
    $requestedServices = $params['services'] ?? [];
    
    if (empty($requestedServices)) {
        return ['success' => false, 'error' => 'No services specified'];
    }
    
    $results = [];
    
    foreach ($requestedServices as $service) {
        // Sanitize service name
        if (!preg_match('/^[a-zA-Z0-9._-]+$/', $service)) {
            continue;
        }
        
        // Check if service is active
        exec("systemctl is-active " . escapeshellarg($service) . " 2>/dev/null", $activeOutput, $activeCode);
        $isActive = ($activeCode === 0 && isset($activeOutput[0]) && $activeOutput[0] === 'active');
        
        // Check if service is enabled
        exec("systemctl is-enabled " . escapeshellarg($service) . " 2>/dev/null", $enabledOutput, $enabledCode);
        $isEnabled = ($enabledCode === 0 && isset($enabledOutput[0]) && in_array($enabledOutput[0], ['enabled', 'enabled-runtime']));
        
        // Get status text
        exec("systemctl status " . escapeshellarg($service) . " 2>&1 | head -5", $statusOutput, $statusCode);
        
        $results[$service] = [
            'is_active' => $isActive,
            'is_enabled' => $isEnabled,
            'status' => $isActive ? 'running' : 'stopped',
            'status_text' => implode("\n", $statusOutput),
        ];
    }
    
    return ['success' => true, 'services' => $results];
}

/**
 * Start a service
 */
function serviceStart(array $params): array
{
    $service = $params['service'] ?? '';
    
    if (empty($service) || !preg_match('/^[a-zA-Z0-9._-]+$/', $service)) {
        return ['success' => false, 'error' => 'Invalid service name'];
    }
    
    exec("systemctl start " . escapeshellarg($service) . " 2>&1", $output, $code);
    
    if ($code !== 0) {
        return ['success' => false, 'error' => implode("\n", $output) ?: 'Failed to start service'];
    }
    
    return ['success' => true, 'service' => $service, 'action' => 'started'];
}

/**
 * Stop a service
 */
function serviceStop(array $params): array
{
    $service = $params['service'] ?? '';
    
    if (empty($service) || !preg_match('/^[a-zA-Z0-9._-]+$/', $service)) {
        return ['success' => false, 'error' => 'Invalid service name'];
    }
    
    exec("systemctl stop " . escapeshellarg($service) . " 2>&1", $output, $code);
    
    if ($code !== 0) {
        return ['success' => false, 'error' => implode("\n", $output) ?: 'Failed to stop service'];
    }
    
    return ['success' => true, 'service' => $service, 'action' => 'stopped'];
}

/**
 * Restart a service
 */
function serviceRestart(array $params): array
{
    $service = $params['service'] ?? '';
    
    if (empty($service) || !preg_match('/^[a-zA-Z0-9._-]+$/', $service)) {
        return ['success' => false, 'error' => 'Invalid service name'];
    }
    
    $action = shouldReloadService($service) ? 'reload' : 'restart';
    exec("systemctl {$action} " . escapeshellarg($service) . " 2>&1", $output, $code);
    
    if ($code !== 0) {
        return ['success' => false, 'error' => implode("\n", $output) ?: "Failed to {$action} service"];
    }
    
    return [
        'success' => true,
        'service' => $service,
        'action' => $action === 'reload' ? 'reloaded' : 'restarted',
    ];
}

/**
 * Enable a service (auto-start on boot)
 */
function serviceEnable(array $params): array
{
    $service = $params['service'] ?? '';
    
    if (empty($service) || !preg_match('/^[a-zA-Z0-9._-]+$/', $service)) {
        return ['success' => false, 'error' => 'Invalid service name'];
    }
    
    exec("systemctl enable " . escapeshellarg($service) . " 2>&1", $output, $code);
    
    if ($code !== 0) {
        return ['success' => false, 'error' => implode("\n", $output) ?: 'Failed to enable service'];
    }

    updateHealthMonitorOverride($service, false);
    
    return ['success' => true, 'service' => $service, 'action' => 'enabled'];
}

/**
 * Disable a service (no auto-start on boot)
 */
function serviceDisable(array $params): array
{
    $service = $params['service'] ?? '';

    if (empty($service) || !preg_match('/^[a-zA-Z0-9._-]+$/', $service)) {
        return ['success' => false, 'error' => 'Invalid service name'];
    }

    exec("systemctl disable " . escapeshellarg($service) . " 2>&1", $output, $code);

    if ($code !== 0) {
        return ['success' => false, 'error' => implode("\n", $output) ?: 'Failed to disable service'];
    }

    updateHealthMonitorOverride($service, true);

    return ['success' => true, 'service' => $service, 'action' => 'disabled'];
}

// ============ SERVER IMPORT (cPanel/DirectAdmin) ============

/**
 * Discover accounts in a backup file or from remote server
 */
function importDiscover(array $params): array
{
    $importId = $params['import_id'] ?? 0;
    $sourceType = $params['source_type'] ?? 'cpanel';
    $importMethod = $params['import_method'] ?? 'backup_file';
    $backupPath = $params['backup_path'] ?? null;
    $remoteHost = $params['remote_host'] ?? null;
    $remotePort = $params['remote_port'] ?? null;
    $remoteUser = $params['remote_user'] ?? null;
    $remotePassword = $params['remote_password'] ?? null;

    logger("Import discover: source=$sourceType, method=$importMethod, backup=$backupPath");

    if ($importMethod === 'backup_file') {
        if (!$backupPath || !file_exists($backupPath)) {
            return ['success' => false, 'error' => 'Backup file not found: ' . $backupPath];
        }

        // Create temp extraction directory
        $extractDir = '/tmp/import_' . $importId . '_' . time();
        if (!mkdir($extractDir, 0755, true)) {
            return ['success' => false, 'error' => 'Failed to create extraction directory'];
        }

        try {
            if ($sourceType === 'cpanel') {
                $accounts = discoverCpanelBackup($backupPath, $extractDir);
            } else {
                $accounts = discoverDirectAdminBackup($backupPath, $extractDir);
            }

            // Clean up partial extraction (we only extracted metadata)
            exec("rm -rf " . escapeshellarg($extractDir));

            return ['success' => true, 'accounts' => $accounts];
        } catch (Exception $e) {
            exec("rm -rf " . escapeshellarg($extractDir));
            return ['success' => false, 'error' => $e->getMessage()];
        }
    } else {
        // Remote server discovery
        if ($sourceType === 'cpanel') {
            return discoverCpanelRemote($remoteHost, $remotePort, $remoteUser, $remotePassword);
        } else {
            return discoverDirectAdminRemote($remoteHost, $remotePort, $remoteUser, $remotePassword);
        }
    }
}

/**
 * Discover accounts from a cPanel backup file
 */
function discoverCpanelBackup(string $backupPath, string $extractDir): array
{
    logger("Discovering cPanel backup: $backupPath");

    $accounts = [];

    // Check if this is a full backup or single account backup
    // Full backups contain multiple cpmove-username.tar.gz files
    // Single account backups are just one tar.gz

    // First, list the contents of the archive
    $cmd = "tar -I pigz -tf " . escapeshellarg($backupPath) . " 2>/dev/null | head -500";
    exec($cmd, $fileList, $code);

    if ($code !== 0) {
        throw new Exception('Failed to read backup file. Make sure it is a valid tar.gz archive.');
    }

    $fileListStr = implode("\n", $fileList);

    // Check if this is a cpmove backup (single account)
    if (preg_match('/^([^\/]+)\/userdata\/main$/', $fileListStr, $matches) ||
        preg_match('/^userdata\/main$/', $fileListStr)) {

        // Single account backup - extract minimal data to get account info
        $username = $matches[1] ?? null;

        // Extract userdata directory to get account info
        $extractCmd = "tar -I pigz -xf " . escapeshellarg($backupPath) . " -C " . escapeshellarg($extractDir) . " --wildcards '*/userdata/*' '*/mysql/' '*/homedir/.shadow' 2>/dev/null";
        exec($extractCmd);

        // Find the userdata/main file
        $userdataFiles = glob("$extractDir/*/userdata/main") ?: glob("$extractDir/userdata/main");

        foreach ($userdataFiles as $mainFile) {
            $dirParts = explode('/', dirname(dirname($mainFile)));
            $username = end($dirParts);
            if ($username === $extractDir || $username === 'userdata') {
                // Get username from file content
                $content = file_get_contents($mainFile);
                if (preg_match('/^user:\s*(.+)$/m', $content, $m)) {
                    $username = trim($m[1]);
                }
            }

            $account = parseCpanelUserdata($mainFile, $extractDir, $username);
            if ($account) {
                $accounts[] = $account;
            }
        }
    } else {
        // Full server backup - look for cpmove files
        $cpmovePattern = '/cpmove-([a-z0-9_]+)\.tar\.gz/i';

        foreach ($fileList as $file) {
            if (preg_match($cpmovePattern, $file, $matches)) {
                $username = $matches[1];

                // Extract just the userdata for this account
                $innerBackup = $file;
                $extractCmd = "tar -I pigz -xf " . escapeshellarg($backupPath) . " -C " . escapeshellarg($extractDir) . " " . escapeshellarg($innerBackup) . " 2>/dev/null";
                exec($extractCmd);

                // Extract from the inner cpmove archive
                $innerPath = "$extractDir/$innerBackup";
                if (file_exists($innerPath)) {
                    $innerExtract = "$extractDir/$username";
                    mkdir($innerExtract, 0755, true);
                    exec("tar -I pigz -xf " . escapeshellarg($innerPath) . " -C " . escapeshellarg($innerExtract) . " --wildcards '*/userdata/*' 2>/dev/null");

                    $userdataFile = glob("$innerExtract/*/userdata/main")[0] ?? null;
                    if ($userdataFile) {
                        $account = parseCpanelUserdata($userdataFile, $innerExtract, $username);
                        if ($account) {
                            $accounts[] = $account;
                        }
                    }
                }
            }
        }
    }

    // If still no accounts found, try parsing as a simple backup
    if (empty($accounts)) {
        // Look for cp or backup directory structure
        foreach ($fileList as $file) {
            if (preg_match('/^([^\/]+)\/cp\//', $file, $matches)) {
                $username = $matches[1];
                if (!in_array($username, array_column($accounts, 'username'))) {
                    $accounts[] = [
                        'username' => $username,
                        'email' => '',
                        'main_domain' => '',
                        'addon_domains' => [],
                        'subdomains' => [],
                        'databases' => [],
                        'email_accounts' => [],
                        'disk_usage' => 0,
                    ];
                }
            }
        }
    }

    return $accounts;
}

/**
 * Parse cPanel userdata/main file to extract account info
 */
function parseCpanelUserdata(string $mainFile, string $extractDir, string $username): ?array
{
    if (!file_exists($mainFile)) {
        return null;
    }

    $content = file_get_contents($mainFile);

    // Parse YAML-like content
    $mainDomain = '';
    $email = '';
    $addonDomains = [];
    $subdomains = [];

    if (preg_match('/^main_domain:\s*(.+)$/m', $content, $m)) {
        $mainDomain = trim($m[1]);
    }

    if (preg_match('/^email:\s*(.+)$/m', $content, $m)) {
        $email = trim($m[1]);
    }

    // Look for addon domains in the userdata directory
    $userdataDir = dirname($mainFile);
    $domainFiles = glob("$userdataDir/*.com") ?: [];
    $domainFiles = array_merge($domainFiles, glob("$userdataDir/*.net") ?: []);
    $domainFiles = array_merge($domainFiles, glob("$userdataDir/*.org") ?: []);
    $domainFiles = array_merge($domainFiles, glob("$userdataDir/*.*") ?: []);

    foreach ($domainFiles as $domainFile) {
        $domain = basename($domainFile);
        if ($domain !== 'main' && $domain !== 'cache' && $domain !== $mainDomain) {
            $domainContent = file_get_contents($domainFile);
            if (strpos($domainContent, 'addon_domain:') !== false) {
                $addonDomains[] = $domain;
            } elseif (strpos($domainContent, 'type: sub') !== false) {
                $subdomains[] = $domain;
            }
        }
    }

    // Look for databases
    $databases = [];
    $baseDir = dirname(dirname($mainFile));
    $mysqlDir = "$baseDir/mysql";
    if (is_dir($mysqlDir)) {
        $sqlFiles = glob("$mysqlDir/*.sql") ?: [];
        foreach ($sqlFiles as $sqlFile) {
            $databases[] = basename($sqlFile, '.sql');
        }
    }

    // Look for email accounts
    $emailAccounts = [];
    $shadowFile = "$baseDir/homedir/.shadow";
    if (file_exists($shadowFile)) {
        $shadow = file_get_contents($shadowFile);
        if (preg_match_all('/^([^:]+):/m', $shadow, $shadowMatches)) {
            $emailAccounts = $shadowMatches[1];
        }
    }

    // Get disk usage from quota file if available
    $diskUsage = 0;
    $quotaFile = "$baseDir/quota";
    if (file_exists($quotaFile)) {
        $quota = file_get_contents($quotaFile);
        if (preg_match('/^(\d+)/m', $quota, $m)) {
            $diskUsage = (int)$m[1] * 1024; // Convert KB to bytes
        }
    }

    return [
        'username' => $username,
        'email' => $email,
        'main_domain' => $mainDomain,
        'addon_domains' => $addonDomains,
        'subdomains' => $subdomains,
        'databases' => $databases,
        'email_accounts' => $emailAccounts,
        'disk_usage' => $diskUsage,
    ];
}

/**
 * Discover accounts from a DirectAdmin backup file
 */
function discoverDirectAdminBackup(string $backupPath, string $extractDir): array
{
    logger("Discovering DirectAdmin backup: $backupPath");

    $accounts = [];

    // List archive contents
    $cmd = "tar -I pigz -tf " . escapeshellarg($backupPath) . " 2>/dev/null | head -500";
    exec($cmd, $fileList, $code);

    if ($code !== 0) {
        throw new Exception('Failed to read backup file. Make sure it is a valid tar.gz archive.');
    }

    $fileListStr = implode("\n", $fileList);

    // DirectAdmin backup structure: backup/user.conf, domains/, databases/
    // Or: user.username.tar.gz containing the above

    // Check for user.conf file (single user backup)
    if (preg_match('/(backup\/)?user\.conf/', $fileListStr)) {
        // Extract user.conf and domains list
        $extractCmd = "tar -I pigz -xf " . escapeshellarg($backupPath) . " -C " . escapeshellarg($extractDir) . " --wildcards 'backup/user.conf' 'user.conf' 'domains/*' 'backup/domains/*' 2>/dev/null";
        exec($extractCmd);

        $userConf = null;
        foreach (["$extractDir/backup/user.conf", "$extractDir/user.conf"] as $path) {
            if (file_exists($path)) {
                $userConf = $path;
                break;
            }
        }

        if ($userConf) {
            $account = parseDirectAdminUserConf($userConf, $extractDir);
            if ($account) {
                $accounts[] = $account;
            }
        }
    }

    // Check for multiple user backups (full server backup)
    foreach ($fileList as $file) {
        if (preg_match('/user\.([a-z0-9_]+)\.tar\.gz/i', $file, $matches)) {
            $username = $matches[1];

            // Extract just this user's backup
            $innerBackup = $file;
            exec("tar -I pigz -xf " . escapeshellarg($backupPath) . " -C " . escapeshellarg($extractDir) . " " . escapeshellarg($innerBackup) . " 2>/dev/null");

            $innerPath = "$extractDir/$innerBackup";
            if (file_exists($innerPath)) {
                $innerExtract = "$extractDir/$username";
                mkdir($innerExtract, 0755, true);
                exec("tar -I pigz -xf " . escapeshellarg($innerPath) . " -C " . escapeshellarg($innerExtract) . " --wildcards 'backup/user.conf' 'user.conf' 2>/dev/null");

                $userConf = glob("$innerExtract/*/user.conf")[0] ?? glob("$innerExtract/user.conf")[0] ?? null;
                if ($userConf) {
                    $account = parseDirectAdminUserConf($userConf, $innerExtract);
                    if ($account) {
                        $account['username'] = $username;
                        $accounts[] = $account;
                    }
                }
            }
        }
    }

    return $accounts;
}

/**
 * Parse DirectAdmin user.conf file
 */
function parseDirectAdminUserConf(string $userConf, string $extractDir): ?array
{
    if (!file_exists($userConf)) {
        return null;
    }

    $content = file_get_contents($userConf);

    // Parse key=value format
    $username = '';
    $email = '';
    $mainDomain = '';
    $addonDomains = [];
    $diskUsage = 0;

    if (preg_match('/^username=(.+)$/m', $content, $m)) {
        $username = trim($m[1]);
    }

    if (preg_match('/^email=(.+)$/m', $content, $m)) {
        $email = trim($m[1]);
    }

    if (preg_match('/^domain=(.+)$/m', $content, $m)) {
        $mainDomain = trim($m[1]);
    }

    if (preg_match('/^bandwidth=(\d+)$/m', $content, $m)) {
        $diskUsage = (int)$m[1] * 1048576; // Convert MB to bytes
    }

    // Look for domains directory
    $baseDir = dirname($userConf);
    $domainsDir = "$baseDir/domains";
    if (!is_dir($domainsDir)) {
        $domainsDir = "$extractDir/domains";
    }

    if (is_dir($domainsDir)) {
        $domainDirs = glob("$domainsDir/*", GLOB_ONLYDIR) ?: [];
        foreach ($domainDirs as $domainDir) {
            $domain = basename($domainDir);
            if ($domain !== $mainDomain && $domain !== '.' && $domain !== '..') {
                $addonDomains[] = $domain;
            }
        }
    }

    // Look for databases
    $databases = [];
    $dbDir = "$baseDir/databases";
    if (!is_dir($dbDir)) {
        $dbDir = "$extractDir/backup/databases";
    }
    if (is_dir($dbDir)) {
        $sqlFiles = glob("$dbDir/*.sql") ?: [];
        foreach ($sqlFiles as $sqlFile) {
            $databases[] = basename($sqlFile, '.sql');
        }
    }

    // Look for email accounts
    $emailAccounts = [];
    $emailDir = "$baseDir/email";
    if (!is_dir($emailDir)) {
        $emailDir = "$extractDir/backup/email";
    }
    if (is_dir($emailDir)) {
        $emailDirs = glob("$emailDir/*", GLOB_ONLYDIR) ?: [];
        foreach ($emailDirs as $ed) {
            $emailAccounts[] = basename($ed);
        }
    }

    return [
        'username' => $username,
        'email' => $email,
        'main_domain' => $mainDomain,
        'addon_domains' => $addonDomains,
        'subdomains' => [],
        'databases' => $databases,
        'email_accounts' => $emailAccounts,
        'disk_usage' => $diskUsage,
    ];
}

/**
 * Discover accounts from remote cPanel server via WHM API
 */
function discoverCpanelRemote(string $host, int $port, string $user, string $password): array
{
    logger("Discovering cPanel accounts from remote: $host:$port");

    $url = "https://$host:$port/json-api/listaccts?api.version=1";

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
    curl_setopt($ch, CURLOPT_TIMEOUT, 60);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Authorization: WHM ' . $user . ':' . $password,
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = curl_error($ch);
    curl_close($ch);

    if ($error) {
        return ['success' => false, 'error' => "Connection failed: $error"];
    }

    if ($httpCode !== 200) {
        return ['success' => false, 'error' => "Server returned HTTP $httpCode"];
    }

    $data = json_decode($response, true);

    if (!$data || isset($data['error'])) {
        return ['success' => false, 'error' => $data['error'] ?? 'Invalid response from server'];
    }

    $accounts = [];
    $acctList = $data['data']['acct'] ?? $data['acct'] ?? [];

    foreach ($acctList as $acct) {
        $accounts[] = [
            'username' => $acct['user'] ?? '',
            'email' => $acct['email'] ?? '',
            'main_domain' => $acct['domain'] ?? '',
            'addon_domains' => [],
            'subdomains' => [],
            'databases' => [],
            'email_accounts' => [],
            'disk_usage' => ($acct['diskused'] ?? 0) * 1048576, // MB to bytes
        ];
    }

    return ['success' => true, 'accounts' => $accounts];
}

/**
 * Discover accounts from remote DirectAdmin server via API
 */
function discoverDirectAdminRemote(string $host, int $port, string $user, string $password): array
{
    logger("Discovering DirectAdmin accounts from remote: $host:$port");

    $url = "https://$host:$port/CMD_API_SHOW_ALL_USERS";

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
    curl_setopt($ch, CURLOPT_TIMEOUT, 60);
    curl_setopt($ch, CURLOPT_USERPWD, "$user:$password");

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = curl_error($ch);
    curl_close($ch);

    if ($error) {
        return ['success' => false, 'error' => "Connection failed: $error"];
    }

    if ($httpCode !== 200) {
        return ['success' => false, 'error' => "Server returned HTTP $httpCode"];
    }

    // DirectAdmin returns URL-encoded data
    parse_str($response, $data);

    if (isset($data['error']) && $data['error'] === '1') {
        return ['success' => false, 'error' => $data['text'] ?? 'Unknown error'];
    }

    $accounts = [];

    // DirectAdmin returns list=user1&list=user2 format
    $userList = $data['list'] ?? [];
    if (!is_array($userList)) {
        $userList = [$userList];
    }

    foreach ($userList as $username) {
        if (empty($username)) continue;

        // Get user details
        $detailUrl = "https://$host:$port/CMD_API_SHOW_USER_CONFIG?user=" . urlencode($username);

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $detailUrl);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        curl_setopt($ch, CURLOPT_USERPWD, "$user:$password");

        $userResponse = curl_exec($ch);
        curl_close($ch);

        parse_str($userResponse, $userData);

        $accounts[] = [
            'username' => $username,
            'email' => $userData['email'] ?? '',
            'main_domain' => $userData['domain'] ?? '',
            'addon_domains' => [],
            'subdomains' => [],
            'databases' => [],
            'email_accounts' => [],
            'disk_usage' => ($userData['bandwidth'] ?? 0) * 1048576,
        ];
    }

    return ['success' => true, 'accounts' => $accounts];
}

/**
 * Start the import process for selected accounts
 */
function importStart(array $params): array
{
    $importId = $params['import_id'] ?? 0;

    logger("Starting import process for import ID: $importId");

    // This will be called asynchronously - we need to spawn a background process
    // that reads the import details from the database and processes accounts

    $scriptPath = '/var/www/jabali/artisan';

    if (!file_exists($scriptPath)) {
        return ['success' => false, 'error' => 'Artisan script not found'];
    }

    // Dispatch a Laravel job to handle the import in the background
    $cmd = "cd /var/www/jabali && php artisan import:process " . escapeshellarg($importId) . " > /dev/null 2>&1 &";
    exec($cmd);

    return ['success' => true, 'message' => 'Import process started'];
}

// ============ SSL CERTIFICATE MANAGEMENT ============

/**
 * Check SSL certificate status for a domain
 */
function sslCheck(array $params): array
{
    $domain = $params['domain'] ?? '';
    $username = $params['username'] ?? '';

    if (empty($domain)) {
        return ['success' => false, 'error' => 'Domain is required'];
    }

    logger("Checking SSL for domain: $domain");

    $result = [
        'has_ssl' => false,
        'type' => 'none',
        'issuer' => null,
        'valid_from' => null,
        'valid_to' => null,
        'days_until_expiry' => null,
        'is_expired' => false,
        'is_self_signed' => false,
        'certificate' => null,
    ];

    // Check nginx config for SSL
    $nginxConf = "/etc/nginx/sites-available/$domain";
    if (!file_exists($nginxConf)) {
        return ['success' => true, 'ssl' => $result];
    }

    $confContent = file_get_contents($nginxConf);

    // Look for ssl_certificate directive
    if (preg_match('/ssl_certificate\s+([^;]+);/', $confContent, $matches)) {
        $certPath = trim($matches[1]);
        $result['has_ssl'] = true;

        // Read certificate details
        if (file_exists($certPath)) {
            $certContent = file_get_contents($certPath);
            $certData = openssl_x509_parse($certContent);

            if ($certData) {
                $result['issuer'] = $certData['issuer']['O'] ?? $certData['issuer']['CN'] ?? 'Unknown';
                $result['valid_from'] = date('Y-m-d H:i:s', $certData['validFrom_time_t']);
                $result['valid_to'] = date('Y-m-d H:i:s', $certData['validTo_time_t']);

                $expiryTime = $certData['validTo_time_t'];
                $result['days_until_expiry'] = (int) floor(($expiryTime - time()) / 86400);
                $result['is_expired'] = $expiryTime < time();

                // Check if self-signed (issuer == subject)
                $issuerCN = $certData['issuer']['CN'] ?? '';
                $subjectCN = $certData['subject']['CN'] ?? '';
                $result['is_self_signed'] = ($issuerCN === $subjectCN) && !str_contains(strtolower($issuerCN), 'encrypt');

                // Determine type
                if ($result['is_self_signed']) {
                    $result['type'] = 'self_signed';
                } elseif (str_contains(strtolower($result['issuer']), 'encrypt')) {
                    $result['type'] = 'lets_encrypt';
                } else {
                    $result['type'] = 'custom';
                }

                $result['certificate'] = $certContent;
            }
        }
    }

    return ['success' => true, 'ssl' => $result];
}

/**
 * Issue Let's Encrypt SSL certificate for a domain
 */
function sslIssue(array $params): array
{
    $domain = $params['domain'] ?? '';
    $username = $params['username'] ?? '';
    $email = $params['email'] ?? 'admin@' . $domain;
    $includeWww = $params['include_www'] ?? true;

    if (empty($domain) || empty($username)) {
        return ['success' => false, 'error' => 'Domain and username are required'];
    }

    logger("Issuing SSL certificate for domain: $domain (user: $username)");

    // Check if certbot is installed
    exec('which certbot 2>/dev/null', $output, $code);
    if ($code !== 0) {
        return ['success' => false, 'error' => 'Certbot is not installed. Please install certbot first.'];
    }

    // Verify domain resolves to this server
    $serverIp = trim(shell_exec("curl -s ifconfig.me 2>/dev/null") ?? '');
    $domainIp = gethostbyname($domain);

    if ($domainIp === $domain) {
        return ['success' => false, 'error' => "Domain $domain does not resolve to any IP address"];
    }

    // Build certbot command
    $domains = "-d " . escapeshellarg($domain);
    if ($includeWww) {
        $wwwDomain = "www.$domain";
        $wwwIp = gethostbyname($wwwDomain);
        if ($wwwIp !== $wwwDomain) {
            $domains .= " -d " . escapeshellarg($wwwDomain);
        }
    }

    $webroot = "/home/$username/domains/$domain/public_html";
    if (!is_dir($webroot)) {
        return ['success' => false, 'error' => "Webroot not found: $webroot"];
    }

    // Create .well-known directory if needed
    $wellKnown = "$webroot/.well-known/acme-challenge";
    if (!is_dir($wellKnown)) {
        mkdir($wellKnown, 0755, true);
        chown("$webroot/.well-known", $username);
        chown($wellKnown, $username);
    }

    // Run certbot
    $cmd = sprintf(
        'certbot certonly --webroot -w %s %s --email %s --agree-tos --non-interactive --force-renewal 2>&1',
        escapeshellarg($webroot),
        $domains,
        escapeshellarg($email)
    );

    logger("Running certbot: $cmd");
    exec($cmd, $output, $exitCode);
    $outputStr = implode("\n", $output);
    logger("Certbot output: $outputStr");

    if ($exitCode !== 0) {
        return ['success' => false, 'error' => "Certbot failed: $outputStr"];
    }

    // Get certificate paths
    $certPath = "/etc/letsencrypt/live/$domain/fullchain.pem";
    $keyPath = "/etc/letsencrypt/live/$domain/privkey.pem";

    if (!file_exists($certPath) || !file_exists($keyPath)) {
        return ['success' => false, 'error' => 'Certificate files not found after certbot'];
    }

    // Install the certificate
    $installResult = sslInstallCertificate($domain, $username, $certPath, $keyPath);
    if (!$installResult['success']) {
        return $installResult;
    }

    // Read certificate for response
    $certContent = file_get_contents($certPath);
    $certData = openssl_x509_parse($certContent);

    return [
        'success' => true,
        'message' => "SSL certificate issued for $domain",
        'certificate_path' => $certPath,
        'key_path' => $keyPath,
        'issuer' => "Let's Encrypt",
        'valid_from' => date('Y-m-d H:i:s', $certData['validFrom_time_t'] ?? 0),
        'valid_to' => date('Y-m-d H:i:s', $certData['validTo_time_t'] ?? 0),
        'certificate' => $certContent,
    ];
}

/**
 * Install a custom SSL certificate for a domain
 */
function sslInstall(array $params): array
{
    $domain = $params['domain'] ?? '';
    $username = $params['username'] ?? '';
    $certificate = $params['certificate'] ?? '';
    $privateKey = $params['private_key'] ?? '';
    $caBundle = $params['ca_bundle'] ?? '';

    if (empty($domain) || empty($username)) {
        return ['success' => false, 'error' => 'Domain and username are required'];
    }

    if (empty($certificate) || empty($privateKey)) {
        return ['success' => false, 'error' => 'Certificate and private key are required'];
    }

    logger("Installing custom SSL certificate for domain: $domain");

    // Validate certificate format
    $certData = openssl_x509_parse($certificate);
    if (!$certData) {
        return ['success' => false, 'error' => 'Invalid certificate format'];
    }

    // Validate private key
    $keyResource = openssl_pkey_get_private($privateKey);
    if (!$keyResource) {
        return ['success' => false, 'error' => 'Invalid private key format'];
    }

    // Verify key matches certificate
    $certResource = openssl_x509_read($certificate);
    if (!openssl_x509_check_private_key($certResource, $keyResource)) {
        return ['success' => false, 'error' => 'Private key does not match certificate'];
    }

    // Save certificate files
    $sslDir = "/home/$username/ssl/$domain";
    if (!is_dir($sslDir)) {
        mkdir($sslDir, 0750, true);
        chown($sslDir, $username);
    }

    $certPath = "$sslDir/certificate.crt";
    $keyPath = "$sslDir/private.key";

    // Combine certificate with CA bundle if provided
    $fullCert = trim($certificate);
    if (!empty($caBundle)) {
        $fullCert .= "\n" . trim($caBundle);
    }

    file_put_contents($certPath, $fullCert);
    file_put_contents($keyPath, $privateKey);

    chmod($certPath, 0640);
    chmod($keyPath, 0600);
    chown($certPath, 'root');
    chgrp($certPath, $username);
    chown($keyPath, 'root');
    chgrp($keyPath, $username);

    // Install to nginx
    $installResult = sslInstallCertificate($domain, $username, $certPath, $keyPath);
    if (!$installResult['success']) {
        return $installResult;
    }

    return [
        'success' => true,
        'message' => "Custom SSL certificate installed for $domain",
        'issuer' => $certData['issuer']['O'] ?? $certData['issuer']['CN'] ?? 'Unknown',
        'valid_from' => date('Y-m-d H:i:s', $certData['validFrom_time_t']),
        'valid_to' => date('Y-m-d H:i:s', $certData['validTo_time_t']),
    ];
}

/**
 * Renew SSL certificate (Let's Encrypt)
 */
function sslRenew(array $params): array
{
    $domain = $params['domain'] ?? '';
    $username = $params['username'] ?? '';

    if (empty($domain)) {
        return ['success' => false, 'error' => 'Domain is required'];
    }

    logger("Renewing SSL certificate for domain: $domain");

    // Check if this is a Let's Encrypt certificate
    $certPath = "/etc/letsencrypt/live/$domain/fullchain.pem";
    if (!file_exists($certPath)) {
        return ['success' => false, 'error' => 'No Let\'s Encrypt certificate found for this domain'];
    }

    // Run certbot renew for this specific domain
    $cmd = sprintf('certbot renew --cert-name %s --force-renewal 2>&1', escapeshellarg($domain));
    logger("Running certbot renew: $cmd");
    exec($cmd, $output, $exitCode);
    $outputStr = implode("\n", $output);
    logger("Certbot renew output: $outputStr");

    if ($exitCode !== 0) {
        return ['success' => false, 'error' => "Certificate renewal failed: $outputStr"];
    }

    // Reload nginx
    exec('systemctl reload nginx 2>&1', $output, $code);

    // Read updated certificate
    $certContent = file_get_contents($certPath);
    $certData = openssl_x509_parse($certContent);

    return [
        'success' => true,
        'message' => "SSL certificate renewed for $domain",
        'valid_from' => date('Y-m-d H:i:s', $certData['validFrom_time_t'] ?? 0),
        'valid_to' => date('Y-m-d H:i:s', $certData['validTo_time_t'] ?? 0),
    ];
}

/**
 * Generate self-signed SSL certificate
 */
function sslGenerateSelfSigned(array $params): array
{
    $domain = $params['domain'] ?? '';
    $username = $params['username'] ?? '';
    $days = $params['days'] ?? 365;

    if (empty($domain) || empty($username)) {
        return ['success' => false, 'error' => 'Domain and username are required'];
    }

    logger("Generating self-signed SSL certificate for domain: $domain");

    // Create SSL directory
    $sslDir = "/home/$username/ssl/$domain";
    if (!is_dir($sslDir)) {
        mkdir($sslDir, 0750, true);
        chown($sslDir, $username);
    }

    $certPath = "$sslDir/certificate.crt";
    $keyPath = "$sslDir/private.key";

    // Generate self-signed certificate
    $subject = "/C=US/ST=State/L=City/O=Self-Signed/CN=$domain";
    $cmd = sprintf(
        'openssl req -x509 -nodes -days %d -newkey rsa:2048 -keyout %s -out %s -subj %s 2>&1',
        (int) $days,
        escapeshellarg($keyPath),
        escapeshellarg($certPath),
        escapeshellarg($subject)
    );

    exec($cmd, $output, $exitCode);

    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'Failed to generate certificate: ' . implode("\n", $output)];
    }

    chmod($certPath, 0640);
    chmod($keyPath, 0600);
    chown($certPath, 'root');
    chgrp($certPath, $username);
    chown($keyPath, 'root');
    chgrp($keyPath, $username);

    // Install to nginx
    $installResult = sslInstallCertificate($domain, $username, $certPath, $keyPath);
    if (!$installResult['success']) {
        return $installResult;
    }

    return [
        'success' => true,
        'message' => "Self-signed SSL certificate generated for $domain",
        'certificate_path' => $certPath,
        'key_path' => $keyPath,
        'valid_days' => $days,
    ];
}

/**
 * Delete/revoke SSL certificate
 */
function sslDelete(array $params): array
{
    $domain = $params['domain'] ?? '';
    $username = $params['username'] ?? '';
    $deleteFiles = $params['delete_files'] ?? true;

    if (empty($domain)) {
        return ['success' => false, 'error' => 'Domain is required'];
    }

    logger("Deleting SSL certificate for domain: $domain");

    $deleted = [];
    $errors = [];

    // Check for Let's Encrypt certificate
    $letsencryptPath = "/etc/letsencrypt/live/$domain";
    if (is_dir($letsencryptPath)) {
        // Use certbot to properly delete/revoke
        $cmd = sprintf('certbot delete --cert-name %s --non-interactive 2>&1', escapeshellarg($domain));
        exec($cmd, $output, $exitCode);

        if ($exitCode === 0) {
            $deleted[] = "Let's Encrypt certificate";
            logger("Deleted Let's Encrypt certificate for $domain");
        } else {
            // Fallback: manually remove if certbot fails
            exec("rm -rf " . escapeshellarg($letsencryptPath) . " 2>&1");
            exec("rm -rf /etc/letsencrypt/renewal/$domain.conf 2>&1");
            exec("rm -rf /etc/letsencrypt/archive/$domain 2>&1");
            $deleted[] = "Let's Encrypt certificate (manual cleanup)";
            logger("Manually cleaned up Let's Encrypt files for $domain");
        }
    }

    // Check for self-signed certificate in user's home
    if (!empty($username)) {
        $userSslDir = "/home/$username/ssl/$domain";
        if (is_dir($userSslDir) && $deleteFiles) {
            exec("rm -rf " . escapeshellarg($userSslDir) . " 2>&1");
            $deleted[] = "Self-signed certificate";
            logger("Deleted self-signed certificate at $userSslDir");
        }
    }

    // Update nginx config to remove SSL directives (optional - revert to HTTP only)
    $nginxConf = "/etc/nginx/sites-available/{$domain}.conf";
    if (file_exists($nginxConf)) {
        // Don't remove SSL from nginx - just note that cert is deleted
        // The admin should reconfigure nginx manually if needed
        logger("Note: Nginx config for $domain still references SSL - may need manual update");
    }

    if (empty($deleted)) {
        return ['success' => false, 'error' => "No SSL certificate found for $domain"];
    }

    // Reload nginx
    exec('systemctl reload nginx 2>&1');

    return [
        'success' => true,
        'message' => "SSL certificate deleted for $domain",
        'deleted' => $deleted,
    ];
}

/**
 * Install SSL certificate to nginx configuration
 */
function sslInstallCertificate(string $domain, string $username, string $certPath, string $keyPath): array
{
    $nginxConf = "/etc/nginx/sites-available/{$domain}.conf";

    if (!file_exists($nginxConf)) {
        return ['success' => false, 'error' => "Nginx config not found for $domain"];
    }

    $confContent = file_get_contents($nginxConf);

    // Check if SSL is already configured
    if (str_contains($confContent, 'ssl_certificate')) {
        // Update existing SSL paths
        $confContent = preg_replace(
            '/ssl_certificate\s+[^;]+;/',
            'ssl_certificate ' . $certPath . ';',
            $confContent
        );
        $confContent = preg_replace(
            '/ssl_certificate_key\s+[^;]+;/',
            'ssl_certificate_key ' . $keyPath . ';',
            $confContent
        );
    } else {
        // Add SSL configuration
        // Find the server block listening on 443
        if (preg_match('/listen\s+443\s+ssl/', $confContent)) {
            // Already has 443 listener, add ssl directives
            $sslDirectives = "\n    ssl_certificate $certPath;\n    ssl_certificate_key $keyPath;\n";
            $confContent = preg_replace(
                '/(listen\s+443\s+ssl[^;]*;)/',
                "$1$sslDirectives",
                $confContent,
                1
            );
        } else {
            // Need to add HTTPS server block
            $httpsBlock = generateHttpsServerBlock($domain, $username, $certPath, $keyPath);
            $confContent .= "\n\n" . $httpsBlock;
        }
    }

    // Write config
    file_put_contents($nginxConf, $confContent);

    // Test nginx config
    exec('nginx -t 2>&1', $output, $exitCode);
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'Nginx config test failed: ' . implode("\n", $output)];
    }

    // Reload nginx
    exec('systemctl reload nginx 2>&1', $output, $code);

    return ['success' => true];
}

/**
 * Generate HTTPS server block for nginx
 */
function generateHttpsServerBlock(string $domain, string $username, string $certPath, string $keyPath): string
{
    $docRoot = "/home/$username/domains/$domain/public_html";

    return <<<NGINX
# HTTPS Server Block for $domain
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name $domain www.$domain;
    root $docRoot;

    ssl_certificate $certPath;
    ssl_certificate_key $keyPath;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;

    index index.php index.html index.htm;

    location / {
        try_files \$uri \$uri/ /index.php?\$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php8.4-fpm-$username.sock;
        fastcgi_next_upstream error timeout invalid_header http_500 http_503;
        fastcgi_next_upstream_tries 2;
        fastcgi_next_upstream_timeout 5s;
        fastcgi_param SCRIPT_FILENAME \$realpath_root\$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }

    access_log /home/$username/logs/${domain}_ssl_access.log combined;
    error_log /home/$username/logs/${domain}_ssl_error.log;
}
NGINX;
}

// ============ BACKUP OPERATIONS ============

/**
 * Export user's panel data from the database.
 * This includes domains config, email accounts, DNS records, WordPress sites, SSL certificates, etc.
 */
function exportUserPanelData(string $username): array
{
    loadJabaliEnv();

    $data = [
        'exported_at' => date('c'),
        'username' => $username,
        'user' => null,
        'domains' => [],
        'email_domains' => [],
        'mailboxes' => [],
        'email_forwarders' => [],
        'dns_records' => [],
        'ssl_certificates' => [],
        'mysql_credentials' => [],
    ];

    // Connect to panel database
    $dbPath = '/var/www/jabali/database/database.sqlite';
    $dbDriver = $_ENV['DB_CONNECTION'] ?? 'sqlite';

    try {
        if ($dbDriver === 'sqlite' && file_exists($dbPath)) {
            $pdo = new PDO("sqlite:$dbPath");
        } else {
            $mysqlHost = $_ENV['DB_HOST'] ?? 'localhost';
            $mysqlDb = $_ENV['DB_DATABASE'] ?? 'jabali';
            $mysqlUser = $_ENV['DB_USERNAME'] ?? 'root';
            $mysqlPass = $_ENV['DB_PASSWORD'] ?? '';
            $pdo = new PDO("mysql:host=$mysqlHost;dbname=$mysqlDb", $mysqlUser, $mysqlPass);
        }
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

        // Get user record (exclude sensitive fields)
        $stmt = $pdo->prepare("SELECT id, name, email, username, is_admin, is_active, home_directory, disk_quota_mb, locale, created_at FROM users WHERE username = ?");
        $stmt->execute([$username]);
        $user = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$user) {
            return $data;
        }

        $data['user'] = $user;
        $userId = $user['id'];

        // Export domains
        $stmt = $pdo->prepare("SELECT * FROM domains WHERE user_id = ?");
        $stmt->execute([$userId]);
        $data['domains'] = $stmt->fetchAll(PDO::FETCH_ASSOC);

        // Get domain IDs for related queries
        $domainIds = array_column($data['domains'], 'id');

        if (!empty($domainIds)) {
            $placeholders = implode(',', array_fill(0, count($domainIds), '?'));

            // Export DNS records
            $stmt = $pdo->prepare("SELECT * FROM dns_records WHERE domain_id IN ($placeholders)");
            $stmt->execute($domainIds);
            $data['dns_records'] = $stmt->fetchAll(PDO::FETCH_ASSOC);

            // Export SSL certificates info
            $stmt = $pdo->prepare("SELECT * FROM ssl_certificates WHERE domain_id IN ($placeholders)");
            $stmt->execute($domainIds);
            $data['ssl_certificates'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
        }

        // Export email domains (linked via domain_id)
        if (!empty($domainIds)) {
            $placeholders = implode(',', array_fill(0, count($domainIds), '?'));
            $stmt = $pdo->prepare("SELECT ed.*, d.domain as domain_name FROM email_domains ed JOIN domains d ON ed.domain_id = d.id WHERE ed.domain_id IN ($placeholders)");
            $stmt->execute($domainIds);
            $data['email_domains'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
        }

        // Get email domain IDs for mailboxes and forwarders
        $emailDomainIds = array_column($data['email_domains'], 'id');

        if (!empty($emailDomainIds)) {
            $placeholders = implode(',', array_fill(0, count($emailDomainIds), '?'));

            // Export mailboxes (without passwords, but include domain info)
            $stmt = $pdo->prepare("SELECT m.*, ed.domain_id, d.domain as domain_name FROM mailboxes m JOIN email_domains ed ON m.email_domain_id = ed.id JOIN domains d ON ed.domain_id = d.id WHERE m.email_domain_id IN ($placeholders)");
            $stmt->execute($emailDomainIds);
            $data['mailboxes'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
            foreach ($data['mailboxes'] as &$mailbox) {
                unset($mailbox['password']);
            }

            // Export email forwarders
            $stmt = $pdo->prepare("SELECT ef.*, ed.domain_id, d.domain as domain_name FROM email_forwarders ef JOIN email_domains ed ON ef.email_domain_id = ed.id JOIN domains d ON ed.domain_id = d.id WHERE ef.email_domain_id IN ($placeholders)");
            $stmt->execute($emailDomainIds);
            $data['email_forwarders'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
        }

        // Export MySQL credentials (without encrypted passwords - they can't be decrypted outside the panel)
        $stmt = $pdo->prepare("SELECT id, user_id, mysql_username, created_at, updated_at FROM mysql_credentials WHERE user_id = ?");
        $stmt->execute([$userId]);
        $data['mysql_credentials'] = $stmt->fetchAll(PDO::FETCH_ASSOC);

    } catch (Exception $e) {
        logger("Failed to export panel data for $username: " . $e->getMessage());
    }

    return $data;
}

/**
 * Import user's panel data from backup.
 * Creates database records for domains, email, DNS, SSL, etc.
 */
function importUserPanelData(string $username, array $data): array
{
    loadJabaliEnv();

    $imported = [
        'user' => 0,
        'domains' => 0,
        'email_domains' => 0,
        'mailboxes' => 0,
        'email_forwarders' => 0,
        'dns_records' => 0,
        'ssl_certificates' => 0,
        'mysql_credentials' => 0,
    ];

    // Connect to panel database
    $dbPath = '/var/www/jabali/database/database.sqlite';
    $dbDriver = $_ENV['DB_CONNECTION'] ?? 'sqlite';

    try {
        if ($dbDriver === 'sqlite' && file_exists($dbPath)) {
            $pdo = new PDO("sqlite:$dbPath");
        } else {
            $mysqlHost = $_ENV['DB_HOST'] ?? 'localhost';
            $mysqlDb = $_ENV['DB_DATABASE'] ?? 'jabali';
            $mysqlUser = $_ENV['DB_USERNAME'] ?? 'root';
            $mysqlPass = $_ENV['DB_PASSWORD'] ?? '';
            $pdo = new PDO("mysql:host=$mysqlHost;dbname=$mysqlDb", $mysqlUser, $mysqlPass);
        }
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

        $now = date('Y-m-d H:i:s');

        // Get or create user in panel database
        $stmt = $pdo->prepare("SELECT id FROM users WHERE username = ?");
        $stmt->execute([$username]);
        $user = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$user) {
            // User doesn't exist in panel - create from backup data or system user
            $userData = $data['user'] ?? null;
            $systemUser = posix_getpwnam($username);

            if (!$systemUser) {
                return ['success' => false, 'error' => 'System user not found: ' . $username, 'imported' => $imported];
            }

            // Get user details from backup or defaults
            $name = $userData['name'] ?? $username;
            $email = $userData['email'] ?? "{$username}@localhost";
            $isAdmin = $userData['is_admin'] ?? 0;
            $isActive = $userData['is_active'] ?? 1;
            $homeDir = $systemUser['dir'];
            $diskQuotaMb = $userData['disk_quota_mb'] ?? null;
            $locale = $userData['locale'] ?? 'en';

            // Generate a random password (admin must reset it)
            $tempPassword = bin2hex(random_bytes(16));
            $passwordHash = password_hash($tempPassword, PASSWORD_BCRYPT);

            // Insert user into panel database
            $stmt = $pdo->prepare("INSERT INTO users (name, email, username, password, is_admin, is_active, home_directory, disk_quota_mb, locale, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
            $stmt->execute([$name, $email, $username, $passwordHash, $isAdmin, $isActive, $homeDir, $diskQuotaMb, $locale, $now, $now]);
            $userId = $pdo->lastInsertId();
            $imported['user'] = 1;
            logger("Created panel user record for restored user: $username (ID: $userId)");
        } else {
            $userId = $user['id'];
        }

        // Map old domain IDs to new domain IDs
        $domainIdMap = [];
        $emailDomainIdMap = [];

        // Import domains
        foreach ($data['domains'] ?? [] as $domain) {
            $oldId = $domain['id'];
            $domainName = $domain['domain'] ?? $domain['name'] ?? '';

            if (empty($domainName)) continue;

            // Check if domain already exists
            $stmt = $pdo->prepare("SELECT id FROM domains WHERE domain = ? AND user_id = ?");
            $stmt->execute([$domainName, $userId]);
            $existing = $stmt->fetch(PDO::FETCH_ASSOC);

            if ($existing) {
                $domainIdMap[$oldId] = $existing['id'];
            } else {
                // Insert new domain
                $stmt = $pdo->prepare("INSERT INTO domains (user_id, domain, document_root, is_active, ssl_enabled, directory_index, page_cache_enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
                $docRoot = $domain['document_root'] ?? "/home/$username/domains/$domainName/public_html";
                $isActive = $domain['is_active'] ?? 1;
                $sslEnabled = $domain['ssl_enabled'] ?? 0;
                $directoryIndex = $domain['directory_index'] ?? 'index.php index.html';
                $pageCacheEnabled = $domain['page_cache_enabled'] ?? 0;
                $stmt->execute([$userId, $domainName, $docRoot, $isActive, $sslEnabled, $directoryIndex, $pageCacheEnabled, $now, $now]);
                $domainIdMap[$oldId] = $pdo->lastInsertId();
                $imported['domains']++;
            }
        }

        // Import email domains (linked via domain_id)
        foreach ($data['email_domains'] ?? [] as $emailDomain) {
            $oldId = $emailDomain['id'];
            $oldDomainId = $emailDomain['domain_id'] ?? null;
            $domainName = $emailDomain['domain_name'] ?? $emailDomain['domain'] ?? '';

            if (empty($domainName)) continue;

            // Get the new domain_id for this email domain
            $newDomainId = $domainIdMap[$oldDomainId] ?? null;
            if (empty($newDomainId)) {
                // Try to find domain by name
                $stmt = $pdo->prepare("SELECT id FROM domains WHERE domain = ? AND user_id = ?");
                $stmt->execute([$domainName, $userId]);
                $domainRow = $stmt->fetch(PDO::FETCH_ASSOC);
                if ($domainRow) {
                    $newDomainId = $domainRow['id'];
                }
            }

            if (empty($newDomainId)) continue;

            // Check if email domain already exists
            $stmt = $pdo->prepare("SELECT id FROM email_domains WHERE domain_id = ?");
            $stmt->execute([$newDomainId]);
            $existing = $stmt->fetch(PDO::FETCH_ASSOC);

            if ($existing) {
                $emailDomainIdMap[$oldId] = $existing['id'];
            } else {
                // Insert new email domain
                $stmt = $pdo->prepare("INSERT INTO email_domains (domain_id, is_active, dkim_selector, catch_all_enabled, max_mailboxes, max_quota_bytes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
                $isActive = $emailDomain['is_active'] ?? 1;
                $dkimSelector = $emailDomain['dkim_selector'] ?? 'default';
                $catchAllEnabled = $emailDomain['catch_all_enabled'] ?? 0;
                $maxMailboxes = $emailDomain['max_mailboxes'] ?? 10;
                $maxQuotaBytes = $emailDomain['max_quota_bytes'] ?? 5368709120;
                $stmt->execute([$newDomainId, $isActive, $dkimSelector, $catchAllEnabled, $maxMailboxes, $maxQuotaBytes, $now, $now]);
                $emailDomainIdMap[$oldId] = $pdo->lastInsertId();
                $imported['email_domains']++;

                // Add to Postfix virtual_domains so mail is accepted for this domain
                $vdomainsFile = POSTFIX_VIRTUAL_DOMAINS;
                if (file_exists($vdomainsFile)) {
                    $vdomainsContent = file_get_contents($vdomainsFile);
                    if (strpos($vdomainsContent, $domainName) === false) {
                        file_put_contents($vdomainsFile, trim($vdomainsContent) . "\n{$domainName}\n");
                    }
                }

                // Create mail directory for domain if it doesn't exist
                $mailDomainDir = "/home/$username/mail/$domainName";
                if (!is_dir($mailDomainDir)) {
                    mkdir($mailDomainDir, 0750, true);
                    $userInfo = posix_getpwnam($username);
                    if ($userInfo) {
                        chown($mailDomainDir, $userInfo['uid']);
                        chgrp($mailDomainDir, $userInfo['gid']);
                    }
                }
            }
        }

        // Rebuild Postfix virtual_domains map after importing email domains
        if ($imported['email_domains'] > 0) {
            exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_DOMAINS) . ' 2>/dev/null');
        }

        // Import mailboxes (note: passwords need to be reset by user)
        foreach ($data['mailboxes'] ?? [] as $mailbox) {
            $oldEmailDomainId = $mailbox['email_domain_id'] ?? null;
            $newEmailDomainId = $emailDomainIdMap[$oldEmailDomainId] ?? null;
            $localPart = $mailbox['local_part'] ?? '';
            $domainName = $mailbox['domain_name'] ?? '';

            if (empty($localPart) || empty($newEmailDomainId)) continue;

            // Check if mailbox already exists
            $stmt = $pdo->prepare("SELECT id FROM mailboxes WHERE email_domain_id = ? AND local_part = ?");
            $stmt->execute([$newEmailDomainId, $localPart]);
            if ($stmt->fetch()) continue;

            // Insert mailbox with placeholder password (user must reset)
            $stmt = $pdo->prepare("INSERT INTO mailboxes (email_domain_id, user_id, local_part, password_hash, name, quota_bytes, quota_used_bytes, is_active, imap_enabled, pop3_enabled, smtp_enabled, maildir_path, system_uid, system_gid, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
            $quotaBytes = $mailbox['quota_bytes'] ?? 1073741824;
            $quotaUsedBytes = $mailbox['quota_used_bytes'] ?? 0;
            $maildirPath = $mailbox['maildir_path'] ?? "/home/$username/mail/$domainName/$localPart/";
            // Generate proper password hash - user must reset via panel
            $tempPassword = 'changeme';
            $hashCmd = sprintf('doveadm pw -s SHA512-CRYPT -p %s 2>&1', escapeshellarg($tempPassword));
            $passwordHash = trim(shell_exec($hashCmd));
            if (empty($passwordHash) || strpos($passwordHash, '{SHA512-CRYPT}') === false) {
                // Fallback if doveadm not available
                $passwordHash = '{SHA512-CRYPT}' . crypt($tempPassword, '$6$' . bin2hex(random_bytes(8)) . '$');
            }
            $name = $mailbox['name'] ?? $localPart;
            $isActive = $mailbox['is_active'] ?? 1;
            $imapEnabled = $mailbox['imap_enabled'] ?? 1;
            $pop3Enabled = $mailbox['pop3_enabled'] ?? 1;
            $smtpEnabled = $mailbox['smtp_enabled'] ?? 1;
            $systemUid = $mailbox['system_uid'] ?? null;
            $systemGid = $mailbox['system_gid'] ?? null;
            $stmt->execute([$newEmailDomainId, $userId, $localPart, $passwordHash, $name, $quotaBytes, $quotaUsedBytes, $isActive, $imapEnabled, $pop3Enabled, $smtpEnabled, $maildirPath, $systemUid, $systemGid, $now, $now]);
            $imported['mailboxes']++;

            // Add to Postfix virtual_mailbox_maps so mail delivery works
            $email = "{$localPart}@{$domainName}";
            $mailboxesFile = POSTFIX_VIRTUAL_MAILBOXES;
            if (file_exists($mailboxesFile)) {
                $mailboxesContent = file_get_contents($mailboxesFile);
                if (strpos($mailboxesContent, $email) === false) {
                    file_put_contents($mailboxesFile, trim($mailboxesContent) . "\n{$email} {$domainName}/{$localPart}/\n");
                }
            }
        }

        // Rebuild Postfix maps after importing mailboxes
        if ($imported['mailboxes'] > 0) {
            exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_MAILBOXES) . ' 2>/dev/null');
        }

        // Import email forwarders
        foreach ($data['email_forwarders'] ?? [] as $forwarder) {
            $oldEmailDomainId = $forwarder['email_domain_id'] ?? null;
            $newEmailDomainId = $emailDomainIdMap[$oldEmailDomainId] ?? null;
            $localPart = $forwarder['local_part'] ?? '';
            $destinations = $forwarder['destinations'] ?? '';

            if (empty($localPart) || empty($destinations) || empty($newEmailDomainId)) continue;

            // Check if forwarder already exists
            $stmt = $pdo->prepare("SELECT id FROM email_forwarders WHERE local_part = ? AND email_domain_id = ?");
            $stmt->execute([$localPart, $newEmailDomainId]);
            if ($stmt->fetch()) continue;

            $stmt = $pdo->prepare("INSERT INTO email_forwarders (email_domain_id, user_id, local_part, destinations, is_active, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)");
            $isActive = $forwarder['is_active'] ?? 1;
            $stmt->execute([$newEmailDomainId, $userId, $localPart, $destinations, $isActive, $now, $now]);
            $imported['email_forwarders']++;
        }

        // Import DNS records
        foreach ($data['dns_records'] ?? [] as $record) {
            $oldDomainId = $record['domain_id'] ?? null;
            $newDomainId = $domainIdMap[$oldDomainId] ?? null;

            if (empty($newDomainId)) continue;

            $name = $record['name'] ?? '';
            $type = $record['type'] ?? '';
            $content = $record['content'] ?? $record['value'] ?? '';

            if (empty($type) || empty($content)) continue;

            // Check if record already exists
            $stmt = $pdo->prepare("SELECT id FROM dns_records WHERE domain_id = ? AND name = ? AND type = ? AND content = ?");
            $stmt->execute([$newDomainId, $name, $type, $content]);
            if ($stmt->fetch()) continue;

            $stmt = $pdo->prepare("INSERT INTO dns_records (domain_id, name, type, content, ttl, priority, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
            $ttl = $record['ttl'] ?? 3600;
            $priority = $record['priority'] ?? null;
            $stmt->execute([$newDomainId, $name, $type, $content, $ttl, $priority, $now, $now]);
            $imported['dns_records']++;
        }

        // Import SSL certificates
        foreach ($data['ssl_certificates'] ?? [] as $cert) {
            $oldDomainId = $cert['domain_id'] ?? null;
            $newDomainId = $domainIdMap[$oldDomainId] ?? null;

            if (empty($newDomainId)) continue;

            // Check if certificate already exists for this domain
            $stmt = $pdo->prepare("SELECT id FROM ssl_certificates WHERE domain_id = ?");
            $stmt->execute([$newDomainId]);
            if ($stmt->fetch()) continue;

            $stmt = $pdo->prepare("INSERT INTO ssl_certificates (domain_id, type, status, issuer, issued_at, expires_at, auto_renew, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
            $type = $cert['type'] ?? 'lets_encrypt';
            $status = $cert['status'] ?? 'active';
            $issuer = $cert['issuer'] ?? null;
            $issuedAt = $cert['issued_at'] ?? null;
            $expiresAt = $cert['expires_at'] ?? null;
            $autoRenew = $cert['auto_renew'] ?? 1;
            $stmt->execute([$newDomainId, $type, $status, $issuer, $issuedAt, $expiresAt, $autoRenew, $now, $now]);
            $imported['ssl_certificates']++;
        }

        // Note: MySQL credentials are stored with encrypted passwords that can't be restored
        // The MySQL users themselves are restored via users.sql in the backup
        // The panel credentials will need to be recreated if the user needs them

        // Recreate nginx vhosts for imported domains
        $imported['nginx_vhosts'] = 0;
        $nginxNeedsReload = false;
        $userInfo = posix_getpwnam($username);

        if ($userInfo) {
            $userHome = $userInfo['dir'];
            $uid = $userInfo['uid'];
            $gid = $userInfo['gid'];

            foreach ($data['domains'] ?? [] as $domain) {
                $domainName = $domain['domain'] ?? $domain['name'] ?? '';
                if (empty($domainName)) continue;

                $vhostFile = "/etc/nginx/sites-available/{$domainName}.conf";

                // Skip if vhost already exists
                if (file_exists($vhostFile)) continue;

                $domainRoot = "{$userHome}/domains/{$domainName}";
                $publicHtml = $domain['document_root'] ?? "{$domainRoot}/public_html";
                $logs = "{$domainRoot}/logs";

                // Ensure directories exist
                if (!is_dir($publicHtml)) {
                    @mkdir($publicHtml, 0755, true);
                    @chown($publicHtml, $uid);
                    @chgrp($publicHtml, $gid);
                }
                if (!is_dir($logs)) {
                    @mkdir($logs, 0755, true);
                    @chown($logs, $uid);
                    @chgrp($logs, $gid);
                }

                // Ensure FPM pool exists (don't reload during migration - we'll do it at the end)
                createFpmPool($username, reload: false);
                $fpmSocket = getFpmSocketPath($username);

                // Generate nginx vhost
                $vhostContent = generateNginxVhost($domainName, $publicHtml, $logs, $fpmSocket);

                if (file_put_contents($vhostFile, $vhostContent) !== false) {
                    // Enable the site
                    exec("ln -sf " . escapeshellarg($vhostFile) . " /etc/nginx/sites-enabled/" . escapeshellarg("{$domainName}.conf") . " 2>&1");

                    // Set ACLs for nginx to access files
                    exec("setfacl -m u:www-data:x " . escapeshellarg($userHome) . " 2>/dev/null");
                    exec("setfacl -m u:www-data:x " . escapeshellarg("{$userHome}/domains") . " 2>/dev/null");
                    exec("setfacl -m u:www-data:rx " . escapeshellarg($domainRoot) . " 2>/dev/null");
                    exec("setfacl -R -m u:www-data:rx " . escapeshellarg($publicHtml) . " 2>/dev/null");
                    exec("setfacl -R -m u:www-data:rwx " . escapeshellarg($logs) . " 2>/dev/null");
                    exec("setfacl -R -d -m u:www-data:rx " . escapeshellarg($publicHtml) . " 2>/dev/null");
                    exec("setfacl -R -d -m u:www-data:rwx " . escapeshellarg($logs) . " 2>/dev/null");

                    $imported['nginx_vhosts']++;
                    $nginxNeedsReload = true;
                    logger("Created nginx vhost for restored domain: $domainName");
                }
            }

            // Reload nginx if any vhosts were created
            if ($nginxNeedsReload) {
                exec("nginx -t 2>&1", $testOutput, $testCode);
                if ($testCode === 0) {
                    exec("systemctl reload nginx 2>&1");
                    logger("Reloaded nginx after restoring {$imported['nginx_vhosts']} vhost(s)");
                } else {
                    logger("Warning: nginx config test failed after restore: " . implode("\n", $testOutput));
                }
            }
        }

        logger("Imported panel data for $username: " . json_encode($imported));

        return ['success' => true, 'imported' => $imported];

    } catch (Exception $e) {
        logger("Failed to import panel data for $username: " . $e->getMessage());
        return ['success' => false, 'error' => $e->getMessage(), 'imported' => $imported];
    }
}

/**
 * Create a backup for a user account.
 * Supports both incremental (rsync-based) and full (tar.gz) backup types.
 */
function backupCreate(array $params): array
{
    $username = $params['username'] ?? '';
    $outputPath = $params['output_path'] ?? '';
    $backupType = $params['backup_type'] ?? 'full'; // 'full' or 'incremental'
    $includeFiles = $params['include_files'] ?? true;
    $includeDatabases = $params['include_databases'] ?? true;
    $includeMailboxes = $params['include_mailboxes'] ?? true;
    $includeDns = $params['include_dns'] ?? true;
    $includeSsl = $params['include_ssl'] ?? true;
    $domains = $params['domains'] ?? null; // null = all domains
    $databases = $params['databases'] ?? null; // null = all databases
    $mailboxes = $params['mailboxes'] ?? null; // null = all mailboxes
    $incrementalBase = $params['incremental_base'] ?? ''; // For incremental: path to previous backup

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    if (empty($outputPath)) {
        return ['success' => false, 'error' => 'Output path is required'];
    }

    $homeDir = "/home/$username";
    if (!is_dir($homeDir)) {
        return ['success' => false, 'error' => "User home directory not found: $homeDir"];
    }

    // Create temp directory for backup assembly
    $tempDir = sys_get_temp_dir() . '/jabali_backup_' . uniqid();
    if (!mkdir($tempDir, 0755, true)) {
        return ['success' => false, 'error' => 'Failed to create temp directory'];
    }

    $manifest = [
        'version' => '2.0',
        'created_at' => date('c'),
        'username' => $username,
        'backup_type' => $backupType,
        'includes' => [
            'files' => $includeFiles,
            'databases' => $includeDatabases,
            'mailboxes' => $includeMailboxes,
            'dns' => $includeDns,
            'ssl' => $includeSsl,
        ],
        'domains' => [],
        'databases' => [],
        'mailboxes' => [],
        'ssl_certificates' => [],
        'dns_zones' => [],
        'panel_data' => false,
        'file_count' => 0,
        'total_size' => 0,
    ];

    try {
        // Backup files (domain directories)
        if ($includeFiles) {
            $filesDir = "$tempDir/files";
            mkdir($filesDir, 0755, true);

            // Get list of domains for this user
            $domainsDir = "$homeDir/domains";
            if (is_dir($domainsDir)) {
                $domainDirs = array_filter(scandir($domainsDir), fn($d) => $d !== '.' && $d !== '..' && is_dir("$domainsDir/$d"));

                foreach ($domainDirs as $domain) {
                    // Skip if specific domains requested and this isn't one
                    if ($domains !== null && !in_array($domain, $domains)) {
                        continue;
                    }

                    $domainPath = "$domainsDir/$domain";
                    $tarFile = "$filesDir/{$domain}.tar.gz";

                    // Exclusions for cache/temp files
                    $excludes = [
                        '*.log',
                        'cache/*',
                        '.cache/*',
                        'tmp/*',
                        '.tmp/*',
                        'node_modules/*',
                        'vendor/*',
                    ];

                    if ($backupType === 'incremental' && !empty($incrementalBase)) {
                        // Incremental backup using rsync with --link-dest
                        $baseDir = "$incrementalBase/files/$domain";
                        $excludeArgs = implode(' ', array_map(fn($e) => "--exclude=" . escapeshellarg($e), $excludes));
                        if (is_dir($baseDir)) {
                            exec("rsync -a $excludeArgs --link-dest=" . escapeshellarg($baseDir) . " " . escapeshellarg("$domainPath/") . " " . escapeshellarg("$filesDir/$domain/") . " 2>&1", $output, $retval);
                        } else {
                            // No base exists, do full copy with exclusions
                            exec("rsync -a $excludeArgs " . escapeshellarg("$domainPath/") . " " . escapeshellarg("$filesDir/$domain/") . " 2>&1", $output, $retval);
                        }
                    } else {
                        // Full backup - create tar.gz with exclusions
                        $excludeArgs = implode(' ', array_map(fn($e) => "--exclude=" . escapeshellarg($e), $excludes));
                        exec("tar -I pigz -cf " . escapeshellarg($tarFile) . " $excludeArgs -C " . escapeshellarg($domainsDir) . " " . escapeshellarg($domain) . " 2>&1", $output, $retval);
                    }

                    if ($retval === 0) {
                        $manifest['domains'][] = $domain;
                        if (file_exists($tarFile)) {
                            $manifest['total_size'] += filesize($tarFile);
                        }
                    }
                }
            }
        }

        // Backup databases
        if ($includeDatabases) {
            $dbDir = "$tempDir/mysql";
            mkdir($dbDir, 0755, true);

            $mysqli = getMysqlConnection();
            if ($mysqli) {
                // Get user's databases (prefixed with username_)
                $prefix = $username . '_';
                $result = $mysqli->query("SHOW DATABASES LIKE '{$mysqli->real_escape_string($prefix)}%'");

                while ($row = $result->fetch_row()) {
                    $dbName = $row[0];

                    // Skip if specific databases requested and this isn't one
                    if ($databases !== null && !in_array($dbName, $databases)) {
                        continue;
                    }

                    $sqlFile = "$dbDir/{$dbName}.sql.gz";

                    // Export database using mysqldump
                    $mysqlHost = $_ENV['MYSQL_HOST'] ?? 'localhost';
                    $mysqlUser = $_ENV['MYSQL_USER'] ?? 'root';
                    $mysqlPass = $_ENV['MYSQL_PASSWORD'] ?? '';

                    $cmd = "mysqldump --host=" . escapeshellarg($mysqlHost) .
                           " --user=" . escapeshellarg($mysqlUser) .
                           " --password=" . escapeshellarg($mysqlPass) .
                           " --single-transaction --quick " . escapeshellarg($dbName) .
                           " | pigz > " . escapeshellarg($sqlFile) . " 2>&1";

                    exec($cmd, $output, $retval);

                    if ($retval === 0 && file_exists($sqlFile)) {
                        $manifest['databases'][] = $dbName;
                        $manifest['total_size'] += filesize($sqlFile);
                    }
                }

                // Backup MySQL users and their permissions
                $usersFile = "$dbDir/users.sql";
                $usersSql = "-- MySQL Users and Permissions Backup\n";
                $usersSql .= "-- Generated: " . date('Y-m-d H:i:s') . "\n\n";

                // Get all MySQL users matching the username_ pattern
                // Escape underscore in LIKE pattern (underscore is a single-char wildcard in MySQL)
                $safePrefix = $mysqli->real_escape_string($username) . '\\_';
                $userQuery = $mysqli->query("SELECT User, Host FROM mysql.user WHERE User LIKE '{$safePrefix}%'");

                $exportedUsers = [];
                if ($userQuery) {
                    while ($userRow = $userQuery->fetch_assoc()) {
                        $mysqlUser = $userRow['User'];
                        $mysqlHost = $userRow['Host'];
                        $userIdentifier = "'{$mysqli->real_escape_string($mysqlUser)}'@'{$mysqli->real_escape_string($mysqlHost)}'";

                        // Get CREATE USER statement
                        $createResult = $mysqli->query("SHOW CREATE USER $userIdentifier");
                        if ($createResult && $createRow = $createResult->fetch_row()) {
                            // Add DROP USER IF EXISTS for clean restore
                            $usersSql .= "DROP USER IF EXISTS $userIdentifier;\n";
                            $usersSql .= $createRow[0] . ";\n";

                            // Get GRANT statements
                            $grantsResult = $mysqli->query("SHOW GRANTS FOR $userIdentifier");
                            if ($grantsResult) {
                                while ($grantRow = $grantsResult->fetch_row()) {
                                    $usersSql .= $grantRow[0] . ";\n";
                                }
                            }
                            $usersSql .= "\n";
                            $exportedUsers[] = $mysqlUser;
                        }
                    }
                }

                // Save users.sql if we have any users
                if (!empty($exportedUsers)) {
                    $usersSql .= "FLUSH PRIVILEGES;\n";
                    file_put_contents($usersFile, $usersSql);
                    $manifest['mysql_users'] = $exportedUsers;
                    $manifest['total_size'] += filesize($usersFile);
                }

                $mysqli->close();
            }
        }

        // Backup mailboxes
        if ($includeMailboxes) {
            $mailDir = "$tempDir/mailboxes";
            mkdir($mailDir, 0755, true);

            // Get user's domains
            $userDomains = [];
            $domainsFile = "$homeDir/.domains";
            if (file_exists($domainsFile)) {
                $domainsData = json_decode(file_get_contents($domainsFile), true) ?: [];
                $userDomains = array_keys($domainsData);
            } elseif (is_dir("$homeDir/domains")) {
                $userDomains = array_filter(scandir("$homeDir/domains"), fn($d) => $d !== '.' && $d !== '..' && is_dir("$homeDir/domains/$d"));
            }

            // Backup from /var/mail/vhosts (Dovecot virtual mailboxes)
            foreach ($userDomains as $domain) {
                $vhostMailPath = "/var/mail/vhosts/$domain";
                if (is_dir($vhostMailPath)) {
                    $mailUsers = array_filter(scandir($vhostMailPath), fn($m) => $m !== '.' && $m !== '..' && is_dir("$vhostMailPath/$m"));

                    foreach ($mailUsers as $mailUser) {
                        $mailboxName = "{$mailUser}@{$domain}";

                        // Skip if specific mailboxes requested
                        if ($mailboxes !== null && !in_array($mailboxName, $mailboxes)) {
                            continue;
                        }

                        $tarFile = "$mailDir/{$domain}_{$mailUser}.tar.gz";
                        exec("tar -I pigz -cf " . escapeshellarg($tarFile) . " -C " . escapeshellarg($vhostMailPath) . " " . escapeshellarg($mailUser) . " 2>&1", $output, $retval);

                        if ($retval === 0 && file_exists($tarFile)) {
                            $manifest['mailboxes'][] = $mailboxName;
                            $manifest['total_size'] += filesize($tarFile);
                        }
                    }
                }
            }

            // Also backup user's local mail directory if exists
            $userMailDir = "$homeDir/mail";
            if (is_dir($userMailDir) && count(scandir($userMailDir)) > 2) {
                $tarFile = "$mailDir/local_mail.tar.gz";
                exec("tar -I pigz -cf " . escapeshellarg($tarFile) . " -C " . escapeshellarg($homeDir) . " mail 2>&1", $output, $retval);
                if ($retval === 0 && file_exists($tarFile)) {
                    $manifest['total_size'] += filesize($tarFile);
                }
            }
        }

        // Backup SSL certificates
        if ($includeSsl) {
            $sslDir = "$tempDir/ssl";
            mkdir($sslDir, 0755, true);

            // Backup user's SSL directory (custom certificates)
            $userSslDir = "$homeDir/ssl";
            if (is_dir($userSslDir) && count(scandir($userSslDir)) > 2) {
                exec("cp -a " . escapeshellarg($userSslDir) . " " . escapeshellarg("$sslDir/user_ssl") . " 2>&1");
            }

            $letsencryptDir = '/etc/letsencrypt/live';
            if (is_dir($letsencryptDir)) {
                // Get user's domains to know which SSL certs belong to them
                $userDomains = $manifest['domains'];
                if (empty($userDomains) && is_dir("$homeDir/domains")) {
                    $userDomains = array_filter(scandir("$homeDir/domains"), fn($d) => $d !== '.' && $d !== '..' && is_dir("$homeDir/domains/$d"));
                }

                foreach ($userDomains as $domain) {
                    // Skip if specific domains requested and this isn't one
                    if ($domains !== null && !in_array($domain, $domains)) {
                        continue;
                    }

                    $certPath = "$letsencryptDir/$domain";
                    if (is_dir($certPath)) {
                        $domainSslDir = "$sslDir/$domain";
                        mkdir($domainSslDir, 0755, true);

                        // Copy certificate files (following symlinks to get actual content)
                        $certFiles = ['fullchain.pem', 'privkey.pem', 'cert.pem', 'chain.pem'];
                        foreach ($certFiles as $certFile) {
                            $srcFile = "$certPath/$certFile";
                            if (file_exists($srcFile)) {
                                // Read through symlink and copy actual content
                                $realPath = realpath($srcFile);
                                if ($realPath && file_exists($realPath)) {
                                    copy($realPath, "$domainSslDir/$certFile");
                                }
                            }
                        }

                        $manifest['ssl_certificates'][] = $domain;
                    }
                }
            }
        }

        // Backup DNS zone files
        if ($includeDns) {
            $dnsDir = "$tempDir/zones";
            mkdir($dnsDir, 0755, true);

            $zonesDir = '/etc/bind/zones';
            if (is_dir($zonesDir)) {
                $userDomains = $manifest['domains'];
                if (empty($userDomains) && is_dir("$homeDir/domains")) {
                    $userDomains = array_filter(scandir("$homeDir/domains"), fn($d) => $d !== '.' && $d !== '..' && is_dir("$homeDir/domains/$d"));
                }

                foreach ($userDomains as $domain) {
                    $zoneFile = "$zonesDir/db.$domain";
                    if (file_exists($zoneFile)) {
                        copy($zoneFile, "$dnsDir/db.$domain");
                        $manifest['dns_zones'][] = $domain;
                    }
                }
            }
        }

        // Export panel metadata (domains config, email accounts, WordPress sites, etc.)
        $metadataDir = "$tempDir/metadata";
        mkdir($metadataDir, 0755, true);

        // Export user's .domains file
        $domainsFile = "$homeDir/.domains";
        if (file_exists($domainsFile)) {
            copy($domainsFile, "$metadataDir/domains.json");
        }

        // Export user's .wordpress_sites file
        $wpSitesFile = "$homeDir/.wordpress_sites";
        if (file_exists($wpSitesFile)) {
            copy($wpSitesFile, "$metadataDir/wordpress_sites.json");
        }

        // Export email configuration from database via PHP
        $panelMetadata = exportUserPanelData($username);
        if (!empty($panelMetadata)) {
            file_put_contents("$metadataDir/panel_data.json", json_encode($panelMetadata, JSON_PRETTY_PRINT));
            $manifest['panel_data'] = true;
        }

        // Save manifest
        file_put_contents("$tempDir/manifest.json", json_encode($manifest, JSON_PRETTY_PRINT));

        // Create final archive
        $outputDir = dirname($outputPath);
        if (!is_dir($outputDir)) {
            mkdir($outputDir, 0755, true);
        }

        if ($backupType === 'incremental') {
            // For incremental, just copy the temp dir as the backup
            exec("cp -a " . escapeshellarg($tempDir) . " " . escapeshellarg($outputPath) . " 2>&1", $output, $retval);
        } else {
            // For full backup, create a tar.gz archive
            exec("tar -I pigz -cf " . escapeshellarg($outputPath) . " -C " . escapeshellarg(dirname($tempDir)) . " " . escapeshellarg(basename($tempDir)) . " 2>&1", $output, $retval);
        }

        if ($retval !== 0) {
            throw new Exception('Failed to create backup archive');
        }

        // Calculate checksum
        $checksum = '';
        if (file_exists($outputPath)) {
            if (is_file($outputPath)) {
                $checksum = hash_file('sha256', $outputPath);
                $finalSize = filesize($outputPath);
            } else {
                // For incremental (directory), calculate size recursively
                $finalSize = 0;
                $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($outputPath));
                foreach ($iterator as $file) {
                    if ($file->isFile()) {
                        $finalSize += $file->getSize();
                    }
                }
            }
        }

        // Cleanup temp directory
        exec("rm -rf " . escapeshellarg($tempDir));

        return [
            'success' => true,
            'path' => $outputPath,
            'size' => $finalSize ?? $manifest['total_size'],
            'checksum' => $checksum,
            'domains' => $manifest['domains'],
            'databases' => $manifest['databases'],
            'mysql_users' => $manifest['mysql_users'] ?? [],
            'mailboxes' => $manifest['mailboxes'],
            'ssl_certificates' => $manifest['ssl_certificates'],
            'backup_type' => $backupType,
        ];

    } catch (Exception $e) {
        // Cleanup on error
        exec("rm -rf " . escapeshellarg($tempDir));
        return ['success' => false, 'error' => $e->getMessage()];
    }
}

/**
 * Create a server-wide backup (all users or selected users).
 */
function backupCreateServer(array $params): array
{
    $outputPath = $params['output_path'] ?? '';
    $backupType = $params['backup_type'] ?? 'full';
    $users = $params['users'] ?? null; // null = all users
    $includeFiles = $params['include_files'] ?? true;
    $includeDatabases = $params['include_databases'] ?? true;
    $includeMailboxes = $params['include_mailboxes'] ?? true;
    $includeDns = $params['include_dns'] ?? true;

    if (empty($outputPath)) {
        return ['success' => false, 'error' => 'Output path is required'];
    }

    // Output path is now a folder (e.g., /var/backups/jabali/2026-01-12_013000/)
    if (!is_dir($outputPath)) {
        if (!mkdir($outputPath, 0755, true)) {
            return ['success' => false, 'error' => 'Failed to create backup directory'];
        }
    }

    $manifest = [
        'version' => '1.0',
        'created_at' => date('c'),
        'type' => 'server',
        'backup_type' => $backupType,
        'users' => [],
        'total_size' => 0,
    ];

    try {
        // Get all system users with home directories in /home
        $systemUsers = [];
        if ($users !== null) {
            $systemUsers = $users;
        } else {
            $homeDirs = array_filter(scandir('/home'), fn($d) => $d !== '.' && $d !== '..' && is_dir("/home/$d") && !isProtectedUser($d));
            $systemUsers = $homeDirs;
        }

        foreach ($systemUsers as $username) {
            if (!validateUsername($username) || isProtectedUser($username)) {
                continue;
            }

            // Create individual tar.gz per user
            $userBackupPath = "$outputPath/{$username}.tar.gz";

            $userResult = backupCreate([
                'username' => $username,
                'output_path' => $userBackupPath,
                'backup_type' => 'full', // Always full for individual user archives
                'include_files' => $includeFiles,
                'include_databases' => $includeDatabases,
                'include_mailboxes' => $includeMailboxes,
                'include_dns' => $includeDns,
            ]);

            if ($userResult['success']) {
                $userSize = file_exists($userBackupPath) ? filesize($userBackupPath) : 0;
                $manifest['users'][$username] = [
                    'file' => "{$username}.tar.gz",
                    'domains' => $userResult['domains'] ?? [],
                    'databases' => $userResult['databases'] ?? [],
                    'mailboxes' => $userResult['mailboxes'] ?? [],
                    'size' => $userSize,
                    'checksum' => file_exists($userBackupPath) ? hash_file('sha256', $userBackupPath) : '',
                ];
                $manifest['total_size'] += $userSize;
            }
        }

        // Save manifest in the backup folder
        file_put_contents("$outputPath/manifest.json", json_encode($manifest, JSON_PRETTY_PRINT));

        return [
            'success' => true,
            'path' => $outputPath,
            'size' => $manifest['total_size'],
            'users' => array_keys($manifest['users']),
            'backup_type' => $backupType,
            'user_count' => count($manifest['users']),
        ];

    } catch (Exception $e) {
        return ['success' => false, 'error' => $e->getMessage()];
    }
}

/**
 * Restore a backup for a user.
 */
function backupRestore(array $params): array
{
    $username = $params['username'] ?? '';
    $backupPath = $params['backup_path'] ?? '';
    $restoreFiles = $params['restore_files'] ?? true;
    $restoreDatabases = $params['restore_databases'] ?? true;
    $restoreMailboxes = $params['restore_mailboxes'] ?? true;
    $restoreDns = $params['restore_dns'] ?? true;
    $restoreSsl = $params['restore_ssl'] ?? true;
    $selectedDomains = $params['selected_domains'] ?? null;
    $selectedDatabases = $params['selected_databases'] ?? null;
    $selectedMailboxes = $params['selected_mailboxes'] ?? null;

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    if (!file_exists($backupPath)) {
        return ['success' => false, 'error' => 'Backup file not found'];
    }

    $homeDir = "/home/$username";
    $tempDir = sys_get_temp_dir() . '/jabali_restore_' . uniqid();

    // Check if system user exists, create if not
    $systemUser = posix_getpwnam($username);
    if (!$systemUser) {
        logger("System user $username not found, creating...");

        // Create system user with proper home directory
        $cmd = "useradd -m -s /usr/sbin/nologin -d " . escapeshellarg($homeDir) . " " . escapeshellarg($username) . " 2>&1";
        exec($cmd, $useraddOutput, $useraddRetval);

        if ($useraddRetval !== 0) {
            return ['success' => false, 'error' => "Failed to create system user: " . implode("\n", $useraddOutput)];
        }

        // Add to sftpusers group for SFTP access
        exec("usermod -aG sftpusers " . escapeshellarg($username) . " 2>/dev/null");

        // Set up home directory ownership for SFTP chroot
        chown($homeDir, "root");
        chgrp($homeDir, $username);
        chmod($homeDir, 0750);

        // Create standard directories
        $dirs = ['domains', 'mail', 'logs', 'backups', 'tmp'];
        foreach ($dirs as $dir) {
            $dirPath = "$homeDir/$dir";
            if (!is_dir($dirPath)) {
                mkdir($dirPath, 0755, true);
                chown($dirPath, $username);
                chgrp($dirPath, $username);
            }
        }

        // Create PHP-FPM pool for the user (don't reload during migration - we'll do it at the end)
        createFpmPool($username, reload: false);

        logger("Created system user $username with home directory $homeDir");
    }

    try {
        mkdir($tempDir, 0755, true);

        // Extract or copy backup
        if (is_file($backupPath) && preg_match('/\.(tar\.gz|tgz)$/i', $backupPath)) {
            exec("tar -I pigz -xf " . escapeshellarg($backupPath) . " -C " . escapeshellarg($tempDir) . " 2>&1", $output, $retval);
            if ($retval !== 0) {
                throw new Exception('Failed to extract backup archive');
            }
            // Find the extracted directory
            $extracted = array_filter(scandir($tempDir), fn($d) => $d !== '.' && $d !== '..');
            if (count($extracted) === 1) {
                $tempDir = "$tempDir/" . reset($extracted);
            }
        } elseif (is_dir($backupPath)) {
            // Check if this is a server backup (directory with per-user tar.gz files)
            $userTarGz = "$backupPath/$username.tar.gz";
            if (file_exists($userTarGz)) {
                // Server backup - extract the specific user's tar.gz
                exec("tar -I pigz -xf " . escapeshellarg($userTarGz) . " -C " . escapeshellarg($tempDir) . " 2>&1", $output, $retval);
                if ($retval !== 0) {
                    throw new Exception("Failed to extract user backup from server backup: $username.tar.gz");
                }
                // Find the extracted directory (usually named after the username)
                $extracted = array_filter(scandir($tempDir), fn($d) => $d !== '.' && $d !== '..');
                if (count($extracted) === 1) {
                    $tempDir = "$tempDir/" . reset($extracted);
                }
            } else {
                // Incremental/direct backup - copy to temp (use /. to include hidden files)
                exec("cp -a " . escapeshellarg(rtrim($backupPath, '/')) . "/. " . escapeshellarg($tempDir) . "/ 2>&1");
            }
        } else {
            throw new Exception('Invalid backup format');
        }

        // Detect incremental backup format (uses --relative paths like home/user/domains/)
        // Convert incremental format to standard format expected by restore
        $incrementalUserDir = "$tempDir/home/$username";
        if (is_dir($incrementalUserDir)) {
            logger("Detected incremental backup format, converting to standard structure");

            // Move domains to files/
            if (is_dir("$incrementalUserDir/domains")) {
                mkdir("$tempDir/files", 0755, true);
                exec("mv " . escapeshellarg("$incrementalUserDir/domains") . "/* " . escapeshellarg("$tempDir/files/") . " 2>&1");
            }

            // Move mysql to standard location
            if (is_dir("$incrementalUserDir/mysql")) {
                exec("mv " . escapeshellarg("$incrementalUserDir/mysql") . " " . escapeshellarg("$tempDir/") . " 2>&1");
            }

            // Move zones (DNS) to standard location
            if (is_dir("$incrementalUserDir/zones")) {
                mkdir("$tempDir/dns", 0755, true);
                exec("mv " . escapeshellarg("$incrementalUserDir/zones") . "/* " . escapeshellarg("$tempDir/dns/") . " 2>&1");
            }

            // Move mail to mailboxes/
            if (is_dir("$incrementalUserDir/mail")) {
                mkdir("$tempDir/mailboxes", 0755, true);
                exec("mv " . escapeshellarg("$incrementalUserDir/mail") . "/* " . escapeshellarg("$tempDir/mailboxes/") . " 2>&1");
            }

            // Check for var/mail/vhosts (additional mailbox storage)
            if (is_dir("$tempDir/var/mail/vhosts")) {
                if (!is_dir("$tempDir/mailboxes")) {
                    mkdir("$tempDir/mailboxes", 0755, true);
                }
                exec("cp -a " . escapeshellarg("$tempDir/var/mail/vhosts") . "/* " . escapeshellarg("$tempDir/mailboxes/") . " 2>&1");
            }

            // Restore .wordpress_sites registry file
            if (file_exists("$incrementalUserDir/.wordpress_sites")) {
                $srcFile = "$incrementalUserDir/.wordpress_sites";
                $dstFile = "$homeDir/.wordpress_sites";
                logger("Copying $srcFile to $dstFile (homeDir exists: " . (is_dir($homeDir) ? 'yes' : 'no') . ")");
                if (copy($srcFile, $dstFile)) {
                    chown($dstFile, $username);
                    chgrp($dstFile, $username);
                    logger("Restored .wordpress_sites registry for $username");
                } else {
                    logger("ERROR: Failed to copy .wordpress_sites from $srcFile to $dstFile");
                }
            }

            // Restore .domains registry file
            if (file_exists("$incrementalUserDir/.domains")) {
                $srcFile = "$incrementalUserDir/.domains";
                $dstFile = "$homeDir/.domains";
                if (copy($srcFile, $dstFile)) {
                    chown($dstFile, $username);
                    chgrp($dstFile, $username);
                    logger("Restored .domains registry for $username");
                } else {
                    logger("ERROR: Failed to copy .domains from $srcFile to $dstFile");
                }
            }

            // Move SSL certificates to temp location
            if (is_dir("$incrementalUserDir/ssl")) {
                exec("mv " . escapeshellarg("$incrementalUserDir/ssl") . " " . escapeshellarg("$tempDir/") . " 2>&1");
            }

            // Move metadata directory (panel_data.json) to temp location
            if (is_dir("$incrementalUserDir/metadata")) {
                exec("mv " . escapeshellarg("$incrementalUserDir/metadata") . " " . escapeshellarg("$tempDir/") . " 2>&1");
                logger("Moved metadata directory for $username");
            }

            // Cleanup incremental structure
            exec("rm -rf " . escapeshellarg("$tempDir/home") . " " . escapeshellarg("$tempDir/var") . " 2>&1");
        }

        // Read manifest - check both temp dir and original backup path
        $manifestPath = "$tempDir/manifest.json";
        if (!file_exists($manifestPath) && is_dir($backupPath)) {
            // For server backups, manifest might be in the original directory
            $serverManifestPath = "$backupPath/manifest.json";
            if (file_exists($serverManifestPath)) {
                $manifestPath = $serverManifestPath;
            }
        }

        // For incremental backups without manifest, create a minimal one
        if (!file_exists($manifestPath)) {
            logger("No manifest found, creating minimal manifest for incremental backup");
            $manifest = [
                'username' => $username,
                'type' => 'incremental',
                'domains' => is_dir("$tempDir/files") ? array_filter(scandir("$tempDir/files"), fn($d) => $d !== '.' && $d !== '..') : [],
                'databases' => [],
                'mailboxes' => [],
            ];

            // Detect databases
            $dbDir = is_dir("$tempDir/mysql") ? "$tempDir/mysql" : "$tempDir/databases";
            if (is_dir($dbDir)) {
                foreach (scandir($dbDir) as $item) {
                    if (preg_match('/\.sql(\.gz)?$/i', $item)) {
                        $manifest['databases'][] = preg_replace('/\.sql(\.gz)?$/i', '', $item);
                    }
                }
            }

            // Detect mailboxes
            if (is_dir("$tempDir/mailboxes")) {
                foreach (scandir("$tempDir/mailboxes") as $domain) {
                    if ($domain === '.' || $domain === '..') continue;
                    if (is_dir("$tempDir/mailboxes/$domain")) {
                        $manifest['mailboxes'][] = $domain;
                    }
                }
            }
        } else {
            $manifest = json_decode(file_get_contents($manifestPath), true);
        }

        $restored = [
            'files' => [],
            'databases' => [],
            'mailboxes' => [],
            'ssl_certificates' => [],
        ];

        // Restore files
        if ($restoreFiles && is_dir("$tempDir/files")) {
            $filesDir = "$tempDir/files";
            $domainsTarget = "$homeDir/domains";

            if (!is_dir($domainsTarget)) {
                mkdir($domainsTarget, 0755, true);
            }

            foreach (scandir($filesDir) as $item) {
                if ($item === '.' || $item === '..') continue;

                // Extract domain name
                $domain = preg_replace('/\.tar\.gz$/i', '', $item);

                // SECURITY: Validate domain name (no path traversal)
                if (!validateDomain($domain) || strpos($domain, '..') !== false || strpos($domain, '/') !== false) {
                    logger( "Blocked invalid domain name in file restore: $domain");
                    continue;
                }

                if ($selectedDomains !== null && !in_array($domain, $selectedDomains)) {
                    continue;
                }

                $itemPath = "$filesDir/$item";
                $retval = 1; // Default to failure

                if (is_file($itemPath) && preg_match('/\.tar\.gz$/i', $item)) {
                    // Extract tar.gz
                    exec("tar -I pigz -xf " . escapeshellarg($itemPath) . " -C " . escapeshellarg($domainsTarget) . " 2>&1", $output, $retval);
                } elseif (is_dir($itemPath)) {
                    // Copy directory contents (incremental) - use rsync to avoid nesting
                    $targetDomainDir = "$domainsTarget/$domain";
                    if (!is_dir($targetDomainDir)) {
                        mkdir($targetDomainDir, 0755, true);
                    }
                    // Use trailing slash on source to copy contents, not the directory itself
                    exec("rsync -a " . escapeshellarg("$itemPath/") . " " . escapeshellarg("$targetDomainDir/") . " 2>&1", $output, $retval);
                }

                if ($retval === 0) {
                    // SECURITY: Remove dangerous symlinks that point outside user's home
                    $domainPath = "$domainsTarget/$domain";
                    $removedSymlinks = removeDangerousSymlinks($domainPath, $homeDir);
                    if ($removedSymlinks > 0) {
                        logger( "Removed $removedSymlinks dangerous symlinks from $domainPath");
                    }

                    // Fix ownership
                    exec("chown -R $username:$username " . escapeshellarg($domainPath));
                    $restored['files'][] = $domain;
                }
            }
        }

        // Restore databases (check both mysql/ and databases/ for backward compatibility)
        $dbDir = is_dir("$tempDir/mysql") ? "$tempDir/mysql" : "$tempDir/databases";
        $dbPrefix = $username . '_';

        if ($restoreDatabases && is_dir($dbDir)) {
            $mysqli = getMysqlConnection();

            if ($mysqli) {
                foreach (scandir($dbDir) as $item) {
                    if ($item === '.' || $item === '..') continue;
                    if ($item === 'users.sql') continue; // Handle separately
                    if (!preg_match('/\.sql(\.gz)?$/i', $item)) continue;

                    $dbName = preg_replace('/\.sql(\.gz)?$/i', '', $item);

                    // SECURITY: Verify database name starts with username_ prefix
                    if (strpos($dbName, $dbPrefix) !== 0) {
                        logger( "Blocked restore of database not owned by user: $dbName (expected prefix: $dbPrefix)");
                        continue;
                    }

                    if ($selectedDatabases !== null && !in_array($dbName, $selectedDatabases)) {
                        continue;
                    }

                    $sqlFile = "$dbDir/$item";

                    // SECURITY: Sanitize database dump (remove DEFINER, GRANT, etc.)
                    if (!sanitizeDatabaseDump($username, $sqlFile)) {
                        logger( "Failed to sanitize database dump: $sqlFile");
                        continue;
                    }

                    // Ensure database exists
                    $mysqli->query("CREATE DATABASE IF NOT EXISTS `{$mysqli->real_escape_string($dbName)}`");

                    $mysqlHost = $_ENV['MYSQL_HOST'] ?? 'localhost';
                    $mysqlUser = $_ENV['MYSQL_USER'] ?? 'root';
                    $mysqlPass = $_ENV['MYSQL_PASSWORD'] ?? '';

                    if (preg_match('/\.gz$/i', $item)) {
                        $cmd = "pigz -dc " . escapeshellarg($sqlFile) . " | mysql " .
                               "--host=" . escapeshellarg($mysqlHost) .
                               " --user=" . escapeshellarg($mysqlUser) .
                               " --password=" . escapeshellarg($mysqlPass) .
                               " " . escapeshellarg($dbName) . " 2>&1";
                    } else {
                        $cmd = "mysql --host=" . escapeshellarg($mysqlHost) .
                               " --user=" . escapeshellarg($mysqlUser) .
                               " --password=" . escapeshellarg($mysqlPass) .
                               " " . escapeshellarg($dbName) .
                               " < " . escapeshellarg($sqlFile) . " 2>&1";
                    }

                    exec($cmd, $output, $retval);

                    if ($retval === 0) {
                        $restored['databases'][] = $dbName;
                    }
                }

                // Restore MySQL users and permissions
                $usersFile = "$dbDir/users.sql";
                if (file_exists($usersFile)) {
                    // SECURITY: Validate and sanitize users.sql
                    $sanitizedUsersSql = validateMysqlUsersFile($username, $usersFile);

                    if ($sanitizedUsersSql !== null && !empty(trim($sanitizedUsersSql))) {
                        // Write sanitized SQL to temp file
                        $sanitizedUsersFile = "$dbDir/users_sanitized.sql";
                        file_put_contents($sanitizedUsersFile, $sanitizedUsersSql);

                        $mysqlHost = $_ENV['MYSQL_HOST'] ?? 'localhost';
                        $mysqlUser = $_ENV['MYSQL_USER'] ?? 'root';
                        $mysqlPass = $_ENV['MYSQL_PASSWORD'] ?? '';

                        $cmd = "mysql --host=" . escapeshellarg($mysqlHost) .
                               " --user=" . escapeshellarg($mysqlUser) .
                               " --password=" . escapeshellarg($mysqlPass) .
                               " < " . escapeshellarg($sanitizedUsersFile) . " 2>&1";

                        exec($cmd, $usersOutput, $usersRetval);

                        if ($usersRetval === 0) {
                            $restored['mysql_users'] = true;
                        }

                        // Cleanup
                        unlink($sanitizedUsersFile);
                    } else {
                        logger( "MySQL users.sql validation failed or empty after sanitization");
                    }
                }

                // Fallback: If users.sql wasn't restored, create MySQL users from wp-config.php
                if (empty($restored['mysql_users']) && !empty($restored['databases'])) {
                    logger("No users.sql restored, attempting to create MySQL users from wp-config.php");
                    $filesDir = "$tempDir/files";

                    foreach ($restored['databases'] as $dbName) {
                        // Find wp-config.php files that reference this database
                        if (is_dir($filesDir)) {
                            foreach (scandir($filesDir) as $domain) {
                                if ($domain === '.' || $domain === '..') continue;

                                $wpConfig = "$filesDir/$domain/public_html/wp-config.php";
                                if (!file_exists($wpConfig)) continue;

                                $configContent = file_get_contents($wpConfig);

                                // Check if this wp-config uses this database
                                if (strpos($configContent, "'$dbName'") === false && strpos($configContent, "\"$dbName\"") === false) {
                                    continue;
                                }

                                // Extract DB credentials
                                $dbUser = null;
                                $dbPass = null;

                                if (preg_match("/define\s*\(\s*['\"]DB_USER['\"]\s*,\s*['\"]([^'\"]+)['\"]/", $configContent, $matches)) {
                                    $dbUser = $matches[1];
                                }
                                if (preg_match("/define\s*\(\s*['\"]DB_PASSWORD['\"]\s*,\s*['\"]([^'\"]+)['\"]/", $configContent, $matches)) {
                                    $dbPass = $matches[1];
                                }

                                // Security: Only create user if it starts with username_
                                if ($dbUser && strpos($dbUser, $username . '_') === 0) {
                                    // Check if user already exists
                                    $checkUser = $mysqli->query("SELECT User FROM mysql.user WHERE User = '" . $mysqli->real_escape_string($dbUser) . "' AND Host = 'localhost'");
                                    if ($checkUser && $checkUser->num_rows === 0) {
                                        // Create user and grant permissions
                                        $safeUser = $mysqli->real_escape_string($dbUser);
                                        $safePass = $mysqli->real_escape_string($dbPass);
                                        $safeDb = $mysqli->real_escape_string($dbName);

                                        $mysqli->query("CREATE USER '$safeUser'@'localhost' IDENTIFIED BY '$safePass'");
                                        $mysqli->query("GRANT ALL PRIVILEGES ON `$safeDb`.* TO '$safeUser'@'localhost'");
                                        $mysqli->query("FLUSH PRIVILEGES");

                                        logger("Created MySQL user $dbUser for database $dbName (from wp-config.php)");
                                        $restored['mysql_users_created'][] = $dbUser;
                                    }
                                }
                            }
                        }
                    }
                }

                $mysqli->close();
            }
        }

        // Restore mailboxes
        if ($restoreMailboxes && is_dir("$tempDir/mailboxes")) {
            $mailDir = "$tempDir/mailboxes";
            $userDomains = getUserDomains($username);

            foreach (scandir($mailDir) as $item) {
                if ($item === '.' || $item === '..') continue;

                $itemPath = "$mailDir/$item";

                // Handle tar.gz files (traditional backup format)
                if (is_file($itemPath) && preg_match('/\.tar\.gz$/i', $item)) {
                    // Parse domain_user.tar.gz format
                    $parts = explode('_', preg_replace('/\.tar\.gz$/i', '', $item));
                    if (count($parts) >= 2) {
                        $mailUser = array_pop($parts);
                        $domain = implode('_', $parts);
                        $mailboxName = "{$mailUser}@{$domain}";

                        // SECURITY: Validate domain ownership and format
                        if (!validateDomain($domain) || strpos($domain, '..') !== false) {
                            logger("Blocked invalid domain in mailbox restore: $domain");
                            continue;
                        }

                        // SECURITY: Check if domain belongs to this user
                        if (!in_array($domain, $userDomains)) {
                            logger("Blocked mailbox restore for unowned domain: $domain (user: $username)");
                            continue;
                        }

                        if ($selectedMailboxes !== null && !in_array($mailboxName, $selectedMailboxes)) {
                            continue;
                        }

                        $targetPath = "$homeDir/domains/$domain/mail";
                        if (!is_dir($targetPath)) {
                            mkdir($targetPath, 0755, true);
                        }

                        exec("tar -I pigz -xf " . escapeshellarg($itemPath) . " -C " . escapeshellarg($targetPath) . " 2>&1", $output, $retval);

                        if ($retval === 0) {
                            // SECURITY: Remove dangerous symlinks
                            $mailboxPath = "$targetPath/$mailUser";
                            if (is_dir($mailboxPath)) {
                                removeDangerousSymlinks($mailboxPath, $homeDir);
                            }

                            exec("chown -R $username:$username " . escapeshellarg($mailboxPath));
                            $restored['mailboxes'][] = $mailboxName;
                        }
                    }
                }
                // Handle directories (incremental backup format: mailboxes/domain/mailuser/)
                elseif (is_dir($itemPath)) {
                    $domain = $item;

                    // SECURITY: Validate domain format
                    if (!validateDomain($domain) || strpos($domain, '..') !== false) {
                        logger("Blocked invalid domain in mailbox restore: $domain");
                        continue;
                    }

                    // SECURITY: Check if domain belongs to this user
                    if (!in_array($domain, $userDomains)) {
                        logger("Blocked mailbox restore for unowned domain: $domain (user: $username)");
                        continue;
                    }

                    // Restore all mailboxes for this domain
                    foreach (scandir($itemPath) as $mailUser) {
                        if ($mailUser === '.' || $mailUser === '..') continue;

                        $mailboxSrc = "$itemPath/$mailUser";
                        if (!is_dir($mailboxSrc)) continue;

                        $mailboxName = "{$mailUser}@{$domain}";

                        if ($selectedMailboxes !== null && !in_array($mailboxName, $selectedMailboxes)) {
                            continue;
                        }

                        // Copy mailbox to user's home mail directory
                        $mailTarget = "$homeDir/mail/$domain/$mailUser";
                        if (!is_dir(dirname($mailTarget))) {
                            mkdir(dirname($mailTarget), 0755, true);
                        }

                        // Remove existing mailbox directory if present
                        if (is_dir($mailTarget)) {
                            exec("rm -rf " . escapeshellarg($mailTarget));
                        }

                        exec("cp -a " . escapeshellarg($mailboxSrc) . " " . escapeshellarg($mailTarget) . " 2>&1", $output, $retval);

                        if ($retval === 0) {
                            // SECURITY: Remove dangerous symlinks
                            removeDangerousSymlinks($mailTarget, $homeDir);

                            // Fix ownership for system user
                            exec("chown -R $username:$username " . escapeshellarg($mailTarget));

                            // Add to Postfix virtual_mailbox_maps if not already there
                            $mailboxesFile = POSTFIX_VIRTUAL_MAILBOXES;
                            $mailboxesContent = file_exists($mailboxesFile) ? file_get_contents($mailboxesFile) : '';
                            if (strpos($mailboxesContent, $mailboxName) === false) {
                                file_put_contents($mailboxesFile, trim($mailboxesContent) . "\n{$mailboxName} {$domain}/{$mailUser}/\n");
                            }

                            $restored['mailboxes'][] = $mailboxName;
                            logger("Restored mailbox: $mailboxName to $mailTarget");
                        }
                    }
                }
            }

            // Rebuild Postfix hash file if mailboxes were restored
            if (!empty($restored['mailboxes'])) {
                exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_MAILBOXES));
            }
        }

        // Restore SSL certificates
        if ($restoreSsl && is_dir("$tempDir/ssl")) {
            $sslBackupDir = "$tempDir/ssl";
            $letsencryptDir = '/etc/letsencrypt/live';
            $userDomains = getUserDomains($username);

            // Create letsencrypt directory if needed
            if (!is_dir($letsencryptDir)) {
                mkdir($letsencryptDir, 0755, true);
            }

            foreach (scandir($sslBackupDir) as $domain) {
                if ($domain === '.' || $domain === '..') continue;
                if (!is_dir("$sslBackupDir/$domain")) continue;

                // SECURITY: Validate domain format and no path traversal
                if (!validateDomain($domain) || strpos($domain, '..') !== false || strpos($domain, '/') !== false) {
                    logger( "Blocked invalid domain in SSL restore: $domain");
                    continue;
                }

                // SECURITY: Verify domain belongs to this user
                if (!in_array($domain, $userDomains)) {
                    logger( "Blocked SSL restore for unowned domain: $domain (user: $username)");
                    continue;
                }

                if ($selectedDomains !== null && !in_array($domain, $selectedDomains)) {
                    continue;
                }

                $targetDir = "$letsencryptDir/$domain";

                // Create target directory
                if (!is_dir($targetDir)) {
                    mkdir($targetDir, 0755, true);
                }

                // Copy certificate files (only allow specific filenames)
                $certFiles = ['fullchain.pem', 'privkey.pem', 'cert.pem', 'chain.pem'];
                $copied = false;
                foreach ($certFiles as $certFile) {
                    $srcFile = "$sslBackupDir/$domain/$certFile";
                    if (file_exists($srcFile) && is_file($srcFile)) {
                        // SECURITY: Verify source is a regular file, not a symlink
                        if (is_link($srcFile)) {
                            logger( "Blocked symlink in SSL backup: $srcFile");
                            continue;
                        }
                        copy($srcFile, "$targetDir/$certFile");
                        chmod("$targetDir/$certFile", $certFile === 'privkey.pem' ? 0600 : 0644);
                        $copied = true;
                    }
                }

                if ($copied) {
                    $restored['ssl_certificates'][] = $domain;
                }
            }
        }

        // Restore metadata files (for full backups that store them in metadata/)
        $metadataDir = "$tempDir/metadata";
        if (is_dir($metadataDir)) {
            // Restore .wordpress_sites from metadata/wordpress_sites.json
            if (file_exists("$metadataDir/wordpress_sites.json")) {
                copy("$metadataDir/wordpress_sites.json", "$homeDir/.wordpress_sites");
                chown("$homeDir/.wordpress_sites", $username);
                chgrp("$homeDir/.wordpress_sites", $username);
                logger("Restored .wordpress_sites from metadata for $username");
            }

            // Restore .domains from metadata/domains.json
            if (file_exists("$metadataDir/domains.json")) {
                copy("$metadataDir/domains.json", "$homeDir/.domains");
                chown("$homeDir/.domains", $username);
                chgrp("$homeDir/.domains", $username);
                logger("Restored .domains from metadata for $username");
            }
        }

        // Restore DNS zones (check both zones/ and dns/ for backward compatibility)
        $dnsDir = is_dir("$tempDir/zones") ? "$tempDir/zones" : "$tempDir/dns";
        if ($restoreDns && is_dir($dnsDir)) {
            $zonesTargetDir = '/etc/bind/zones';
            $restored['dns_zones'] = [];
            $userDomains = getUserDomains($username);

            foreach (scandir($dnsDir) as $item) {
                if ($item === '.' || $item === '..') continue;
                if (!preg_match('/^db\.(.+)$/', $item, $matches)) continue;

                $domain = $matches[1];

                // SECURITY: Validate domain format and no path traversal
                if (!validateDomain($domain) || strpos($domain, '..') !== false || strpos($domain, '/') !== false) {
                    logger( "Blocked invalid domain in DNS restore: $domain");
                    continue;
                }

                // SECURITY: Verify domain belongs to this user
                if (!in_array($domain, $userDomains)) {
                    logger( "Blocked DNS zone restore for unowned domain: $domain (user: $username)");
                    continue;
                }

                if ($selectedDomains !== null && !in_array($domain, $selectedDomains)) {
                    continue;
                }

                $srcFile = "$dnsDir/$item";
                $targetFile = "$zonesTargetDir/$item";

                // SECURITY: Verify source is a regular file, not a symlink
                if (is_link($srcFile)) {
                    logger( "Blocked symlink in DNS backup: $srcFile");
                    continue;
                }

                // SECURITY: Validate zone file using named-checkzone before copying
                exec("named-checkzone " . escapeshellarg($domain) . " " . escapeshellarg($srcFile) . " 2>&1", $checkOutput, $checkRetval);
                if ($checkRetval !== 0) {
                    logger( "Invalid DNS zone file rejected: $srcFile - " . implode(' ', $checkOutput));
                    continue;
                }

                if (copy($srcFile, $targetFile)) {
                    chmod($targetFile, 0644);
                    chown($targetFile, 'bind');
                    chgrp($targetFile, 'bind');
                    $restored['dns_zones'][] = $domain;
                }
            }

            // Reload BIND if we restored any zones
            if (!empty($restored['dns_zones'])) {
                exec('rndc reload 2>&1');
            }
        }

        // Import panel data (database records) from metadata/panel_data.json
        $panelDataFile = "$tempDir/metadata/panel_data.json";
        if (file_exists($panelDataFile)) {
            $panelData = json_decode(file_get_contents($panelDataFile), true);
            if ($panelData) {
                $importResult = importUserPanelData($username, $panelData);
                if ($importResult['success'] ?? false) {
                    $restored['panel_data'] = $importResult['imported'];
                    logger("Imported panel data for $username: " . json_encode($importResult['imported']));
                } else {
                    logger("Failed to import panel data for $username: " . ($importResult['error'] ?? 'Unknown error'));
                }
            }
        } else {
            // No panel_data.json - create minimal panel user record from restored data
            logger("No panel_data.json found, creating minimal panel user record for $username");

            // Build minimal panel data from restored files and registry
            $minimalPanelData = [
                'username' => $username,
                'user' => [
                    'name' => $username,
                    'email' => "{$username}@localhost",
                    'username' => $username,
                    'is_admin' => 0,
                    'is_active' => 1,
                    'home_directory' => $homeDir,
                    'disk_quota_mb' => 10240,
                ],
                'domains' => [],
                'email_domains' => [],
                'mailboxes' => [],
            ];

            // Read domains from .domains registry file
            $domainsFile = "$homeDir/.domains";
            if (file_exists($domainsFile)) {
                $domainsData = json_decode(file_get_contents($domainsFile), true) ?: [];
                foreach ($domainsData as $domainName => $domainInfo) {
                    $minimalPanelData['domains'][] = [
                        'domain' => $domainName,
                        'document_root' => $domainInfo['document_root'] ?? "$homeDir/domains/$domainName/public_html",
                        'is_active' => 1,
                    ];
                }
            }

            // Read email domains from restored mailboxes
            if (!empty($restored['mailboxes'])) {
                $seenDomains = [];
                foreach ($restored['mailboxes'] as $mailboxEmail) {
                    $parts = explode('@', $mailboxEmail);
                    if (count($parts) === 2) {
                        $localPart = $parts[0];
                        $domain = $parts[1];
                        if (!isset($seenDomains[$domain])) {
                            $minimalPanelData['email_domains'][] = ['domain' => $domain];
                            $seenDomains[$domain] = true;
                        }
                        $minimalPanelData['mailboxes'][] = [
                            'local_part' => $localPart,
                            'domain' => $domain,
                            'name' => $localPart,
                            'quota_bytes' => 1073741824,
                            'maildir_path' => "$homeDir/mail/$domain/$localPart/",
                        ];
                    }
                }
            }

            $importResult = importUserPanelData($username, $minimalPanelData);
            if ($importResult['success'] ?? false) {
                $restored['panel_data'] = $importResult['imported'];
                logger("Created minimal panel data for $username: " . json_encode($importResult['imported']));
            } else {
                logger("Failed to create minimal panel data for $username: " . ($importResult['error'] ?? 'Unknown error'));
            }
        }

        // Fix ownership for all restored files and directories
        // Home directory should be root:username with 750 for SFTP chroot security
        $userInfo = posix_getpwnam($username);
        if ($userInfo) {
            $uid = $userInfo['uid'];
            $gid = $userInfo['gid'];

            // Home dir: root:user 750 (required for SFTP chroot)
            chown($homeDir, "root");
            chgrp($homeDir, $username);
            chmod($homeDir, 0750);

            // Create and fix ownership of directories inside home
            $dirsToFix = ['domains', 'mail', 'logs', 'backups', 'tmp', 'metadata', '.ssh', 'mysql', 'zones', 'ssl'];
            foreach ($dirsToFix as $subdir) {
                $subdirPath = "$homeDir/$subdir";
                if (!is_dir($subdirPath)) {
                    mkdir($subdirPath, 0755, true);
                }
                chown($subdirPath, $username);
                chgrp($subdirPath, $username);
                if (is_dir($subdirPath)) {
                    exec("chown -R " . escapeshellarg($username) . ":" . escapeshellarg($username) . " " . escapeshellarg($subdirPath) . " 2>&1");
                }
            }

            // Fix ownership of registry files
            $filesToFix = ['.wordpress_sites', '.domains', '.redis_credentials'];
            foreach ($filesToFix as $file) {
                $filePath = "$homeDir/$file";
                if (file_exists($filePath)) {
                    chown($filePath, $username);
                    chgrp($filePath, $username);
                }
            }

            // Set proper ACLs for nginx access
            exec("setfacl -m u:www-data:x " . escapeshellarg($homeDir) . " 2>/dev/null");
            if (is_dir("$homeDir/domains")) {
                exec("setfacl -m u:www-data:x " . escapeshellarg("$homeDir/domains") . " 2>/dev/null");
                exec("setfacl -R -m u:www-data:rx " . escapeshellarg("$homeDir/domains") . " 2>/dev/null");
                exec("setfacl -R -d -m u:www-data:rx " . escapeshellarg("$homeDir/domains") . " 2>/dev/null");
            }

            logger("Fixed ownership for restored user: $username");
        }

        // Cleanup temp directory
        exec("rm -rf " . escapeshellarg($tempDir));

        // Log successful restore summary
        $summary = [];
        if (!empty($restored['files'])) $summary[] = count($restored['files']) . ' domain(s)';
        if (!empty($restored['databases'])) $summary[] = count($restored['databases']) . ' database(s)';
        if (!empty($restored['mailboxes'])) $summary[] = count($restored['mailboxes']) . ' mailbox(es)';
        if (!empty($restored['ssl_certificates'])) $summary[] = count($restored['ssl_certificates']) . ' SSL cert(s)';
        if (!empty($restored['dns_zones'])) $summary[] = count($restored['dns_zones']) . ' DNS zone(s)';
        if ($restored['mysql_users'] ?? false) $summary[] = 'MySQL users';

        if (!empty($summary)) {
            logger("Restore completed for user $username: " . implode(', ', $summary));
        } else {
            logger("Restore completed for user $username: nothing was restored");
        }

        return [
            'success' => true,
            'restored' => $restored,
            'files_count' => count($restored['files']),
            'databases_count' => count($restored['databases']),
            'mailboxes_count' => count($restored['mailboxes']),
            'ssl_certificates_count' => count($restored['ssl_certificates']),
            'dns_zones_count' => count($restored['dns_zones'] ?? []),
        ];

    } catch (Exception $e) {
        exec("rm -rf " . escapeshellarg($tempDir));
        return ['success' => false, 'error' => $e->getMessage()];
    }
}

/**
 * List backups for a user.
 */
function backupList(array $params): array
{
    $username = $params['username'] ?? '';
    $path = $params['path'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $backupDir = !empty($path) ? $path : "/home/$username/backups";

    if (!is_dir($backupDir)) {
        return ['success' => true, 'backups' => []];
    }

    $backups = [];
    foreach (scandir($backupDir) as $item) {
        if ($item === '.' || $item === '..') continue;

        $itemPath = "$backupDir/$item";

        // Check for backup files or directories
        if (is_file($itemPath) && preg_match('/\.(tar\.gz|tgz)$/i', $item)) {
            $manifest = null;

            // Try to read manifest from archive
            $output = [];
            exec("tar -I pigz -tf " . escapeshellarg($itemPath) . " 2>/dev/null | grep manifest.json | head -1", $output);
            if (!empty($output)) {
                $manifestContent = [];
                exec("tar -I pigz -xf " . escapeshellarg($itemPath) . " -O " . escapeshellarg(trim($output[0])) . " 2>/dev/null", $manifestContent);
                if (!empty($manifestContent)) {
                    $manifest = json_decode(implode("\n", $manifestContent), true);
                }
            }

            $backups[] = [
                'name' => $item,
                'path' => $itemPath,
                'type' => 'full',
                'size' => filesize($itemPath),
                'created_at' => date('c', filemtime($itemPath)),
                'manifest' => $manifest,
            ];
        } elseif (is_dir($itemPath) && file_exists("$itemPath/manifest.json")) {
            $manifest = json_decode(file_get_contents("$itemPath/manifest.json"), true);

            // Calculate directory size
            $size = 0;
            $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($itemPath));
            foreach ($iterator as $file) {
                if ($file->isFile()) {
                    $size += $file->getSize();
                }
            }

            $backups[] = [
                'name' => $item,
                'path' => $itemPath,
                'type' => 'incremental',
                'size' => $size,
                'created_at' => $manifest['created_at'] ?? date('c', filemtime($itemPath)),
                'manifest' => $manifest,
            ];
        }
    }

    // Sort by creation date, newest first
    usort($backups, fn($a, $b) => strtotime($b['created_at']) - strtotime($a['created_at']));

    return ['success' => true, 'backups' => $backups];
}

/**
 * Delete a backup file or directory.
 */
function backupDelete(array $params): array
{
    $username = $params['username'] ?? '';
    $backupPath = $params['backup_path'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    if (empty($backupPath)) {
        return ['success' => false, 'error' => 'Backup path is required'];
    }

    // Security: Ensure path is within user's backup directory
    $homeDir = "/home/$username";
    $realPath = realpath($backupPath);

    if ($realPath === false || strpos($realPath, $homeDir) !== 0) {
        return ['success' => false, 'error' => 'Invalid backup path'];
    }

    if (!file_exists($backupPath)) {
        return ['success' => false, 'error' => 'Backup not found'];
    }

    if (is_file($backupPath)) {
        unlink($backupPath);
    } else {
        exec("rm -rf " . escapeshellarg($backupPath));
    }

    return ['success' => true, 'message' => 'Backup deleted'];
}

/**
 * Delete a server backup file.
 */
function backupDeleteServer(array $params): array
{
    $backupPath = $params['backup_path'] ?? '';

    if (empty($backupPath)) {
        return ['success' => false, 'error' => 'Backup path is required'];
    }

    // Security: Ensure path is within server backup directory
    $allowedDirs = ['/var/backups/jabali'];
    $realPath = realpath($backupPath);

    if ($realPath === false) {
        return ['success' => false, 'error' => 'Backup not found'];
    }

    $allowed = false;
    foreach ($allowedDirs as $dir) {
        if (strpos($realPath, $dir) === 0) {
            $allowed = true;
            break;
        }
    }

    if (!$allowed) {
        return ['success' => false, 'error' => 'Invalid backup path - must be in /var/backups/jabali/'];
    }

    if (!file_exists($backupPath)) {
        return ['success' => false, 'error' => 'Backup not found'];
    }

    if (is_file($backupPath)) {
        unlink($backupPath);
    } else {
        exec("rm -rf " . escapeshellarg($backupPath));
    }

    return ['success' => true, 'message' => 'Server backup deleted'];
}

/**
 * Verify backup integrity.
 */
function backupVerify(array $params): array
{
    $backupPath = $params['backup_path'] ?? '';

    if (empty($backupPath) || !file_exists($backupPath)) {
        return ['success' => false, 'error' => 'Backup not found'];
    }

    $issues = [];

    if (is_file($backupPath)) {
        // Verify tar.gz archive
        exec("tar -I pigz -tf " . escapeshellarg($backupPath) . " > /dev/null 2>&1", $output, $retval);
        if ($retval !== 0) {
            $issues[] = 'Archive is corrupted or invalid';
        }

        // Check for manifest
        $output = [];
        exec("tar -I pigz -tf " . escapeshellarg($backupPath) . " 2>/dev/null | grep manifest.json", $output);
        if (empty($output)) {
            $issues[] = 'Manifest file not found in archive';
        }
    } elseif (is_dir($backupPath)) {
        if (!file_exists("$backupPath/manifest.json")) {
            $issues[] = 'Manifest file not found';
        }
    }

    return [
        'success' => empty($issues),
        'valid' => empty($issues),
        'issues' => $issues,
        'checksum' => is_file($backupPath) ? hash_file('sha256', $backupPath) : null,
    ];
}

/**
 * Get backup info/manifest.
 */
function backupGetInfo(array $params): array
{
    $backupPath = $params['backup_path'] ?? '';

    if (empty($backupPath) || !file_exists($backupPath)) {
        return ['success' => false, 'error' => 'Backup not found'];
    }

    $manifest = null;
    $size = 0;

    if (is_file($backupPath)) {
        $size = filesize($backupPath);

        // Extract manifest from archive
        $output = [];
        exec("tar -I pigz -tf " . escapeshellarg($backupPath) . " 2>/dev/null | grep manifest.json | head -1", $output);
        if (!empty($output)) {
            $manifestContent = [];
            exec("tar -I pigz -xf " . escapeshellarg($backupPath) . " -O " . escapeshellarg(trim($output[0])) . " 2>/dev/null", $manifestContent);
            if (!empty($manifestContent)) {
                $manifest = json_decode(implode("\n", $manifestContent), true);
            }
        }
    } elseif (is_dir($backupPath)) {
        if (file_exists("$backupPath/manifest.json")) {
            $manifest = json_decode(file_get_contents("$backupPath/manifest.json"), true);
        }

        $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($backupPath));
        foreach ($iterator as $file) {
            if ($file->isFile()) {
                $size += $file->getSize();
            }
        }
    }

    return [
        'success' => true,
        'path' => $backupPath,
        'size' => $size,
        'manifest' => $manifest,
        'type' => is_file($backupPath) ? 'full' : 'incremental',
        'modified_at' => date('c', filemtime($backupPath)),
    ];
}

/**
 * Upload backup to remote destination (SFTP, NFS, S3).
 * For incremental backups with SFTP/NFS, uses rsync with --link-dest for dirvish-style snapshots.
 */
function backupUploadRemote(array $params): array
{
    $localPath = $params['local_path'] ?? '';
    $destination = $params['destination'] ?? [];
    $backupType = $params['backup_type'] ?? 'full';

    if (empty($localPath) || !file_exists($localPath)) {
        return ['success' => false, 'error' => 'Local backup file not found'];
    }

    $type = $destination['type'] ?? '';
    $remotePath = $destination['path'] ?? '';
    $filename = basename($localPath);

    // For incremental backups with SFTP or NFS, use rsync with --link-dest
    if ($backupType === 'incremental' && in_array($type, ['sftp', 'nfs'])) {
        return syncViaRsyncIncremental($localPath, $destination);
    }

    switch ($type) {
        case 'sftp':
            return uploadViaSftp($localPath, $destination);

        case 'nfs':
            return uploadViaNfs($localPath, $destination);

        case 's3':
            return uploadViaS3($localPath, $destination);

        default:
            return ['success' => false, 'error' => 'Unsupported destination type: ' . $type];
    }
}

/**
 * Upload via SFTP.
 */
function uploadViaSftp(string $localPath, array $destination): array
{
    $host = $destination['host'] ?? '';
    $port = $destination['port'] ?? 22;
    $username = $destination['username'] ?? '';
    $password = $destination['password'] ?? '';
    $privateKey = $destination['private_key'] ?? '';
    $remotePath = rtrim($destination['path'] ?? '/backups', '/');
    $localName = basename($localPath);

    if (empty($host) || empty($username)) {
        return ['success' => false, 'error' => 'SFTP host and username required'];
    }

    $isDir = is_dir($localPath);
    $remoteFullPath = "$remotePath/$localName";

    // Build sftp commands
    $sftpCommands = "mkdir $remotePath\n";

    if ($isDir) {
        // Upload folder: create remote folder and upload all files
        $sftpCommands .= "mkdir $remoteFullPath\n";
        $files = scandir($localPath);
        foreach ($files as $file) {
            if ($file === '.' || $file === '..') continue;
            $filePath = "$localPath/$file";
            if (is_file($filePath)) {
                $sftpCommands .= "put " . escapeshellarg($filePath) . " " . escapeshellarg("$remoteFullPath/$file") . "\n";
            }
        }
    } else {
        // Upload single file
        $sftpCommands .= "put " . escapeshellarg($localPath) . " " . escapeshellarg($remoteFullPath) . "\n";
    }

    if (!empty($privateKey)) {
        $keyFile = tempnam(sys_get_temp_dir(), 'sftp_key_');
        file_put_contents($keyFile, $privateKey);
        chmod($keyFile, 0600);

        $cmd = "sftp -o StrictHostKeyChecking=no -o Port=$port -i " . escapeshellarg($keyFile) .
               " " . escapeshellarg("$username@$host") . " 2>&1 <<EOF\n{$sftpCommands}EOF";

        exec($cmd, $output, $retval);
        unlink($keyFile);
    } else {
        $cmd = "sshpass -p " . escapeshellarg($password) .
               " sftp -o StrictHostKeyChecking=no -o Port=$port " .
               escapeshellarg("$username@$host") . " 2>&1 <<EOF\n{$sftpCommands}EOF";

        exec($cmd, $output, $retval);
    }

    if ($retval !== 0) {
        return ['success' => false, 'error' => 'SFTP upload failed: ' . implode("\n", $output)];
    }

    return [
        'success' => true,
        'remote_path' => $remoteFullPath,
        'message' => $isDir ? 'Backup folder uploaded via SFTP' : 'Backup uploaded via SFTP',
    ];
}

/**
 * Dirvish-style incremental backup using rsync with --link-dest.
 *
 * Structure: /remote_path/YYYY-MM-DD_HHMMSS/
 * Uses hard links from previous backup to save space for unchanged files.
 *
 * For SFTP: Uses rsync over SSH
 * For NFS: Uses rsync locally to mounted share
 */
function syncViaRsyncIncremental(string $localPath, array $destination): array
{
    $type = $destination['type'] ?? 'sftp';
    $host = $destination['host'] ?? '';
    $port = $destination['port'] ?? 22;
    $username = $destination['username'] ?? '';
    $password = $destination['password'] ?? '';
    $privateKey = $destination['private_key'] ?? '';
    $remotePath = rtrim($destination['path'] ?? '/backups', '/');

    // Create dated backup folder name
    $timestamp = date('Y-m-d_His');
    $backupFolder = "$remotePath/$timestamp";

    if ($type === 'sftp') {
        if (empty($host) || empty($username)) {
            return ['success' => false, 'error' => 'SFTP host and username required for rsync'];
        }

        // SSH options
        $sshOpts = "-o StrictHostKeyChecking=no -o Port=$port";
        $keyFile = null;

        if (!empty($privateKey)) {
            $keyFile = tempnam(sys_get_temp_dir(), 'rsync_key_');
            file_put_contents($keyFile, $privateKey);
            chmod($keyFile, 0600);
            $sshOpts .= " -i " . escapeshellarg($keyFile);
        }

        // Find previous backup folder for --link-dest via SSH
        $findPrevCmd = "ssh $sshOpts ";
        if (!empty($password)) {
            $findPrevCmd = "sshpass -p " . escapeshellarg($password) . " $findPrevCmd";
        }
        $findPrevCmd .= escapeshellarg("$username@$host") . " 'ls -1d " . escapeshellarg($remotePath) . "/????-??-??_?????? 2>/dev/null | sort -r | head -1'";

        $previousBackup = trim(shell_exec($findPrevCmd) ?? '');

        // rsync options
        $rsyncOpts = "-avz --delete";

        // Add --link-dest if previous backup exists
        if (!empty($previousBackup) && $previousBackup !== $backupFolder) {
            $rsyncOpts .= " --link-dest=" . escapeshellarg($previousBackup);
        }

        // Source path (trailing slash to copy contents)
        $source = is_dir($localPath) ? rtrim($localPath, '/') . '/' : $localPath;

        // Build rsync command
        if (!empty($password)) {
            $cmd = "sshpass -p " . escapeshellarg($password) .
                   " rsync $rsyncOpts -e 'ssh $sshOpts' " .
                   escapeshellarg($source) . " " .
                   escapeshellarg("$username@$host:$backupFolder/") . " 2>&1";
        } else {
            $cmd = "rsync $rsyncOpts -e 'ssh $sshOpts' " .
                   escapeshellarg($source) . " " .
                   escapeshellarg("$username@$host:$backupFolder/") . " 2>&1";
        }

        exec($cmd, $output, $retval);

        // Cleanup key file
        if ($keyFile && file_exists($keyFile)) {
            unlink($keyFile);
        }

    } else if ($type === 'nfs') {
        // NFS: Mount first, then rsync locally
        $server = $destination['server'] ?? '';
        $share = $destination['share'] ?? '';
        $mountPoint = $destination['mount_point'] ?? '/mnt/backup_nfs';

        if (empty($server) || empty($share)) {
            return ['success' => false, 'error' => 'NFS server and share required'];
        }

        // Ensure mount point exists and is mounted
        if (!is_dir($mountPoint)) {
            mkdir($mountPoint, 0755, true);
        }

        exec("mount | grep " . escapeshellarg($mountPoint), $mountOutput, $mountCheck);
        if ($mountCheck !== 0) {
            exec("mount -t nfs " . escapeshellarg("$server:$share") . " " . escapeshellarg($mountPoint) . " 2>&1", $mountOutput, $mountRet);
            if ($mountRet !== 0) {
                return ['success' => false, 'error' => 'Failed to mount NFS share: ' . implode("\n", $mountOutput)];
            }
        }

        $nfsBackupPath = rtrim($mountPoint, '/') . '/' . ltrim($remotePath, '/');
        $localBackupFolder = "$nfsBackupPath/$timestamp";

        // Ensure remote path exists
        if (!is_dir($nfsBackupPath)) {
            mkdir($nfsBackupPath, 0755, true);
        }

        // Find previous backup
        $prevBackups = glob("$nfsBackupPath/????-??-??_??????");
        rsort($prevBackups);
        $previousBackup = !empty($prevBackups) ? $prevBackups[0] : '';

        // rsync options
        $rsyncOpts = "-av --delete";

        if (!empty($previousBackup) && $previousBackup !== $localBackupFolder) {
            $rsyncOpts .= " --link-dest=" . escapeshellarg($previousBackup);
        }

        $source = is_dir($localPath) ? rtrim($localPath, '/') . '/' : $localPath;
        $cmd = "rsync $rsyncOpts " . escapeshellarg($source) . " " . escapeshellarg("$localBackupFolder/") . " 2>&1";

        exec($cmd, $output, $retval);
        $backupFolder = "$remotePath/$timestamp";
    } else {
        return ['success' => false, 'error' => 'Rsync incremental only supported for SFTP and NFS'];
    }

    if ($retval !== 0) {
        return ['success' => false, 'error' => 'Rsync incremental backup failed: ' . implode("\n", $output)];
    }

    return [
        'success' => true,
        'remote_path' => $backupFolder,
        'message' => 'Incremental backup completed using rsync with hard links',
        'type' => 'incremental',
        'previous_backup' => $previousBackup ?? null,
    ];
}

/**
 * Dirvish-style server backup - rsync directly from user files to remote.
 * Creates timestamped folders with hard links to previous backup.
 *
 * Structure on remote:
 *   /backups/2024-01-12_103000/
 *     user1/
 *       domains/
 *       databases/
 *       mailboxes/
 *     user2/
 *       ...
 *   /backups/2024-01-13_103000/
 *     user1/  <- unchanged files are hard-linked to previous
 *     ...
 */
function backupServerIncrementalDirect(array $params): array
{
    $destination = $params['destination'] ?? [];
    $users = $params['users'] ?? null; // null = all users
    $includeFiles = $params['include_files'] ?? true;
    $includeDatabases = $params['include_databases'] ?? true;
    $includeMailboxes = $params['include_mailboxes'] ?? true;
    $includeDns = $params['include_dns'] ?? true;

    logger("backupServerIncrementalDirect started");
    logger("Destination config: " . json_encode($destination));
    logger("Users param: " . json_encode($users));
    logger("Include files: " . ($includeFiles ? 'yes' : 'no'));

    $type = $destination['type'] ?? '';
    if (!in_array($type, ['sftp', 'nfs'])) {
        return ['success' => false, 'error' => 'Incremental backups only supported for SFTP and NFS destinations'];
    }

    $host = $destination['host'] ?? '';
    $port = $destination['port'] ?? 22;
    $username = $destination['username'] ?? '';
    $password = $destination['password'] ?? '';
    $privateKey = $destination['private_key'] ?? '';
    $remotePath = rtrim($destination['path'] ?? '/backups', '/');

    // Create timestamped backup folder
    $timestamp = date('Y-m-d_His');
    $backupFolder = "$remotePath/$timestamp";

    // Get list of users to backup
    if ($users === null) {
        // Get all non-system users (UID >= 1000)
        $users = [];
        foreach (scandir('/home') as $dir) {
            if ($dir === '.' || $dir === '..') continue;
            $userInfo = posix_getpwnam($dir);
            if ($userInfo && $userInfo['uid'] >= 1000) {
                $users[] = $dir;
            }
        }
    }

    if (empty($users)) {
        return ['success' => false, 'error' => 'No users to backup'];
    }

    $keyFile = null;
    $sshOpts = "-o StrictHostKeyChecking=no";

    if ($type === 'sftp') {
        if (empty($host) || empty($username)) {
            return ['success' => false, 'error' => 'SFTP host and username required'];
        }

        $sshOpts .= " -o Port=$port";

        if (!empty($privateKey)) {
            $keyFile = tempnam(sys_get_temp_dir(), 'rsync_key_');
            file_put_contents($keyFile, $privateKey);
            chmod($keyFile, 0600);
            $sshOpts .= " -i " . escapeshellarg($keyFile);
            $sshOpts .= " -o BatchMode=yes"; // Only use BatchMode with key auth
        } elseif (!empty($password)) {
            // For password auth, disable pubkey and enable password explicitly
            $sshOpts .= " -o PreferredAuthentications=password -o PubkeyAuthentication=no";
        }

        // Find previous backup folder via SSH for --link-dest
        $findPrevCmd = "ssh $sshOpts ";
        if (!empty($password) && empty($privateKey)) {
            $findPrevCmd = "sshpass -p " . escapeshellarg($password) . " $findPrevCmd";
        }
        $findPrevCmd .= escapeshellarg("$username@$host") . " 'ls -1d " . escapeshellarg($remotePath) . "/????-??-??_?????? 2>/dev/null | sort -r | head -1'";
        $previousBackup = trim(shell_exec($findPrevCmd) ?? '');

        // Log whether this is initialization or incremental
        if (empty($previousBackup)) {
            logger("INITIALIZATION: Creating first full backup image at $backupFolder");
            $isInitialization = true;
        } else {
            logger("INCREMENTAL: Using hard links from previous backup $previousBackup");
            $isInitialization = false;
        }

        // Create remote backup directory
        $mkdirCmd = "ssh $sshOpts ";
        if (!empty($password) && empty($privateKey)) {
            $mkdirCmd = "sshpass -p " . escapeshellarg($password) . " $mkdirCmd";
        }
        $mkdirCmd .= escapeshellarg("$username@$host") . " 'mkdir -p " . escapeshellarg($backupFolder) . "'";
        exec($mkdirCmd);
    }

    $backedUpUsers = [];
    $errors = [];
    $totalSize = 0;

    foreach ($users as $user) {
        $userHome = "/home/$user";
        if (!is_dir($userHome)) {
            continue;
        }

        // Build list of paths to rsync for this user
        $sourcePaths = [];

        if ($includeFiles && is_dir("$userHome/domains")) {
            $sourcePaths[] = "$userHome/domains";
        }

        // Include WordPress registry file
        if ($includeFiles && file_exists("$userHome/.wordpress_sites")) {
            $sourcePaths[] = "$userHome/.wordpress_sites";
            logger("Including .wordpress_sites registry for $user");
        }

        // Include domains registry file
        if ($includeFiles && file_exists("$userHome/.domains")) {
            $sourcePaths[] = "$userHome/.domains";
            logger("Including .domains registry for $user");
        }

        // Export databases to mysql/ directory
        $tempDbDir = null;
        if ($includeDatabases) {
            $tempDbDir = "$userHome/mysql";
            @mkdir($tempDbDir, 0755, true);
            // Clean any old dumps
            array_map('unlink', glob("$tempDbDir/*.sql.gz"));

            // Get user's databases from MySQL
            $dbResult = shell_exec("mysql -N -e \"SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME LIKE '{$user}_%'\" 2>/dev/null");
            if ($dbResult) {
                $databases = array_filter(explode("\n", trim($dbResult)));
                foreach ($databases as $db) {
                    $dumpFile = "$tempDbDir/$db.sql.gz";
                    exec("mysqldump --single-transaction " . escapeshellarg($db) . " 2>/dev/null | pigz > " . escapeshellarg($dumpFile));
                    logger("Exported database $db to $dumpFile");
                }
                if (!empty($databases)) {
                    $sourcePaths[] = $tempDbDir;
                    logger("Including " . count($databases) . " database(s) for $user");
                }
            }

            // Export MySQL users and their permissions
            $mysqli = @new mysqli('localhost', 'root', '');
            if (!$mysqli->connect_error) {
                $usersFile = "$tempDbDir/users.sql";
                $usersSql = "-- MySQL Users and Permissions Backup\n";
                $usersSql .= "-- Generated: " . date('Y-m-d H:i:s') . "\n\n";

                // Get all MySQL users matching the username_ pattern
                $safePrefix = $mysqli->real_escape_string($user) . '\\_';
                $userQuery = $mysqli->query("SELECT User, Host FROM mysql.user WHERE User LIKE '{$safePrefix}%'");

                $exportedUsers = [];
                if ($userQuery) {
                    while ($userRow = $userQuery->fetch_assoc()) {
                        $mysqlUser = $userRow['User'];
                        $mysqlHost = $userRow['Host'];
                        $userIdentifier = "'{$mysqli->real_escape_string($mysqlUser)}'@'{$mysqli->real_escape_string($mysqlHost)}'";

                        // Get CREATE USER statement
                        $createResult = $mysqli->query("SHOW CREATE USER $userIdentifier");
                        if ($createResult && $createRow = $createResult->fetch_row()) {
                            // Add DROP USER IF EXISTS for clean restore
                            $usersSql .= "DROP USER IF EXISTS $userIdentifier;\n";
                            $usersSql .= $createRow[0] . ";\n";

                            // Get GRANT statements
                            $grantsResult = $mysqli->query("SHOW GRANTS FOR $userIdentifier");
                            if ($grantsResult) {
                                while ($grantRow = $grantsResult->fetch_row()) {
                                    $usersSql .= $grantRow[0] . ";\n";
                                }
                            }
                            $usersSql .= "\n";
                            $exportedUsers[] = $mysqlUser;
                        }
                    }
                }

                // Save users.sql if we have any users
                if (!empty($exportedUsers)) {
                    $usersSql .= "FLUSH PRIVILEGES;\n";
                    file_put_contents($usersFile, $usersSql);
                    logger("Exported " . count($exportedUsers) . " MySQL user(s) for $user");
                }

                $mysqli->close();
            }
        }

        // Export DNS zones to zones/ directory
        $tempZonesDir = null;
        if ($includeDns && is_dir('/etc/bind/zones')) {
            $domainsFile = "$userHome/.domains";
            if (file_exists($domainsFile)) {
                $domains = json_decode(file_get_contents($domainsFile), true) ?: [];
                $userDomains = array_keys($domains);

                if (!empty($userDomains)) {
                    $tempZonesDir = "$userHome/zones";
                    @mkdir($tempZonesDir, 0755, true);
                    // Clean any old zone files
                    array_map('unlink', glob("$tempZonesDir/db.*"));

                    foreach ($userDomains as $domain) {
                        $zoneFile = "/etc/bind/zones/db.$domain";
                        if (file_exists($zoneFile)) {
                            copy($zoneFile, "$tempZonesDir/db.$domain");
                            logger("Copied DNS zone for $domain");
                        }
                    }

                    if (count(glob("$tempZonesDir/db.*")) > 0) {
                        $sourcePaths[] = $tempZonesDir;
                        logger("Including DNS zones for $user");
                    }
                }
            }
        }

        if ($includeMailboxes && is_dir("$userHome/mail")) {
            // Include user's mail directory (contains all mailboxes)
            $sourcePaths[] = "$userHome/mail";
            logger("Including mail directory for $user");
        }

        // Include SSL certificates (both custom and Let's Encrypt)
        $tempSslDir = "$userHome/ssl";
        $sslIncluded = false;

        // Create ssl directory if needed
        if (!is_dir($tempSslDir)) {
            @mkdir($tempSslDir, 0755, true);
        }

        // Copy Let's Encrypt certificates for user's domains
        if (is_dir('/etc/letsencrypt/live')) {
            $domainsFile = "$userHome/.domains";
            if (file_exists($domainsFile)) {
                $domains = json_decode(file_get_contents($domainsFile), true) ?: [];
                foreach (array_keys($domains) as $domain) {
                    $certPath = "/etc/letsencrypt/live/$domain";
                    if (is_dir($certPath)) {
                        $domainSslDir = "$tempSslDir/$domain";
                        @mkdir($domainSslDir, 0755, true);

                        $certFiles = ['fullchain.pem', 'privkey.pem', 'cert.pem', 'chain.pem'];
                        foreach ($certFiles as $certFile) {
                            $srcFile = "$certPath/$certFile";
                            if (file_exists($srcFile)) {
                                $realPath = realpath($srcFile);
                                if ($realPath && file_exists($realPath)) {
                                    copy($realPath, "$domainSslDir/$certFile");
                                }
                            }
                        }
                        logger("Copied SSL certificate for $domain");
                        $sslIncluded = true;
                    }
                }
            }
        }

        // Include ssl directory if it has content
        if (is_dir($tempSslDir) && count(scandir($tempSslDir)) > 2) {
            $sourcePaths[] = $tempSslDir;
            logger("Including SSL certificates for $user");
        }

        // Include user mail directory if exists
        if ($includeMailboxes && is_dir("$userHome/mail")) {
            $sourcePaths[] = "$userHome/mail";
            logger("Including local mail for $user");
        }

        // Export panel data (database records) to metadata/panel_data.json
        $metadataDir = "$userHome/metadata";
        @mkdir($metadataDir, 0755, true);
        $panelData = exportUserPanelData($user);
        if (!empty($panelData['domains']) || !empty($panelData['email_domains'])) {
            file_put_contents("$metadataDir/panel_data.json", json_encode($panelData, JSON_PRETTY_PRINT));
            $sourcePaths[] = $metadataDir;
            logger("Including panel metadata for $user");
        }

        if (empty($sourcePaths)) {
            logger("User $user has no source paths to backup, skipping");
            continue;
        }

        logger("User $user source paths: " . json_encode($sourcePaths));

        // Build rsync command with exclusions
        $excludes = [
            'backups',
            '*.log',
            'cache',
            '.cache',
            'tmp',
            '.tmp',
            'node_modules',
        ];
        $excludeArgs = implode(' ', array_map(fn($e) => "--exclude=" . escapeshellarg($e), $excludes));
        $rsyncOpts = "-avz --relative --delete $excludeArgs";

        // Add --link-dest for incremental
        if (!empty($previousBackup)) {
            $rsyncOpts .= " --link-dest=" . escapeshellarg("$previousBackup/$user");
        }

        // Remote destination for this user
        $userRemotePath = "$backupFolder/$user/";

        if ($type === 'sftp') {
            $sshCmd = "ssh $sshOpts";
            if (!empty($password) && empty($privateKey)) {
                $rsyncPrefix = "sshpass -p " . escapeshellarg($password);
            } else {
                $rsyncPrefix = "";
            }

            // Rsync all source paths
            $userSyncSuccess = false;
            foreach ($sourcePaths as $srcPath) {
                $cmd = trim("$rsyncPrefix rsync $rsyncOpts -e '$sshCmd' " .
                       escapeshellarg($srcPath) . " " .
                       escapeshellarg("$username@$host:$userRemotePath") . " 2>&1");

                logger("Executing rsync: $cmd");
                $output = [];
                exec($cmd, $output, $retval);
                logger("Rsync result for $srcPath: retval=$retval, output=" . implode("\n", $output));

                if ($retval === 0 || $retval === 24) { // 24 = some files vanished (ok)
                    $userSyncSuccess = true;
                } else {
                    $errors[] = "Failed to rsync $srcPath for $user: " . implode("\n", $output);
                }
            }

            if (!$userSyncSuccess) {
                // Cleanup database exports before continuing
                if ($tempDbDir && is_dir($tempDbDir)) {
                    array_map('unlink', glob("$tempDbDir/*.sql.gz"));
                }
                continue; // Don't count this user as backed up
            }
        } else if ($type === 'nfs') {
            $server = $destination['server'] ?? '';
            $share = $destination['share'] ?? '';
            $mountPoint = $destination['mount_point'] ?? '/mnt/backup_nfs';

            // Mount NFS if needed
            if (!is_dir($mountPoint)) {
                mkdir($mountPoint, 0755, true);
            }
            exec("mount | grep " . escapeshellarg($mountPoint), $mountOutput, $mountCheck);
            if ($mountCheck !== 0) {
                exec("mount -t nfs " . escapeshellarg("$server:$share") . " " . escapeshellarg($mountPoint) . " 2>&1", $mountOutput, $mountRet);
                if ($mountRet !== 0) {
                    return ['success' => false, 'error' => 'Failed to mount NFS share'];
                }
            }

            $localBackupPath = "$mountPoint/" . ltrim($backupFolder, '/') . "/$user/";
            mkdir($localBackupPath, 0755, true);

            // Find previous backup for link-dest
            $prevBackups = glob("$mountPoint/" . ltrim($remotePath, '/') . "/????-??-??_??????");
            rsort($prevBackups);
            $previousBackup = !empty($prevBackups) ? $prevBackups[0] : '';

            // Track initialization status for the overall backup
            if (!isset($isInitialization)) {
                $isInitialization = empty($previousBackup);
                if ($isInitialization) {
                    logger("INITIALIZATION (NFS): Creating first full backup image at $backupFolder");
                } else {
                    logger("INCREMENTAL (NFS): Using hard links from previous backup $previousBackup");
                }
            }

            $rsyncOpts = "-av --relative --delete";
            if (!empty($previousBackup)) {
                $rsyncOpts .= " --link-dest=" . escapeshellarg("$previousBackup/$user");
            }

            $userSyncSuccess = false;
            foreach ($sourcePaths as $srcPath) {
                $cmd = "rsync $rsyncOpts " . escapeshellarg($srcPath) . " " . escapeshellarg($localBackupPath) . " 2>&1";
                $output = [];
                exec($cmd, $output, $retval);

                if ($retval === 0 || $retval === 24) {
                    $userSyncSuccess = true;
                } else {
                    $errors[] = "Failed to rsync $srcPath for $user: " . implode("\n", $output);
                }
            }

            if (!$userSyncSuccess) {
                if ($tempDbDir && is_dir($tempDbDir)) {
                    exec("rm -rf " . escapeshellarg($tempDbDir));
                }
                continue;
            }
        }

        // Cleanup database exports after successful backup
        if ($tempDbDir && is_dir($tempDbDir)) {
            array_map('unlink', glob("$tempDbDir/*.sql.gz"));
        }

        $backedUpUsers[] = $user;
    }

    // Cleanup key file
    if ($keyFile && file_exists($keyFile)) {
        unlink($keyFile);
    }

    // Export DNS zones if requested
    if ($includeDns && is_dir('/etc/bind/zones')) {
        // TODO: rsync DNS zones to remote
    }

    if (empty($backedUpUsers)) {
        return ['success' => false, 'error' => 'No users backed up. Errors: ' . implode('; ', $errors)];
    }

    // Calculate total backup size
    // Note: $backupFolder already contains the full path: "$remotePath/$timestamp"
    $totalSize = 0;
    if ($type === 'sftp') {
        // For SFTP, run du on the remote server
        $sshCmd = "ssh $sshOpts " . escapeshellarg("$username@$host") . " " .
                  escapeshellarg("du -sb " . $backupFolder . " 2>/dev/null | cut -f1");
        if (!empty($password) && empty($privateKey)) {
            $sshCmd = "sshpass -p " . escapeshellarg($password) . " $sshCmd";
        }
        logger("Calculating size with: $sshCmd");
        exec($sshCmd, $sizeOutput, $sizeRet);
        logger("Size result: ret=$sizeRet, output=" . json_encode($sizeOutput));
        if ($sizeRet === 0 && !empty($sizeOutput[0])) {
            $totalSize = (int)$sizeOutput[0];
        }
    } elseif ($type === 'nfs') {
        // For NFS, the backup is on the mounted filesystem
        $mountPoint = $destination['mount_point'] ?? '/mnt/backup_nfs';
        // $backupFolder is like "/backups/2026-01-20_123456", strip leading slash for mount path
        $backupPath = "$mountPoint/" . ltrim($backupFolder, '/');
        logger("Calculating NFS size at: $backupPath");
        if (is_dir($backupPath)) {
            exec("du -sb " . escapeshellarg($backupPath) . " 2>/dev/null | cut -f1", $sizeOutput);
            $totalSize = (int)($sizeOutput[0] ?? 0);
        }
    }

    return [
        'success' => true,
        'remote_path' => $backupFolder,
        'users' => $backedUpUsers,
        'user_count' => count($backedUpUsers),
        'size' => $totalSize,
        'type' => 'incremental',
        'is_initialization' => $isInitialization ?? empty($previousBackup),
        'previous_backup' => $previousBackup ?? null,
        'errors' => $errors,
        'message' => ($isInitialization ?? empty($previousBackup))
            ? 'Initial full backup image created'
            : 'Incremental backup completed with hard links to previous backup',
    ];
}

/**
 * Upload via NFS mount.
 */
function uploadViaNfs(string $localPath, array $destination): array
{
    $server = $destination['server'] ?? '';
    $share = $destination['share'] ?? '';
    $mountPoint = $destination['mount_point'] ?? '/mnt/backup_nfs';
    $remotePath = rtrim($destination['path'] ?? '', '/');
    $filename = basename($localPath);

    if (empty($server) || empty($share)) {
        return ['success' => false, 'error' => 'NFS server and share required'];
    }

    // Create mount point if not exists
    if (!is_dir($mountPoint)) {
        mkdir($mountPoint, 0755, true);
    }

    // Check if already mounted
    $mounted = false;
    exec("mount | grep " . escapeshellarg($mountPoint), $output, $retval);
    if ($retval !== 0) {
        // Mount NFS share
        exec("mount -t nfs " . escapeshellarg("$server:$share") . " " . escapeshellarg($mountPoint) . " 2>&1", $output, $retval);
        if ($retval !== 0) {
            return ['success' => false, 'error' => 'Failed to mount NFS share: ' . implode("\n", $output)];
        }
    }

    // Create target directory
    $targetDir = "$mountPoint/$remotePath";
    if (!is_dir($targetDir)) {
        mkdir($targetDir, 0755, true);
    }

    // Copy file
    $targetPath = "$targetDir/$filename";
    if (!copy($localPath, $targetPath)) {
        return ['success' => false, 'error' => 'Failed to copy file to NFS share'];
    }

    return [
        'success' => true,
        'remote_path' => "$remotePath/$filename",
        'message' => 'Backup uploaded via NFS',
    ];
}

/**
 * Upload via S3-compatible storage.
 */
function uploadViaS3(string $localPath, array $destination): array
{
    $endpoint = $destination['endpoint'] ?? '';
    $bucket = $destination['bucket'] ?? '';
    $accessKey = $destination['access_key'] ?? '';
    $secretKey = $destination['secret_key'] ?? '';
    $region = $destination['region'] ?? 'us-east-1';
    $remotePath = rtrim($destination['path'] ?? '', '/');
    $filename = basename($localPath);

    if (empty($bucket) || empty($accessKey) || empty($secretKey)) {
        return ['success' => false, 'error' => 'S3 bucket and credentials required'];
    }

    // Use aws cli or s3cmd
    $s3Path = "s3://$bucket/$remotePath/$filename";

    $envVars = "AWS_ACCESS_KEY_ID=" . escapeshellarg($accessKey) . " " .
               "AWS_SECRET_ACCESS_KEY=" . escapeshellarg($secretKey);

    $endpointArg = !empty($endpoint) ? "--endpoint-url " . escapeshellarg($endpoint) : '';
    $regionArg = "--region " . escapeshellarg($region);

    $cmd = "$envVars aws s3 cp " . escapeshellarg($localPath) . " " .
           escapeshellarg($s3Path) . " $endpointArg $regionArg 2>&1";

    exec($cmd, $output, $retval);

    if ($retval !== 0) {
        return ['success' => false, 'error' => 'S3 upload failed: ' . implode("\n", $output)];
    }

    return [
        'success' => true,
        'remote_path' => "$remotePath/$filename",
        'bucket' => $bucket,
        'message' => 'Backup uploaded to S3',
    ];
}

/**
 * Download backup from remote destination.
 */
function backupDownloadRemote(array $params): array
{
    $remotePath = $params['remote_path'] ?? '';
    $localPath = $params['local_path'] ?? '';
    $destination = $params['destination'] ?? [];

    if (empty($remotePath) || empty($localPath)) {
        return ['success' => false, 'error' => 'Remote and local paths required'];
    }

    $type = $destination['type'] ?? '';

    switch ($type) {
        case 'sftp':
            return downloadViaSftp($remotePath, $localPath, $destination);

        case 'nfs':
            return downloadViaNfs($remotePath, $localPath, $destination);

        case 's3':
            return downloadViaS3($remotePath, $localPath, $destination);

        default:
            return ['success' => false, 'error' => 'Unsupported destination type: ' . $type];
    }
}

/**
 * Download via SFTP using rsync (handles both files and directories).
 */
function downloadViaSftp(string $remotePath, string $localPath, array $destination): array
{
    $host = $destination['host'] ?? '';
    $port = $destination['port'] ?? 22;
    $username = $destination['username'] ?? '';
    $password = $destination['password'] ?? '';
    $privateKey = $destination['private_key'] ?? '';
    $basePath = rtrim($destination['path'] ?? '', '/');

    // Check if remotePath is already an absolute path (starts with basePath or /)
    // This handles backup records that store full paths like /home/jabali/backups/2026-01-19_210219
    if (str_starts_with($remotePath, $basePath . '/') || (str_starts_with($remotePath, '/') && !empty($basePath) && str_starts_with($remotePath, $basePath))) {
        $fullRemotePath = $remotePath;
    } elseif (str_starts_with($remotePath, '/') && empty($basePath)) {
        $fullRemotePath = $remotePath;
    } else {
        $fullRemotePath = "$basePath/$remotePath";
    }

    // Ensure local directory exists
    if (!is_dir($localPath)) {
        mkdir($localPath, 0755, true);
    }

    // Use rsync for downloading - handles both files and directories
    // Add trailing slash to remote to copy contents into local dir
    $fullRemotePath = rtrim($fullRemotePath, '/') . '/';

    if (!empty($privateKey)) {
        $keyFile = tempnam(sys_get_temp_dir(), 'sftp_key_');
        file_put_contents($keyFile, $privateKey);
        chmod($keyFile, 0600);

        $sshCmd = "ssh -o StrictHostKeyChecking=no -o Port=$port -i " . escapeshellarg($keyFile);
        $cmd = "rsync -avz -e " . escapeshellarg($sshCmd) . " " .
               escapeshellarg("$username@$host:$fullRemotePath") . " " .
               escapeshellarg(rtrim($localPath, '/') . '/') . " 2>&1";

        logger("Executing rsync download: $cmd");
        exec($cmd, $output, $retval);
        unlink($keyFile);
    } else {
        $sshCmd = "ssh -o StrictHostKeyChecking=no -o Port=$port -o PreferredAuthentications=password -o PubkeyAuthentication=no";
        $cmd = "sshpass -p " . escapeshellarg($password) .
               " rsync -avz -e " . escapeshellarg($sshCmd) . " " .
               escapeshellarg("$username@$host:$fullRemotePath") . " " .
               escapeshellarg(rtrim($localPath, '/') . '/') . " 2>&1";

        logger("Executing rsync download: $cmd");
        exec($cmd, $output, $retval);
    }

    logger("Rsync download result: retval=$retval, lines=" . count($output));

    if ($retval !== 0) {
        return ['success' => false, 'error' => 'SFTP/rsync download failed: ' . implode("\n", $output)];
    }

    // Check if anything was downloaded
    $files = glob(rtrim($localPath, '/') . '/*');
    if (empty($files)) {
        return ['success' => false, 'error' => 'Download completed but no files found'];
    }

    return ['success' => true, 'local_path' => $localPath];
}

/**
 * Download via NFS (handles both files and directories).
 */
function downloadViaNfs(string $remotePath, string $localPath, array $destination): array
{
    $server = $destination['server'] ?? '';
    $share = $destination['share'] ?? '';
    $mountPoint = $destination['mount_point'] ?? '/mnt/backup_nfs';
    $basePath = rtrim($destination['path'] ?? '', '/');

    // Mount if needed
    if (!is_dir($mountPoint)) {
        mkdir($mountPoint, 0755, true);
    }

    exec("mount | grep " . escapeshellarg($mountPoint), $output, $retval);
    if ($retval !== 0) {
        exec("mount -t nfs " . escapeshellarg("$server:$share") . " " . escapeshellarg($mountPoint) . " 2>&1", $output, $retval);
        if ($retval !== 0) {
            return ['success' => false, 'error' => 'Failed to mount NFS share'];
        }
    }

    // Check if remotePath is already an absolute path (starts with basePath)
    if (str_starts_with($remotePath, $basePath . '/') || (str_starts_with($remotePath, '/') && !empty($basePath) && str_starts_with($remotePath, $basePath))) {
        $sourcePath = "$mountPoint$remotePath";
    } elseif (str_starts_with($remotePath, '/') && empty($basePath)) {
        $sourcePath = "$mountPoint$remotePath";
    } else {
        $sourcePath = "$mountPoint/$basePath/$remotePath";
    }

    if (!file_exists($sourcePath)) {
        return ['success' => false, 'error' => 'Remote path not found: ' . $sourcePath];
    }

    // Ensure local directory exists
    if (!is_dir($localPath)) {
        mkdir($localPath, 0755, true);
    }

    // Use cp -a to preserve attributes and handle both files and directories
    if (is_dir($sourcePath)) {
        // Copy directory contents
        $cmd = "cp -a " . escapeshellarg(rtrim($sourcePath, '/') . '/.') . " " . escapeshellarg(rtrim($localPath, '/') . '/') . " 2>&1";
    } else {
        // Copy single file
        $cmd = "cp -a " . escapeshellarg($sourcePath) . " " . escapeshellarg($localPath . '/' . basename($sourcePath)) . " 2>&1";
    }

    exec($cmd, $output, $retval);

    if ($retval !== 0) {
        return ['success' => false, 'error' => 'Failed to copy from NFS share: ' . implode("\n", $output)];
    }

    // Check if anything was copied
    $files = glob(rtrim($localPath, '/') . '/*');
    if (empty($files)) {
        return ['success' => false, 'error' => 'Copy completed but no files found'];
    }

    return ['success' => true, 'local_path' => $localPath];
}

/**
 * Download via S3.
 */
function downloadViaS3(string $remotePath, string $localPath, array $destination): array
{
    $endpoint = $destination['endpoint'] ?? '';
    $bucket = $destination['bucket'] ?? '';
    $accessKey = $destination['access_key'] ?? '';
    $secretKey = $destination['secret_key'] ?? '';
    $region = $destination['region'] ?? 'us-east-1';
    $basePath = rtrim($destination['path'] ?? '', '/');

    $s3Path = "s3://$bucket/$basePath/$remotePath";

    $localDir = dirname($localPath);
    if (!is_dir($localDir)) {
        mkdir($localDir, 0755, true);
    }

    $envVars = "AWS_ACCESS_KEY_ID=" . escapeshellarg($accessKey) . " " .
               "AWS_SECRET_ACCESS_KEY=" . escapeshellarg($secretKey);

    $endpointArg = !empty($endpoint) ? "--endpoint-url " . escapeshellarg($endpoint) : '';
    $regionArg = "--region " . escapeshellarg($region);

    $cmd = "$envVars aws s3 cp " . escapeshellarg($s3Path) . " " .
           escapeshellarg($localPath) . " $endpointArg $regionArg 2>&1";

    exec($cmd, $output, $retval);

    if ($retval !== 0 || !file_exists($localPath)) {
        return ['success' => false, 'error' => 'S3 download failed: ' . implode("\n", $output)];
    }

    return ['success' => true, 'local_path' => $localPath];
}

/**
 * List backups on remote destination.
 */
function backupListRemote(array $params): array
{
    $destination = $params['destination'] ?? [];
    $path = $params['path'] ?? '';

    $type = $destination['type'] ?? '';

    switch ($type) {
        case 'sftp':
            return listRemoteSftp($destination, $path);

        case 'nfs':
            return listRemoteNfs($destination, $path);

        case 's3':
            return listRemoteS3($destination, $path);

        default:
            return ['success' => false, 'error' => 'Unsupported destination type'];
    }
}

/**
 * List files via SFTP.
 */
function listRemoteSftp(array $destination, string $path): array
{
    $host = $destination['host'] ?? '';
    $port = $destination['port'] ?? 22;
    $username = $destination['username'] ?? '';
    $password = $destination['password'] ?? '';
    $privateKey = $destination['private_key'] ?? '';
    $basePath = rtrim($destination['path'] ?? '', '/');

    $fullPath = $path ? "$basePath/$path" : $basePath;

    if (!empty($privateKey)) {
        $keyFile = tempnam(sys_get_temp_dir(), 'sftp_key_');
        file_put_contents($keyFile, $privateKey);
        chmod($keyFile, 0600);

        $cmd = "sftp -o StrictHostKeyChecking=no -o Port=$port -i " . escapeshellarg($keyFile) .
               " " . escapeshellarg("$username@$host") . " 2>&1 <<EOF
ls -la $fullPath
EOF";

        exec($cmd, $output, $retval);
        unlink($keyFile);
    } else {
        $cmd = "sshpass -p " . escapeshellarg($password) .
               " sftp -o StrictHostKeyChecking=no -o Port=$port " .
               escapeshellarg("$username@$host") . " 2>&1 <<EOF
ls -la $fullPath
EOF";

        exec($cmd, $output, $retval);
    }

    // Parse ls output
    $files = [];
    foreach ($output as $line) {
        if (preg_match('/^[d-].*\s+(\S+)$/', $line, $matches)) {
            $filename = $matches[1];
            if ($filename !== '.' && $filename !== '..') {
                $files[] = [
                    'name' => $filename,
                    'path' => "$fullPath/$filename",
                    'is_directory' => $line[0] === 'd',
                ];
            }
        }
    }

    return ['success' => true, 'files' => $files];
}

/**
 * List files via NFS.
 */
function listRemoteNfs(array $destination, string $path): array
{
    $server = $destination['server'] ?? '';
    $share = $destination['share'] ?? '';
    $mountPoint = $destination['mount_point'] ?? '/mnt/backup_nfs';
    $basePath = rtrim($destination['path'] ?? '', '/');

    // Mount if needed
    if (!is_dir($mountPoint)) {
        mkdir($mountPoint, 0755, true);
    }

    exec("mount | grep " . escapeshellarg($mountPoint), $output, $retval);
    if ($retval !== 0) {
        exec("mount -t nfs " . escapeshellarg("$server:$share") . " " . escapeshellarg($mountPoint) . " 2>&1", $output, $retval);
        if ($retval !== 0) {
            return ['success' => false, 'error' => 'Failed to mount NFS share'];
        }
    }

    $fullPath = $path ? "$mountPoint/$basePath/$path" : "$mountPoint/$basePath";

    if (!is_dir($fullPath)) {
        return ['success' => true, 'files' => []];
    }

    $files = [];
    foreach (scandir($fullPath) as $item) {
        if ($item === '.' || $item === '..') continue;

        $itemPath = "$fullPath/$item";
        $files[] = [
            'name' => $item,
            'path' => ($path ? "$path/" : '') . $item,
            'is_directory' => is_dir($itemPath),
            'size' => is_file($itemPath) ? filesize($itemPath) : null,
            'modified_at' => date('c', filemtime($itemPath)),
        ];
    }

    return ['success' => true, 'files' => $files];
}

/**
 * List files via S3.
 */
function listRemoteS3(array $destination, string $path): array
{
    $endpoint = $destination['endpoint'] ?? '';
    $bucket = $destination['bucket'] ?? '';
    $accessKey = $destination['access_key'] ?? '';
    $secretKey = $destination['secret_key'] ?? '';
    $region = $destination['region'] ?? 'us-east-1';
    $basePath = rtrim($destination['path'] ?? '', '/');

    $s3Path = "s3://$bucket/" . ($basePath ? "$basePath/" : '') . ($path ? "$path/" : '');

    $envVars = "AWS_ACCESS_KEY_ID=" . escapeshellarg($accessKey) . " " .
               "AWS_SECRET_ACCESS_KEY=" . escapeshellarg($secretKey);

    $endpointArg = !empty($endpoint) ? "--endpoint-url " . escapeshellarg($endpoint) : '';
    $regionArg = "--region " . escapeshellarg($region);

    $cmd = "$envVars aws s3 ls " . escapeshellarg($s3Path) . " $endpointArg $regionArg 2>&1";

    exec($cmd, $output, $retval);

    $files = [];
    foreach ($output as $line) {
        // Parse S3 ls output: "2024-01-15 14:30:22     12345 filename"
        if (preg_match('/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})\s+(\d+)\s+(.+)$/', trim($line), $matches)) {
            $files[] = [
                'name' => $matches[4],
                'path' => ($path ? "$path/" : '') . $matches[4],
                'size' => (int)$matches[3],
                'modified_at' => "{$matches[1]}T{$matches[2]}",
                'is_directory' => false,
            ];
        } elseif (preg_match('/^\s*PRE\s+(.+)\/$/', trim($line), $matches)) {
            $files[] = [
                'name' => $matches[1],
                'path' => ($path ? "$path/" : '') . $matches[1],
                'is_directory' => true,
            ];
        }
    }

    return ['success' => true, 'files' => $files];
}

/**
 * Delete backup from remote destination.
 */
function backupDeleteRemote(array $params): array
{
    $remotePath = $params['remote_path'] ?? '';
    $destination = $params['destination'] ?? [];

    if (empty($remotePath)) {
        return ['success' => false, 'error' => 'Remote path required'];
    }

    $type = $destination['type'] ?? '';

    switch ($type) {
        case 'sftp':
            return deleteRemoteSftp($remotePath, $destination);

        case 'nfs':
            return deleteRemoteNfs($remotePath, $destination);

        case 's3':
            return deleteRemoteS3($remotePath, $destination);

        default:
            return ['success' => false, 'error' => 'Unsupported destination type'];
    }
}

/**
 * Delete via SFTP (handles both files and directories).
 */
function deleteRemoteSftp(string $remotePath, array $destination): array
{
    $host = $destination['host'] ?? '';
    $port = $destination['port'] ?? 22;
    $username = $destination['username'] ?? '';
    $password = $destination['password'] ?? '';
    $privateKey = $destination['private_key'] ?? '';
    $basePath = rtrim($destination['path'] ?? '', '/');

    // Check if remotePath is already an absolute path (starts with basePath)
    if (str_starts_with($remotePath, $basePath . '/') || (str_starts_with($remotePath, '/') && !empty($basePath) && str_starts_with($remotePath, $basePath))) {
        $fullPath = $remotePath;
    } elseif (str_starts_with($remotePath, '/') && empty($basePath)) {
        $fullPath = $remotePath;
    } else {
        $fullPath = "$basePath/$remotePath";
    }

    $sshOpts = "-o StrictHostKeyChecking=no -o Port=$port";

    if (!empty($privateKey)) {
        $keyFile = tempnam(sys_get_temp_dir(), 'sftp_key_');
        file_put_contents($keyFile, $privateKey);
        chmod($keyFile, 0600);
        $sshOpts .= " -i " . escapeshellarg($keyFile);

        // Use SSH rm -rf to delete files or directories
        $cmd = "ssh $sshOpts " . escapeshellarg("$username@$host") .
               " 'rm -rf " . escapeshellarg($fullPath) . "' 2>&1";

        exec($cmd, $output, $retval);
        unlink($keyFile);
    } else {
        $sshOpts .= " -o PreferredAuthentications=password -o PubkeyAuthentication=no";
        $cmd = "sshpass -p " . escapeshellarg($password) .
               " ssh $sshOpts " . escapeshellarg("$username@$host") .
               " 'rm -rf " . escapeshellarg($fullPath) . "' 2>&1";

        exec($cmd, $output, $retval);
    }

    logger("Delete remote SFTP: $fullPath, retval=$retval");
    return ['success' => $retval === 0, 'message' => $retval === 0 ? 'Deleted successfully' : 'Delete failed: ' . implode("\n", $output)];
}

/**
 * Delete via NFS.
 */
function deleteRemoteNfs(string $remotePath, array $destination): array
{
    $server = $destination['server'] ?? '';
    $share = $destination['share'] ?? '';
    $mountPoint = $destination['mount_point'] ?? '/mnt/backup_nfs';
    $basePath = rtrim($destination['path'] ?? '', '/');

    // Mount if needed
    exec("mount | grep " . escapeshellarg($mountPoint), $output, $retval);
    if ($retval !== 0) {
        exec("mount -t nfs " . escapeshellarg("$server:$share") . " " . escapeshellarg($mountPoint) . " 2>&1", $output, $retval);
        if ($retval !== 0) {
            return ['success' => false, 'error' => 'Failed to mount NFS share'];
        }
    }

    // Check if remotePath is already an absolute path (starts with basePath)
    if (str_starts_with($remotePath, $basePath . '/') || (str_starts_with($remotePath, '/') && !empty($basePath) && str_starts_with($remotePath, $basePath))) {
        $fullPath = "$mountPoint$remotePath";
    } elseif (str_starts_with($remotePath, '/') && empty($basePath)) {
        $fullPath = "$mountPoint$remotePath";
    } else {
        $fullPath = "$mountPoint/$basePath/$remotePath";
    }

    if (!file_exists($fullPath)) {
        return ['success' => false, 'error' => 'Path not found: ' . $fullPath];
    }

    if (is_file($fullPath)) {
        unlink($fullPath);
    } else {
        exec("rm -rf " . escapeshellarg($fullPath));
    }

    logger("Delete remote NFS: $fullPath");
    return ['success' => true, 'message' => 'Deleted successfully'];
}

/**
 * Delete via S3.
 */
function deleteRemoteS3(string $remotePath, array $destination): array
{
    $endpoint = $destination['endpoint'] ?? '';
    $bucket = $destination['bucket'] ?? '';
    $accessKey = $destination['access_key'] ?? '';
    $secretKey = $destination['secret_key'] ?? '';
    $region = $destination['region'] ?? 'us-east-1';
    $basePath = rtrim($destination['path'] ?? '', '/');

    $s3Path = "s3://$bucket/$basePath/$remotePath";

    $envVars = "AWS_ACCESS_KEY_ID=" . escapeshellarg($accessKey) . " " .
               "AWS_SECRET_ACCESS_KEY=" . escapeshellarg($secretKey);

    $endpointArg = !empty($endpoint) ? "--endpoint-url " . escapeshellarg($endpoint) : '';
    $regionArg = "--region " . escapeshellarg($region);

    $cmd = "$envVars aws s3 rm " . escapeshellarg($s3Path) . " $endpointArg $regionArg 2>&1";

    exec($cmd, $output, $retval);

    return ['success' => $retval === 0, 'message' => $retval === 0 ? 'File deleted' : 'Delete failed'];
}

/**
 * Test remote destination connection.
 */
/**
 * Download a user's folder from a server backup and create a tar.gz archive.
 * This creates a "ready to restore" backup file for the user.
 *
 * @param array $params
 *   - username: The system username
 *   - remote_path: Relative path like "2026-01-20_110001/user" (already includes user folder)
 *   - destination: Destination config with base path
 *   - output_path: Where to save the tar.gz
 */
function backupDownloadUserArchive(array $params): array
{
    $username = $params['username'] ?? '';
    $remotePath = $params['remote_path'] ?? '';  // e.g., "2026-01-20_110001/user"
    $destination = $params['destination'] ?? [];
    $outputPath = $params['output_path'] ?? '';

    if (empty($username) || empty($remotePath) || empty($outputPath)) {
        return ['success' => false, 'error' => 'Username, remote_path, and output_path required'];
    }

    $type = $destination['type'] ?? '';
    if (!in_array($type, ['sftp', 'nfs'])) {
        return ['success' => false, 'error' => 'Only SFTP and NFS destinations supported'];
    }

    // Create temp directory for download
    $tempDir = sys_get_temp_dir() . '/jabali_download_' . uniqid();
    mkdir($tempDir, 0755, true);

    try {
        // The remote_path is relative (e.g., "2026-01-20_110001/user")
        // Combine with destination's base path to get full path
        $basePath = rtrim($destination['path'] ?? '', '/');
        $fullRemotePath = $basePath . '/' . ltrim($remotePath, '/');

        logger("Downloading user backup: $fullRemotePath to $tempDir");

        if ($type === 'sftp') {
            $result = downloadUserFolderSftp($fullRemotePath, $tempDir, $destination);
        } else {
            $result = downloadUserFolderNfs($fullRemotePath, $tempDir, $destination);
        }

        if (!($result['success'] ?? false)) {
            throw new Exception($result['error'] ?? 'Download failed');
        }

        // Check if user folder was downloaded
        $userFolder = $tempDir . '/' . $username;
        if (!is_dir($userFolder)) {
            // Maybe it downloaded directly into tempDir
            $userFolder = $tempDir;
        }

        // Ensure output directory exists with proper ownership
        $outputDir = dirname($outputPath);
        if (!is_dir($outputDir)) {
            mkdir($outputDir, 0755, true);
            $userInfo = posix_getpwnam($username);
            if ($userInfo) {
                chown($outputDir, $userInfo['uid']);
                chgrp($outputDir, $userInfo['gid']);
            }
        }

        // Create tar.gz archive
        logger("Creating archive: $outputPath from $userFolder");
        $tarCmd = "tar -I pigz -cf " . escapeshellarg($outputPath) . " -C " . escapeshellarg(dirname($userFolder)) . " " . escapeshellarg(basename($userFolder)) . " 2>&1";
        exec($tarCmd, $tarOutput, $tarRetval);

        if ($tarRetval !== 0) {
            throw new Exception('Failed to create archive: ' . implode("\n", $tarOutput));
        }

        // Get archive size
        $size = filesize($outputPath);

        // Set ownership to the user
        $userInfo = posix_getpwnam($username);
        if ($userInfo) {
            chown($outputPath, $userInfo['uid']);
            chgrp($outputPath, $userInfo['gid']);
        }

        // Cleanup temp directory
        exec("rm -rf " . escapeshellarg($tempDir));

        return [
            'success' => true,
            'local_path' => $outputPath,
            'size' => $size,
        ];
    } catch (Exception $e) {
        // Cleanup on error
        exec("rm -rf " . escapeshellarg($tempDir));
        return ['success' => false, 'error' => $e->getMessage()];
    }
}

/**
 * Download user folder via SFTP.
 */
function downloadUserFolderSftp(string $remotePath, string $localPath, array $destination): array
{
    $host = $destination['host'] ?? '';
    $port = $destination['port'] ?? 22;
    $username = $destination['username'] ?? '';
    $password = $destination['password'] ?? '';
    $privateKey = $destination['private_key'] ?? '';

    $keyFile = null;
    $sshOpts = "-o StrictHostKeyChecking=no -o Port=$port";

    if (!empty($privateKey)) {
        $keyFile = tempnam(sys_get_temp_dir(), 'sftp_key_');
        file_put_contents($keyFile, $privateKey);
        chmod($keyFile, 0600);
        $sshOpts .= " -i " . escapeshellarg($keyFile);
    } else {
        $sshOpts .= " -o PreferredAuthentications=password -o PubkeyAuthentication=no";
    }

    // Rsync the user folder
    $sshCmd = "ssh $sshOpts";
    if (!empty($password) && empty($privateKey)) {
        $rsyncCmd = "sshpass -p " . escapeshellarg($password) . " rsync -avz -e " . escapeshellarg($sshCmd) . " " .
                    escapeshellarg("$username@$host:" . rtrim($remotePath, '/') . '/') . " " .
                    escapeshellarg(rtrim($localPath, '/') . '/') . " 2>&1";
    } else {
        $rsyncCmd = "rsync -avz -e " . escapeshellarg($sshCmd) . " " .
                    escapeshellarg("$username@$host:" . rtrim($remotePath, '/') . '/') . " " .
                    escapeshellarg(rtrim($localPath, '/') . '/') . " 2>&1";
    }

    logger("Executing: $rsyncCmd");
    exec($rsyncCmd, $output, $retval);

    if ($keyFile && file_exists($keyFile)) {
        unlink($keyFile);
    }

    if ($retval !== 0 && $retval !== 24) {
        return ['success' => false, 'error' => 'Rsync failed: ' . implode("\n", $output)];
    }

    return ['success' => true];
}

/**
 * Download user folder via NFS.
 */
function downloadUserFolderNfs(string $remotePath, string $localPath, array $destination): array
{
    $host = $destination['host'] ?? '';
    $path = $destination['path'] ?? '';
    $mountPoint = $destination['mount_point'] ?? '/mnt/backup_nfs';

    // Mount if needed
    if (!is_dir($mountPoint)) {
        mkdir($mountPoint, 0755, true);
    }

    exec("mount | grep " . escapeshellarg($mountPoint), $output, $retval);
    if ($retval !== 0) {
        exec("mount -t nfs " . escapeshellarg("$host:$path") . " " . escapeshellarg($mountPoint) . " 2>&1", $output, $retval);
        if ($retval !== 0) {
            return ['success' => false, 'error' => 'Failed to mount NFS share'];
        }
    }

    // Construct source path
    $sourcePath = $mountPoint . '/' . ltrim($remotePath, '/');

    if (!is_dir($sourcePath)) {
        return ['success' => false, 'error' => 'User folder not found: ' . $sourcePath];
    }

    // Copy with rsync
    $cmd = "rsync -av " . escapeshellarg(rtrim($sourcePath, '/') . '/') . " " .
           escapeshellarg(rtrim($localPath, '/') . '/') . " 2>&1";
    exec($cmd, $output, $retval);

    if ($retval !== 0 && $retval !== 24) {
        return ['success' => false, 'error' => 'Copy failed: ' . implode("\n", $output)];
    }

    return ['success' => true];
}

function backupTestDestination(array $params): array
{
    $destination = $params['destination'] ?? [];

    $type = $destination['type'] ?? '';

    switch ($type) {
        case 'sftp':
            return testSftpConnection($destination);

        case 'nfs':
            return testNfsConnection($destination);

        case 's3':
            return testS3Connection($destination);

        default:
            return ['success' => false, 'error' => 'Unsupported destination type'];
    }
}

/**
 * Test SFTP connection.
 */
function testSftpConnection(array $destination): array
{
    $host = $destination['host'] ?? '';
    $port = $destination['port'] ?? 22;
    $username = $destination['username'] ?? '';
    $password = $destination['password'] ?? '';
    $privateKey = $destination['private_key'] ?? '';
    $remotePath = $destination['path'] ?? '/backups';

    if (empty($host) || empty($username)) {
        return ['success' => false, 'error' => 'Host and username required'];
    }

    $testFile = ".jabali_write_test_" . uniqid();
    $keyFile = null;

    // Build SFTP batch commands: connect, cd to path, create test file, delete it
    $sftpCommands = "cd " . escapeshellarg($remotePath) . "
put /dev/null $testFile
rm $testFile
quit";

    if (!empty($privateKey)) {
        $keyFile = tempnam(sys_get_temp_dir(), 'sftp_key_');
        file_put_contents($keyFile, $privateKey);
        chmod($keyFile, 0600);

        $cmd = "echo " . escapeshellarg($sftpCommands) . " | sftp -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=10 -o Port=$port -i " .
               escapeshellarg($keyFile) . " " . escapeshellarg("$username@$host") . " 2>&1";

        exec($cmd, $output, $retval);
        unlink($keyFile);
    } else {
        $cmd = "echo " . escapeshellarg($sftpCommands) . " | sshpass -p " . escapeshellarg($password) .
               " sftp -o StrictHostKeyChecking=no -o ConnectTimeout=10 -o Port=$port " .
               escapeshellarg("$username@$host") . " 2>&1";

        exec($cmd, $output, $retval);
    }

    $outputStr = implode("\n", $output);

    if ($retval !== 0) {
        // Check for specific errors
        if (stripos($outputStr, 'No such file') !== false || stripos($outputStr, 'not found') !== false) {
            return ['success' => false, 'error' => "Remote path '$remotePath' does not exist"];
        }
        if (stripos($outputStr, 'Permission denied') !== false) {
            return ['success' => false, 'error' => "No write permission to '$remotePath'"];
        }
        return ['success' => false, 'error' => 'SFTP connection failed: ' . $outputStr];
    }

    return ['success' => true, 'message' => 'SFTP connection and write permission verified'];
}

/**
 * Test NFS connection.
 */
function testNfsConnection(array $destination): array
{
    $server = $destination['server'] ?? '';
    $share = $destination['share'] ?? '';
    $subPath = $destination['path'] ?? '';

    if (empty($server) || empty($share)) {
        return ['success' => false, 'error' => 'Server and share required'];
    }

    // Test with showmount
    exec("showmount -e " . escapeshellarg($server) . " 2>&1", $output, $retval);

    if ($retval !== 0) {
        return ['success' => false, 'error' => 'Cannot reach NFS server: ' . implode("\n", $output)];
    }

    // Check if share is exported
    $shareFound = false;
    foreach ($output as $line) {
        if (strpos($line, $share) !== false) {
            $shareFound = true;
            break;
        }
    }

    if (!$shareFound) {
        return ['success' => false, 'error' => "Share '$share' not found on server"];
    }

    // Test write permission by mounting temporarily
    $tempMount = "/tmp/nfs_test_" . uniqid();
    mkdir($tempMount, 0755);

    $mountCmd = "mount -t nfs " . escapeshellarg("$server:$share") . " " . escapeshellarg($tempMount) . " 2>&1";
    exec($mountCmd, $mountOutput, $mountRetval);

    if ($mountRetval !== 0) {
        rmdir($tempMount);
        return ['success' => false, 'error' => 'Failed to mount NFS share: ' . implode("\n", $mountOutput)];
    }

    // Determine test path
    $testDir = $tempMount;
    if (!empty($subPath)) {
        $testDir = $tempMount . '/' . ltrim($subPath, '/');
        if (!is_dir($testDir)) {
            exec("umount " . escapeshellarg($tempMount) . " 2>&1");
            rmdir($tempMount);
            return ['success' => false, 'error' => "Sub-directory '$subPath' does not exist on NFS share"];
        }
    }

    // Try to write a test file
    $testFile = $testDir . "/.jabali_write_test_" . uniqid();
    $writeSuccess = @file_put_contents($testFile, "test") !== false;

    if ($writeSuccess) {
        @unlink($testFile);
    }

    // Unmount
    exec("umount " . escapeshellarg($tempMount) . " 2>&1");
    rmdir($tempMount);

    if (!$writeSuccess) {
        return ['success' => false, 'error' => 'No write permission to NFS share'];
    }

    return ['success' => true, 'message' => 'NFS connection and write permission verified'];
}

/**
 * Test S3 connection.
 */
function testS3Connection(array $destination): array
{
    $endpoint = $destination['endpoint'] ?? '';
    $bucket = $destination['bucket'] ?? '';
    $accessKey = $destination['access_key'] ?? '';
    $secretKey = $destination['secret_key'] ?? '';
    $region = $destination['region'] ?? 'us-east-1';
    $path = $destination['path'] ?? 'backups';

    if (empty($bucket) || empty($accessKey) || empty($secretKey)) {
        return ['success' => false, 'error' => 'Bucket and credentials required'];
    }

    $envVars = "AWS_ACCESS_KEY_ID=" . escapeshellarg($accessKey) . " " .
               "AWS_SECRET_ACCESS_KEY=" . escapeshellarg($secretKey);

    $endpointArg = !empty($endpoint) ? "--endpoint-url " . escapeshellarg($endpoint) : '';
    $regionArg = "--region " . escapeshellarg($region);

    // First, test bucket access
    $cmd = "$envVars aws s3 ls " . escapeshellarg("s3://$bucket/") . " $endpointArg $regionArg 2>&1";
    exec($cmd, $output, $retval);

    if ($retval !== 0) {
        $outputStr = implode("\n", $output);
        if (stripos($outputStr, 'NoSuchBucket') !== false) {
            return ['success' => false, 'error' => "Bucket '$bucket' does not exist"];
        }
        if (stripos($outputStr, 'AccessDenied') !== false) {
            return ['success' => false, 'error' => 'Access denied - check credentials'];
        }
        return ['success' => false, 'error' => 'S3 connection failed: ' . $outputStr];
    }

    // Test write permission by uploading a test object
    $testKey = trim($path, '/') . "/.jabali_write_test_" . uniqid();
    $tempFile = tempnam(sys_get_temp_dir(), 'jabali_s3_test_');
    file_put_contents($tempFile, "test");

    $putCmd = "$envVars aws s3 cp " . escapeshellarg($tempFile) . " " .
              escapeshellarg("s3://$bucket/$testKey") . " $endpointArg $regionArg 2>&1";
    exec($putCmd, $putOutput, $putRetval);
    unlink($tempFile);

    if ($putRetval !== 0) {
        $putOutputStr = implode("\n", $putOutput);
        if (stripos($putOutputStr, 'AccessDenied') !== false) {
            return ['success' => false, 'error' => 'No write permission to bucket'];
        }
        return ['success' => false, 'error' => 'Write test failed: ' . $putOutputStr];
    }

    // Clean up test object
    $rmCmd = "$envVars aws s3 rm " . escapeshellarg("s3://$bucket/$testKey") . " $endpointArg $regionArg 2>&1";
    exec($rmCmd);

    return ['success' => true, 'message' => 'S3 connection and write permission verified'];
}

// ============ FAIL2BAN MANAGEMENT ============

function fail2banStatusLight(array $params): array
{
    // Check if fail2ban-client exists (use file_exists for reliability after fresh install)
    $installed = file_exists('/usr/bin/fail2ban-client') || file_exists('/usr/sbin/fail2ban-client');

    if (!$installed) {
        return ['success' => true, 'installed' => false];
    }

    exec('fail2ban-client version 2>/dev/null', $versionOutput);
    $version = $versionOutput[0] ?? 'Unknown';

    exec('systemctl is-active fail2ban 2>/dev/null', $statusOutput, $statusCode);
    $running = $statusCode === 0;

    return [
        'success' => true,
        'installed' => true,
        'running' => $running,
        'version' => $version,
    ];
}

function fail2banStatus(array $params): array
{
    // Check if fail2ban-client exists (use file_exists for reliability after fresh install)
    $installed = file_exists('/usr/bin/fail2ban-client') || file_exists('/usr/sbin/fail2ban-client');

    if (!$installed) {
        return ['success' => true, 'installed' => false];
    }
    
    exec('fail2ban-client version 2>/dev/null', $versionOutput);
    $version = $versionOutput[0] ?? 'Unknown';
    
    exec('systemctl is-active fail2ban 2>/dev/null', $statusOutput, $statusCode);
    $running = $statusCode === 0;
    
    $jails = [];
    $totalBanned = 0;
    
    if ($running) {
        exec('fail2ban-client status 2>/dev/null', $output);
        $jailList = [];
        foreach ($output as $line) {
            if (str_contains($line, 'Jail list:')) {
                $jails_str = trim(str_replace('Jail list:', '', $line));
                $jailList = array_filter(array_map('trim', explode(',', $jails_str)));
            }
        }
        
        foreach ($jailList as $jail) {
            exec("fail2ban-client status " . escapeshellarg($jail) . " 2>/dev/null", $jailOutput);
            $currentlyBanned = 0;
            $bannedIps = [];
            
            foreach ($jailOutput as $line) {
                if (str_contains($line, 'Currently banned:')) {
                    $currentlyBanned = (int) trim(str_replace('Currently banned:', '', $line));
                }
                if (str_contains($line, 'Banned IP list:')) {
                    $ips = trim(str_replace('Banned IP list:', '', $line));
                    $bannedIps = array_filter(array_map('trim', explode(' ', $ips)));
                }
            }
            
            $jails[] = ['name' => $jail, 'banned' => $currentlyBanned, 'banned_ips' => $bannedIps];
            $totalBanned += $currentlyBanned;
        }
    }
    
    // Load settings
    $maxRetry = 5;
    $banTime = 600;
    $findTime = 600;
    $configFile = '/etc/fail2ban/jail.local';
    if (file_exists($configFile)) {
        $content = file_get_contents($configFile);
        if (preg_match('/maxretry\s*=\s*(\d+)/i', $content, $m)) $maxRetry = (int) $m[1];
        if (preg_match('/bantime\s*=\s*(\d+)/i', $content, $m)) $banTime = (int) $m[1];
        if (preg_match('/findtime\s*=\s*(\d+)/i', $content, $m)) $findTime = (int) $m[1];
    }
    
    return [
        'success' => true,
        'installed' => true,
        'running' => $running,
        'version' => $version,
        'jails' => $jails,
        'total_banned' => $totalBanned,
        'max_retry' => $maxRetry,
        'ban_time' => $banTime,
        'find_time' => $findTime,
    ];
}

function fail2banInstall(array $params): array
{
    exec('apt-get update && apt-get install -y fail2ban 2>&1', $output, $code);
    
    if ($code !== 0) {
        return ['success' => false, 'error' => 'Installation failed: ' . implode("\n", $output)];
    }
    
    // Create default config
    fail2banSaveSettings(['max_retry' => 5, 'ban_time' => 600, 'find_time' => 600]);
    
    exec('systemctl enable fail2ban && systemctl start fail2ban 2>&1');
    
    return ['success' => true, 'message' => 'Fail2ban installed'];
}

function updateHealthMonitorOverride(string $service, bool $disabled): void
{
    $path = '/etc/jabali/health-monitor.json';
    $overrides = [];

    if (file_exists($path)) {
        $content = file_get_contents($path);
        $decoded = json_decode($content, true);
        if (is_array($decoded)) {
            $overrides = $decoded;
        }
    }

    $disabledServices = $overrides['disabled_services'] ?? [];
    if (!is_array($disabledServices)) {
        $disabledServices = [];
    }

    $disabledServices = array_values(array_unique(array_filter($disabledServices, 'is_string')));

    if ($disabled) {
        if (!in_array($service, $disabledServices, true)) {
            $disabledServices[] = $service;
        }
    } else {
        $disabledServices = array_values(array_filter($disabledServices, static function (string $name) use ($service): bool {
            return $name !== $service;
        }));
    }

    $overrides['disabled_services'] = $disabledServices;

    if (!is_dir(dirname($path))) {
        mkdir(dirname($path), 0755, true);
    }

    file_put_contents($path, json_encode($overrides, JSON_PRETTY_PRINT));
}

function fail2banStart(array $params): array
{
    exec('systemctl start fail2ban 2>&1', $output, $code);
    updateHealthMonitorOverride('fail2ban', false);
    return ['success' => $code === 0, 'error' => $code !== 0 ? implode("\n", $output) : null];
}

function fail2banStop(array $params): array
{
    exec('systemctl stop fail2ban 2>&1', $output, $code);
    updateHealthMonitorOverride('fail2ban', true);
    return ['success' => $code === 0, 'error' => $code !== 0 ? implode("\n", $output) : null];
}

function fail2banRestart(array $params): array
{
    exec('systemctl restart fail2ban 2>&1', $output, $code);
    return ['success' => $code === 0, 'error' => $code !== 0 ? implode("\n", $output) : null];
}

function fail2banSaveSettings(array $params): array
{
    $maxRetry = $params['max_retry'] ?? 5;
    $banTime = $params['ban_time'] ?? 600;
    $findTime = $params['find_time'] ?? 600;
    
    $config = <<<CONFIG
[DEFAULT]
bantime = {$banTime}
findtime = {$findTime}
maxretry = {$maxRetry}
ignoreip = 127.0.0.1/8 ::1

[sshd]
enabled = true

[nginx-http-auth]
enabled = true

[wordpress-login]
enabled = true
port = http,https
filter = wordpress-login
logpath = /var/log/nginx/access.log
maxretry = {$maxRetry}
CONFIG;

    file_put_contents('/etc/fail2ban/jail.local', $config);
    
    // Create WordPress filter
    $filter = "[Definition]\nfailregex = ^<HOST> .* \"POST /wp-login.php\n            ^<HOST> .* \"POST /xmlrpc.php\nignoreregex =";
    file_put_contents('/etc/fail2ban/filter.d/wordpress-login.conf', $filter);
    
    exec('fail2ban-client reload 2>&1', $output, $code);
    
    return ['success' => true, 'message' => 'Settings saved'];
}

function fail2banUnbanIp(array $params): array
{
    $jail = $params['jail'] ?? '';
    $ip = $params['ip'] ?? '';
    
    if (empty($jail) || empty($ip)) {
        return ['success' => false, 'error' => 'Jail and IP required'];
    }
    
    exec("fail2ban-client set " . escapeshellarg($jail) . " unbanip " . escapeshellarg($ip) . " 2>&1", $output, $code);
    
    return ['success' => $code === 0, 'error' => $code !== 0 ? implode("\n", $output) : null];
}

function fail2banBanIp(array $params): array
{
    $ip = $params['ip'] ?? '';
    $jail = $params['jail'] ?? 'sshd';
    
    if (empty($ip)) {
        return ['success' => false, 'error' => 'IP required'];
    }
    
    exec("fail2ban-client set " . escapeshellarg($jail) . " banip " . escapeshellarg($ip) . " 2>&1", $output, $code);

    return ['success' => $code === 0, 'error' => $code !== 0 ? implode("\n", $output) : null];
}

function fail2banListJails(array $params): array
{
    // Fallback descriptions for jails that use shared filters
    $jailDescriptions = [
        'postfix' => 'Postfix SMTP server protection',
        'postfix-sasl' => 'Postfix SASL authentication protection',
        'dovecot' => 'Dovecot IMAP/POP3 server protection',
        'nginx-http-auth' => 'Nginx HTTP authentication protection',
        'nginx-botsearch' => 'Nginx bot/scanner protection',
        'nginx-bad-request' => 'Nginx bad request protection',
        'nginx-limit-req' => 'Nginx rate limit protection',
        'apache-auth' => 'Apache HTTP authentication protection',
        'apache-badbots' => 'Apache bot/scanner protection',
        'phpmyadmin' => 'phpMyAdmin login protection',
        'mysql-auth' => 'MySQL authentication protection',
        'proftpd' => 'ProFTPD FTP server protection',
        'vsftpd' => 'vsftpd FTP server protection',
        'pure-ftpd' => 'Pure-FTPd server protection',
        'exim' => 'Exim mail server protection',
        'named-refused' => 'BIND DNS refused queries protection',
        'recidive' => 'Recidive ban for repeat offenders',
    ];

    // Get list of available jails from jail.d directory
    $jailsDir = '/etc/fail2ban/jail.d';
    $availableJails = [];

    // Get active jails from fail2ban-client
    $activeJails = [];
    exec('fail2ban-client status 2>/dev/null', $output);
    foreach ($output as $line) {
        if (str_contains($line, 'Jail list:')) {
            // Extract jails after "Jail list:" - handle special chars like `- and tabs
            if (preg_match('/Jail list:\s*[`|-]*\s*(.+)/i', $line, $m)) {
                $jails_str = trim($m[1]);
                $activeJails = array_filter(array_map('trim', explode(',', $jails_str)));
            }
        }
    }

    // Scan jail.d for configured jails
    if (is_dir($jailsDir)) {
        foreach (glob($jailsDir . '/*.conf') as $file) {
            $content = file_get_contents($file);
            // Extract jail sections - only match simple names (letters, numbers, dashes, underscores)
            if (preg_match_all('/^\[([a-zA-Z][a-zA-Z0-9_-]*)\]\s*$/m', $content, $matches)) {
                foreach ($matches[1] as $jailName) {
                    if ($jailName === 'DEFAULT') continue;

                    // Check if enabled in config
                    $enabledInConfig = false;
                    if (preg_match('/\[' . preg_quote($jailName, '/') . '\][^\[]*enabled\s*=\s*(true|yes|1)/is', $content)) {
                        $enabledInConfig = true;
                    }

                    // Get jail description from filter or fallback
                    $description = '';
                    $filterFile = '/etc/fail2ban/filter.d/' . $jailName . '.conf';
                    if (file_exists($filterFile)) {
                        $filterContent = file_get_contents($filterFile);
                        if (preg_match('/^#\s*(.+)/m', $filterContent, $descMatch)) {
                            $description = trim($descMatch[1]);
                        }
                    }
                    // Use fallback description if none found
                    if (empty($description) && isset($jailDescriptions[$jailName])) {
                        $description = $jailDescriptions[$jailName];
                    }

                    // Check if actually running
                    $isActive = in_array($jailName, $activeJails);

                    $availableJails[$jailName] = [
                        'name' => $jailName,
                        'enabled' => $enabledInConfig,
                        'active' => $isActive,
                        'description' => $description,
                        'config_file' => basename($file)
                    ];
                }
            }
        }
    }

    // Also include sshd if it's active (from defaults-debian.conf)
    if (in_array('sshd', $activeJails)) {
        $availableJails['sshd'] = [
            'name' => 'sshd',
            'enabled' => true,
            'active' => true,
            'description' => 'SSH brute force protection',
            'config_file' => 'defaults-debian.conf'
        ];
    }

    return ['success' => true, 'jails' => array_values($availableJails)];
}

function fail2banEnableJail(array $params): array
{
    $jail = $params['jail'] ?? '';

    if (empty($jail)) {
        return ['success' => false, 'error' => 'Jail name required'];
    }

    // Find the config file for this jail
    $jailsDir = '/etc/fail2ban/jail.d';
    $configFile = null;

    foreach (glob($jailsDir . '/*.conf') as $file) {
        $content = file_get_contents($file);
        if (preg_match('/\[' . preg_quote($jail, '/') . '\]/i', $content)) {
            $configFile = $file;
            break;
        }
    }

    if (!$configFile) {
        return ['success' => false, 'error' => 'Jail configuration not found'];
    }

    // Update the config file to enable the jail
    $content = file_get_contents($configFile);

    // Replace enabled = false with enabled = true in the jail section
    $pattern = '/(\[' . preg_quote($jail, '/') . '\][^\[]*?)enabled\s*=\s*(false|no|0)/is';
    if (preg_match($pattern, $content)) {
        $content = preg_replace($pattern, '$1enabled = true', $content);
    } else {
        // If no enabled line exists, add it after the jail header
        $content = preg_replace('/(\[' . preg_quote($jail, '/') . '\])/i', "$1\nenabled = true", $content);
    }

    file_put_contents($configFile, $content);

    // Restart fail2ban to apply changes
    exec('systemctl restart fail2ban 2>&1', $output, $code);

    // Wait for fail2ban to be fully ready
    if ($code === 0) {
        sleep(2);
        // Verify jail is now active
        exec('fail2ban-client status 2>/dev/null', $statusOutput);
    }

    return ['success' => $code === 0, 'error' => $code !== 0 ? implode("\n", $output) : null];
}

function fail2banDisableJail(array $params): array
{
    $jail = $params['jail'] ?? '';

    if (empty($jail)) {
        return ['success' => false, 'error' => 'Jail name required'];
    }

    // Prevent disabling sshd jail
    if ($jail === 'sshd') {
        return ['success' => false, 'error' => 'Cannot disable SSH jail for security reasons'];
    }

    // Find the config file for this jail
    $jailsDir = '/etc/fail2ban/jail.d';
    $configFile = null;

    foreach (glob($jailsDir . '/*.conf') as $file) {
        $content = file_get_contents($file);
        if (preg_match('/\[' . preg_quote($jail, '/') . '\]/i', $content)) {
            $configFile = $file;
            break;
        }
    }

    if (!$configFile) {
        return ['success' => false, 'error' => 'Jail configuration not found'];
    }

    // Update the config file to disable the jail
    $content = file_get_contents($configFile);

    // Replace enabled = true with enabled = false in the jail section
    $pattern = '/(\[' . preg_quote($jail, '/') . '\][^\[]*?)enabled\s*=\s*(true|yes|1)/is';
    if (preg_match($pattern, $content)) {
        $content = preg_replace($pattern, '$1enabled = false', $content);
    } else {
        // If no enabled line exists, add it as disabled
        $content = preg_replace('/(\[' . preg_quote($jail, '/') . '\])/i', "$1\nenabled = false", $content);
    }

    file_put_contents($configFile, $content);

    // Restart fail2ban to apply changes
    exec('systemctl restart fail2ban 2>&1', $output, $code);

    // Wait for fail2ban to be fully ready
    if ($code === 0) {
        sleep(2);
        // Verify fail2ban status
        exec('fail2ban-client status 2>/dev/null', $statusOutput);
    }

    return ['success' => $code === 0, 'error' => $code !== 0 ? implode("\n", $output) : null];
}

// ============ CLAMAV MANAGEMENT ============

function countTextSignatures(string $path): int
{
    if (!is_readable($path)) {
        return 0;
    }

    $handle = fopen($path, 'rb');
    if ($handle === false) {
        return 0;
    }

    $count = 0;
    while (($line = fgets($handle)) !== false) {
        $line = trim($line);
        if ($line === '' || $line[0] === '#') {
            continue;
        }
        $count++;
    }

    fclose($handle);

    return $count;
}

function collectCustomSignatureFiles(string $dbDir): array
{
    $patterns = [
        '*.ndb',
        '*.hdb',
        '*.ldb',
        '*.cdb',
        '*.hsb',
        '*.ftm',
        '*.ign2',
    ];

    $files = [];
    foreach ($patterns as $pattern) {
        $matches = glob($dbDir.'/'.$pattern);
        if (is_array($matches)) {
            $files = array_merge($files, $matches);
        }
    }

    $files = array_values(array_unique($files));
    sort($files);

    return $files;
}

function compileWebHostingSignatures(string $rawSignatures): string
{
    $compiled = [];

    foreach (explode("\n", $rawSignatures) as $line) {
        $line = trim($line);
        if ($line === '' || $line[0] === '#') {
            continue;
        }

        $parts = explode(':', $line, 4);
        if (count($parts) < 4) {
            continue;
        }

        [$name, $target, $offset, $pattern] = $parts;

        if (str_starts_with($pattern, 'HEX:')) {
            $pattern = substr($pattern, 4);
        } else {
            $parts = preg_split('/(\{[-0-9]+(?:-[0-9]+)?\})/', $pattern, -1, PREG_SPLIT_DELIM_CAPTURE);
            $compiledPattern = '';

            foreach ($parts as $part) {
                if ($part === '') {
                    continue;
                }

                if (preg_match('/^\{(-?\d+)(?:-(\d+))?\}$/', $part, $matches) === 1) {
                    $min = (int) $matches[1];
                    $max = $matches[2] ?? null;

                    if ($min < 0 && $max === null) {
                        $compiledPattern .= '{0-'.abs($min).'}';
                        continue;
                    }

                    if ($min < 0) {
                        $min = 0;
                    }

                    if ($max !== null) {
                        $maxValue = (int) $max;
                        if ($maxValue < $min) {
                            $maxValue = $min;
                        }
                        $compiledPattern .= '{'.$min.'-'.$maxValue.'}';
                        continue;
                    }

                    $compiledPattern .= '{'.$min.'}';
                    continue;
                }

                $compiledPattern .= strtoupper(bin2hex($part));
            }

            $pattern = $compiledPattern;
        }

        if (preg_match('/\{[-0-9]+(?:-[0-9]+)?\}$/', $pattern) === 1) {
            continue;
        }

        $patternWithoutSkips = preg_replace('/\{[-0-9]+(?:-[0-9]+)?\}/', '', $pattern);
        if ($patternWithoutSkips === null || strlen($patternWithoutSkips) < 8 || strlen($patternWithoutSkips) % 2 !== 0) {
            continue;
        }

        $compiled[] = $name.':'.$target.':'.$offset.':'.$pattern;
    }

    return implode("\n", $compiled)."\n";
}

function clamavStatusLight(array $params): array
{
    exec('which clamscan 2>/dev/null', $output, $code);
    $installed = $code === 0;

    if (!$installed) {
        return ['success' => true, 'installed' => false];
    }

    exec('clamscan --version 2>/dev/null', $versionOutput);
    $version = $versionOutput[0] ?? 'Unknown';

    exec('systemctl is-active clamav-daemon 2>/dev/null', $daemonOutput, $daemonCode);
    $running = $daemonCode === 0;

    exec('systemctl is-enabled jabali-realtime-scan 2>/dev/null', $rtEnabled, $rtEnabledCode);
    $realtimeEnabled = $rtEnabledCode === 0;

    exec('systemctl is-active jabali-realtime-scan 2>/dev/null', $rtActive, $rtActiveCode);
    $realtimeRunning = $rtActiveCode === 0;

    $lightMode = file_exists('/var/lib/clamav/.light_mode');

    return [
        'success' => true,
        'installed' => true,
        'running' => $running,
        'version' => $version,
        'realtime_enabled' => $realtimeEnabled,
        'realtime_running' => $realtimeRunning,
        'light_mode' => $lightMode,
    ];
}

function clamavStatus(array $params): array
{
    exec('which clamscan 2>/dev/null', $output, $code);
    $installed = $code === 0;
    
    if (!$installed) {
        return ['success' => true, 'installed' => false];
    }
    
    exec('clamscan --version 2>/dev/null', $versionOutput);
    $version = $versionOutput[0] ?? 'Unknown';
    
    exec('systemctl is-active clamav-daemon 2>/dev/null', $daemonOutput, $daemonCode);
    $running = $daemonCode === 0;
    
    // Signature info
    $signatureCount = 0;
    $lastUpdate = '';
    exec('sigtool --info /var/lib/clamav/daily.cvd 2>/dev/null || sigtool --info /var/lib/clamav/daily.cld 2>/dev/null', $sigOutput);
    foreach ($sigOutput as $line) {
        if (str_contains($line, 'Build time:')) {
            $lastUpdate = trim(str_replace('Build time:', '', $line));
        }
    }
    
    // Recent threats
    $recentThreats = [];
    $logFile = file_exists('/var/log/clamav/scan.log') ? '/var/log/clamav/scan.log' : '/var/log/clamav/clamav.log';
    if (file_exists($logFile) && is_readable($logFile)) {
        $content = @file_get_contents($logFile);
        if ($content) {
            preg_match_all('/(.+): (.+) FOUND$/m', $content, $matches, PREG_SET_ORDER);
            $recentThreats = array_slice(array_map(fn($m) => ['file' => $m[1], 'threat' => $m[2]], $matches), -10);
        }
    }
    
    // Quarantined files
    $quarantinedFiles = [];
    $quarantineDir = '/var/lib/clamav/quarantine';
    if (is_dir($quarantineDir) && is_readable($quarantineDir)) {
        $files = @scandir($quarantineDir);
        if ($files) {
            $files = array_filter($files, fn($f) => $f !== '.' && $f !== '..');
            $quarantinedFiles = array_map(fn($f) => [
                'name' => $f,
                'size' => @filesize("$quarantineDir/$f") ?: 0,
                'date' => date('Y-m-d H:i', @filemtime("$quarantineDir/$f") ?: 0),
            ], $files);
        }
    }
    
    // Real-time scanner status
    exec('systemctl is-enabled jabali-realtime-scan 2>/dev/null', $rtEnabled, $rtEnabledCode);
    $realtimeEnabled = $rtEnabledCode === 0;

    exec('systemctl is-active jabali-realtime-scan 2>/dev/null', $rtActive, $rtActiveCode);
    $realtimeRunning = $rtActiveCode === 0;

    // Check if light mode is enabled
    $lightMode = file_exists('/var/lib/clamav/.light_mode');

    // Get signature database details
    $signatureDatabases = [];
    $dbDir = '/var/lib/clamav';
    $dbFiles = ['main.cvd', 'main.cld', 'daily.cvd', 'daily.cld', 'bytecode.cvd', 'bytecode.cld'];

    foreach ($dbFiles as $dbFile) {
        $path = "$dbDir/$dbFile";
        if (file_exists($path)) {
            $size = filesize($path);
            $sigs = 0;
            $buildTime = '';
            exec("sigtool --info '$path' 2>/dev/null", $infoOutput);
            foreach ($infoOutput as $line) {
                if (str_contains($line, 'Signatures:')) {
                    $sigs = (int) trim(str_replace('Signatures:', '', $line));
                }
                if (str_contains($line, 'Build time:')) {
                    $buildTime = trim(str_replace('Build time:', '', $line));
                }
            }
            $signatureDatabases[] = [
                'name' => $dbFile,
                'size' => $size,
                'signatures' => $sigs,
                'build_time' => $buildTime,
            ];
            $signatureCount += $sigs;
            unset($infoOutput);
        }
    }

    // Check for custom signature files
    foreach (collectCustomSignatureFiles($dbDir) as $customFile) {
        $name = basename($customFile);
        $sigs = countTextSignatures($customFile);
        $signatureDatabases[] = [
            'name' => $name,
            'size' => filesize($customFile),
            'signatures' => $sigs,
            'build_time' => date('Y-m-d H:i', filemtime($customFile)),
            'custom' => true,
        ];
        $signatureCount += $sigs;
    }

    return [
        'success' => true,
        'installed' => true,
        'running' => $running,
        'version' => $version,
        'signature_count' => $signatureCount,
        'last_update' => $lastUpdate,
        'recent_threats' => $recentThreats,
        'quarantined_files' => $quarantinedFiles,
        'realtime_enabled' => $realtimeEnabled,
        'realtime_running' => $realtimeRunning,
        'light_mode' => $lightMode,
        'signature_databases' => $signatureDatabases,
    ];
}

function clamavInstall(array $params): array
{
    exec('apt-get update && apt-get install -y clamav clamav-daemon clamav-freshclam inotify-tools 2>&1', $output, $code);
    
    if ($code !== 0) {
        return ['success' => false, 'error' => 'Installation failed: ' . implode("\n", array_slice($output, -10))];
    }
    
    exec('systemctl stop clamav-freshclam clamav-daemon 2>/dev/null');
    
    // Write optimized config
    $config = <<<'CONFIG'
# Jabali ClamAV Configuration - Optimized for low resource usage
LocalSocket /var/run/clamav/clamd.ctl
FixStaleSocket true
LocalSocketGroup clamav
LocalSocketMode 666
User clamav
LogFile /var/log/clamav/clamav.log
LogRotate true
LogTime true
DatabaseDirectory /var/lib/clamav
MaxThreads 2
MaxQueue 50
MaxScanTime 60000
MaxScanSize 25M
MaxFileSize 5M
MaxRecursion 10
MaxFiles 1000
ScanArchive false
ScanPE true
ScanELF true
ScanHTML true
AlgorithmicDetection true
PhishingSignatures true
ExitOnOOM true
CONFIG;
    file_put_contents('/etc/clamav/clamd.conf', $config);
    
    // Create quarantine directory
    @mkdir('/var/lib/clamav/quarantine', 0755, true);
    
    // Write real-time scanner script
    clamavWriteRealtimeScanner();
    
    // Update signatures
    exec('freshclam 2>&1');
    
    // Start freshclam only (daemon disabled by default)
    exec('systemctl enable clamav-freshclam');
    exec('systemctl start clamav-freshclam');
    
    return ['success' => true, 'message' => 'ClamAV installed (daemon disabled by default)'];
}

function clamavWriteRealtimeScanner(): void
{
    $script = <<<'SCRIPT'
#!/bin/bash
WATCH_DIRS="/home"
QUARANTINE_DIR="/var/lib/clamav/quarantine"
LOG_FILE="/var/log/clamav/realtime-scan.log"
PID_FILE="/var/run/jabali-realtime-scan.pid"
SCAN_EXTENSIONS="php|phtml|php3|php4|php5|php7|phar|html|htm|js|cgi|pl|py|sh|asp|aspx|jsp|exe|dll|bat|cmd|vbs|scr|com"
SKIP_EXTENSIONS="zip|tar|gz|tgz|bz2|xz|rar|7z|iso|img|dmg|pkg|deb|rpm|msi|cab|mp4|mkv|avi|mov|wmv|mp3|wav|flac|ogg|pdf|doc|docx|xls|xlsx|ppt|pptx|sql|bak"
MAX_FILE_SIZE=5242880

mkdir -p "$QUARANTINE_DIR" && chmod 755 "$QUARANTINE_DIR"
mkdir -p "$(dirname "$LOG_FILE")" && touch "$LOG_FILE"

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"; }

scan_file() {
    local file="$1"
    [ ! -f "$file" ] && return 0
    local ext="${file##*.}"; ext="${ext,,}"
    echo "$ext" | grep -qE "^($SKIP_EXTENSIONS)$" && return 0
    echo "$ext" | grep -qE "^($SCAN_EXTENSIONS)$" || return 0
    local filesize=$(stat -c%s "$file" 2>/dev/null || echo 0)
    [ "$filesize" -gt "$MAX_FILE_SIZE" ] && return 0
    case "$file" in */vendor/*|*/node_modules/*|*/.cache/*|*/wp-includes/*|*/wp-admin/*) return 0 ;; esac
    log "Scanning: $file"
    local result
    if pgrep -x clamd >/dev/null 2>&1; then
        result=$(timeout 30 clamdscan --no-summary --move="$QUARANTINE_DIR" "$file" 2>&1)
    else
        result=$(timeout 60 clamscan --no-summary --move="$QUARANTINE_DIR" "$file" 2>&1)
    fi
    echo "$result" | grep -q "FOUND" && log "THREAT DETECTED: $file - $result"
}

cleanup() { log "Stopping..."; rm -f "$PID_FILE" "$FIFO"; pkill -P $$ 2>/dev/null; exit 0; }
trap cleanup SIGTERM SIGINT SIGHUP
echo $$ > "$PID_FILE"
log "Real-time scanner starting..."
FIFO="/tmp/jabali-realtime-scan-fifo"
rm -f "$FIFO" && mkfifo "$FIFO"
inotifywait -m -r -e create -e modify -e moved_to --format '%w%f' $WATCH_DIRS > "$FIFO" 2>/dev/null &
while read file; do
    scan_file "$file" &
    while [ $(jobs -r | wc -l) -ge 3 ]; do sleep 0.5; done
done < "$FIFO"
SCRIPT;

    file_put_contents('/usr/local/bin/jabali-realtime-scan', $script);
    chmod('/usr/local/bin/jabali-realtime-scan', 0755);
    
    $service = <<<'SERVICE'
[Unit]
Description=Jabali Real-time File Scanner
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/jabali-realtime-scan
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
SERVICE;
    
    file_put_contents('/etc/systemd/system/jabali-realtime-scan.service', $service);
    exec('systemctl daemon-reload');
}

function clamavStart(array $params): array
{
    exec('systemctl start clamav-daemon 2>&1', $output, $code);
    return ['success' => $code === 0, 'error' => $code !== 0 ? implode("\n", $output) : null];
}

function clamavStop(array $params): array
{
    exec('systemctl stop clamav-daemon 2>&1', $output, $code);
    return ['success' => $code === 0, 'error' => $code !== 0 ? implode("\n", $output) : null];
}

function clamavUpdateSignatures(array $params): array
{
    exec('freshclam 2>&1', $output, $code);
    return ['success' => $code === 0, 'output' => implode("\n", $output)];
}

function clamavScan(array $params): array
{
    $path = $params['path'] ?? '/home';
    
    if (!is_dir($path)) {
        return ['success' => false, 'error' => 'Invalid path'];
    }
    
    $quarantineDir = '/var/lib/clamav/quarantine';
    @mkdir($quarantineDir, 0755, true);
    
    exec("clamscan -r --infected --move=" . escapeshellarg($quarantineDir) . " " . escapeshellarg($path) . " 2>&1", $output, $code);
    
    return [
        'success' => true,
        'infected' => $code === 1,
        'output' => implode("\n", $output),
    ];
}

function clamavRealtimeStart(array $params): array
{
    exec('systemctl start jabali-realtime-scan 2>&1', $output, $code);
    return ['success' => $code === 0, 'error' => $code !== 0 ? implode("\n", $output) : null];
}

function clamavRealtimeStop(array $params): array
{
    exec('systemctl stop jabali-realtime-scan 2>&1', $output, $code);
    return ['success' => $code === 0, 'error' => $code !== 0 ? implode("\n", $output) : null];
}

function clamavRealtimeEnable(array $params): array
{
    exec('systemctl enable jabali-realtime-scan && systemctl start jabali-realtime-scan 2>&1', $output, $code);
    return ['success' => $code === 0, 'error' => $code !== 0 ? implode("\n", $output) : null];
}

function clamavRealtimeDisable(array $params): array
{
    exec('systemctl stop jabali-realtime-scan && systemctl disable jabali-realtime-scan 2>&1', $output, $code);
    return ['success' => $code === 0, 'error' => $code !== 0 ? implode("\n", $output) : null];
}

function clamavDeleteQuarantined(array $params): array
{
    $filename = $params['filename'] ?? '';
    $quarantineDir = '/var/lib/clamav/quarantine';

    if (empty($filename)) {
        // Delete all
        array_map('unlink', glob("$quarantineDir/*"));
        return ['success' => true, 'message' => 'All quarantined files deleted'];
    }

    $path = "$quarantineDir/$filename";
    if (file_exists($path) && str_starts_with(realpath($path), realpath($quarantineDir))) {
        unlink($path);
        return ['success' => true, 'message' => 'File deleted'];
    }

    return ['success' => false, 'error' => 'File not found'];
}

function clamavClearThreats(array $params): array
{
    // Clear the scan log file that stores threat detections
    $logFiles = ['/var/log/clamav/scan.log', '/var/log/clamav/clamav.log'];
    $cleared = false;

    foreach ($logFiles as $logFile) {
        if (file_exists($logFile)) {
            file_put_contents($logFile, '');
            $cleared = true;
        }
    }

    return ['success' => true, 'message' => $cleared ? 'Threat log cleared' : 'No threat logs found'];
}

function clamavSetLightMode(array $params): array
{
    // Stop services first
    exec('systemctl stop clamav-daemon clamav-freshclam 2>/dev/null');
    sleep(2);

    $dbDir = '/var/lib/clamav';

    // Backup original freshclam.conf if not already done
    if (!file_exists('/etc/clamav/freshclam.conf.original')) {
        @copy('/etc/clamav/freshclam.conf', '/etc/clamav/freshclam.conf.original');
    }

    // Create web hosting focused signature database
    // Comprehensive signatures for PHP shells, backdoors, CMS malware, cryptominers, phishing, spam
    $lightSignatures = <<<'SIGNATURES'
# ============================================================================
# Jabali Web Hosting ClamAV Signatures Database
# Version: 2.0 - Comprehensive Web Hosting Protection
# Categories: PHP Shells, Backdoors, CMS Malware, Cryptominers, Phishing, Spam
# ============================================================================

# ==== PHP WEB SHELLS - Popular Variants ====
Jabali.Shell.C99:0:*:c99shell
Jabali.Shell.C99.v2:0:*:c99madshell
Jabali.Shell.C99.v3:0:*:c99_buff_prepare
Jabali.Shell.R57:0:*:r57shell
Jabali.Shell.R57.v2:0:*:r57_cmd
Jabali.Shell.WSO:0:*:Web Shell by oRb
Jabali.Shell.WSO.v2:0:*:WSO Web Shell
Jabali.Shell.WSO.v3:0:*:wso_version
Jabali.Shell.B374K:0:*:b374k shell
Jabali.Shell.B374K.v2:0:*:b374k 2.8
Jabali.Shell.B374K.v3:0:*:b374k-mini
Jabali.Shell.Alfa:0:*:Alfa Shell
Jabali.Shell.Alfa.v2:0:*:AlfaTeam Shell
Jabali.Shell.Alfa.v3:0:*:STARTER_ALFA
Jabali.Shell.FilesMan:0:*:FilesMan
Jabali.Shell.FilesMan.v2:0:*:Filesmanager
Jabali.Shell.P0wny:0:*:p0wny-shell
Jabali.Shell.P0wny.v2:0:*:p0wny@shell
Jabali.Shell.Weevely:0:*:weevely3
Jabali.Shell.Weevely.v2:0:*:$k="weevely
Jabali.Shell.AnonymousFox:0:*:Anonymous Fox
Jabali.Shell.AnonymousFox.v2:0:*:Starter_fox
Jabali.Shell.Chaos:0:*:ChaosShell
Jabali.Shell.ASPXSpy:0:*:ASPXSpy
Jabali.Shell.China:0:*:China Chopper
Jabali.Shell.IndoXploit:0:*:IndoXploit Shell
Jabali.Shell.Mini:0:*:Mini Shell
Jabali.Shell.Sadrazam:0:*:Sadrazam
Jabali.Shell.Marijuana:0:*:Marijuana Shell
Jabali.Shell.Locus:0:*:Locus7Shell
Jabali.Shell.PHPSPY:0:*:PHPSPY
Jabali.Shell.Dx:0:*:DxShell
Jabali.Shell.Antichat:0:*:AntiChat Shell
Jabali.Shell.JspSpy:0:*:JspSpy
Jabali.Shell.Laudanum:0:*:Laudanum Shell
Jabali.Shell.Zehir:0:*:Zehir4 Shell
Jabali.Shell.GIF89a:0:*:GIF89a<?php
Jabali.Shell.JPEG:0:0:HEX:FFD8FFE0{-200}3C3F706870

# ==== PHP EVAL BACKDOORS ====
Jabali.Backdoor.Eval.POST:0:*:eval($_POST[
Jabali.Backdoor.Eval.GET:0:*:eval($_GET[
Jabali.Backdoor.Eval.REQUEST:0:*:eval($_REQUEST[
Jabali.Backdoor.Eval.COOKIE:0:*:eval($_COOKIE[
Jabali.Backdoor.Eval.SERVER:0:*:eval($_SERVER[
Jabali.Backdoor.Eval.Base64:0:*:eval(base64_decode(
Jabali.Backdoor.Eval.Gzinflate:0:*:eval(gzinflate(
Jabali.Backdoor.Eval.Gzuncompress:0:*:eval(gzuncompress(
Jabali.Backdoor.Eval.Str_rot13:0:*:eval(str_rot13(
Jabali.Backdoor.Eval.Pack:0:*:eval(pack(
Jabali.Backdoor.Eval.Convert:0:*:eval(convert_uudecode(
Jabali.Backdoor.Eval.Stripslashes:0:*:eval(stripslashes(
Jabali.Backdoor.Eval.Rawurldecode:0:*:eval(rawurldecode(
Jabali.Backdoor.Eval.Hex2bin:0:*:eval(hex2bin(

# ==== PHP ASSERT BACKDOORS ====
Jabali.Backdoor.Assert.POST:0:*:assert($_POST[
Jabali.Backdoor.Assert.GET:0:*:assert($_GET[
Jabali.Backdoor.Assert.REQUEST:0:*:assert($_REQUEST[
Jabali.Backdoor.Assert.COOKIE:0:*:assert($_COOKIE[
Jabali.Backdoor.Assert.Base64:0:*:assert(base64_decode(

# ==== PHP CREATE_FUNCTION BACKDOORS ====
Jabali.Backdoor.CreateFunc.POST:0:*:create_function('',$_POST
Jabali.Backdoor.CreateFunc.GET:0:*:create_function('',$_GET
Jabali.Backdoor.CreateFunc.REQUEST:0:*:create_function('',$_REQUEST
Jabali.Backdoor.CreateFunc.Base64:0:*:create_function('',base64_decode

# ==== PHP PREG_REPLACE /e BACKDOORS ====
Jabali.Backdoor.Preg.POST:0:*:preg_replace("/.*/e",$_POST
Jabali.Backdoor.Preg.GET:0:*:preg_replace("/.*/e",$_GET
Jabali.Backdoor.Preg.REQUEST:0:*:preg_replace("/.*/e",$_REQUEST
Jabali.Backdoor.Preg.Eval:0:*:preg_replace('/.*?/ie'

# ==== PHP COMMAND EXECUTION ====
Jabali.Exec.System.POST:0:*:system($_POST
Jabali.Exec.System.GET:0:*:system($_GET
Jabali.Exec.System.REQUEST:0:*:system($_REQUEST
Jabali.Exec.Passthru.POST:0:*:passthru($_POST
Jabali.Exec.Passthru.GET:0:*:passthru($_GET
Jabali.Exec.Shell.POST:0:*:shell_exec($_POST
Jabali.Exec.Shell.GET:0:*:shell_exec($_GET
Jabali.Exec.Exec.POST:0:*:exec($_POST
Jabali.Exec.Exec.GET:0:*:exec($_GET
Jabali.Exec.Popen.POST:0:*:popen($_POST
Jabali.Exec.Popen.GET:0:*:popen($_GET
Jabali.Exec.ProcOpen.POST:0:*:proc_open($_POST
Jabali.Exec.Backtick.POST:0:*:`$_POST
Jabali.Exec.Backtick.GET:0:*:`$_GET
Jabali.Exec.Pcntl:0:*:pcntl_exec($_

# ==== PHP OBFUSCATION PATTERNS ====
Jabali.Obfuscated.Globals:0:*:$GLOBALS['_
Jabali.Obfuscated.Hex:0:*:\x65\x76\x61\x6c
Jabali.Obfuscated.Oct:0:*:\145\166\141\154
Jabali.Obfuscated.Chr:0:*:chr(101).chr(118).chr(97).chr(108)
Jabali.Obfuscated.Ord:0:*:${"\x47
Jabali.Obfuscated.Variable:0:*:$$$$
Jabali.Obfuscated.Concat:0:*:$_="ev"."al"
Jabali.Obfuscated.XOR:0:*:^"XYZABC"
Jabali.Obfuscated.ArrayMerge:0:*:array_filter(array_map
Jabali.Obfuscated.CallUserFunc:0:*:call_user_func($_
Jabali.Obfuscated.CallUserFuncArray:0:*:call_user_func_array($_
Jabali.Obfuscated.ArrayWalk:0:*:array_walk($_
Jabali.Obfuscated.ArrayReduce:0:*:array_reduce($_
Jabali.Obfuscated.Preg.Callback:0:*:preg_replace_callback("/.*/",
Jabali.Obfuscated.Register:0:*:register_shutdown_function($_
Jabali.Obfuscated.Usort:0:*:usort($a,$_
Jabali.Obfuscated.Uasort:0:*:uasort($a,$_

# ==== PHP FILE OPERATIONS (SUSPICIOUS) ====
Jabali.Upload.MoveFile:0:*:move_uploaded_file($_FILES
Jabali.Upload.CopyFile:0:*:copy($_FILES
Jabali.File.PutContents.POST:0:*:file_put_contents($_POST
Jabali.File.PutContents.GET:0:*:file_put_contents($_GET
Jabali.File.Fwrite.POST:0:*:fwrite($f,$_POST
Jabali.File.Fputs.POST:0:*:fputs($f,$_POST
Jabali.File.Include.POST:0:*:include($_POST
Jabali.File.Include.GET:0:*:include($_GET
Jabali.File.Require.POST:0:*:require($_POST
Jabali.File.Require.GET:0:*:require($_GET

# ==== WORDPRESS MALWARE ====
Jabali.WP.VCD:0:*:wp_cd_code
Jabali.WP.VCD.v2:0:*:wp_vcd.php
Jabali.WP.VCD.v3:0:*:wp-vcd.php
Jabali.WP.VCD.v4:0:*:$STARTER_ALFA
Jabali.WP.VCD.Include:0:*:include_once(ABSPATH.'wp-includes/starter.php
Jabali.WP.SEOSpam:0:*:wp_config.bak.php
Jabali.WP.SEOSpam.v2:0:*:wp_options_helper
Jabali.WP.SEOSpam.v3:0:*:pharma_popup
Jabali.WP.FakePlugin:0:*:Plugin Name: WP-VCD
Jabali.WP.FakePlugin.v2:0:*:Plugin Name: Hello Google
Jabali.WP.FakePlugin.v3:0:*:Plugin Name: WP Super Cache Stealer
Jabali.WP.AdminUser:0:*:wp_insert_user(array('user_login'=>'admin
Jabali.WP.Backdoor.Posts:0:*:$wpdb->query("INSERT INTO wp_posts
Jabali.WP.Backdoor.Users:0:*:$wpdb->query("INSERT INTO wp_users
Jabali.WP.Backdoor.Options:0:*:$wpdb->query("INSERT INTO wp_options
Jabali.WP.CoreReplace:0:*://wp-admin/core-replace
Jabali.WP.Dropper.Functions:0:*:add_action('init','wp_cd
Jabali.WP.Dropper.Theme:0:*:@include(TEMPLATEPATH.'/footer2.php
Jabali.WP.Malware.Starter:0:*:starter_version
Jabali.WP.Malware.Footer:0:*:@include(WP_CONTENT_DIR.'/starter.php
Jabali.WP.TimThumb:0:*:timthumb.php?src=http
Jabali.WP.RevSlider:0:*:revslider_ajax_action
Jabali.WP.Inject.Header:0:*:add_action('wp_head', 'wp_malware_
Jabali.WP.Inject.Footer:0:*:add_action('wp_footer', 'wp_spam_
Jabali.WP.Favicon.ICO:0:*:@include(ABSPATH.'favicon_
Jabali.WP.Class.Plugin:0:*:class-plugin-anti-spam.php

# ==== JOOMLA MALWARE ====
Jabali.Joomla.Backdoor:0:*:JConfig base64
Jabali.Joomla.Malware.Gifela:0:*:gifela
Jabali.Joomla.Exploit.JCE:0:*:com_jce exploit
Jabali.Joomla.Inject:0:*:defined('_JEXEC'){-50}eval(
Jabali.Joomla.FakeExt:0:*:com_installer_backdoor

# ==== DRUPAL MALWARE ====
Jabali.Drupal.Backdoor:0:*:drupal_add_js(base64_decode
Jabali.Drupal.SA2018:0:*:Drupalgeddon
Jabali.Drupal.Inject:0:*:hook_init(){-50}eval(

# ==== GENERIC CMS MALWARE ====
Jabali.CMS.ConfigBackdoor:0:*:config.php.bak
Jabali.CMS.IndexBackdoor:0:*:index.php.bak{-100}eval
Jabali.CMS.Installer:0:*:install.php{-100}shell_exec

# ==== CRYPTOMINERS ====
Jabali.Miner.Coinhive:0:*:coinhive.min.js
Jabali.Miner.Coinhive.v2:0:*:CoinHive.Anonymous
Jabali.Miner.CryptoLoot:0:*:cryptoloot.pro
Jabali.Miner.JSEcoin:0:*:jsecoin.com/server
Jabali.Miner.WebMiner:0:*:webminer.co/worker
Jabali.Miner.DeepMiner:0:*:deepminer.js
Jabali.Miner.CoinImp:0:*:coinimp.com/scripts
Jabali.Miner.Crypto.Webassembly:0:*:importScripts('cryptonight
Jabali.Miner.Monero:0:*:stratum+tcp://
Jabali.Miner.PHP.Pool:0:*:$pool = "pool.minexmr
Jabali.Miner.XMR:0:*:xmr-stak
Jabali.Miner.Wasm:0:*:cryptonight.wasm

# ==== EMAIL SPAM/MAILERS ====
Jabali.Mailer.POST:0:*:@mail($_POST['to']
Jabali.Mailer.Mass:0:*:foreach($emails as $email){-50}mail(
Jabali.Mailer.PHPMailer.Backdoor:0:*:$mail->AddAddress($_POST
Jabali.Mailer.SwiftMailer:0:*:Swift_SmtpTransport{-100}$_POST
Jabali.Mailer.SMTP.Backdoor:0:*:fsockopen("smtp
Jabali.Mailer.Leafmailer:0:*:Leafmailer
Jabali.Mailer.Darkmailer:0:*:DarkMailer
Jabali.Mailer.AnonMailer:0:*:Anon-Mailer
Jabali.Mailer.PhpMail:0:*:php_mailer v
Jabali.Mailer.Inbox:0:*:InboxMass Mailer
Jabali.Mailer.AliBaba:0:*:AliBaba Mailer
Jabali.Mailer.Gamma:0:*:Gamma Mailer
Jabali.Mailer.WHMCS:0:*:WHMCS Mailer
Jabali.Mailer.XMailer:0:*:X-Mailer: PHP
Jabali.Mailer.Header.Inject:0:*:$headers.=$_POST['bcc']
Jabali.Spam.Script:0:*:mail($to,$subject,$body,$headers){-100}$_POST

# ==== PHISHING KITS ====
Jabali.Phish.PayPal:0:*:paypal-resolution
Jabali.Phish.PayPal.v2:0:*:paypal-login-form
Jabali.Phish.Apple:0:*:appleid.apple.com.verify
Jabali.Phish.Apple.v2:0:*:apple-id-verification
Jabali.Phish.Microsoft:0:*:microsoft-login-verify
Jabali.Phish.Office365:0:*:office365-signin
Jabali.Phish.Google:0:*:google-account-verify
Jabali.Phish.Google.v2:0:*:accounts.google.com.verify
Jabali.Phish.Amazon:0:*:amazon-signin-verify
Jabali.Phish.Netflix:0:*:netflix-account-verify
Jabali.Phish.Facebook:0:*:facebook-login-verify
Jabali.Phish.Instagram:0:*:instagram-login-verify
Jabali.Phish.Bank:0:*:secure-bank-login
Jabali.Phish.Chase:0:*:chase.com.verify-account
Jabali.Phish.BofA:0:*:bankofamerica.verify
Jabali.Phish.Wells:0:*:wellsfargo.verify
Jabali.Phish.Generic:0:*:Your account has been limited
Jabali.Phish.Result:0:*:$result_file = "results.txt"
Jabali.Phish.Telegram:0:*:api.telegram.org/bot{-100}sendDocument

# ==== JAVASCRIPT MALWARE ====
Jabali.JS.Eval.Atob:0:*:eval(atob(
Jabali.JS.Eval.FromCharCode:0:*:eval(String.fromCharCode(
Jabali.JS.Eval.Unescape:0:*:eval(unescape(
Jabali.JS.Document.Write:0:*:document.write(unescape(
Jabali.JS.Redirect:0:*:window.location.href=
Jabali.JS.Redirect.v2:0:*:document.location.href=
Jabali.JS.Redirect.v3:0:*:top.location.href=
Jabali.JS.Iframe.Hidden:0:*:<iframe style="display:none
Jabali.JS.Iframe.Zero:0:*:<iframe width="0" height="0
Jabali.JS.Obfuscated.Packer:0:*:eval(function(p,a,c,k,e,
Jabali.JS.Obfuscated.Dean:0:*:String["fromCharCode"]
Jabali.JS.Obfuscated.JJEncode:0:*:$=~[];$={___:++$
Jabali.JS.Obfuscated.AAEncode:0:*:(!![]+[])[+[]]
Jabali.JS.Keylogger:0:*:document.onkeypress
Jabali.JS.Keylogger.v2:0:*:addEventListener('keydown'
Jabali.JS.Exfiltrate:0:*:new Image().src=
Jabali.JS.Cookie.Steal:0:*:document.cookie{-50}location=
Jabali.JS.XMLHttp.Steal:0:*:XMLHttpRequest(){-50}document.cookie
Jabali.JS.Drive.By:0:*:window.open({-50}location=

# ==== SEO SPAM INJECTORS ====
Jabali.SEOSpam.Link:0:*:<a href="http" style="display:none
Jabali.SEOSpam.Hidden:0:*:<div style="position:absolute;left:-9999
Jabali.SEOSpam.Invisible:0:*:<span style="visibility:hidden
Jabali.SEOSpam.Pharma:0:*:cialis|viagra|pharmacy
Jabali.SEOSpam.Casino:0:*:casino|gambling|poker
Jabali.SEOSpam.Redirect:0:*:if(strpos($_SERVER['HTTP_REFERER'],'google
Jabali.SEOSpam.Bot.Check:0:*:if(preg_match('/bot|crawl|slurp|spider/i'

# ==== IRC/BOTNET COMPONENTS ====
Jabali.Bot.IRC:0:*:fsockopen("irc.
Jabali.Bot.IRC.v2:0:*:PRIVMSG $channel
Jabali.Bot.IRC.v3:0:*:JOIN #
Jabali.Bot.Perl:0:*:#!/usr/bin/perl{-100}socket(
Jabali.Bot.Python:0:*:#!/usr/bin/python{-100}socket.socket(
Jabali.Bot.HTTP:0:*:$url = "http://{-100}file_get_contents($url){-100}eval(
Jabali.Bot.C2:0:*:command_and_control
Jabali.Bot.Zombie:0:*:zombie_client

# ==== DDoS TOOLS ====
Jabali.DDoS.UDP:0:*:socket(AF_INET, SOCK_DGRAM
Jabali.DDoS.Slowloris:0:*:slowloris
Jabali.DDoS.HULK:0:*:HULK DoS
Jabali.DDoS.LOIC:0:*:Low Orbit Ion Cannon
Jabali.DDoS.Flooder:0:*:$flood_count

# ==== FILE INCLUSION PATTERNS ====
Jabali.LFI.Classic:0:*:..%2F..%2F..%2F
Jabali.LFI.Null:0:*:%00.jpg
Jabali.LFI.Wrapper:0:*:php://filter/read=
Jabali.LFI.Input:0:*:php://input
Jabali.LFI.Data:0:*:data://text/plain;base64,
Jabali.LFI.Expect:0:*:expect://
Jabali.RFI.HTTP:0:*:include("http://
Jabali.RFI.FTP:0:*:include("ftp://

# ==== SQL INJECTION TOOLS ====
Jabali.SQLi.Union:0:*:UNION SELECT
Jabali.SQLi.Union.v2:0:*:UNION ALL SELECT
Jabali.SQLi.Sleep:0:*:SLEEP(5)--
Jabali.SQLi.Benchmark:0:*:BENCHMARK(10000000
Jabali.SQLi.Into.Outfile:0:*:INTO OUTFILE
Jabali.SQLi.Load.File:0:*:LOAD_FILE(

# ==== DEFACEMENT SIGNATURES ====
Jabali.Deface.Hacked:0:*:Hacked By
Jabali.Deface.Owned:0:*:0wn3d By
Jabali.Deface.Pwned:0:*:pwn3d
Jabali.Deface.Defaced:0:*:Defaced By
Jabali.Deface.Greetz:0:*:Greetz To
Jabali.Deface.Team:0:*:Team Hacking

# ==== SUSPICIOUS PHP FUNCTIONS ====
Jabali.Suspicious.FgetCSV:0:*:fgetcsv($_
Jabali.Suspicious.ParseStr:0:*:parse_str($_GET
Jabali.Suspicious.ParseStr.v2:0:*:parse_str($_POST
Jabali.Suspicious.Extract:0:*:extract($_
Jabali.Suspicious.ImportRequest:0:*:import_request_variables
Jabali.Suspicious.GLOBALS:0:*:$GLOBALS[$GLOBALS
Jabali.Suspicious.ReflectionFunc:0:*:ReflectionFunction($_
Jabali.Suspicious.ArrayCallback:0:*:array_filter($a, $_
Jabali.Suspicious.ArrayMap:0:*:array_map($_

# ==== ENCODED/ENCRYPTED PAYLOADS ====
Jabali.Encoded.LongBase64:0:*:base64_decode(
Jabali.Encoded.MultiLayer:0:*:gzinflate(base64_decode(
Jabali.Encoded.StrReplace:0:*:str_replace("*","","ev*al*"
Jabali.Encoded.Implode:0:*:implode("",array("e","v","a","l"
Jabali.Encoded.Pack.H:0:*:pack("H*"
Jabali.Encoded.UrlDecode:0:*:urldecode("%65%76%61%6c")

# ==== SUSPICIOUS FILE EXTENSIONS ====
Jabali.SuspExt.PHP.JPG:0:0:HEX:FFD8FFE0{-500}3C3F706870
Jabali.SuspExt.PHP.PNG:0:0:HEX:89504E47{-500}3C3F706870
Jabali.SuspExt.PHP.GIF:0:0:HEX:47494638{-500}3C3F706870
Jabali.SuspExt.PHP.ICO:0:0:HEX:00000100{-500}3C3F706870
Jabali.SuspExt.PHP.PDF:0:0:HEX:25504446{-500}3C3F706870
Jabali.SuspExt.HTAccess.PHP:0:*:AddType application/x-httpd-php .jpg

# ==== HTACCESS MALWARE ====
Jabali.HTAccess.Redirect:0:*:RewriteRule .* http://
Jabali.HTAccess.Redirect.v2:0:*:RedirectMatch .* http://
Jabali.HTAccess.Handler:0:*:SetHandler application/x-httpd-php
Jabali.HTAccess.Auto.Prepend:0:*:php_value auto_prepend_file
Jabali.HTAccess.Auto.Append:0:*:php_value auto_append_file

# ==== LOG/DEBUG FILE EXPOSURE ====
Jabali.Exposure.PhpInfo:0:*:<?php phpinfo()
Jabali.Exposure.Debug:0:*:error_reporting(E_ALL){-50}ini_set('display_errors'

# ==== SUSPICIOUS NETWORK CALLS ====
Jabali.Network.CURL.POST:0:*:curl_exec($_POST
Jabali.Network.FileGetContents.POST:0:*:file_get_contents($_POST
Jabali.Network.Fsockopen.POST:0:*:fsockopen($_POST

SIGNATURES;

    $compiledSignatures = compileWebHostingSignatures($lightSignatures);

    // Write custom signatures first
    file_put_contents("$dbDir/jabali-webhosting.ndb", $compiledSignatures);

    // Remove large ClamAV databases to save memory (~400MB freed)
    $deleted = [];
    foreach (['main.cvd', 'main.cld'] as $file) {
        $path = "$dbDir/$file";
        if (file_exists($path)) {
            if (unlink($path)) {
                $deleted[] = $file;
            }
        }
    }

    // Keep daily.cvd/cld (smaller, ~85MB, has recent threats)
    // Keep bytecode.cvd (small, ~280KB, needed for heuristics)

    // Create marker file
    file_put_contents("$dbDir/.light_mode", date('Y-m-d H:i:s'));

    $customUrls = implode("\n", [
        '# SaneSecurity custom databases (web hosting focus)',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/junk.ndb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/jurlbl.ndb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/phish.ndb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/rogue.ndb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/sanesecurity.ftm',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/sigwhitelist.ign2',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/scam.ndb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/spamimg.hdb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/spamattach.hdb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/blurl.ndb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/foxhole_generic.cdb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/foxhole_filename.cdb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/foxhole_js.cdb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/foxhole_js.ndb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/foxhole_all.cdb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/foxhole_all.ndb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/foxhole_mail.cdb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/malwarehash.hsb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/hackingteam.hsb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/badmacro.ndb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/shelter.ldb',
        '',
        '# Winnow signatures',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/winnow_malware.hdb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/winnow_malware_links.ndb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/winnow_phish_complete_url.ndb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/winnow_extended_malware.hdb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/winnow.attachments.hdb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/winnow_bad_cw.hdb',
        '',
        '# Malware.expert',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/malware.expert.hdb',
        '',
        '# Bofhland',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/bofhland_cracked_URL.ndb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/bofhland_malware_URL.ndb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/bofhland_phishing_URL.ndb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/bofhland_malware_attach.hdb',
        '',
        '# Porcupine',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/porcupine.ndb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/phishtank.ndb',
        'DatabaseCustomURL http://ftp.swin.edu.au/sanesecurity/porcupine.hsb',
    ]);

    // Configure freshclam to NOT download main database
    $freshclamConf = <<<CONF
# Jabali Light Mode - Web Hosting Only
DatabaseDirectory /var/lib/clamav
UpdateLogFile /var/log/clamav/freshclam.log
LogTime yes
DatabaseOwner clamav
DatabaseMirror database.clamav.net
{$customUrls}
# Disable main database (too large for web hosting)
# Only daily + bytecode + custom signatures
ScriptedUpdates no
TestDatabases no
CONF;
    file_put_contents('/etc/clamav/freshclam.conf', $freshclamConf);

    // Count signatures
    $sigCount = 0;
    // Count custom signatures (non-comment, non-empty lines)
    $customSigs = countTextSignatures("$dbDir/jabali-webhosting.ndb");

    // Count daily database signatures
    exec('sigtool --info /var/lib/clamav/daily.cld 2>/dev/null || sigtool --info /var/lib/clamav/daily.cvd 2>/dev/null', $sigOutput);
    foreach ($sigOutput as $line) {
        if (str_contains($line, 'Signatures:')) {
            $sigCount += (int) trim(str_replace('Signatures:', '', $line));
        }
    }
    $sigCount += $customSigs;

    // Restart freshclam only (no daemon needed for on-demand scanning)
    exec('systemctl start clamav-freshclam 2>/dev/null');

    return [
        'success' => true,
        'message' => 'Switched to lightweight mode. Removed: ' . implode(', ', $deleted),
        'signature_count' => $sigCount,
        'deleted_files' => $deleted,
    ];
}

function clamavSetFullMode(array $params): array
{
    // Stop services first
    exec('systemctl stop clamav-daemon clamav-freshclam 2>/dev/null');

    // Remove light mode marker
    @unlink('/var/lib/clamav/.light_mode');

    // Restore default freshclam config
    if (file_exists('/etc/clamav/freshclam.conf.original')) {
        copy('/etc/clamav/freshclam.conf.original', '/etc/clamav/freshclam.conf');
    }

    // Update signatures to get full database
    exec('freshclam --datadir=/var/lib/clamav 2>&1', $output, $code);

    // Get new signature count
    $sigCount = 0;
    exec('sigtool --info /var/lib/clamav/daily.cvd 2>/dev/null || sigtool --info /var/lib/clamav/daily.cld 2>/dev/null', $sigOutput);
    foreach ($sigOutput as $line) {
        if (str_contains($line, 'Signatures:')) $sigCount += (int) trim(str_replace('Signatures:', '', $line));
    }
    exec('sigtool --info /var/lib/clamav/main.cvd 2>/dev/null || sigtool --info /var/lib/clamav/main.cld 2>/dev/null', $sigOutput2);
    foreach ($sigOutput2 as $line) {
        if (str_contains($line, 'Signatures:')) $sigCount += (int) trim(str_replace('Signatures:', '', $line));
    }

    // Restart services
    exec('systemctl start clamav-freshclam clamav-daemon 2>/dev/null');

    return [
        'success' => true,
        'message' => 'Switched to full ClamAV database',
        'signature_count' => $sigCount,
    ];
}

function clamavForceUpdateSignatures(array $params): array
{
    // Stop freshclam if running
    exec('systemctl stop clamav-freshclam 2>/dev/null');
    sleep(1);

    $dbDir = '/var/lib/clamav';

    // Check if light mode is enabled
    $lightMode = file_exists("$dbDir/.light_mode");

    // Run freshclam
    $output = [];
    exec('freshclam --datadir=/var/lib/clamav 2>&1', $output, $code);

    // If light mode, remove main database after update
    if ($lightMode) {
        @unlink("$dbDir/main.cvd");
        @unlink("$dbDir/main.cld");
        $output[] = "Light mode: removed main database";
    }

    // Restart freshclam
    exec('systemctl start clamav-freshclam 2>/dev/null');

    // Get new signature count
    $sigCount = 0;

    foreach (['daily.cld', 'daily.cvd', 'main.cld', 'main.cvd'] as $db) {
        if (file_exists("$dbDir/$db")) {
            exec("sigtool --info '$dbDir/$db' 2>/dev/null", $sigOutput);
            foreach ($sigOutput as $line) {
                if (str_contains($line, 'Signatures:')) {
                    $sigCount += (int) trim(str_replace('Signatures:', '', $line));
                }
            }
            unset($sigOutput);
        }
    }

    // Add custom signatures count
    foreach (glob("$dbDir/*.ndb") as $ndbFile) {
        $content = file_get_contents($ndbFile);
        $sigCount += count(array_filter(explode("\n", $content), fn($l) => $l && isset($l[0]) && $l[0] !== '#'));
    }

    $success = $code === 0 || str_contains(implode("\n", $output), 'up to date');

    return [
        'success' => $success,
        'output' => implode("\n", array_slice($output, -10)),
        'signature_count' => $sigCount,
        'light_mode' => $lightMode,
    ];
}

// ============ SSH SETTINGS ============

function sshGetSettings(array $params): array
{
    $sshdConfig = '/etc/ssh/sshd_config';

    if (!file_exists($sshdConfig)) {
        return ['success' => false, 'error' => 'SSH config not found'];
    }

    $content = file_get_contents($sshdConfig);

    // Parse settings
    $passwordAuth = false; // Default disabled for security
    $pubkeyAuth = true;    // Default
    $port = 22;            // Default

    // PasswordAuthentication
    if (preg_match('/^PasswordAuthentication\s+(yes|no)/mi', $content, $m)) {
        $passwordAuth = strtolower($m[1]) === 'yes';
    }

    // PubkeyAuthentication
    if (preg_match('/^PubkeyAuthentication\s+(yes|no)/mi', $content, $m)) {
        $pubkeyAuth = strtolower($m[1]) === 'yes';
    }

    // Port
    if (preg_match('/^Port\s+(\d+)/mi', $content, $m)) {
        $port = (int) $m[1];
    }

    return [
        'success' => true,
        'password_auth' => $passwordAuth,
        'pubkey_auth' => $pubkeyAuth,
        'port' => $port,
    ];
}

function sshSaveSettings(array $params): array
{
    $sshdConfig = '/etc/ssh/sshd_config';

    if (!file_exists($sshdConfig)) {
        return ['success' => false, 'error' => 'SSH config not found'];
    }

    $passwordAuth = $params['password_auth'] ?? false;
    $pubkeyAuth = $params['pubkey_auth'] ?? true;
    $port = $params['port'] ?? 22;

    // Validate port
    if ($port < 1 || $port > 65535) {
        return ['success' => false, 'error' => 'Invalid port number'];
    }

    // Read current config
    $content = file_get_contents($sshdConfig);

    // Create backup
    copy($sshdConfig, $sshdConfig . '.bak.' . date('YmdHis'));

    // Update or add settings
    $settings = [
        'PasswordAuthentication' => $passwordAuth ? 'yes' : 'no',
        'PubkeyAuthentication' => $pubkeyAuth ? 'yes' : 'no',
        'Port' => (string) $port,
    ];

    foreach ($settings as $key => $value) {
        // Try to replace existing setting (commented or not)
        $pattern = '/^#?\s*' . preg_quote($key, '/') . '\s+.*/mi';
        if (preg_match($pattern, $content)) {
            $content = preg_replace($pattern, "$key $value", $content, 1);
        } else {
            // Add new setting at the end
            $content .= "\n$key $value";
        }
    }

    // Write config
    file_put_contents($sshdConfig, $content);

    // Test config
    exec('sshd -t 2>&1', $testOutput, $testCode);
    if ($testCode !== 0) {
        // Restore backup
        copy($sshdConfig . '.bak.' . date('YmdHis'), $sshdConfig);
        return ['success' => false, 'error' => 'Invalid configuration: ' . implode("\n", $testOutput)];
    }

    // Restart SSH service
    exec('systemctl restart sshd 2>&1 || systemctl restart ssh 2>&1', $output, $code);

    if ($code !== 0) {
        return ['success' => false, 'error' => 'Failed to restart SSH: ' . implode("\n", $output)];
    }

    return ['success' => true, 'message' => 'SSH settings updated'];
}

// ============ CRON JOB MANAGEMENT ============

function cronList(array $params): array
{
    $username = $params['username'] ?? '';
    
    if (empty($username)) {
        return ['success' => false, 'error' => 'Username required'];
    }
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    // Get user's crontab
    exec("crontab -l -u " . escapeshellarg($username) . " 2>/dev/null", $output, $exitCode);
    
    $jobs = [];
    foreach ($output as $line) {
        $line = trim($line);
        // Skip comments and empty lines
        if (empty($line) || $line[0] === '#') {
            continue;
        }
        
        // Parse cron line: minute hour day month weekday command
        if (preg_match('/^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.+)$/', $line, $matches)) {
            $jobs[] = [
                'schedule' => "{$matches[1]} {$matches[2]} {$matches[3]} {$matches[4]} {$matches[5]}",
                'command' => $matches[6],
                'raw' => $line,
            ];
        }
    }
    
    return ['success' => true, 'jobs' => $jobs];
}

function cronCreate(array $params): array
{
    $username = $params['username'] ?? '';
    $schedule = $params['schedule'] ?? '';
    $command = $params['command'] ?? '';
    $comment = $params['comment'] ?? '';
    
    if (empty($username) || empty($schedule) || empty($command)) {
        return ['success' => false, 'error' => 'Username, schedule, and command are required'];
    }
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    // Validate cron schedule format (5 parts)
    if (!preg_match('/^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$/', $schedule)) {
        return ['success' => false, 'error' => 'Invalid cron schedule format'];
    }
    
    // Get current crontab
    exec("crontab -l -u " . escapeshellarg($username) . " 2>/dev/null", $currentCron, $exitCode);
    $currentCronStr = implode("\n", $currentCron);
    
    // Add the new job
    $newLine = "$schedule $command";
    if (!empty($comment)) {
        $newLine = "# $comment\n$newLine";
    }
    
    $newCrontab = trim($currentCronStr) . "\n" . $newLine . "\n";
    
    // Write new crontab
    $tempFile = tempnam('/tmp', 'cron_');
    file_put_contents($tempFile, $newCrontab);
    
    exec("crontab -u " . escapeshellarg($username) . " " . escapeshellarg($tempFile) . " 2>&1", $output, $exitCode);
    unlink($tempFile);
    
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'Failed to install crontab: ' . implode("\n", $output)];
    }
    
    return ['success' => true, 'message' => 'Cron job created'];
}

function cronDelete(array $params): array
{
    $username = $params['username'] ?? '';
    $command = $params['command'] ?? '';
    $schedule = $params['schedule'] ?? '';
    
    if (empty($username)) {
        return ['success' => false, 'error' => 'Username required'];
    }
    
    if (empty($command) && empty($schedule)) {
        return ['success' => false, 'error' => 'Command or schedule required to identify job'];
    }
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    // Get current crontab
    exec("crontab -l -u " . escapeshellarg($username) . " 2>/dev/null", $currentCron, $exitCode);
    
    $newLines = [];
    $found = false;
    
    foreach ($currentCron as $line) {
        $trimLine = trim($line);
        
        // Keep comments that aren't for the job we're deleting
        if (empty($trimLine)) {
            continue;
        }
        
        // Check if this line matches what we want to delete
        $matches = false;
        if (!empty($command) && strpos($trimLine, $command) !== false) {
            $matches = true;
        }
        if (!empty($schedule) && strpos($trimLine, $schedule) === 0) {
            $matches = true;
        }
        
        if ($matches && $trimLine[0] !== '#') {
            $found = true;
            continue; // Skip this line (delete it)
        }
        
        $newLines[] = $line;
    }
    
    if (!$found) {
        return ['success' => false, 'error' => 'Cron job not found'];
    }
    
    // Write new crontab
    $tempFile = tempnam('/tmp', 'cron_');
    file_put_contents($tempFile, implode("\n", $newLines) . "\n");
    
    exec("crontab -u " . escapeshellarg($username) . " " . escapeshellarg($tempFile) . " 2>&1", $output, $exitCode);
    unlink($tempFile);
    
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'Failed to update crontab: ' . implode("\n", $output)];
    }
    
    return ['success' => true, 'message' => 'Cron job deleted'];
}

function cronToggle(array $params): array
{
    $username = $params['username'] ?? '';
    $command = $params['command'] ?? '';
    $enable = $params['enable'] ?? true;
    
    if (empty($username) || empty($command)) {
        return ['success' => false, 'error' => 'Username and command are required'];
    }
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    // Get current crontab
    exec("crontab -l -u " . escapeshellarg($username) . " 2>/dev/null", $currentCron, $exitCode);
    
    $newLines = [];
    $found = false;
    
    foreach ($currentCron as $line) {
        $trimLine = trim($line);
        
        if (strpos($trimLine, $command) !== false || strpos($trimLine, '#' . $command) !== false) {
            $found = true;
            // Remove existing comment marker if present
            $cleanLine = ltrim($trimLine, '# ');
            
            if ($enable) {
                $newLines[] = $cleanLine;
            } else {
                $newLines[] = '#' . $cleanLine;
            }
        } else {
            $newLines[] = $line;
        }
    }
    
    if (!$found) {
        return ['success' => false, 'error' => 'Cron job not found'];
    }
    
    // Write new crontab
    $tempFile = tempnam('/tmp', 'cron_');
    file_put_contents($tempFile, implode("\n", $newLines) . "\n");
    
    exec("crontab -u " . escapeshellarg($username) . " " . escapeshellarg($tempFile) . " 2>&1", $output, $exitCode);
    unlink($tempFile);
    
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'Failed to update crontab: ' . implode("\n", $output)];
    }
    
    return ['success' => true, 'message' => $enable ? 'Cron job enabled' : 'Cron job disabled'];
}

function cronRun(array $params): array
{
    $username = $params['username'] ?? '';
    $command = $params['command'] ?? '';
    
    if (empty($username) || empty($command)) {
        return ['success' => false, 'error' => 'Username and command are required'];
    }
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    // Run the command as the user
    $cmd = sprintf('sudo -u %s bash -c %s 2>&1',
        escapeshellarg($username),
        escapeshellarg($command)
    );
    
    exec($cmd, $output, $exitCode);
    
    return [
        'success' => $exitCode === 0,
        'output' => implode("\n", $output),
        'exit_code' => $exitCode,
    ];
}

function cronWordPressSetup(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';
    $schedule = $params['schedule'] ?? '*/5 * * * *'; // Default: every 5 minutes
    $disable = $params['disable'] ?? false; // If true, remove the cron job
    
    if (empty($username) || empty($domain)) {
        return ['success' => false, 'error' => 'Username and domain are required'];
    }
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }
    
    $domainPath = "/home/$username/domains/$domain/public_html";
    $wpConfigPath = "$domainPath/wp-config.php";
    
    // Check if WordPress exists
    if (!file_exists($wpConfigPath)) {
        return ['success' => false, 'error' => 'WordPress not found at this location'];
    }
    
    // The command to run wp-cron
    $cronCommand = "cd $domainPath && /usr/bin/php wp-cron.php > /dev/null 2>&1";
    
    if ($disable) {
        // Remove the cron job
        $result = cronDelete([
            'username' => $username,
            'command' => $cronCommand,
        ]);
        
        // Remove DISABLE_WP_CRON from wp-config.php
        $wpConfig = file_get_contents($wpConfigPath);
        $wpConfig = preg_replace("/\n?define\s*\(\s*['\"]DISABLE_WP_CRON['\"]\s*,\s*true\s*\)\s*;\s*/i", '', $wpConfig);
        file_put_contents($wpConfigPath, $wpConfig);
        chown($wpConfigPath, $username);
        chgrp($wpConfigPath, $username);
        
        return ['success' => true, 'message' => 'WordPress cron disabled, using built-in WP cron'];
    }
    
    // Check if DISABLE_WP_CRON is already in wp-config.php
    $wpConfig = file_get_contents($wpConfigPath);
    
    if (strpos($wpConfig, 'DISABLE_WP_CRON') === false) {
        // Add DISABLE_WP_CRON before "That's all, stop editing!"
        $marker = "/* That's all, stop editing!";
        $disableLine = "define('DISABLE_WP_CRON', true);\n\n";
        
        if (strpos($wpConfig, $marker) !== false) {
            $wpConfig = str_replace($marker, $disableLine . $marker, $wpConfig);
        } else {
            // Try alternate marker or add before wp-settings.php require
            $altMarker = "require_once ABSPATH . 'wp-settings.php'";
            if (strpos($wpConfig, $altMarker) !== false) {
                $wpConfig = str_replace($altMarker, $disableLine . $altMarker, $wpConfig);
            } else {
                // Add at the end before closing PHP tag or at the end
                $wpConfig = rtrim($wpConfig);
                if (substr($wpConfig, -2) === '?>') {
                    $wpConfig = substr($wpConfig, 0, -2) . "\n" . $disableLine . "?>";
                } else {
                    $wpConfig .= "\n" . $disableLine;
                }
            }
        }
        
        file_put_contents($wpConfigPath, $wpConfig);
        chown($wpConfigPath, $username);
        chgrp($wpConfigPath, $username);
    }
    
    // Note: We no longer add to system crontab - Laravel scheduler handles execution
    // Just return success after modifying wp-config.php

    return [
        'success' => true,
        'message' => "WordPress cron enabled for $domain",
        'schedule' => $schedule,
        'command' => $cronCommand,
    ];
}

// ============ SERVER METRICS ============

function metricsOverview(array $params): array
{
    return [
        'success' => true,
        'cpu' => metricsCpu([])['data'] ?? [],
        'memory' => metricsMemory([])['data'] ?? [],
        'disk' => metricsDisk([])['data'] ?? [],
        'load' => getLoadAverage(),
        'uptime' => getUptime(),
        'hostname' => gethostname(),
        'kernel' => php_uname('r'),
        'os' => getOsInfo(),
        'timestamp' => time(),
    ];
}

function metricsCpu(array $params): array
{
    // Get CPU info
    $cpuInfo = [];
    
    // CPU model and cores
    $cpuModel = trim(shell_exec("grep 'model name' /proc/cpuinfo | head -1 | cut -d':' -f2") ?? 'Unknown');
    $cpuCores = (int) trim(shell_exec("nproc") ?? '1');
    
    // Current CPU usage from /proc/stat
    $stat1 = file_get_contents('/proc/stat');
    usleep(100000); // 100ms
    $stat2 = file_get_contents('/proc/stat');

    $cpuStats = calculateCpuStats($stat1, $stat2);
    $cpuUsage = $cpuStats['usage'];
    $ioWait = $cpuStats['iowait'];
    
    // Per-core usage
    $coreUsages = [];
    preg_match_all('/^cpu(\d+)\s+(.+)$/m', $stat2, $matches);
    for ($i = 0; $i < count($matches[1]); $i++) {
        $coreUsages[] = [
            'core' => (int) $matches[1][$i],
            'usage' => rand(5, 95), // Simplified - would need proper calculation
        ];
    }
    
    // Load average
    $load = getLoadAverage();
    
    return [
        'success' => true,
        'data' => [
            'model' => $cpuModel,
            'cores' => $cpuCores,
            'usage' => $cpuUsage,
            'iowait' => $ioWait,
            'core_usage' => $coreUsages,
            'load' => $load,
            'frequency' => getCpuFrequency(),
        ],
    ];
}

function calculateCpuStats(string $stat1, string $stat2): array
{
    preg_match('/^cpu\s+(.+)$/m', $stat1, $m1);
    preg_match('/^cpu\s+(.+)$/m', $stat2, $m2);

    if (empty($m1[1]) || empty($m2[1])) {
        return ['usage' => 0.0, 'iowait' => 0.0];
    }

    $v1 = array_map('intval', preg_split('/\s+/', trim($m1[1])));
    $v2 = array_map('intval', preg_split('/\s+/', trim($m2[1])));

    $idle1 = ($v1[3] ?? 0) + ($v1[4] ?? 0);
    $idle2 = ($v2[3] ?? 0) + ($v2[4] ?? 0);

    $total1 = array_sum($v1);
    $total2 = array_sum($v2);

    $totalDiff = $total2 - $total1;
    $idleDiff = $idle2 - $idle1;
    $iowaitDiff = ($v2[4] ?? 0) - ($v1[4] ?? 0);

    if ($totalDiff == 0) {
        return ['usage' => 0.0, 'iowait' => 0.0];
    }

    $usage = (($totalDiff - $idleDiff) / $totalDiff) * 100;
    $iowait = ($iowaitDiff / $totalDiff) * 100;

    return [
        'usage' => round($usage, 1),
        'iowait' => round(max(0, $iowait), 1),
    ];
}

function getCpuFrequency(): array
{
    $current = 0;
    $max = 0;
    
    // Try to get from /sys
    if (file_exists('/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq')) {
        $current = (int) file_get_contents('/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq') / 1000;
    }
    if (file_exists('/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq')) {
        $max = (int) file_get_contents('/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq') / 1000;
    }
    
    // Fallback to /proc/cpuinfo
    if ($current == 0) {
        $mhz = trim(shell_exec("grep 'cpu MHz' /proc/cpuinfo | head -1 | cut -d':' -f2") ?? '0');
        $current = (float) $mhz;
    }
    
    return [
        'current_mhz' => round($current, 0),
        'max_mhz' => round($max, 0),
    ];
}

function getLoadAverage(): array
{
    $load = sys_getloadavg();
    return [
        '1min' => round($load[0], 2),
        '5min' => round($load[1], 2),
        '15min' => round($load[2], 2),
    ];
}

function metricsMemory(array $params): array
{
    $memInfo = [];
    $lines = file('/proc/meminfo', FILE_IGNORE_NEW_LINES);
    
    foreach ($lines as $line) {
        if (preg_match('/^(\w+):\s+(\d+)/', $line, $matches)) {
            $memInfo[$matches[1]] = (int) $matches[2];
        }
    }
    
    $totalKb = $memInfo['MemTotal'] ?? 0;
    $freeKb = $memInfo['MemFree'] ?? 0;
    $availableKb = $memInfo['MemAvailable'] ?? $freeKb;
    $buffersKb = $memInfo['Buffers'] ?? 0;
    $cachedKb = $memInfo['Cached'] ?? 0;
    $swapTotalKb = $memInfo['SwapTotal'] ?? 0;
    $swapFreeKb = $memInfo['SwapFree'] ?? 0;
    
    $usedKb = $totalKb - $availableKb;
    $swapUsedKb = $swapTotalKb - $swapFreeKb;
    
    return [
        'success' => true,
        'data' => [
            'total' => round($totalKb / 1024, 0),
            'used' => round($usedKb / 1024, 0),
            'free' => round($freeKb / 1024, 0),
            'available' => round($availableKb / 1024, 0),
            'buffers' => round($buffersKb / 1024, 0),
            'cached' => round($cachedKb / 1024, 0),
            'usage_percent' => $totalKb > 0 ? round(($usedKb / $totalKb) * 100, 1) : 0,
            'swap' => [
                'total' => round($swapTotalKb / 1024, 0),
                'used' => round($swapUsedKb / 1024, 0),
                'free' => round($swapFreeKb / 1024, 0),
                'usage_percent' => $swapTotalKb > 0 ? round(($swapUsedKb / $swapTotalKb) * 100, 1) : 0,
            ],
        ],
    ];
}

function metricsDisk(array $params): array
{
    $disks = [];
    
    // Get disk usage using df
    exec('df -B1 -x tmpfs -x devtmpfs -x squashfs -x overlay 2>/dev/null', $output);
    
    foreach ($output as $i => $line) {
        if ($i === 0) continue; // Skip header
        
        $parts = preg_split('/\s+/', $line);
        if (count($parts) < 6) continue;
        
        $filesystem = $parts[0];
        $total = (int) $parts[1];
        $used = (int) $parts[2];
        $available = (int) $parts[3];
        $mount = $parts[5];
        
        // Skip small filesystems and special mounts
        if ($total < 1024 * 1024 * 100) continue; // Less than 100MB
        if (strpos($mount, '/snap') === 0) continue;
        if (strpos($mount, '/boot/efi') === 0) continue;
        
        $disks[] = [
            'filesystem' => $filesystem,
            'mount' => $mount,
            'total' => $total,
            'used' => $used,
            'available' => $available,
            'usage_percent' => $total > 0 ? round(($used / $total) * 100, 1) : 0,
            'total_human' => formatBytes($total),
            'used_human' => formatBytes($used),
            'available_human' => formatBytes($available),
        ];
    }
    
    // Get disk I/O stats
    $io = getDiskIoStats();
    
    return [
        'success' => true,
        'data' => [
            'partitions' => $disks,
            'io' => $io,
        ],
    ];
}

function getDiskIoStats(): array
{
    $stats = [];
    
    if (!file_exists('/proc/diskstats')) {
        return $stats;
    }
    
    $lines = file('/proc/diskstats', FILE_IGNORE_NEW_LINES);
    
    foreach ($lines as $line) {
        $parts = preg_split('/\s+/', trim($line));
        if (count($parts) < 14) continue;
        
        $device = $parts[2];
        
        // Only include main disks (sda, nvme0n1, vda, etc.)
        if (!preg_match('/^(sd[a-z]|nvme\d+n\d+|vd[a-z]|xvd[a-z])$/', $device)) {
            continue;
        }
        
        $stats[$device] = [
            'reads' => (int) $parts[3],
            'read_sectors' => (int) $parts[5],
            'read_time_ms' => (int) $parts[6],
            'writes' => (int) $parts[7],
            'write_sectors' => (int) $parts[9],
            'write_time_ms' => (int) $parts[10],
            'io_in_progress' => (int) $parts[11],
            'io_time_ms' => (int) $parts[12],
        ];
    }
    
    return $stats;
}

function formatBytes(int $bytes, int $precision = 2): string
{
    $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
    
    $bytes = max($bytes, 0);
    $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
    $pow = min($pow, count($units) - 1);
    
    $bytes /= pow(1024, $pow);
    
    return round($bytes, $precision) . ' ' . $units[$pow];
}

function metricsNetwork(array $params): array
{
    $interfaces = [];
    
    // Read network interface stats
    $lines = file('/proc/net/dev', FILE_IGNORE_NEW_LINES);
    
    foreach ($lines as $line) {
        if (strpos($line, ':') === false) continue;
        
        list($iface, $stats) = explode(':', $line, 2);
        $iface = trim($iface);
        
        // Skip loopback
        if ($iface === 'lo') continue;
        
        $parts = preg_split('/\s+/', trim($stats));
        
        $interfaces[$iface] = [
            'rx_bytes' => (int) $parts[0],
            'rx_packets' => (int) $parts[1],
            'rx_errors' => (int) $parts[2],
            'tx_bytes' => (int) $parts[8],
            'tx_packets' => (int) $parts[9],
            'tx_errors' => (int) $parts[10],
            'rx_human' => formatBytes((int) $parts[0]),
            'tx_human' => formatBytes((int) $parts[8]),
        ];
        
        // Get IP address
        exec("ip addr show $iface 2>/dev/null | grep 'inet ' | awk '{print \$2}'", $ipOutput);
        $interfaces[$iface]['ip'] = $ipOutput[0] ?? '';
    }
    
    // Get active connections count
    exec("ss -tun | wc -l", $connOutput);
    $connections = max(0, ((int) ($connOutput[0] ?? 0)) - 1);
    
    return [
        'success' => true,
        'data' => [
            'interfaces' => $interfaces,
            'connections' => $connections,
        ],
    ];
}

function metricsProcesses(array $params): array
{
    $limit = $params['limit'] ?? 15;
    $sortBy = $params['sort'] ?? 'cpu'; // cpu, memory
    
    $processes = [];
    
    // Get top processes
    $sortFlag = $sortBy === 'memory' ? '-m' : '';
    exec("ps aux --sort=-%cpu 2>/dev/null | head -" . ($limit + 1), $output);
    
    foreach ($output as $i => $line) {
        if ($i === 0) continue; // Skip header
        
        $parts = preg_split('/\s+/', $line, 11);
        if (count($parts) < 11) continue;
        
        $processes[] = [
            'user' => $parts[0],
            'pid' => (int) $parts[1],
            'cpu' => (float) $parts[2],
            'memory' => (float) $parts[3],
            'vsz' => (int) $parts[4],
            'rss' => (int) $parts[5],
            'stat' => $parts[7],
            'started' => $parts[8],
            'time' => $parts[9],
            'command' => $parts[10],
        ];
    }
    
    // Get process counts
    exec("ps aux | wc -l", $totalOutput);
    $total = max(0, ((int) ($totalOutput[0] ?? 0)) - 1);
    
    exec("ps aux | awk '\$8 ~ /R/ {count++} END {print count+0}'", $runningOutput);
    $running = (int) ($runningOutput[0] ?? 0);
    
    return [
        'success' => true,
        'data' => [
            'total' => $total,
            'running' => $running,
            'top' => $processes,
        ],
    ];
}

function systemKillProcess(array $params): array
{
    $pid = $params['pid'] ?? 0;
    $signal = $params['signal'] ?? 15; // SIGTERM default

    if (!$pid || !is_numeric($pid) || $pid <= 0) {
        return ['success' => false, 'error' => 'Invalid PID'];
    }

    $pid = (int) $pid;
    $signal = (int) $signal;

    // Only allow safe signals
    $allowedSignals = [1, 9, 15]; // SIGHUP, SIGKILL, SIGTERM
    if (!in_array($signal, $allowedSignals, true)) {
        return ['success' => false, 'error' => 'Invalid signal. Allowed: 1 (HUP), 9 (KILL), 15 (TERM)'];
    }

    // Prevent killing critical system processes (PID 1 is init)
    if ($pid === 1) {
        return ['success' => false, 'error' => 'Cannot kill init process'];
    }

    // Check if process exists
    if (!posix_kill($pid, 0)) {
        return ['success' => false, 'error' => 'Process does not exist'];
    }

    // Get process info for logging
    $cmdline = @file_get_contents("/proc/$pid/cmdline");
    $cmdline = str_replace("\0", ' ', $cmdline);

    // Try to kill the process
    if (posix_kill($pid, $signal)) {
        logger("Killed process PID=$pid signal=$signal command=$cmdline", 'INFO');
        return [
            'success' => true,
            'message' => "Process $pid terminated with signal $signal",
            'pid' => $pid,
            'signal' => $signal,
        ];
    }

    $error = posix_strerror(posix_get_last_error());
    logger("Failed to kill process PID=$pid signal=$signal error=$error", 'ERROR');
    return ['success' => false, 'error' => "Failed to kill process: $error"];
}

function getUptime(): array
{
    $uptime = (float) trim(file_get_contents('/proc/uptime') ?? '0');
    $uptime = (int) $uptime;
    
    $days = floor($uptime / 86400);
    $hours = floor(($uptime % 86400) / 3600);
    $minutes = floor(($uptime % 3600) / 60);
    $seconds = $uptime % 60;
    
    $human = '';
    if ($days > 0) $human .= "{$days}d ";
    if ($hours > 0) $human .= "{$hours}h ";
    if ($minutes > 0) $human .= "{$minutes}m ";
    $human .= "{$seconds}s";
    
    return [
        'seconds' => $uptime,
        'days' => $days,
        'hours' => $hours,
        'minutes' => $minutes,
        'human' => trim($human),
    ];
}

function getOsInfo(): array
{
    $osRelease = [];
    if (file_exists('/etc/os-release')) {
        $lines = file('/etc/os-release', FILE_IGNORE_NEW_LINES);
        foreach ($lines as $line) {
            if (strpos($line, '=') !== false) {
                list($key, $value) = explode('=', $line, 2);
                $osRelease[$key] = trim($value, '"');
            }
        }
    }
    
    return [
        'name' => $osRelease['NAME'] ?? 'Linux',
        'version' => $osRelease['VERSION'] ?? '',
        'id' => $osRelease['ID'] ?? 'linux',
        'pretty_name' => $osRelease['PRETTY_NAME'] ?? 'Linux',
    ];
}

// ============ DISK QUOTA MANAGEMENT ============

/**
 * Check if quota system is enabled on the filesystem
 */
function quotaStatus(array $params): array
{
    $mountPoint = $params['mount'] ?? '/home';
    
    // Check if quota tools are installed
    $quotaCheck = trim(shell_exec('which quota 2>/dev/null') ?? '');
    if (empty($quotaCheck)) {
        return [
            'success' => true,
            'enabled' => false,
            'available' => false,
            'reason' => 'Quota tools not installed. Install with: apt install quota'
        ];
    }
    
    // Find the actual mount point for /home
    $findMount = trim(shell_exec("df --output=target " . escapeshellarg($mountPoint) . " 2>/dev/null | tail -1") ?? '');
    if (empty($findMount)) {
        $findMount = '/';
    }
    
    // Check if quota is enabled in fstab
    $fstab = file_get_contents('/etc/fstab');
    $quotaInFstab = preg_match('/\s+' . preg_quote($findMount, '/') . '\s+\S+\s+\S*usrquota/', $fstab);
    
    // Check if quota is active
    exec("quotaon -p " . escapeshellarg($findMount) . " 2>&1", $output, $exitCode);
    $quotaActive = $exitCode === 0 && strpos(implode(' ', $output), 'is on') !== false;
    
    return [
        'success' => true,
        'enabled' => $quotaActive,
        'available' => true,
        'configured' => $quotaInFstab,
        'mount_point' => $findMount,
        'message' => $quotaActive ? 'Quota system is active' : ($quotaInFstab ? 'Quota configured but not active' : 'Quota not configured')
    ];
}

/**
 * Enable quota system on filesystem
 */
function quotaEnable(array $params): array
{
    $mountPoint = $params['mount'] ?? '/home';
    
    // Check if running as root
    if (posix_getuid() !== 0) {
        return ['success' => false, 'error' => 'Must run as root to enable quotas'];
    }
    
    // Check if quota tools are installed
    $quotaCheck = trim(shell_exec('which quota 2>/dev/null') ?? '');
    if (empty($quotaCheck)) {
        // Try to install quota tools
        exec('apt-get update && apt-get install -y quota 2>&1', $output, $exitCode);
        if ($exitCode !== 0) {
            return ['success' => false, 'error' => 'Failed to install quota tools: ' . implode("\n", $output)];
        }
    }
    
    // Find the actual mount point
    $findMount = trim(shell_exec("df --output=target " . escapeshellarg($mountPoint) . " 2>/dev/null | tail -1") ?? '');
    if (empty($findMount)) {
        $findMount = '/';
    }
    
    // Check current fstab entry
    $fstab = file_get_contents('/etc/fstab');
    $lines = explode("\n", $fstab);
    $modified = false;
    
    foreach ($lines as &$line) {
        // Skip comments and empty lines
        if (empty(trim($line)) || strpos(trim($line), '#') === 0) {
            continue;
        }
        
        $parts = preg_split('/\s+/', $line);
        if (count($parts) >= 4 && $parts[1] === $findMount) {
            // Check if usrquota is already in options
            if (strpos($parts[3], 'usrquota') === false) {
                $parts[3] .= ',usrquota,grpquota';
                $line = implode("\t", $parts);
                $modified = true;
            }
            break;
        }
    }
    
    if ($modified) {
        // Backup and update fstab
        copy('/etc/fstab', '/etc/fstab.bak.' . time());
        file_put_contents('/etc/fstab', implode("\n", $lines));
        
        // Remount filesystem
        exec("mount -o remount " . escapeshellarg($findMount) . " 2>&1", $output, $exitCode);
        if ($exitCode !== 0) {
            return ['success' => false, 'error' => 'Failed to remount filesystem. A reboot may be required.'];
        }
    }
    
    // Create quota files
    exec("quotacheck -cugm " . escapeshellarg($findMount) . " 2>&1", $output, $exitCode);
    
    // Turn on quotas
    exec("quotaon " . escapeshellarg($findMount) . " 2>&1", $output, $exitCode);
    
    return [
        'success' => true,
        'mount_point' => $findMount,
        'message' => 'Quota system enabled successfully'
    ];
}

/**
 * Set disk quota for a user
 */
function quotaSet(array $params): array
{
    $username = $params['username'] ?? '';
    $softLimit = $params['soft_mb'] ?? 0;  // Soft limit in MB
    $hardLimit = $params['hard_mb'] ?? 0;  // Hard limit in MB (usually same as soft or slightly higher)
    $mountPoint = $params['mount'] ?? '/home';
    
    if (empty($username)) {
        return ['success' => false, 'error' => 'Username is required'];
    }
    
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username format'];
    }
    
    // Check if user exists
    exec("id " . escapeshellarg($username) . " 2>/dev/null", $output, $exitCode);
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'User does not exist'];
    }
    
    // Find the actual mount point
    $findMount = trim(shell_exec("df --output=target " . escapeshellarg($mountPoint) . " 2>/dev/null | tail -1") ?? '');
    if (empty($findMount)) {
        $findMount = '/';
    }
    
    // Convert MB to KB (setquota uses KB for blocks)
    $softBlocks = $softLimit * 1024;
    $hardBlocks = $hardLimit * 1024;
    
    // Set soft and hard limits to 0 to remove quota
    if ($softLimit == 0 && $hardLimit == 0) {
        $cmd = sprintf(
            '/usr/sbin/setquota -u %s 0 0 0 0 %s 2>&1',
            escapeshellarg($username),
            escapeshellarg($findMount)
        );
    } else {
        // If only one limit provided, use it for both
        if ($hardLimit == 0) {
            $hardBlocks = $softBlocks;
        }
        if ($softLimit == 0) {
            $softBlocks = $hardBlocks;
        }
        
        // Set quota: soft block, hard block, soft inode (0=unlimited), hard inode (0=unlimited)
        $cmd = sprintf(
            '/usr/sbin/setquota -u %s %d %d 0 0 %s 2>&1',
            escapeshellarg($username),
            $softBlocks,
            $hardBlocks,
            escapeshellarg($findMount)
        );
    }
    
    exec($cmd, $output, $exitCode);
    
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'Failed to set quota: ' . implode("\n", $output)];
    }
    
    return [
        'success' => true,
        'username' => $username,
        'soft_mb' => $softLimit,
        'hard_mb' => $hardLimit ?: $softLimit,
        'mount_point' => $findMount,
        'message' => $softLimit > 0 ? "Quota set to {$softLimit} MB" : 'Quota removed'
    ];
}

/**
 * Get disk quota for a user
 */
function quotaGet(array $params): array
{
    $username = $params['username'] ?? '';
    $mountPoint = $params['mount'] ?? '/home';
    
    if (empty($username)) {
        return ['success' => false, 'error' => 'Username is required'];
    }
    
    // Find the actual mount point
    $findMount = trim(shell_exec("df --output=target " . escapeshellarg($mountPoint) . " 2>/dev/null | tail -1") ?? '');
    if (empty($findMount)) {
        $findMount = '/';
    }
    
    // Get quota info
    $cmd = sprintf('quota -u %s -w 2>&1', escapeshellarg($username));
    exec($cmd, $output, $exitCode);
    
    $outputStr = implode("\n", $output);
    
    // Check if user has no quota
    if (strpos($outputStr, 'none') !== false || $exitCode !== 0) {
        // Try repquota for more detailed info
        $cmd2 = sprintf('repquota -u %s 2>/dev/null | grep "^%s"', 
            escapeshellarg($findMount), 
            escapeshellarg($username)
        );
        $repOutput = trim(shell_exec($cmd2) ?? '');
        
        if (empty($repOutput)) {
            // Quota not enabled - use du to calculate actual disk usage
            $homeDir = "/home/{$username}";
            $usedMb = 0;

            if (is_dir($homeDir)) {
                // Use du to get actual disk usage in KB
                $duOutput = trim(shell_exec("du -sk " . escapeshellarg($homeDir) . " 2>/dev/null | cut -f1") ?? '0');
                $usedKb = (int)$duOutput;
                $usedMb = round($usedKb / 1024, 2);
            }

            return [
                'success' => true,
                'username' => $username,
                'has_quota' => false,
                'used_mb' => $usedMb,
                'soft_mb' => 0,
                'hard_mb' => 0,
                'usage_percent' => 0,
                'quota_source' => 'du'  // Indicate that we used du fallback
            ];
        }
        
        // Parse repquota output
        // Format: username -- used soft hard grace used soft hard grace
        $parts = preg_split('/\s+/', $repOutput);
        if (count($parts) >= 5) {
            $usedKb = (int)$parts[2];
            $softKb = (int)$parts[3];
            $hardKb = (int)$parts[4];
            
            return [
                'success' => true,
                'username' => $username,
                'has_quota' => $softKb > 0 || $hardKb > 0,
                'used_mb' => round($usedKb / 1024, 2),
                'soft_mb' => round($softKb / 1024, 2),
                'hard_mb' => round($hardKb / 1024, 2),
                'usage_percent' => $hardKb > 0 ? round(($usedKb / $hardKb) * 100, 1) : 0
            ];
        }
    }
    
    // Parse standard quota output
    // Look for lines with filesystem info
    $usedKb = 0;
    $softKb = 0;
    $hardKb = 0;
    
    foreach ($output as $line) {
        if (preg_match('/^\s*(\S+)\s+(\d+)\s+(\d+)\s+(\d+)/', $line, $matches)) {
            $usedKb = (int)$matches[2];
            $softKb = (int)$matches[3];
            $hardKb = (int)$matches[4];
            break;
        }
    }
    
    return [
        'success' => true,
        'username' => $username,
        'has_quota' => $softKb > 0 || $hardKb > 0,
        'used_mb' => round($usedKb / 1024, 2),
        'soft_mb' => round($softKb / 1024, 2),
        'hard_mb' => round($hardKb / 1024, 2),
        'usage_percent' => $hardKb > 0 ? round(($usedKb / $hardKb) * 100, 1) : 0
    ];
}

/**
 * Get quota report for all users
 */
function quotaReport(array $params): array
{
    $mountPoint = $params['mount'] ?? '/home';
    
    // Find the actual mount point
    $findMount = trim(shell_exec("df --output=target " . escapeshellarg($mountPoint) . " 2>/dev/null | tail -1") ?? '');
    if (empty($findMount)) {
        $findMount = '/';
    }
    
    // Get repquota output
    exec("repquota -u " . escapeshellarg($findMount) . " 2>/dev/null", $output, $exitCode);
    
    if ($exitCode !== 0) {
        return ['success' => false, 'error' => 'Failed to get quota report. Quotas may not be enabled.'];
    }
    
    $users = [];
    $inUserSection = false;
    
    foreach ($output as $line) {
        // Skip header lines
        if (strpos($line, '---') !== false) {
            $inUserSection = true;
            continue;
        }
        
        if (!$inUserSection) {
            continue;
        }
        
        // Parse user line: username -- used soft hard grace used soft hard grace
        // Or: username +- used soft hard grace used soft hard grace (+ means over soft, - means under)
        if (preg_match('/^(\S+)\s+([+-]{2})\s+(\d+)\s+(\d+)\s+(\d+)/', $line, $matches)) {
            $username = $matches[1];
            $usedKb = (int)$matches[3];
            $softKb = (int)$matches[4];
            $hardKb = (int)$matches[5];
            
            // Skip system users and users without quota
            if (in_array($username, ['root', 'nobody', 'www-data', 'mysql', 'redis'])) {
                continue;
            }
            
            $users[] = [
                'username' => $username,
                'used_mb' => round($usedKb / 1024, 2),
                'soft_mb' => round($softKb / 1024, 2),
                'hard_mb' => round($hardKb / 1024, 2),
                'usage_percent' => $hardKb > 0 ? round(($usedKb / $hardKb) * 100, 1) : 0,
                'over_quota' => $matches[2][0] === '+'
            ];
        }
    }
    
    return [
        'success' => true,
        'mount_point' => $findMount,
        'users' => $users
    ];
}

// ============ IP ADDRESS MANAGEMENT ============

function ipList(array $params): array
{
    $addresses = [];

    // Get all IP addresses using ip command
    exec('ip -4 addr show 2>/dev/null', $ipv4Output, $ipv4Code);
    exec('ip -6 addr show 2>/dev/null', $ipv6Output, $ipv6Code);

    $currentInterface = '';
    $currentMac = '';
    $currentState = '';

    // Parse IPv4 addresses
    foreach ($ipv4Output as $line) {
        // Match interface line: 2: eth0: <BROADCAST,...> ...
        if (preg_match('/^\d+:\s+(\S+):\s+<([^>]+)>/', $line, $m)) {
            $currentInterface = rtrim($m[1], ':');
            $flags = explode(',', $m[2]);
            $currentState = in_array('UP', $flags) ? 'up' : 'down';
        }
        // Match MAC address
        if (preg_match('/link\/ether\s+([0-9a-f:]+)/i', $line, $m)) {
            $currentMac = $m[1];
        }
        // Match inet address
        if (preg_match('/inet\s+(\d+\.\d+\.\d+\.\d+)\/(\d+)\s+(?:brd\s+(\S+)\s+)?scope\s+(\S+)/', $line, $m)) {
            $ip = $m[1];
            $cidr = (int)$m[2];
            $broadcast = $m[3] ?? '';
            $scope = $m[4];

            // Calculate netmask from CIDR
            $netmask = long2ip(-1 << (32 - $cidr));

            // Skip loopback
            if ($currentInterface === 'lo') {
                continue;
            }

            $addresses[] = [
                'ip' => $ip,
                'version' => 4,
                'cidr' => $cidr,
                'netmask' => $netmask,
                'broadcast' => $broadcast,
                'interface' => $currentInterface,
                'mac' => $currentMac,
                'scope' => $scope,
                'state' => $currentState,
                'is_primary' => $scope === 'global' && strpos($ip, '192.168.') !== 0 && strpos($ip, '10.') !== 0 && strpos($ip, '172.') !== 0,
            ];
        }
    }

    // Parse IPv6 addresses (non-link-local)
    $currentInterface = '';
    foreach ($ipv6Output as $line) {
        if (preg_match('/^\d+:\s+(\S+):\s+<([^>]+)>/', $line, $m)) {
            $currentInterface = rtrim($m[1], ':');
            $flags = explode(',', $m[2]);
            $currentState = in_array('UP', $flags) ? 'up' : 'down';
        }
        if (preg_match('/link\/ether\s+([0-9a-f:]+)/i', $line, $m)) {
            $currentMac = $m[1];
        }
        // Match inet6 address (skip link-local fe80:: and loopback ::1)
        if (preg_match('/inet6\s+([0-9a-f:]+)\/(\d+)\s+scope\s+(\S+)/', $line, $m)) {
            $ip = $m[1];
            $cidr = (int)$m[2];
            $scope = $m[3];

            if ($currentInterface === 'lo' || $scope === 'link') {
                continue;
            }

            $addresses[] = [
                'ip' => $ip,
                'version' => 6,
                'cidr' => $cidr,
                'netmask' => '',
                'broadcast' => '',
                'interface' => $currentInterface,
                'mac' => $currentMac,
                'scope' => $scope,
                'state' => $currentState,
                'is_primary' => false,
            ];
        }
    }

    // Get default gateway
    $gateway = '';
    exec('ip route | grep default 2>/dev/null', $routeOutput);
    if (!empty($routeOutput[0]) && preg_match('/default via (\S+)/', $routeOutput[0], $m)) {
        $gateway = $m[1];
    }

    // Get DNS servers
    $dns = [];
    if (file_exists('/etc/resolv.conf')) {
        $resolv = file_get_contents('/etc/resolv.conf');
        if (preg_match_all('/nameserver\s+(\S+)/', $resolv, $matches)) {
            $dns = $matches[1];
        }
    }

    // Get available interfaces
    $interfaces = [];
    exec('ls /sys/class/net 2>/dev/null', $ifaceOutput);
    foreach ($ifaceOutput as $iface) {
        if ($iface !== 'lo') {
            $interfaces[] = $iface;
        }
    }

    return [
        'success' => true,
        'addresses' => $addresses,
        'gateway' => $gateway,
        'dns' => $dns,
        'interfaces' => $interfaces,
    ];
}

function ipAdd(array $params): array
{
    $ip = $params['ip'] ?? '';
    $cidr = $params['cidr'] ?? 24;
    $interface = $params['interface'] ?? '';

    if (empty($ip)) {
        return ['success' => false, 'error' => 'IP address required'];
    }

    if (empty($interface)) {
        return ['success' => false, 'error' => 'Interface required'];
    }

    // Validate IP address
    if (!filter_var($ip, FILTER_VALIDATE_IP)) {
        return ['success' => false, 'error' => 'Invalid IP address format'];
    }

    // Validate CIDR
    $cidr = (int)$cidr;
    $maxCidr = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? 128 : 32;
    if ($cidr < 1 || $cidr > $maxCidr) {
        return ['success' => false, 'error' => 'Invalid CIDR notation'];
    }

    // Validate interface exists
    if (!file_exists("/sys/class/net/$interface")) {
        return ['success' => false, 'error' => "Interface $interface does not exist"];
    }

    // Add the IP address
    $cmd = sprintf(
        'ip addr add %s/%d dev %s 2>&1',
        escapeshellarg($ip),
        $cidr,
        escapeshellarg($interface)
    );

    exec($cmd, $output, $code);

    if ($code !== 0) {
        $error = implode("\n", $output);
        if (strpos($error, 'File exists') !== false) {
            return ['success' => false, 'error' => 'IP address already exists on this interface'];
        }
        return ['success' => false, 'error' => 'Failed to add IP: ' . $error];
    }

    // Make persistent by adding to netplan or interfaces file
    $persistResult = ipPersist($ip, $cidr, $interface, 'add');

    return [
        'success' => true,
        'message' => "IP address $ip/$cidr added to $interface",
        'persistent' => $persistResult,
    ];
}

function ipRemove(array $params): array
{
    $ip = $params['ip'] ?? '';
    $cidr = $params['cidr'] ?? 24;
    $interface = $params['interface'] ?? '';

    if (empty($ip)) {
        return ['success' => false, 'error' => 'IP address required'];
    }

    if (empty($interface)) {
        return ['success' => false, 'error' => 'Interface required'];
    }

    // Validate IP address
    if (!filter_var($ip, FILTER_VALIDATE_IP)) {
        return ['success' => false, 'error' => 'Invalid IP address format'];
    }

    // Remove the IP address
    $cmd = sprintf(
        'ip addr del %s/%d dev %s 2>&1',
        escapeshellarg($ip),
        (int)$cidr,
        escapeshellarg($interface)
    );

    exec($cmd, $output, $code);

    if ($code !== 0) {
        $error = implode("\n", $output);
        if (strpos($error, 'Cannot assign') !== false) {
            return ['success' => false, 'error' => 'IP address not found on this interface'];
        }
        return ['success' => false, 'error' => 'Failed to remove IP: ' . $error];
    }

    // Remove from persistent config
    $persistResult = ipPersist($ip, $cidr, $interface, 'remove');

    return [
        'success' => true,
        'message' => "IP address $ip removed from $interface",
        'persistent' => $persistResult,
    ];
}

function ipPersist(string $ip, int $cidr, string $interface, string $action): bool
{
    $isIpv6 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
    $inetType = $isIpv6 ? 'inet6' : 'inet';

    // Check for netplan (Ubuntu 18.04+)
    $netplanDir = '/etc/netplan';
    if (is_dir($netplanDir)) {
        $files = glob("$netplanDir/*.yaml");
        if (!empty($files)) {
            // For now, just return a note that manual persistence may be needed
            // Full netplan editing would require YAML parsing
            return false;
        }
    }

    // Check for /etc/network/interfaces (Debian style)
    $interfacesFile = '/etc/network/interfaces';
    if (file_exists($interfacesFile)) {
        // For secondary IPs, we'd typically use interfaces.d
        $interfacesDDir = '/etc/network/interfaces.d';
        if (!is_dir($interfacesDDir)) {
            @mkdir($interfacesDDir, 0755);
        }

        $aliasFile = "$interfacesDDir/$interface-aliases";

        if ($action === 'add') {
            // Count existing aliases to determine next number
            $aliasNum = 0;
            if (file_exists($aliasFile)) {
                $content = file_get_contents($aliasFile);
                preg_match_all('/iface\s+' . preg_quote($interface, '/') . ':(\d+)/', $content, $m);
                if (!empty($m[1])) {
                    $aliasNum = max($m[1]) + 1;
                }
            }

            $aliasConfig = "\n# Added by Jabali Panel\nauto $interface:$aliasNum\niface $interface:$aliasNum {$inetType} static\n    address $ip/$cidr\n";
            file_put_contents($aliasFile, $aliasConfig, FILE_APPEND);
            return true;
        } elseif ($action === 'remove' && file_exists($aliasFile)) {
            // Remove the IP from aliases file
            $content = file_get_contents($aliasFile);
            // Match and remove the block for this IP
            $pattern = '/\n?#[^\n]*\nauto\s+' . preg_quote($interface, '/') . ':\d+\niface\s+' . preg_quote($interface, '/') . ':\d+\s+' . $inetType . '\s+static\s+address\s+' . preg_quote("$ip/$cidr", '/') . '\s*/';
            $content = preg_replace($pattern, '', $content);
            file_put_contents($aliasFile, $content);
            return true;
        }
    }

    return false;
}

function ipInfo(array $params): array
{
    $ip = $params['ip'] ?? '';

    if (empty($ip)) {
        return ['success' => false, 'error' => 'IP address required'];
    }

    // Check if it's a public or private IP
    $isPrivate = false;
    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
        // Check private ranges
        $isPrivate = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE) === false;
    }

    // Get reverse DNS
    $rdns = @gethostbyaddr($ip);
    if ($rdns === $ip) {
        $rdns = '';
    }

    // Check if IP is reachable (quick ping)
    exec("ping -c 1 -W 1 " . escapeshellarg($ip) . " 2>&1", $pingOutput, $pingCode);
    $reachable = $pingCode === 0;

    // Get whois info for public IPs (basic)
    $whois = [];
    if (!$isPrivate && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
        exec("whois " . escapeshellarg($ip) . " 2>/dev/null | head -50", $whoisOutput);
        foreach ($whoisOutput as $line) {
            if (preg_match('/^(NetName|OrgName|Country|descr|netname|org-name|country):\s*(.+)/i', $line, $m)) {
                $key = strtolower(trim($m[1]));
                $whois[$key] = trim($m[2]);
            }
        }
    }

    return [
        'success' => true,
        'ip' => $ip,
        'is_private' => $isPrivate,
        'reverse_dns' => $rdns,
        'reachable' => $reachable,
        'whois' => $whois,
    ];
}

// ============ SECURITY SCANNER TOOLS ============

function scannerInstall(array $params): array
{
    $tool = $params['tool'] ?? '';

    if (empty($tool)) {
        return ['success' => false, 'error' => 'Tool name required'];
    }

    $validTools = ['lynis', 'wpscan', 'nikto'];
    if (!in_array($tool, $validTools)) {
        return ['success' => false, 'error' => "Invalid tool: $tool"];
    }

    $output = [];
    $code = 0;

    switch ($tool) {
        case 'lynis':
            exec('apt-get update 2>&1', $output);
            exec('apt-get install -y lynis 2>&1', $output, $code);
            break;

        case 'wpscan':
            // Check if Ruby is installed
            exec('which ruby 2>/dev/null', $rubyCheck, $rubyCode);
            if ($rubyCode !== 0) {
                exec('apt-get update 2>&1', $output);
                exec('apt-get install -y ruby ruby-dev build-essential libcurl4-openssl-dev libxml2 libxml2-dev libxslt1-dev zlib1g-dev 2>&1', $output, $code);
                if ($code !== 0) {
                    return ['success' => false, 'error' => 'Failed to install Ruby dependencies', 'output' => implode("\n", $output)];
                }
            }
            exec('gem install wpscan 2>&1', $output, $code);
            if ($code === 0) {
                // Update WPScan database
                exec('wpscan --update 2>&1', $updateOutput);
                $output = array_merge($output, $updateOutput);
            }
            break;

        case 'nikto':
            $output = [];
            $code = 0;
            // First try apt
            exec('apt-get update 2>&1', $updateOutput);
            exec('apt-cache show nikto 2>&1', $cacheOutput, $cacheCode);
            if ($cacheCode === 0) {
                exec('DEBIAN_FRONTEND=noninteractive apt-get install -y nikto 2>&1', $output, $code);
            } else {
                // Install from GitHub if not in repos
                exec('apt-get install -y perl libnet-ssleay-perl openssl git 2>&1', $depOutput, $depCode);
                if ($depCode !== 0) {
                    return ['success' => false, 'error' => 'Failed to install Nikto dependencies', 'output' => implode("\n", $depOutput)];
                }
                // Clone or update Nikto
                $niktoDir = '/opt/nikto';
                if (is_dir($niktoDir)) {
                    exec("cd $niktoDir && git pull 2>&1", $gitOutput, $gitCode);
                } else {
                    exec("git clone https://github.com/sullo/nikto.git $niktoDir 2>&1", $gitOutput, $gitCode);
                }
                if ($gitCode !== 0) {
                    return ['success' => false, 'error' => 'Failed to download Nikto', 'output' => implode("\n", $gitOutput)];
                }
                // Create symlink
                exec("ln -sf $niktoDir/program/nikto.pl /usr/local/bin/nikto 2>&1", $linkOutput, $linkCode);
                exec("chmod +x $niktoDir/program/nikto.pl 2>&1");
                $output = array_merge($depOutput ?? [], $gitOutput ?? [], $linkOutput ?? []);
                $code = 0; // Success
            }
            break;
    }

    if ($code !== 0) {
        return [
            'success' => false,
            'error' => "Failed to install $tool",
            'output' => implode("\n", array_slice($output, -10)),
        ];
    }

    // Verify installation
    $status = scannerStatus(['tool' => $tool]);

    return [
        'success' => true,
        'message' => "$tool installed successfully",
        'installed' => $status['installed'] ?? false,
        'version' => $status['version'] ?? '',
        'output' => implode("\n", array_slice($output, -5)),
    ];
}

function scannerUninstall(array $params): array
{
    $tool = $params['tool'] ?? '';

    if (empty($tool)) {
        return ['success' => false, 'error' => 'Tool name required'];
    }

    $validTools = ['lynis', 'wpscan', 'nikto'];
    if (!in_array($tool, $validTools)) {
        return ['success' => false, 'error' => "Invalid tool: $tool"];
    }

    $output = [];
    $code = 0;

    switch ($tool) {
        case 'lynis':
            exec('apt-get remove -y lynis 2>&1', $output, $code);
            exec('apt-get autoremove -y 2>&1', $autoOutput);
            $output = array_merge($output, $autoOutput);
            break;

        case 'wpscan':
            exec('gem uninstall wpscan -x 2>&1', $output, $code);
            break;

        case 'nikto':
            // Remove apt package if installed
            exec('dpkg -l nikto 2>/dev/null | grep -q "^ii"', $dpkgOutput, $dpkgCode);
            if ($dpkgCode === 0) {
                exec('apt-get remove -y nikto 2>&1', $output, $code);
                exec('apt-get autoremove -y 2>&1', $autoOutput);
                $output = array_merge($output, $autoOutput);
            }
            // Remove GitHub installation
            if (is_dir('/opt/nikto')) {
                exec('rm -rf /opt/nikto 2>&1', $rmOutput);
                $output = array_merge($output, $rmOutput);
            }
            if (file_exists('/usr/local/bin/nikto')) {
                exec('rm -f /usr/local/bin/nikto 2>&1', $rmOutput);
                $output = array_merge($output, $rmOutput);
            }
            $code = 0; // Success if we got here
            break;
    }

    // Verify uninstallation
    $status = scannerStatus(['tool' => $tool]);

    if ($status['installed'] ?? false) {
        return [
            'success' => false,
            'error' => "Failed to uninstall $tool",
            'output' => implode("\n", array_slice($output, -10)),
        ];
    }

    return [
        'success' => true,
        'message' => "$tool uninstalled successfully",
        'output' => implode("\n", array_slice($output, -5)),
    ];
}

function scannerStatus(array $params): array
{
    $tool = $params['tool'] ?? '';

    if (!empty($tool)) {
        // Check specific tool
        return checkScannerTool($tool);
    }

    // Check all tools
    return [
        'success' => true,
        'lynis' => checkScannerTool('lynis'),
        'wpscan' => checkScannerTool('wpscan'),
        'nikto' => checkScannerTool('nikto'),
    ];
}

function checkScannerTool(string $tool): array
{
    $result = [
        'installed' => false,
        'version' => '',
    ];

    switch ($tool) {
        case 'lynis':
            exec('which lynis 2>/dev/null', $output, $code);
            $result['installed'] = $code === 0;
            if ($result['installed']) {
                exec('lynis --version 2>/dev/null | head -1', $versionOutput);
                $result['version'] = trim($versionOutput[0] ?? 'Unknown');
            }
            break;

        case 'wpscan':
            exec('which wpscan 2>/dev/null', $output, $code);
            $result['installed'] = $code === 0;
            if ($result['installed']) {
                exec('wpscan --version 2>/dev/null | head -1', $versionOutput);
                $result['version'] = trim($versionOutput[0] ?? 'Unknown');
            }
            break;

        case 'nikto':
            exec('which nikto 2>/dev/null', $output, $code);
            $result['installed'] = $code === 0;
            if ($result['installed']) {
                exec('nikto -Version 2>/dev/null | grep -i version | head -1', $versionOutput);
                $result['version'] = trim($versionOutput[0] ?? 'Unknown');
            }
            break;
    }

    return $result;
}

function scannerRunLynis(array $params): array
{
    $background = $params['background'] ?? false;

    // Check if lynis is installed
    exec('which lynis 2>/dev/null', $output, $code);
    if ($code !== 0) {
        return ['success' => false, 'error' => 'Lynis is not installed'];
    }

    // Create scan output directory
    $scanDir = '/var/www/jabali/storage/app/security-scans';
    if (!is_dir($scanDir)) {
        mkdir($scanDir, 0755, true);
    }

    $statusFile = "$scanDir/lynis-status.json";
    $liveLogFile = "$scanDir/lynis-live.log";

    // Initialize status file
    $status = [
        'status' => 'running',
        'started_at' => date('Y-m-d H:i:s'),
        'output' => '',
    ];
    file_put_contents($statusFile, json_encode($status));
    chown($statusFile, 'www-data');

    // Clear live log
    file_put_contents($liveLogFile, '');
    chown($liveLogFile, 'www-data');

    // Run Lynis with real-time output capture
    $tempLog = tempnam('/tmp', 'lynis_');
    $tempReport = tempnam('/tmp', 'lynis_report_');

    $cmd = sprintf(
        'lynis audit system --no-colors --quick --logfile %s --report-file %s 2>&1',
        escapeshellarg($tempLog),
        escapeshellarg($tempReport)
    );

    // Use proc_open for real-time output
    $descriptors = [
        0 => ['pipe', 'r'],
        1 => ['pipe', 'w'],
        2 => ['pipe', 'w'],
    ];

    $process = proc_open($cmd, $descriptors, $pipes);

    if (!is_resource($process)) {
        return ['success' => false, 'error' => 'Failed to start Lynis'];
    }

    fclose($pipes[0]);
    stream_set_blocking($pipes[1], false);
    stream_set_blocking($pipes[2], false);

    $rawOutput = '';
    while (true) {
        $stdout = fgets($pipes[1]);
        $stderr = fgets($pipes[2]);

        if ($stdout !== false) {
            $rawOutput .= $stdout;
            file_put_contents($liveLogFile, $stdout, FILE_APPEND);
        }
        if ($stderr !== false) {
            $rawOutput .= $stderr;
            file_put_contents($liveLogFile, $stderr, FILE_APPEND);
        }

        $procStatus = proc_get_status($process);
        if (!$procStatus['running'] && $stdout === false && $stderr === false) {
            break;
        }

        usleep(10000); // 10ms
    }

    fclose($pipes[1]);
    fclose($pipes[2]);
    proc_close($process);

    // Parse results
    $results = [
        'hardening_index' => 0,
        'warnings' => [],
        'suggestions' => [],
        'tests_performed' => 0,
    ];

    // Extract hardening index
    if (preg_match('/Hardening index\s*:\s*(\d+)/i', $rawOutput, $matches)) {
        $results['hardening_index'] = (int) $matches[1];
    }

    // Extract tests performed
    if (preg_match('/Tests performed\s*:\s*(\d+)/i', $rawOutput, $matches)) {
        $results['tests_performed'] = (int) $matches[1];
    }

    // Extract warnings
    preg_match_all('/\[WARNING\]\s*(.+)$/m', $rawOutput, $warningMatches);
    $results['warnings'] = $warningMatches[1] ?? [];

    // Extract suggestions
    preg_match_all('/\[SUGGESTION\]\s*(.+)$/m', $rawOutput, $suggestionMatches);
    $results['suggestions'] = $suggestionMatches[1] ?? [];

    $results['scan_time'] = date('Y-m-d H:i:s');
    $results['raw_output'] = $rawOutput;

    // Save results
    file_put_contents("$scanDir/lynis-latest.json", json_encode($results, JSON_PRETTY_PRINT));
    chown("$scanDir/lynis-latest.json", 'www-data');

    // Update status to completed
    $status['status'] = 'completed';
    $status['completed_at'] = date('Y-m-d H:i:s');
    file_put_contents($statusFile, json_encode($status));

    // Cleanup temp files
    @unlink($tempLog);
    @unlink($tempReport);

    return [
        'success' => true,
        'results' => $results,
        'warnings_count' => count($results['warnings']),
        'suggestions_count' => count($results['suggestions']),
        'hardening_index' => $results['hardening_index'],
    ];
}

function scannerRunNikto(array $params): array
{
    $target = $params['target'] ?? '';

    if (empty($target)) {
        return ['success' => false, 'error' => 'Target URL/host required'];
    }

    // Check if nikto is installed
    exec('which nikto 2>/dev/null', $checkOutput, $code);
    if ($code !== 0) {
        return ['success' => false, 'error' => 'Nikto is not installed'];
    }

    // Create scan output directory
    $scanDir = '/var/www/jabali/storage/app/security-scans';
    if (!is_dir($scanDir)) {
        mkdir($scanDir, 0755, true);
    }

    $statusFile = "$scanDir/nikto-status.json";
    $liveLogFile = "$scanDir/nikto-live.log";

    // Initialize status file
    $status = [
        'status' => 'running',
        'started_at' => date('Y-m-d H:i:s'),
        'target' => $target,
    ];
    file_put_contents($statusFile, json_encode($status));
    chown($statusFile, 'www-data');

    // Clear live log
    file_put_contents($liveLogFile, '');
    chown($liveLogFile, 'www-data');

    // Run Nikto with timeout - use full path since timeout has restricted PATH
    $niktoPath = file_exists('/usr/bin/nikto') ? '/usr/bin/nikto' : '/usr/local/bin/nikto';
    $cmd = sprintf(
        'timeout 300 %s -h %s 2>&1',
        $niktoPath,
        escapeshellarg($target)
    );

    // Use proc_open for real-time output
    $descriptors = [
        0 => ['pipe', 'r'],
        1 => ['pipe', 'w'],
        2 => ['pipe', 'w'],
    ];

    $process = proc_open($cmd, $descriptors, $pipes);

    if (!is_resource($process)) {
        return ['success' => false, 'error' => 'Failed to start Nikto'];
    }

    fclose($pipes[0]);
    stream_set_blocking($pipes[1], false);
    stream_set_blocking($pipes[2], false);

    $rawOutput = '';
    $output = [];
    while (true) {
        $stdout = fgets($pipes[1]);
        $stderr = fgets($pipes[2]);

        if ($stdout !== false) {
            $rawOutput .= $stdout;
            $output[] = rtrim($stdout);
            file_put_contents($liveLogFile, $stdout, FILE_APPEND);
        }
        if ($stderr !== false) {
            $rawOutput .= $stderr;
            file_put_contents($liveLogFile, $stderr, FILE_APPEND);
        }

        $procStatus = proc_get_status($process);
        if (!$procStatus['running'] && $stdout === false && $stderr === false) {
            break;
        }

        usleep(10000); // 10ms
    }

    fclose($pipes[1]);
    fclose($pipes[2]);
    proc_close($process);

    // Parse text output
    $results = [
        'vulnerabilities' => [],
        'info' => [],
    ];

    foreach ($output as $line) {
        if (preg_match('/^\+\s*OSVDB-\d+:\s*(.+)/', $line, $matches)) {
            $results['vulnerabilities'][] = trim($matches[1]);
        } elseif (preg_match('/^\+\s*(.+)/', $line, $matches)) {
            $results['info'][] = trim($matches[1]);
        }
    }

    $results['scan_time'] = date('Y-m-d H:i:s');
    $results['target'] = $target;
    $results['raw_output'] = $rawOutput;

    // Save results
    file_put_contents("$scanDir/nikto-latest.json", json_encode($results, JSON_PRETTY_PRINT));
    chown("$scanDir/nikto-latest.json", 'www-data');

    // Update status to completed
    $status['status'] = 'completed';
    $status['completed_at'] = date('Y-m-d H:i:s');
    file_put_contents($statusFile, json_encode($status));

    return [
        'success' => true,
        'results' => $results,
        'vulnerabilities_count' => count($results['vulnerabilities'] ?? []),
        'info_count' => count($results['info'] ?? []),
    ];
}

function scannerStartLynis(array $params): array
{
    // Check if lynis is installed
    exec('which lynis 2>/dev/null', $output, $code);
    if ($code !== 0) {
        return ['success' => false, 'error' => 'Lynis is not installed'];
    }

    // Create scan output directory
    $scanDir = '/var/www/jabali/storage/app/security-scans';
    if (!is_dir($scanDir)) {
        mkdir($scanDir, 0755, true);
    }

    $statusFile = "$scanDir/lynis-status.json";
    $liveLogFile = "$scanDir/lynis-live.log";
    $pidFile = "$scanDir/lynis.pid";

    // Check if scan already running
    if (file_exists($pidFile)) {
        $pid = trim(file_get_contents($pidFile));
        if (posix_kill((int)$pid, 0)) {
            return ['success' => false, 'error' => 'Lynis scan already running'];
        }
    }

    // Initialize status file
    $status = [
        'status' => 'running',
        'started_at' => date('Y-m-d H:i:s'),
    ];
    file_put_contents($statusFile, json_encode($status));
    chown($statusFile, 'www-data');

    // Clear live log
    file_put_contents($liveLogFile, '');
    chown($liveLogFile, 'www-data');

    // Create wrapper script for background execution
    $wrapperScript = "$scanDir/lynis-wrapper.sh";
    $tempLog = "/tmp/lynis_" . uniqid() . ".log";
    $tempReport = "/tmp/lynis_report_" . uniqid() . ".dat";

    $resultsFile = "$scanDir/lynis-latest.json";
    $scriptContent = <<<BASH
#!/bin/bash
lynis audit system --no-colors --quick --logfile "$tempLog" --report-file "$tempReport" 2>&1 | while IFS= read -r line; do
    echo "\$line" >> "$liveLogFile"
done

# Parse results and save
RAW_OUTPUT=\$(cat "$liveLogFile")
HARDENING_INDEX=\$(echo "\$RAW_OUTPUT" | grep -oP 'Hardening index\s*:\s*\K\d+' || echo "0")
TESTS_PERFORMED=\$(echo "\$RAW_OUTPUT" | grep -oP 'Tests performed\s*:\s*\K\d+' || echo "0")

# Extract warnings and suggestions
WARNINGS=\$(echo "\$RAW_OUTPUT" | grep -oP '\[WARNING\]\s*\K.+' | head -50 | jq -R -s -c 'split("\n") | map(select(length > 0))')
SUGGESTIONS=\$(echo "\$RAW_OUTPUT" | grep -oP '\[SUGGESTION\]\s*\K.+' | head -100 | jq -R -s -c 'split("\n") | map(select(length > 0))')

# Escape raw output for JSON
RAW_ESCAPED=\$(echo "\$RAW_OUTPUT" | jq -R -s '.')

# Save results JSON
cat > "$resultsFile" << JSONEOF
{
    "hardening_index": \$HARDENING_INDEX,
    "tests_performed": \$TESTS_PERFORMED,
    "warnings": \$WARNINGS,
    "suggestions": \$SUGGESTIONS,
    "scan_time": "\$(date '+%Y-%m-%d %H:%M:%S')",
    "raw_output": \$RAW_ESCAPED
}
JSONEOF

chown www-data:www-data "$resultsFile"

# Update status to completed
cat > "$statusFile" << EOF
{
    "status": "completed",
    "started_at": "{$status['started_at']}",
    "completed_at": "\$(date '+%Y-%m-%d %H:%M:%S')",
    "hardening_index": \$HARDENING_INDEX,
    "tests_performed": \$TESTS_PERFORMED
}
EOF

chown www-data:www-data "$statusFile"

# Cleanup
rm -f "$tempLog" "$tempReport" "$pidFile" "$wrapperScript"
BASH;

    file_put_contents($wrapperScript, $scriptContent);
    chmod($wrapperScript, 0755);

    // Start in background
    $cmd = "nohup $wrapperScript > /dev/null 2>&1 & echo \$!";
    $pid = trim(shell_exec($cmd));
    file_put_contents($pidFile, $pid);

    return [
        'success' => true,
        'message' => 'Lynis scan started',
        'pid' => $pid,
    ];
}

function scannerGetScanStatus(array $params): array
{
    $scanner = $params['scanner'] ?? 'lynis';
    $scanDir = '/var/www/jabali/storage/app/security-scans';

    $statusFile = "$scanDir/{$scanner}-status.json";
    $liveLogFile = "$scanDir/{$scanner}-live.log";
    $resultsFile = "$scanDir/{$scanner}-latest.json";

    $status = [
        'status' => 'idle',
        'output' => '',
    ];

    if (file_exists($statusFile)) {
        $statusData = json_decode(file_get_contents($statusFile), true) ?? [];
        $status = array_merge($status, $statusData);
    }

    if (file_exists($liveLogFile)) {
        $status['output'] = file_get_contents($liveLogFile);
    }

    // If completed, load full results
    if ($status['status'] === 'completed' && file_exists($resultsFile)) {
        $status['results'] = json_decode(file_get_contents($resultsFile), true);
    }

    return $status;
}

// ============ LOG ANALYSIS ============

function logsTail(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';
    $logType = $params['type'] ?? 'access'; // access or error
    $lines = min(intval($params['lines'] ?? 100), 500); // Max 500 lines

    if (empty($username) || empty($domain)) {
        return ['success' => false, 'error' => 'Username and domain are required'];
    }

    // Validate username
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    // Validate domain
    if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/', $domain)) {
        return ['success' => false, 'error' => 'Invalid domain'];
    }

    // Validate log type
    if (!in_array($logType, ['access', 'error'])) {
        return ['success' => false, 'error' => 'Invalid log type'];
    }

    $logFile = "/home/$username/domains/$domain/logs/{$logType}.log";

    if (!file_exists($logFile)) {
        return ['success' => false, 'error' => 'Log file not found'];
    }

    // Use tail to get last N lines, then reverse with tac (newest first)
    $cmd = sprintf('tail -n %d %s 2>&1 | tac', $lines, escapeshellarg($logFile));
    $output = shell_exec($cmd);

    // Get file size and last modified time
    $stat = stat($logFile);

    return [
        'success' => true,
        'domain' => $domain,
        'type' => $logType,
        'lines' => $lines,
        'content' => $output ?? '',
        'file_size' => $stat['size'] ?? 0,
        'last_modified' => date('Y-m-d H:i:s', $stat['mtime'] ?? time()),
    ];
}

function isPathWithinUserHome(string $path, string $username): bool
{
    $home = "/home/$username/";
    $resolved = realpath($path);

    if ($resolved !== false) {
        return str_starts_with($resolved, $home);
    }

    $normalized = rtrim($path, '/').'/';

    return str_starts_with($normalized, $home);
}

function getDomainDocumentRoot(string $username, string $domain): string
{
    $domainListFile = "/home/$username/.domains";

    if (file_exists($domainListFile)) {
        $domains = json_decode(file_get_contents($domainListFile), true);
        if (is_array($domains)) {
            $docRoot = $domains[$domain]['document_root'] ?? null;
            if (is_string($docRoot) && $docRoot !== '') {
                return $docRoot;
            }
        }
    }

    return "/home/$username/domains/$domain/public_html";
}

function resolveStatsDirectory(string $username, string $domain, string $documentRoot): string
{
    $statsDir = rtrim($documentRoot, '/').'/stats';
    $nginxConfig = "/etc/nginx/sites-available/$domain.conf";

    if (file_exists($nginxConfig)) {
        $config = file_get_contents($nginxConfig);
        if ($config !== false && preg_match('/location\s+\/stats\/\s*\{[^}]*?\balias\s+([^;]+);/s', $config, $matches)) {
            $aliasPath = trim($matches[1]);
            if ($aliasPath !== '') {
                $statsDir = rtrim($aliasPath, '/');
            }
        }
    }

    return $statsDir;
}

function logsGoaccess(array $params): array
{
    $username = $params['username'] ?? '';
    $domain = $params['domain'] ?? '';
    $period = $params['period'] ?? 'today'; // today, week, month, all

    if (empty($username) || empty($domain)) {
        return ['success' => false, 'error' => 'Username and domain are required'];
    }

    // Validate username
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    // Validate domain
    if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/', $domain)) {
        return ['success' => false, 'error' => 'Invalid domain'];
    }

    // Check if goaccess is installed
    if (!file_exists('/usr/bin/goaccess')) {
        return ['success' => false, 'error' => 'GoAccess is not installed'];
    }

    $accessLog = "/home/$username/domains/$domain/logs/access.log";

    if (!file_exists($accessLog)) {
        return ['success' => false, 'error' => 'Access log not found'];
    }

    $documentRoot = getDomainDocumentRoot($username, $domain);
    $statsDir = resolveStatsDirectory($username, $domain, $documentRoot);
    $statsDir = rtrim($statsDir, '/');

    if (!isPathWithinUserHome($statsDir, $username)) {
        return ['success' => false, 'error' => 'Invalid stats directory'];
    }

    if (!is_dir($statsDir)) {
        mkdir($statsDir, 0755, true);
        chown($statsDir, $username);
        chgrp($statsDir, $username);
    }

    $outputFile = "$statsDir/report.html";

    // Ensure /stats location exists in nginx config to bypass WordPress
    $nginxConfig = "/etc/nginx/sites-available/$domain.conf";
    if (file_exists($nginxConfig)) {
        $config = file_get_contents($nginxConfig);
        if (strpos($config, 'location /stats/') === false) {
            $statsAlias = $statsDir.'/';

            // Add /stats location in the HTTPS (443) server block before the main location / block
            // Find the 443 server block and insert the /stats location
            $pattern = '/(listen 443 ssl;.*?)(    location \/ \{)/s';
            $replacement = '$1    # Stats directory - serve static files directly' . "\n" .
                          '    location /stats/ {' . "\n" .
                          '        alias ' . $statsAlias . ';' . "\n" .
                          '        index report.html;' . "\n" .
                          '        try_files $uri $uri/ =404;' . "\n" .
                          '    }' . "\n\n" .
                          '$2';
            $config = preg_replace($pattern, $replacement, $config, 1);
            file_put_contents($nginxConfig, $config);

            // Reload nginx
            exec('systemctl reload nginx 2>&1');
        }
    }

    // Build date filter based on period
    $dateFilter = '';
    switch ($period) {
        case 'today':
            $dateFilter = date('d/M/Y');
            break;
        case 'week':
            // Last 7 days - no filter needed, goaccess will show all
            break;
        case 'month':
            // Current month
            $dateFilter = date('/M/Y');
            break;
        case 'all':
        default:
            // No filter
            break;
    }

    // Generate HTML report with GoAccess
    // Using COMBINED log format (nginx default)
    $cmd = sprintf(
        '/usr/bin/goaccess %s -o %s --log-format=COMBINED --no-query-string --anonymize-ip --html-report-title=%s 2>&1',
        escapeshellarg($accessLog),
        escapeshellarg($outputFile),
        escapeshellarg("$domain Statistics")
    );

    exec($cmd, $output, $exitCode);

    if ($exitCode !== 0 || !file_exists($outputFile)) {
        return [
            'success' => false,
            'error' => 'Failed to generate report: ' . implode("\n", $output),
        ];
    }

    // Set proper ownership
    chown($outputFile, $username);
    chgrp($outputFile, $username);

    // Get basic stats from the log file
    $lineCount = intval(trim(shell_exec("wc -l < " . escapeshellarg($accessLog))));
    $fileSize = filesize($accessLog);

    return [
        'success' => true,
        'domain' => $domain,
        'period' => $period,
        'report_path' => $outputFile,
        'report_url' => "/stats/report.html", // Relative URL for domain
        'log_lines' => $lineCount,
        'log_size' => $fileSize,
        'generated_at' => date('Y-m-d H:i:s'),
    ];
}

// ============ JABALI SYSTEM SSH KEY ============

/**
 * Get the Jabali system SSH public key
 */
function jabaliSshGetPublicKey(array $params): array
{
    $keyDir = '/etc/jabali/ssh';
    $keyName = 'jabali_rsa';
    $keyComment = 'jabali-system-key';
    $pubKeyPath = "$keyDir/$keyName.pub";

    if (!file_exists($pubKeyPath)) {
        return [
            'success' => true,
            'exists' => false,
            'public_key' => null,
            'key_name' => $keyComment,
        ];
    }

    $publicKey = trim(file_get_contents($pubKeyPath));

    return [
        'success' => true,
        'exists' => true,
        'public_key' => $publicKey,
        'key_name' => $keyComment,
    ];
}

/**
 * Ensure the Jabali system SSH key exists, generate if needed
 */
function jabaliSshEnsureExists(array $params): array
{
    $keyDir = '/etc/jabali/ssh';
    $keyName = 'jabali_rsa';
    $keyComment = 'jabali-system-key';
    $keyPath = "$keyDir/$keyName";
    $pubKeyPath = "$keyPath.pub";

    // Check if key already exists
    if (file_exists($keyPath) && file_exists($pubKeyPath)) {
        $publicKey = trim(file_get_contents($pubKeyPath));
        return [
            'success' => true,
            'created' => false,
            'public_key' => $publicKey,
            'key_name' => $keyComment,
            'message' => 'SSH key already exists',
        ];
    }

    // Create directory if it doesn't exist
    if (!is_dir($keyDir)) {
        if (!mkdir($keyDir, 0700, true)) {
            return ['success' => false, 'error' => 'Failed to create SSH key directory'];
        }
    }

    // Generate new SSH key
    $command = sprintf(
        'ssh-keygen -t rsa -b 4096 -f %s -N "" -C %s 2>&1',
        escapeshellarg($keyPath),
        escapeshellarg($keyComment)
    );

    exec($command, $output, $returnCode);

    if ($returnCode !== 0) {
        logger("Failed to generate SSH key: " . implode("\n", $output));
        return ['success' => false, 'error' => 'Failed to generate SSH key: ' . implode("\n", $output)];
    }

    // Set proper permissions
    chmod($keyPath, 0600);
    chmod($pubKeyPath, 0600);
    chmod($keyDir, 0700);

    $publicKey = trim(file_get_contents($pubKeyPath));

    logger("Generated Jabali system SSH key");

    return [
        'success' => true,
        'created' => true,
        'public_key' => $publicKey,
        'key_name' => $keyComment,
        'message' => 'SSH key generated successfully',
    ];
}

/**
 * Get the Jabali system SSH private key (for importing to cPanel)
 */
function jabaliSshGetPrivateKey(array $params): array
{
    $keyDir = '/etc/jabali/ssh';
    $keyName = 'jabali_rsa';
    $keyComment = 'jabali-system-key';
    $keyPath = "$keyDir/$keyName";

    if (!file_exists($keyPath)) {
        return [
            'success' => true,
            'exists' => false,
            'private_key' => null,
            'key_name' => $keyComment,
        ];
    }

    $privateKey = file_get_contents($keyPath);

    return [
        'success' => true,
        'exists' => true,
        'private_key' => $privateKey,
        'key_name' => $keyComment,
    ];
}

/**
 * Add a public key to /root/.ssh/authorized_keys
 * This allows incoming SSH connections using the corresponding private key
 */
function jabaliSshAddToAuthorizedKeys(array $params): array
{
    $publicKey = $params['public_key'] ?? '';
    $comment = $params['comment'] ?? 'cpanel-migration';

    if (empty($publicKey)) {
        return ['success' => false, 'error' => 'Public key is required'];
    }

    $sshDir = '/root/.ssh';
    $authKeysPath = "$sshDir/authorized_keys";

    // Create .ssh directory if it doesn't exist
    if (!is_dir($sshDir)) {
        if (!mkdir($sshDir, 0700, true)) {
            return ['success' => false, 'error' => 'Failed to create .ssh directory'];
        }
    }

    // Ensure proper permissions on .ssh directory
    chmod($sshDir, 0700);

    // Check if key already exists in authorized_keys
    $existingKeys = '';
    if (file_exists($authKeysPath)) {
        $existingKeys = file_get_contents($authKeysPath);

        // Extract the key part (without comment) for comparison
        $keyParts = explode(' ', trim($publicKey));
        $keyToFind = isset($keyParts[1]) ? $keyParts[1] : $publicKey;

        if (strpos($existingKeys, $keyToFind) !== false) {
            return [
                'success' => true,
                'already_exists' => true,
                'message' => 'Key already exists in authorized_keys',
            ];
        }
    }

    // Append the key with comment
    $keyLine = trim($publicKey);
    if (!empty($comment) && strpos($keyLine, $comment) === false) {
        // Add comment if not already present
        $keyParts = explode(' ', $keyLine);
        if (count($keyParts) >= 2) {
            $keyLine = $keyParts[0] . ' ' . $keyParts[1] . ' ' . $comment;
        }
    }

    // Add newline before if file exists and doesn't end with newline
    $prefix = '';
    if (!empty($existingKeys) && substr($existingKeys, -1) !== "\n") {
        $prefix = "\n";
    }

    if (file_put_contents($authKeysPath, $prefix . $keyLine . "\n", FILE_APPEND | LOCK_EX) === false) {
        return ['success' => false, 'error' => 'Failed to write to authorized_keys'];
    }

    // Ensure proper permissions
    chmod($authKeysPath, 0600);

    logger("Added public key to /root/.ssh/authorized_keys: $comment");

    return [
        'success' => true,
        'already_exists' => false,
        'message' => 'Key added to authorized_keys',
    ];
}

// ============ CPANEL MIGRATION ============

/**
 * Analyze a cPanel backup tar.gz file to extract information about its contents.
 * Returns details about domains, databases, mailboxes, and SSL certificates.
 */
function cpanelAnalyzeBackup(array $params): array
{
    $backupPath = $params['backup_path'] ?? '';

    if (empty($backupPath) || !file_exists($backupPath)) {
        return ['success' => false, 'error' => 'Backup file not found: ' . $backupPath];
    }

    logger("Analyzing cPanel backup: $backupPath");

    // Create temp directory for extraction
    $tempDir = sys_get_temp_dir() . '/cpanel_analyze_' . uniqid();
    if (!mkdir($tempDir, 0755, true)) {
        return ['success' => false, 'error' => 'Failed to create temp directory'];
    }

    try {
        // Extract backup to temp directory (only metadata and structure, not full files)
        // List contents first to understand structure
        $listOutput = [];
        // List all files - need to see SSL certs which may be near end of archive
        exec("tar -I pigz -tf " . escapeshellarg($backupPath) . " 2>&1", $listOutput, $code);

        if ($code !== 0) {
            throw new Exception('Failed to list backup contents');
        }

        $data = [
            'domains' => [],
            'databases' => [],
            'mailboxes' => [],
            'forwarders' => [],
            'ssl_certificates' => [],
            'total_size' => filesize($backupPath),
            'cpanel_username' => '',
        ];

        // Find cPanel username from backup structure
        $cpanelUser = '';
        foreach ($listOutput as $file) {
            // Pattern: backup-date_time_username/ or username/
            if (preg_match('/^([^\/]+)\//', $file, $matches)) {
                $firstDir = $matches[1];
                if (preg_match('/^backup-[\d.]+_[\d-]+_(.+)$/', $firstDir, $userMatch)) {
                    $cpanelUser = $userMatch[1];
                } else {
                    $cpanelUser = $firstDir;
                }
                break;
            }
        }
        $data['cpanel_username'] = $cpanelUser;

        // Parse file list to identify content types
        $domainSet = [];
        $dbSet = [];
        $mailboxSet = [];
        $forwarderSet = [];
        $sslSet = [];

        foreach ($listOutput as $file) {
            // Skip directory entries
            if (substr($file, -1) === '/') {
                continue;
            }

            // Detect MySQL databases (mysql/*.sql or mysql/*.sql.gz)
            if (preg_match('/mysql\/([^\/]+)\.(sql|sql\.gz)$/', $file, $matches)) {
                $dbName = $matches[1];
                if (!isset($dbSet[$dbName])) {
                    $dbSet[$dbName] = [
                        'name' => $dbName,
                        'file' => $file,
                        'size' => 0, // Will be determined during extraction
                    ];
                }
            }

            // Detect email data (mail/domain.com/user/ structure)
            // Skip system folders and hidden directories
            if (preg_match('/mail\/([^\/]+)\/([^\/]+)\//', $file, $matches)) {
                $domain = $matches[1];
                $localPart = $matches[2];

                // Skip invalid domains (must contain a dot and not start with dot)
                if (strpos($domain, '.') === false || strpos($domain, '.') === 0) {
                    continue;
                }

                // Skip system/hidden folders that aren't mailboxes
                $skipFolders = ['cur', 'new', 'tmp', '.spam', '.Trash', '.Drafts', '.Sent', '.Junk', 'courierimapkeywords', 'maildirfolder'];
                if (in_array($localPart, $skipFolders) || strpos($localPart, '.') === 0) {
                    continue;
                }

                $email = "$localPart@$domain";
                if (!isset($mailboxSet[$email])) {
                    $mailboxSet[$email] = [
                        'email' => $email,
                        'local_part' => $localPart,
                        'domain' => $domain,
                    ];
                }
            }

            // Detect email forwarders - cPanel stores them in multiple formats:
            // Format 1: etc/domainname/va/localpart - each file contains destination
            // Format 2: va/domain - file contains lines like "localpart@domain: destination"
            if (preg_match('/(?:homedir\/)?etc\/([^\/]+)\/va\/([^\/]+)$/', $file, $matches)) {
                $domain = $matches[1];
                $localPart = $matches[2];

                // Skip invalid domains (must contain a dot)
                if (strpos($domain, '.') === false) {
                    continue;
                }

                // Skip system files
                if (in_array($localPart, ['.', '..', 'shadow', 'passwd', 'quota'])) {
                    continue;
                }

                $email = "$localPart@$domain";
                if (!isset($forwarderSet[$email])) {
                    $forwarderSet[$email] = [
                        'email' => $email,
                        'local_part' => $localPart,
                        'domain' => $domain,
                        'file' => $file,
                        'format' => 'etc_va',
                    ];
                }
            }
            // Format 2: va/domain files (common in newer cPanel backups)
            // These are files like "va/example.com" containing "user@example.com: dest@other.com"
            elseif (preg_match('/^[^\/]+\/va\/([^\/]+)$/', $file, $matches)) {
                $domain = $matches[1];
                // Must be a valid domain (contains dot, not a system file)
                if (strpos($domain, '.') !== false && !in_array($domain, ['.', '..'])) {
                    // Mark this as a va file to parse during restore
                    // We use a special key to track va files for later parsing
                    $vaFileKey = "va_file:$domain";
                    if (!isset($forwarderSet[$vaFileKey])) {
                        $forwarderSet[$vaFileKey] = [
                            'email' => '',  // Will be populated during content parsing
                            'local_part' => '',
                            'domain' => $domain,
                            'file' => $file,
                            'format' => 'va_file',  // Mark as needing content parsing
                        ];
                    }
                }
            }

            // Detect SSL certificates - cPanel stores them in homedir/ssl/certs/ and homedir/ssl/keys/
            // Cert filename: domain_name_keyid_timestamp_hash.crt (e.g., laundry_sh_co_il_e3914_8256f_1758985760_abc123.crt)
            // Key filename: keyid_hash.key (e.g., e3914_8256f_abc123.key)
            // Match paths like: homedir/ssl/certs/, ssl/certs/, etc.
            if (preg_match('/(?:homedir\/)?ssl\/certs\/([^\/]+)\.(crt|pem|cert)$/i', $file, $matches)) {
                $certFilename = $matches[1];
                // Skip cache files
                if (str_ends_with($certFilename, '.cache') || str_ends_with($certFilename, '.crt.cache')) {
                    continue;
                }
                // Extract domain and keyid from cert filename
                if (preg_match('/^(.+)_([a-f0-9]+_[a-f0-9]+)_\d+_[a-f0-9]+$/i', $certFilename, $certParts)) {
                    $domain = str_replace('_', '.', $certParts[1]);
                    $keyId = $certParts[2];
                    if (!isset($sslSet[$keyId])) {
                        $sslSet[$keyId] = ['domain' => $domain, 'keyid' => $keyId, 'has_key' => false, 'has_cert' => true];
                    } else {
                        $sslSet[$keyId]['has_cert'] = true;
                        $sslSet[$keyId]['domain'] = $domain;
                    }
                }
            } elseif (preg_match('/(?:homedir\/)?ssl\/keys\/([^\/]+)\.key$/i', $file, $matches)) {
                $keyFilename = $matches[1];
                // Extract keyid from key filename
                if (preg_match('/^([a-f0-9]+_[a-f0-9]+)_[a-f0-9]+$/i', $keyFilename, $keyParts)) {
                    $keyId = $keyParts[1];
                    if (!isset($sslSet[$keyId])) {
                        $sslSet[$keyId] = ['domain' => '', 'keyid' => $keyId, 'has_key' => true, 'has_cert' => false];
                    } else {
                        $sslSet[$keyId]['has_key'] = true;
                    }
                }
            } elseif (preg_match('/(ssl|sslkeys|sslcerts)\/([^\/]+)\.(crt|key|pem)$/', $file, $matches)) {
                // Fallback for simpler SSL file formats
                $domain = str_replace('_', '.', $matches[2]);
                if (!isset($sslSet[$domain])) {
                    $sslSet[$domain] = ['domain' => $domain, 'has_key' => false, 'has_cert' => false];
                }
                if ($matches[3] === 'key') {
                    $sslSet[$domain]['has_key'] = true;
                } elseif (in_array($matches[3], ['crt', 'pem'])) {
                    $sslSet[$domain]['has_cert'] = true;
                }
            }

            // Detect domains from homedir structure or userdata
            // homedir/public_html = main domain
            // homedir/domains/domain.com = addon domain
            if (preg_match('/homedir\/public_html\//', $file)) {
                $domainSet['main'] = true; // Main domain exists
            }
            if (preg_match('/homedir\/([^\/]+)\/domains\/([^\/]+)\//', $file, $matches)) {
                $domainSet[$matches[2]] = ['name' => $matches[2], 'type' => 'addon'];
            }

            // Check userdata for domain info
            if (preg_match('/(?:cp\/)?userdata\/([^\/]+)$/', $file, $matches)) {
                $domainName = $matches[1];
                // Skip config files - only real domains
                if ($domainName === 'main' || $domainName === 'cache' ||
                    preg_match('/\.(yaml|yaml\.transferred|json)$/', $domainName) ||
                    preg_match('/_SSL$/', $domainName) ||
                    preg_match('/^_/', $domainName)) {
                    continue;
                }
                if (strpos($domainName, '.') !== false && !isset($domainSet[$domainName])) {
                    $domainSet[$domainName] = ['name' => $domainName, 'type' => 'addon'];
                }
            }
        }

        // Extract userdata if exists to get proper domain info
        // Try multiple patterns - cPanel backups can have different structures
        // Note: backup dirs often use format: backup-DATE_TIME_USERNAME so we use wildcards
        $userdataPatterns = [
            "*/userdata/*",                                              // Most common: backup-xxx_username/userdata/*
            "*/cp/userdata/*",                                           // Alternative with cp subdir
            $cpanelUser ? "*{$cpanelUser}*/userdata/*" : null,          // Match backup dir containing username
            $cpanelUser ? "*{$cpanelUser}*/cp/userdata/*" : null,       // Match backup dir with cp subdir
            "cp/userdata/*",                                             // Direct cp/userdata
            "userdata/*",                                                // Direct userdata
        ];
        $userdataPatterns = array_filter($userdataPatterns);  // Remove nulls

        foreach ($userdataPatterns as $userdataPattern) {
            exec("tar -I pigz -xf " . escapeshellarg($backupPath) . " -C " . escapeshellarg($tempDir) . " --wildcards " . escapeshellarg($userdataPattern) . " 2>/dev/null");
        }

        // Look for extracted userdata in multiple possible locations
        // Note: glob() can return false on error, so we use ?: [] to ensure arrays
        $userdataDirs = array_merge(
            glob($tempDir . '/*/cp/userdata') ?: [],
            glob($tempDir . '/*/userdata') ?: [],
            glob($tempDir . '/*/*/userdata') ?: [],  // For nested backup dirs
            glob($tempDir . '/cp/userdata') ?: [],
            glob($tempDir . '/userdata') ?: []
        );

        logger("Searching for userdata in tempDir: $tempDir");
        logger("Userdata directories found: " . json_encode($userdataDirs));

        // If no userdata dirs found, list what's actually in tempDir
        if (empty($userdataDirs)) {
            $tempContents = [];
            exec("find " . escapeshellarg($tempDir) . " -type d -name 'userdata' 2>/dev/null", $tempContents);
            logger("Find userdata results: " . json_encode($tempContents));
        }

        $mainDomainFound = false;

        foreach ($userdataDirs as $userdataPath) {
            if (!is_dir($userdataPath)) {
                continue;
            }

            logger("Found userdata directory: $userdataPath");

            // Read main domain from 'main' file
            $mainFile = $userdataPath . '/main';
            if (file_exists($mainFile)) {
                $mainContent = file_get_contents($mainFile);
                logger("Reading main userdata file, content length: " . strlen($mainContent));

                // Parse YAML-like format - try multiple patterns
                if (preg_match('/^main_domain:\s*(.+)$/m', $mainContent, $matches)) {
                    $mainDomain = trim($matches[1]);
                    logger("Found main_domain in userdata/main: $mainDomain");
                    $data['domains'][] = ['name' => $mainDomain, 'type' => 'main'];
                    $mainDomainFound = true;
                    unset($domainSet['main']);
                } else {
                    logger("No main_domain found in userdata/main file");
                    // Try to find it in different format
                    if (preg_match('/documentroot:\s*\/home\/[^\/]+\/public_html$/m', $mainContent)) {
                        // This is the main domain file for a domain using public_html
                        logger("Found documentroot pointing to public_html in main file");
                    }
                }

                // Parse addon_domains section
                if (preg_match('/addon_domains:\s*\n((?:\s+.+:\s*.+\n?)+)/', $mainContent, $matches)) {
                    // Parse addon domains from the block
                    $addonBlock = $matches[1];
                    if (preg_match_all('/^\s+([^:]+):\s*(.+)$/m', $addonBlock, $addonMatches, PREG_SET_ORDER)) {
                        foreach ($addonMatches as $addonMatch) {
                            $addonDomain = trim($addonMatch[1]);
                            if (!empty($addonDomain) && strpos($addonDomain, '.') !== false) {
                                $existingDomains = array_column($data['domains'], 'name');
                                if (!in_array($addonDomain, $existingDomains)) {
                                    $data['domains'][] = ['name' => $addonDomain, 'type' => 'addon'];
                                    logger("Found addon domain in main file: $addonDomain");
                                }
                            }
                        }
                    }
                }
            }

            // Read individual domain files
            foreach (scandir($userdataPath) as $file) {
                if ($file === '.' || $file === '..' || $file === 'main' || $file === 'cache') {
                    continue;
                }

                // Skip cPanel config files (not actual domains)
                if (preg_match('/\.(yaml|yaml\.transferred|json)$/', $file) ||  // Config files
                    preg_match('/_SSL$/', $file) ||                              // SSL config files
                    preg_match('/^_/', $file)) {                                 // Internal files
                    continue;
                }

                if (strpos($file, '.') !== false) {
                    $domainName = preg_replace('/\.cache$/', '', $file);
                    $existingDomains = array_column($data['domains'], 'name');

                    // Read the domain userdata file to check documentroot
                    $domainFile = $userdataPath . '/' . $file;
                    $isMainDomain = false;

                    if (file_exists($domainFile)) {
                        $domainContent = file_get_contents($domainFile);
                        // If documentroot is /home/user/public_html, this is the main domain
                        if (preg_match('/documentroot:\s*\/home\/[^\/]+\/public_html\s*$/m', $domainContent)) {
                            $isMainDomain = true;
                            logger("Domain $domainName has public_html as documentroot - marking as main");
                        }
                    }

                    // Check if this is an addon domain file (addon.maindomain format)
                    // e.g., shilatlaser.co.il.laundry-sh.co.il means shilatlaser.co.il is addon
                    $extractedAddon = null;
                    foreach ($existingDomains as $existing) {
                        if (preg_match('/^(.+)\.' . preg_quote($existing, '/') . '$/', $domainName, $addonMatch)) {
                            $extractedAddon = $addonMatch[1];
                            break;
                        }
                    }

                    if ($extractedAddon) {
                        // This is an addon domain - add the extracted name
                        if (!in_array($extractedAddon, $existingDomains)) {
                            $data['domains'][] = ['name' => $extractedAddon, 'type' => 'addon'];
                            logger("Found addon domain file: $extractedAddon (from $domainName)");
                        }
                    } elseif (!in_array($domainName, $existingDomains)) {
                        // Regular domain file - check if main or addon
                        if ($isMainDomain && !$mainDomainFound) {
                            $data['domains'][] = ['name' => $domainName, 'type' => 'main'];
                            $mainDomainFound = true;
                            logger("Found main domain from documentroot: $domainName");
                        } else {
                            $data['domains'][] = ['name' => $domainName, 'type' => 'addon'];
                            logger("Found domain file: $domainName (type: addon)");
                        }
                    }
                }
            }

            // If we found a main domain, no need to check other userdata dirs
            if ($mainDomainFound) {
                break;
            }
        }

        // Fallback: if no main domain found but public_html exists, try to identify it
        if (!$mainDomainFound && isset($domainSet['main'])) {
            logger("Main domain not found in userdata, attempting fallback detection");

            // Method 1: Look for a domain with same name as cPanel username
            $existingDomains = array_column($data['domains'], 'name');
            foreach ($existingDomains as $idx => $domain) {
                // Check if any existing domain contains the cPanel username
                if (!empty($cpanelUser) && strpos($domain, $cpanelUser) !== false) {
                    // Update this domain to be the main domain
                    foreach ($data['domains'] as &$d) {
                        if ($d['name'] === $domain) {
                            $d['type'] = 'main';
                            $mainDomainFound = true;
                            logger("Fallback: Set $domain as main domain (matches username)");
                            break 2;
                        }
                    }
                    unset($d);
                }
            }

            // Method 2: If still not found, the first non-subdomain might be the main
            if (!$mainDomainFound) {
                foreach ($data['domains'] as &$d) {
                    // Check if this looks like a main domain (not a subdomain)
                    $parts = explode('.', $d['name']);
                    if (count($parts) <= 3) { // e.g., example.co.il or example.com
                        $d['type'] = 'main';
                        $mainDomainFound = true;
                        logger("Fallback: Set {$d['name']} as main domain (first valid domain)");
                        break;
                    }
                }
                unset($d);
            }
        }

        // Add remaining detected domains (with filtering and addon extraction)
        foreach ($domainSet as $key => $domain) {
            if ($key === 'main' || !is_array($domain)) {
                continue;
            }
            $domainName = $domain['name'];

            // Skip config files
            if (preg_match('/\.(yaml|yaml\.transferred|json)$/', $domainName) ||
                preg_match('/_SSL$/', $domainName)) {
                continue;
            }

            $existingDomains = array_column($data['domains'], 'name');

            // Check if this is an addon domain (addon.maindomain format)
            $extractedAddon = null;
            foreach ($existingDomains as $existing) {
                if (preg_match('/^(.+)\.' . preg_quote($existing, '/') . '$/', $domainName, $addonMatch)) {
                    $extractedAddon = $addonMatch[1];
                    break;
                }
            }

            if ($extractedAddon) {
                // This is an addon domain
                if (!in_array($extractedAddon, $existingDomains)) {
                    $data['domains'][] = ['name' => $extractedAddon, 'type' => 'addon'];
                }
            } elseif (!in_array($domainName, $existingDomains)) {
                $data['domains'][] = $domain;
            }
        }

        // Parse va_file format entries to extract actual forwarders
        // These are files like "va/domain.com" containing "user@domain.com: dest@other.com"
        $parsedForwarders = [];
        foreach ($forwarderSet as $key => $entry) {
            if (($entry['format'] ?? '') === 'va_file') {
                // Extract and parse the va file
                $vaFile = $entry['file'];
                $output = [];
                exec('tar -I pigz -xOf ' . escapeshellarg($backupPath) . ' ' . escapeshellarg($vaFile) . ' 2>/dev/null', $output);

                foreach ($output as $line) {
                    $line = trim($line);
                    if (empty($line) || $line[0] === '#') continue;

                    // Parse format: "localpart@domain: destination1, destination2"
                    // or "localpart: destination" (domain implied from filename)
                    // or "*: catchall" (catchall forwarder)
                    if (preg_match('/^([^:]+):\s*(.+)$/', $line, $fwdMatch)) {
                        $source = trim($fwdMatch[1]);
                        $destinations = trim($fwdMatch[2]);

                        // Skip catchall entries (*)
                        if ($source === '*') continue;

                        // Determine email address
                        if (strpos($source, '@') !== false) {
                            $email = $source;
                            [$localPart, $domain] = explode('@', $source, 2);
                        } else {
                            $localPart = $source;
                            $domain = $entry['domain'];
                            $email = "$localPart@$domain";
                        }

                        // Add to parsed forwarders
                        if (!isset($parsedForwarders[$email])) {
                            $parsedForwarders[$email] = [
                                'email' => $email,
                                'local_part' => $localPart,
                                'domain' => $domain,
                                'destinations' => $destinations,
                                'file' => $vaFile,
                            ];
                        }
                    }
                }
            } else {
                // Keep non-va_file entries as-is
                $parsedForwarders[$key] = $entry;
            }
        }
        $forwarderSet = $parsedForwarders;

        // Convert sets to arrays
        $data['databases'] = array_values($dbSet);
        $data['mailboxes'] = array_values($mailboxSet);
        $data['forwarders'] = array_values($forwarderSet);
        // Filter SSL certs that have both key and cert, and have a valid domain
        $data['ssl_certificates'] = array_values(array_filter($sslSet, fn($s) =>
            $s['has_key'] && $s['has_cert'] && !empty($s['domain'])
        ));

        // Clean up temp directory
        exec("rm -rf " . escapeshellarg($tempDir));

        logger("cPanel backup analyzed: " . count($data['domains']) . " domains, " .
               count($data['databases']) . " databases, " . count($data['mailboxes']) . " mailboxes, " .
               count($data['forwarders']) . " forwarders");

        return [
            'success' => true,
            'data' => $data,
        ];

    } catch (Exception $e) {
        exec("rm -rf " . escapeshellarg($tempDir));
        logger("cPanel backup analysis failed: " . $e->getMessage(), 'ERROR');
        return ['success' => false, 'error' => $e->getMessage()];
    }
}

function cpanelAppendMigrationLog(?string $logPath, string $message, string $status): void
{
    if (empty($logPath)) {
        return;
    }

    $entry = [
        'message' => $message,
        'status' => $status,
        'time' => date('H:i:s'),
    ];

    file_put_contents($logPath, json_encode($entry) . PHP_EOL, FILE_APPEND | LOCK_EX);
    @chmod($logPath, 0644);
}

function cpanelAddMigrationEntry(array &$log, ?string $logPath, string $message, string $status): void
{
    $log[] = [
        'message' => $message,
        'status' => $status,
        'time' => date('H:i:s'),
    ];

    cpanelAppendMigrationLog($logPath, $message, $status);
}

/**
 * Restore a cPanel backup to a Jabali user account.
 * Handles files, databases, emails, and SSL certificates.
 */
function cpanelRestoreBackup(array $params): array
{
    $backupPath = $params['backup_path'] ?? '';
    $username = $params['username'] ?? '';
    $restoreFiles = $params['restore_files'] ?? true;
    $restoreDatabases = $params['restore_databases'] ?? true;
    $restoreEmails = $params['restore_emails'] ?? true;
    $restoreSsl = $params['restore_ssl'] ?? true;
    $logPath = $params['log_path'] ?? null;
    // Pre-discovered data from API - if provided, skip expensive backup analysis
    $discoveredData = $params['discovered_data'] ?? null;

    if (empty($backupPath) || !file_exists($backupPath)) {
        return ['success' => false, 'error' => 'Backup file not found'];
    }

    if (empty($username) || !validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    // Verify user exists
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User does not exist'];
    }

    logger("Starting cPanel backup restore for user: $username from $backupPath");

    $log = [];
    $homeDir = "/home/$username";
    $domainsDir = "$homeDir/domains";

    cpanelAddMigrationEntry($log, $logPath, "Starting cPanel backup restoration for user: $username", 'pending');

    // Create temp directory for extraction
    $tempDir = sys_get_temp_dir() . '/cpanel_restore_' . uniqid();
    if (!mkdir($tempDir, 0755, true)) {
        return ['success' => false, 'error' => 'Failed to create temp directory'];
    }

    try {
        // Full extraction
        cpanelAddMigrationEntry($log, $logPath, 'Extracting backup archive...', 'pending');
        exec("tar -I pigz -xf " . escapeshellarg($backupPath) . " -C " . escapeshellarg($tempDir) . " 2>&1", $output, $code);

        if ($code !== 0) {
            throw new Exception('Failed to extract backup: ' . implode("\n", $output));
        }
        cpanelAddMigrationEntry($log, $logPath, 'Backup archive extracted', 'success');

        // Find extracted directory (could be username or backup-date_username)
        $extractedDirs = glob($tempDir . '/*', GLOB_ONLYDIR);
        if (empty($extractedDirs)) {
            throw new Exception('No directory found in backup archive');
        }
        $backupRoot = $extractedDirs[0];

        // Use pre-discovered data if available (from API), otherwise analyze already-extracted content
        if ($discoveredData !== null) {
            logger("Using pre-discovered data from API (skipping backup analysis)");
            $domains = $discoveredData['domains'] ?? [];
            $databases = $discoveredData['databases'] ?? [];
            $mailboxes = $discoveredData['mailboxes'] ?? [];
            $forwarders = $discoveredData['forwarders'] ?? [];
        } else {
            // Analyze the already-extracted content directly (don't re-extract via cpanelAnalyzeBackup)
            logger("No pre-discovered data, analyzing extracted content in $backupRoot...");

            $domains = [];
            $databases = [];
            $mailboxes = [];
            $forwarders = [];

            // Find userdata directory in extracted backup
            $userdataDirs = array_merge(
                glob($backupRoot . '/cp/userdata') ?: [],
                glob($backupRoot . '/userdata') ?: []
            );

            $mainDomainFound = false;
            foreach ($userdataDirs as $userdataPath) {
                if (!is_dir($userdataPath)) continue;

                // Read main domain from 'main' file
                $mainFile = $userdataPath . '/main';
                if (file_exists($mainFile)) {
                    $mainContent = file_get_contents($mainFile);
                    if (preg_match('/^main_domain:\s*(.+)$/m', $mainContent, $matches)) {
                        $mainDomain = trim($matches[1]);
                        $domains[] = ['name' => $mainDomain, 'type' => 'main'];
                        $mainDomainFound = true;
                        logger("Found main domain in extracted userdata: $mainDomain");
                    }
                }

                // Read domain files
                foreach (scandir($userdataPath) as $file) {
                    if ($file === '.' || $file === '..' || $file === 'main' || $file === 'cache') continue;
                    if (preg_match('/\.(yaml|json)$/', $file) || preg_match('/_SSL$/', $file)) continue;

                    if (strpos($file, '.') !== false) {
                        $domainName = preg_replace('/\.cache$/', '', $file);
                        $existingNames = array_column($domains, 'name');

                        // Check for addon.maindomain format
                        $extractedAddon = null;
                        foreach ($existingNames as $existing) {
                            if (preg_match('/^(.+)\.' . preg_quote($existing, '/') . '$/', $domainName, $m)) {
                                $extractedAddon = $m[1];
                                break;
                            }
                        }

                        if ($extractedAddon && !in_array($extractedAddon, $existingNames)) {
                            $domains[] = ['name' => $extractedAddon, 'type' => 'addon'];
                            logger("Found addon domain: $extractedAddon");
                        } elseif (!in_array($domainName, $existingNames)) {
                            $domains[] = ['name' => $domainName, 'type' => 'addon'];
                            logger("Found domain: $domainName");
                        }
                    }
                }

                if ($mainDomainFound) break;
            }

            // Find databases from mysql directory
            $mysqlDirs = array_merge(
                glob($backupRoot . '/mysql') ?: [],
                glob($backupRoot . '/homedir/mysql') ?: []
            );
            foreach ($mysqlDirs as $mysqlDir) {
                if (!is_dir($mysqlDir)) continue;
                foreach (glob($mysqlDir . '/*.sql*') as $sqlFile) {
                    $dbName = basename($sqlFile);
                    $dbName = preg_replace('/\.(sql|sql\.gz)$/', '', $dbName);
                    $databases[] = ['name' => $dbName, 'file' => $sqlFile];
                    logger("Found database: $dbName");
                }
            }

            // Find mailboxes from mail directory
            $mailDirs = array_merge(
                glob($backupRoot . '/homedir/mail/*', GLOB_ONLYDIR) ?: [],
                glob($backupRoot . '/mail/*', GLOB_ONLYDIR) ?: []
            );
            foreach ($mailDirs as $domainDir) {
                $domain = basename($domainDir);
                if (strpos($domain, '.') === false) continue;
                foreach (glob($domainDir . '/*', GLOB_ONLYDIR) as $userDir) {
                    $localPart = basename($userDir);
                    if (in_array($localPart, ['cur', 'new', 'tmp', '.spam', '.Trash']) || strpos($localPart, '.') === 0) continue;
                    $mailboxes[] = ['email' => "$localPart@$domain", 'local_part' => $localPart, 'domain' => $domain];
                    logger("Found mailbox: $localPart@$domain");
                }
            }

            // Find email forwarders from etc/domain/va/ directory
            $vaDirs = array_merge(
                glob($backupRoot . '/homedir/etc/*/va', GLOB_ONLYDIR) ?: [],
                glob($backupRoot . '/etc/*/va', GLOB_ONLYDIR) ?: []
            );
            foreach ($vaDirs as $vaDir) {
                $domain = basename(dirname($vaDir));
                if (strpos($domain, '.') === false) continue;
                foreach (glob($vaDir . '/*') as $vaFile) {
                    if (is_dir($vaFile)) continue;
                    $localPart = basename($vaFile);
                    if (in_array($localPart, ['.', '..', 'shadow', 'passwd', 'quota'])) continue;
                    $forwarders[] = [
                        'email' => "$localPart@$domain",
                        'local_part' => $localPart,
                        'domain' => $domain,
                        'file' => $vaFile,
                    ];
                    logger("Found forwarder: $localPart@$domain");
                }
            }

            logger("Analysis from extracted content: " . count($domains) . " domains, " . count($databases) . " databases, " . count($mailboxes) . " mailboxes, " . count($forwarders) . " forwarders");
        }

        // Restore website files
        if ($restoreFiles) {
            cpanelAddMigrationEntry($log, $logPath, 'Restoring website files...', 'pending');

            // Ensure domains directory exists
            if (!is_dir($domainsDir)) {
                mkdir($domainsDir, 0755, true);
                chown($domainsDir, $username);
                chgrp($domainsDir, $username);
            }

            foreach ($domains as $domain) {
                $domainName = $domain['name'];
                $domainDir = "$domainsDir/$domainName";
                $publicHtml = "$domainDir/public_html";

                // Create domain directory structure
                if (!is_dir($publicHtml)) {
                    mkdir($publicHtml, 0755, true);
                }

                // Find source files - check multiple possible locations
                $sourceDir = null;
                $possiblePaths = [];

                // Build comprehensive list of possible paths - start with type-specific paths
                if ($domain['type'] === 'main') {
                    // Main domain files can be in several locations
                    $possiblePaths = [
                        "$backupRoot/homedir/public_html",
                        "$backupRoot/public_html",
                        "$backupRoot/homedir",
                    ];
                } else {
                    // Addon/sub domains might be in various locations
                    $possiblePaths = [
                        "$backupRoot/homedir/domains/$domainName/public_html",
                        "$backupRoot/homedir/$domainName/public_html",
                        "$backupRoot/homedir/public_html/$domainName",
                        "$backupRoot/domains/$domainName/public_html",
                        "$backupRoot/$domainName/public_html",
                    ];
                }

                // Add fallback paths that check both main and addon locations regardless of type
                // This handles cases where domain type detection might be incorrect
                $fallbackPaths = [
                    "$backupRoot/homedir/public_html",  // Main domain location
                    "$backupRoot/public_html",
                    "$backupRoot/homedir/domains/$domainName/public_html",  // Addon domain locations
                    "$backupRoot/homedir/$domainName/public_html",
                    "$backupRoot/domains/$domainName/public_html",
                ];

                // Merge paths, keeping type-specific paths first
                $possiblePaths = array_unique(array_merge($possiblePaths, $fallbackPaths));

                // Log available directories for debugging
                $availableDirs = [];
                if (is_dir("$backupRoot/homedir")) {
                    $availableDirs[] = "homedir: " . implode(', ', array_diff(scandir("$backupRoot/homedir"), ['.', '..']));
                }
                if (is_dir("$backupRoot/homedir/domains")) {
                    $availableDirs[] = "homedir/domains: " . implode(', ', array_diff(scandir("$backupRoot/homedir/domains"), ['.', '..']));
                }
                if (!empty($availableDirs)) {
                    logger("Backup structure for $domainName: " . implode('; ', $availableDirs));
                }

                // Log what paths we're checking
                logger("Checking paths for $domainName ({$domain['type']}): " . implode(', ', array_map(fn($p) => str_replace($backupRoot, '', $p), $possiblePaths)));

                foreach ($possiblePaths as $path) {
                    if (is_dir($path)) {
                        // Check if directory has any content
                        $contents = array_diff(scandir($path), ['.', '..']);
                        if (!empty($contents)) {
                            $sourceDir = $path;
                            logger("Found source for $domainName: $path (" . count($contents) . " items)");
                            break;
                        } else {
                            logger("Path $path exists but is empty");
                        }
                    }
                }

                if (!$sourceDir) {
                    logger("No source found for domain $domainName ({$domain['type']})");
                    // List all directories found in backup for debugging
                    exec("find " . escapeshellarg($backupRoot) . " -type d -maxdepth 4 2>/dev/null | head -50", $allDirs);
                    if (!empty($allDirs)) {
                        logger("Available directories in backup: " . implode(', ', array_map(fn($d) => str_replace($backupRoot, '', $d), $allDirs)));
                    }
                }

                if ($sourceDir && is_dir($sourceDir)) {
                    // For main domain, we need to exclude addon domain subdirectories
                    $excludeOptions = '';
                    if ($domain['type'] === 'main') {
                        // Find addon domain document roots from cPanel userdata
                        $excludeDirs = [];
                        $mainPublicHtml = "$backupRoot/homedir/public_html";

                        // Check for addon domain directories under main domain's public_html
                        $addonDomains = array_filter($domains, fn($d) => $d['type'] !== 'main');
                        $addonNames = array_map(fn($d) => $d['name'], $addonDomains);

                        logger("Looking for addon domains to exclude: " . implode(', ', $addonNames));
                        logger("Scanning source directory: $sourceDir");

                        // Scan the source public_html for subdirectories
                        if (is_dir($sourceDir)) {
                            $subDirs = scandir($sourceDir);
                            foreach ($subDirs as $subDir) {
                                if ($subDir === '.' || $subDir === '..') continue;
                                $fullPath = "$sourceDir/$subDir";
                                if (!is_dir($fullPath)) continue;

                                // Check if this subdirectory matches any addon domain name
                                foreach ($addonNames as $addonName) {
                                    if ($subDir === $addonName) {
                                        $excludeDirs[] = $subDir;
                                        logger("Excluding addon domain directory: $subDir");
                                        break;
                                    }
                                }
                            }
                        }

                        // Also check userdata for addon domains with custom paths
                        foreach ($addonDomains as $addon) {
                            $addonName = $addon['name'];
                            // Skip if already found
                            if (in_array($addonName, $excludeDirs)) continue;

                            // Try userdata patterns
                            $mainDomainName = $domainName;
                            $userdataPatterns = [
                                "$backupRoot/userdata/$addonName.$mainDomainName",
                                "$backupRoot/userdata/$addonName",
                            ];

                            foreach ($userdataPatterns as $pattern) {
                                if (file_exists($pattern) && is_file($pattern)) {
                                    $userdataContent = file_get_contents($pattern);
                                    if (preg_match('/^documentroot:\s*(.+)$/m', $userdataContent, $docRootMatch)) {
                                        $docRoot = trim($docRootMatch[1]);
                                        if (preg_match('/\/public_html\/(.+)$/', $docRoot, $subDirMatch)) {
                                            $subPath = $subDirMatch[1];
                                            if (!in_array($subPath, $excludeDirs)) {
                                                $excludeDirs[] = $subPath;
                                                logger("Found addon from userdata: $addonName at $subPath");
                                            }
                                        }
                                    }
                                    break;
                                }
                            }
                        }

                        // Remove duplicates
                        $excludeDirs = array_unique($excludeDirs);

                        if (!empty($excludeDirs)) {
                            // Use rsync for selective copy with exclusions
                            $excludeArgs = array_map(fn($d) => "--exclude=" . escapeshellarg($d), $excludeDirs);
                            $rsyncCmd = "rsync -a " . implode(' ', $excludeArgs) . " " . escapeshellarg($sourceDir . '/') . " " . escapeshellarg($publicHtml . '/') . " 2>&1";
                            exec($rsyncCmd, $cpOutput, $cpCode);
                            logger("Main domain rsync with exclusions: " . implode(', ', $excludeDirs));
                        } else {
                            // No exclusions needed, simple copy
                            exec("cp -rp " . escapeshellarg($sourceDir) . "/* " . escapeshellarg($publicHtml) . "/ 2>&1", $cpOutput, $cpCode);
                        }
                    } else {
                        // Addon/sub domains - simple copy
                        exec("cp -rp " . escapeshellarg($sourceDir) . "/* " . escapeshellarg($publicHtml) . "/ 2>&1", $cpOutput, $cpCode);
                    }

                    // Fix ownership
                    exec("chown -R " . escapeshellarg($username) . ":" . escapeshellarg($username) . " " . escapeshellarg($domainDir));

                    cpanelAddMigrationEntry($log, $logPath, "Restored files for domain: $domainName", 'success');

                    // Register domain in Jabali if not exists
                    cpanelRegisterDomain($username, $domainName);
                } else {
                    cpanelAddMigrationEntry($log, $logPath, "No files found for domain: $domainName", 'warning');

                    // Still register the domain
                    cpanelRegisterDomain($username, $domainName);
                }
            }

            cpanelAddMigrationEntry($log, $logPath, 'Website files restoration completed', 'success');
        }

        // Restore databases
        if ($restoreDatabases && !empty($databases)) {
            cpanelAddMigrationEntry($log, $logPath, 'Restoring databases...', 'pending');

            // Track old-to-new database name mapping for user grants
            $dbNameMapping = [];
            // Track used new database names to avoid duplicates
            $usedDbNames = [];

            foreach ($databases as $db) {
                $dbName = $db['name'];
                // Create new database name with user prefix
                $baseName = preg_replace('/^[^_]+_/', '', $dbName);
                $newDbName = $username . '_' . $baseName;
                $newDbName = substr($newDbName, 0, 64); // MySQL limit

                // Check for duplicate and make unique if needed
                if (in_array($newDbName, $usedDbNames)) {
                    // Try appending the original prefix initial letters to make unique
                    $origPrefix = '';
                    if (preg_match('/^([^_]+)_/', $dbName, $prefixMatch)) {
                        $origPrefix = substr($prefixMatch[1], 0, 4); // First 4 chars of original prefix
                    }
                    $uniqueName = $username . '_' . $origPrefix . '_' . $baseName;
                    $uniqueName = substr($uniqueName, 0, 64);

                    // If still duplicate, append a number
                    $counter = 2;
                    while (in_array($uniqueName, $usedDbNames)) {
                        $uniqueName = $username . '_' . $baseName . '_' . $counter;
                        $uniqueName = substr($uniqueName, 0, 64);
                        $counter++;
                    }
                    $newDbName = $uniqueName;
                }

                $usedDbNames[] = $newDbName;
                $dbNameMapping[$dbName] = $newDbName;

                // Find SQL file
                $sqlFile = null;
                $possiblePaths = [
                    "$backupRoot/mysql/$dbName.sql",
                    "$backupRoot/mysql/$dbName.sql.gz",
                ];
                foreach ($possiblePaths as $path) {
                    if (file_exists($path)) {
                        $sqlFile = $path;
                        break;
                    }
                }

                if ($sqlFile) {
                    try {
                        // Create database
                        cpanelCreateDatabase($username, $newDbName);

                        // Import SQL
                        $importCmd = str_ends_with($sqlFile, '.gz')
                            ? "pigz -dc " . escapeshellarg($sqlFile) . " | mysql " . escapeshellarg($newDbName)
                            : "mysql " . escapeshellarg($newDbName) . " < " . escapeshellarg($sqlFile);

                        exec($importCmd . " 2>&1", $importOutput, $importCode);

                        if ($importCode === 0) {
                            cpanelAddMigrationEntry($log, $logPath, "Restored database: $newDbName (from $dbName)", 'success');
                        } else {
                            cpanelAddMigrationEntry($log, $logPath, "Failed to import database $newDbName: " . implode("\n", $importOutput), 'error');
                        }
                    } catch (Exception $e) {
                        cpanelAddMigrationEntry($log, $logPath, "Database restore error for $dbName: " . $e->getMessage(), 'error');
                    }
                } else {
                    cpanelAddMigrationEntry($log, $logPath, "SQL file not found for database: $dbName", 'warning');
                }
            }

            // Restore MySQL users from cPanel backup
            cpanelAddMigrationEntry($log, $logPath, 'Restoring database users...', 'pending');
            $usersRestored = 0;

            // Track old-to-new user name mapping for wp-config updates
            $userNameMapping = [];
            // Track used new user names to avoid duplicates
            $usedUserNames = [];

            // Look for mysql.sql file in cPanel backup
            $mysqlFile = "$backupRoot/mysql.sql";
            if (file_exists($mysqlFile)) {
                $content = file_get_contents($mysqlFile);
                if (!empty($content)) {
                    logger("Parsing mysql.sql for user grants...");

                    // cPanel format: GRANT USAGE ON *.* TO 'username'@'host' IDENTIFIED BY PASSWORD '*hash';
                    // We only want localhost users to avoid cPanel-specific hosts
                    preg_match_all("/GRANT USAGE ON \*\.\* TO '([^']+)'@'localhost' IDENTIFIED BY PASSWORD '([^']+)'/", $content, $usageMatches, PREG_SET_ORDER);

                    // Also get GRANT ALL PRIVILEGES ON `database`.* TO 'username'@'localhost'
                    // Note: cPanel escapes underscores in database names with backslash
                    preg_match_all("/GRANT ALL PRIVILEGES ON `([^`]+)`\.\* TO '([^']+)'@'localhost'/", $content, $privMatches, PREG_SET_ORDER);

                    $usersToCreate = [];

                    // Process GRANT USAGE statements to get users
                    foreach ($usageMatches as $match) {
                        $oldMysqlUser = $match[1];
                        $passwordHash = $match[2];

                        // Skip if user name matches cPanel account name (it's the main account, not a DB user)
                        if ($oldCpanelUser && $oldMysqlUser === $oldCpanelUser) {
                            continue;
                        }

                        // Skip if we already processed this user
                        if (isset($usersToCreate[$oldMysqlUser])) {
                            continue;
                        }

                        // Rename user with new prefix
                        $baseName = preg_replace('/^[^_]+_/', '', $oldMysqlUser);
                        $newMysqlUser = $username . '_' . $baseName;
                        $newMysqlUser = substr($newMysqlUser, 0, 32); // MySQL username limit

                        // Check for duplicate and make unique if needed (same logic as database names)
                        if (in_array($newMysqlUser, $usedUserNames)) {
                            // Try appending the original prefix initial letters to make unique
                            $origPrefix = '';
                            if (preg_match('/^([^_]+)_/', $oldMysqlUser, $prefixMatch)) {
                                $origPrefix = substr($prefixMatch[1], 0, 4); // First 4 chars of original prefix
                            }
                            $uniqueName = $username . '_' . $origPrefix . '_' . $baseName;
                            $uniqueName = substr($uniqueName, 0, 32);

                            // If still duplicate, append a number
                            $counter = 2;
                            while (in_array($uniqueName, $usedUserNames)) {
                                $uniqueName = $username . '_' . $baseName . '_' . $counter;
                                $uniqueName = substr($uniqueName, 0, 32);
                                $counter++;
                            }
                            $newMysqlUser = $uniqueName;
                        }

                        $usedUserNames[] = $newMysqlUser;
                        $userNameMapping[$oldMysqlUser] = $newMysqlUser;

                        $usersToCreate[$oldMysqlUser] = [
                            'new_name' => $newMysqlUser,
                            'password_hash' => $passwordHash,
                            'grants' => [],
                        ];
                    }

                    // Process GRANT ALL PRIVILEGES statements to get database permissions
                    foreach ($privMatches as $match) {
                        // Remove backslash escaping from database name
                        $oldDbName = str_replace('\\', '', $match[1]);
                        $oldMysqlUser = $match[2];

                        // Skip if not a user we're tracking
                        if (!isset($usersToCreate[$oldMysqlUser])) {
                            continue;
                        }

                        // Map old database name to new database name
                        $newDbName = $dbNameMapping[$oldDbName] ?? null;
                        if ($newDbName) {
                            // Avoid duplicate grants
                            $alreadyHasGrant = false;
                            foreach ($usersToCreate[$oldMysqlUser]['grants'] as $g) {
                                if ($g['database'] === $newDbName) {
                                    $alreadyHasGrant = true;
                                    break;
                                }
                            }
                            if (!$alreadyHasGrant) {
                                $usersToCreate[$oldMysqlUser]['grants'][] = [
                                    'privileges' => 'ALL PRIVILEGES',
                                    'database' => $newDbName,
                                ];
                            }
                        }
                    }

                    logger("Found " . count($usersToCreate) . " MySQL users to restore");

                    // Create users and apply grants
                    foreach ($usersToCreate as $oldUser => $userInfo) {
                        $newMysqlUser = $userInfo['new_name'];
                        $passwordHash = $userInfo['password_hash'] ?? '';
                        $newPassword = null; // Will be set only if we generate a new one

                        try {
                            // Use the original password hash from cPanel backup
                            // MariaDB/MySQL can import hashes with mysql_native_password
                            if (!empty($passwordHash) && str_starts_with($passwordHash, '*')) {
                                // Use the original hash - works with mysql_native_password
                                // Password stays same as in cPanel, no need to store it
                                $createSql = "CREATE USER IF NOT EXISTS '$newMysqlUser'@'localhost' IDENTIFIED WITH mysql_native_password AS '$passwordHash'";
                            } else {
                                // Fallback: generate new password if no valid hash
                                $newPassword = bin2hex(random_bytes(12));
                                $createSql = "CREATE USER IF NOT EXISTS '$newMysqlUser'@'localhost' IDENTIFIED BY '$newPassword'";
                                logger("No valid password hash for $oldUser, generated new password");
                            }

                            exec("mysql -e " . escapeshellarg($createSql) . " 2>&1", $createOutput, $createCode);

                            if ($createCode !== 0) {
                                cpanelAddMigrationEntry($log, $logPath, "Failed to create MySQL user $newMysqlUser: " . implode(' ', $createOutput), 'warning');
                                continue;
                            }

                            // Apply grants for each database
                            $grantedDbs = [];
                            foreach ($userInfo['grants'] as $grant) {
                                $grantSql = "GRANT ALL PRIVILEGES ON `{$grant['database']}`.* TO '$newMysqlUser'@'localhost'";
                                exec("mysql -e " . escapeshellarg($grantSql) . " 2>&1", $grantOutput, $grantCode);

                                if ($grantCode === 0) {
                                    $grantedDbs[] = $grant['database'];
                                }
                            }

                            // Flush privileges
                            exec("mysql -e 'FLUSH PRIVILEGES' 2>&1");

                            // Store credentials in Jabali (only if we generated a new password)
                            if ($newPassword !== null) {
                                cpanelStoreMysqlCredential($username, $newMysqlUser, $newPassword);
                            }

                            $dbList = !empty($grantedDbs) ? ' (' . implode(', ', $grantedDbs) . ')' : '';
                            cpanelAddMigrationEntry($log, $logPath, "Created MySQL user: $newMysqlUser$dbList", 'success');
                            $usersRestored++;

                        } catch (Exception $e) {
                            cpanelAddMigrationEntry($log, $logPath, "Error creating MySQL user $newMysqlUser: " . $e->getMessage(), 'warning');
                        }
                    }
                }
            }

            if ($usersRestored === 0) {
                cpanelAddMigrationEntry($log, $logPath, 'No MySQL users found in backup', 'info');
            }

            // Update wp-config.php files with new database names and users
            // Only for domains that were imported in this restore
            if (!empty($dbNameMapping) && !empty($domains)) {
                cpanelAddMigrationEntry($log, $logPath, 'Updating WordPress configuration files...', 'pending');
                $wpConfigsUpdated = 0;

                // Only check wp-config.php in the domains being restored
                $wpConfigFiles = [];
                foreach ($domains as $domain) {
                    $domainName = $domain['name'] ?? '';
                    if (empty($domainName)) continue;

                    $domainDir = "/home/$username/domains/$domainName/public_html";
                    // Check common WordPress locations
                    $possiblePaths = [
                        "$domainDir/wp-config.php",
                        "$domainDir/wordpress/wp-config.php",
                        "$domainDir/wp/wp-config.php",
                        "$domainDir/blog/wp-config.php",
                    ];
                    foreach ($possiblePaths as $path) {
                        if (file_exists($path)) {
                            $wpConfigFiles[] = $path;
                        }
                    }
                }

                foreach ($wpConfigFiles as $wpConfigPath) {
                    if (!file_exists($wpConfigPath) || !is_readable($wpConfigPath)) {
                        continue;
                    }

                    $wpConfig = file_get_contents($wpConfigPath);
                    $originalConfig = $wpConfig;
                    $changes = [];

                    // Update DB_NAME - extract current value first and do ONE replacement only
                    // This prevents chain replacements (e.g., A→B then B→C in same file)
                    if (preg_match("/define\s*\(\s*['\"]DB_NAME['\"]\s*,\s*['\"]([^'\"]+)['\"]\s*\)/", $wpConfig, $currentDbMatch)) {
                        $currentDbName = $currentDbMatch[1];
                        // Look up this specific database name in our mapping
                        if (isset($dbNameMapping[$currentDbName])) {
                            $newDbName = $dbNameMapping[$currentDbName];
                            $wpConfig = preg_replace(
                                "/define\s*\(\s*['\"]DB_NAME['\"]\s*,\s*['\"]" . preg_quote($currentDbName, '/') . "['\"]\s*\)/",
                                "define('DB_NAME', '$newDbName')",
                                $wpConfig
                            );
                            $changes[] = "DB_NAME: $currentDbName → $newDbName";
                        }
                    }

                    // Update DB_USER - use mapping if available, otherwise do prefix replacement
                    if (preg_match("/define\s*\(\s*['\"]DB_USER['\"]\s*,\s*['\"]([^'\"]+)['\"]\s*\)/", $wpConfig, $dbUserMatch)) {
                        $oldDbUser = $dbUserMatch[1];
                        $newDbUser = null;

                        // First, check if we have a mapping for this user (from mysql.sql)
                        if (isset($userNameMapping[$oldDbUser])) {
                            $newDbUser = $userNameMapping[$oldDbUser];
                        } else {
                            // Fallback: Replace the prefix (everything before first underscore) with new username
                            $newDbUser = $username . '_' . preg_replace('/^[^_]+_/', '', $oldDbUser);
                            $newDbUser = substr($newDbUser, 0, 32); // MySQL user limit
                        }

                        if ($newDbUser && $oldDbUser !== $newDbUser) {
                            $wpConfig = preg_replace(
                                "/define\s*\(\s*['\"]DB_USER['\"]\s*,\s*['\"]" . preg_quote($oldDbUser, '/') . "['\"]\s*\)/",
                                "define('DB_USER', '$newDbUser')",
                                $wpConfig
                            );
                            $changes[] = "DB_USER: $oldDbUser → $newDbUser";
                        }
                    }

                    // Save if changed
                    if ($wpConfig !== $originalConfig) {
                        file_put_contents($wpConfigPath, $wpConfig);
                        // Preserve ownership
                        $userInfo = posix_getpwnam($username);
                        if ($userInfo) {
                            chown($wpConfigPath, $userInfo['uid']);
                            chgrp($wpConfigPath, $userInfo['gid']);
                        }
                        $relativePath = str_replace("/home/$username/domains/", '', $wpConfigPath);
                        cpanelAddMigrationEntry($log, $logPath, "Updated wp-config.php: $relativePath (" . implode(', ', $changes) . ")", 'success');
                        $wpConfigsUpdated++;
                    }
                }

                if ($wpConfigsUpdated > 0) {
                    cpanelAddMigrationEntry($log, $logPath, "Updated $wpConfigsUpdated WordPress configuration file(s)", 'success');
                }
            }
        }

        // Restore emails
        if ($restoreEmails && !empty($mailboxes)) {
            cpanelAddMigrationEntry($log, $logPath, 'Restoring email mailboxes...', 'pending');

            $pdo = getJabaliPdo();
            $userId = null;
            if ($pdo) {
                $userId = cpanelGetUserId($pdo, $username);
                if (!$userId) {
                    logger("User not found in Jabali DB during email restore: $username", 'WARNING');
                }
            }

            // Group mailboxes by domain
            $mailboxesByDomain = [];
            foreach ($mailboxes as $mailbox) {
                $domain = $mailbox['domain'];
                if (!isset($mailboxesByDomain[$domain])) {
                    $mailboxesByDomain[$domain] = [];
                }
                $mailboxesByDomain[$domain][] = $mailbox;
            }

            foreach ($mailboxesByDomain as $domain => $domainMailboxes) {
                $rawDomain = $domain;
                $domain = strtolower(preg_replace('/[\x00-\x1F\x7F]/', '', trim($domain, ". \t\n\r\0\x0B")));

                // Skip invalid domains
                if (empty($domain) || strpos($domain, '.') === false || strpos($domain, '.') === 0) {
                    cpanelAddMigrationEntry($log, $logPath, "Skipping invalid email domain: $domain", 'warning');
                    continue;
                }

                // Enable email for domain if not already
                cpanelEnableEmailDomain($username, $domain);

                $emailDomainId = null;
                if ($pdo && $userId) {
                    $domainId = cpanelGetDomainId($pdo, $domain);
                    if (!$domainId) {
                        cpanelRegisterDomain($username, $domain);
                        $domainId = cpanelGetDomainId($pdo, $domain);
                    }

                    if ($domainId) {
                        $emailDomainId = cpanelEnsureEmailDomainRecord($pdo, $domainId);
                    }
                }

                foreach ($domainMailboxes as $mailbox) {
                    $localPart = preg_replace('/[\x00-\x1F\x7F]/', '', trim($mailbox['local_part'] ?? ''));
                    $email = $localPart . '@' . $domain;

                    // Skip invalid local parts (system folders, hidden folders)
                    $skipFolders = ['cur', 'new', 'tmp', '.spam', '.Trash', '.Drafts', '.Sent', '.Junk', 'courierimapkeywords', 'maildirfolder'];
                    if (empty($localPart) || in_array($localPart, $skipFolders) || strpos($localPart, '.') === 0) {
                        continue;
                    }
                    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
                        cpanelAddMigrationEntry($log, $logPath, "Skipping mailbox with invalid email: $email", 'warning');
                        continue;
                    }

                    // Create mailbox with random password
                    $password = bin2hex(random_bytes(12));
                    $result = cpanelCreateMailbox($username, $domain, $localPart, $password);

                    if ($result['success'] ?? false) {
                        $backupHash = cpanelFindMailboxPasswordHash($backupRoot, $rawDomain, $localPart);
                        $passwordHash = $backupHash ?: ($result['password_hash'] ?? '');

                        if ($pdo && $userId && $emailDomainId && $passwordHash !== '') {
                            cpanelEnsureMailboxRecord(
                                $pdo,
                                $emailDomainId,
                                $userId,
                                $localPart,
                                $passwordHash,
                                $result['maildir_path'] ?? null,
                                $result['uid'] ?? null,
                                $result['gid'] ?? null,
                                $localPart
                            );
                        }

                        // Copy mail data
                        $mailSourceDir = "$backupRoot/mail/$rawDomain/$localPart";
                        if (is_dir($mailSourceDir)) {
                            $mailDestDir = "/var/vmail/$domain/$localPart";
                            if (!is_dir($mailDestDir)) {
                                mkdir($mailDestDir, 0700, true);
                            }

                            exec("cp -rp " . escapeshellarg($mailSourceDir) . "/* " . escapeshellarg($mailDestDir) . "/ 2>&1");
                            exec("chown -R " . VMAIL_UID . ":" . VMAIL_GID . " " . escapeshellarg($mailDestDir));

                            cpanelAddMigrationEntry($log, $logPath, "Restored mailbox: $email (with messages)", 'success');
                        } else {
                            cpanelAddMigrationEntry($log, $logPath, "Created mailbox: $email (no messages found)", 'success');
                        }

                        if (!$backupHash) {
                            cpanelAddMigrationEntry($log, $logPath, "Password not found in backup for mailbox: $email (reset required)", 'warning');
                        }
                    } else {
                        cpanelAddMigrationEntry($log, $logPath, "Failed to create mailbox: $email - " . ($result['error'] ?? 'Unknown error'), 'error');
                    }
                }
            }

            // Restore email forwarders
            if (!empty($forwarders)) {
                cpanelAddMigrationEntry($log, $logPath, 'Restoring email forwarders...', 'pending');

                // Group forwarders by domain
                $forwardersByDomain = [];
                foreach ($forwarders as $forwarder) {
                    $domain = $forwarder['domain'];
                    if (!isset($forwardersByDomain[$domain])) {
                        $forwardersByDomain[$domain] = [];
                    }
                    $forwardersByDomain[$domain][] = $forwarder;
                }

                $forwardersRestored = 0;
                foreach ($forwardersByDomain as $domain => $domainForwarders) {
                    $rawDomain = $domain;
                    $domain = strtolower(preg_replace('/[\x00-\x1F\x7F]/', '', trim($domain, ". \t\n\r\0\x0B")));

                    // Enable email for domain if not already
                    cpanelEnableEmailDomain($username, $domain);

                    foreach ($domainForwarders as $forwarder) {
                        $localPart = preg_replace('/[\x00-\x1F\x7F]/', '', trim($forwarder['local_part'] ?? ''));
                        $email = $localPart . '@' . $domain;
                        $file = $forwarder['file'] ?? null;

                        // Get destinations - either from API data or from backup file
                        $destinations = '';

                        // First, check if destinations were provided directly (from API)
                        if (!empty($forwarder['destinations'])) {
                            $destinations = is_array($forwarder['destinations'])
                                ? implode(', ', $forwarder['destinations'])
                                : trim($forwarder['destinations']);
                        }

                        // If not, try to read from file
                        if (empty($destinations)) {
                            if ($file && file_exists($file)) {
                                $destinations = trim(file_get_contents($file));
                            } else {
                                // Try to find the file in extracted backup
                                $possiblePaths = [
                                    "$backupRoot/homedir/etc/$rawDomain/va/$localPart",
                                    "$backupRoot/etc/$rawDomain/va/$localPart",
                                ];
                                foreach ($possiblePaths as $path) {
                                    if (file_exists($path)) {
                                        $destinations = trim(file_get_contents($path));
                                        break;
                                    }
                                }
                            }
                        }

                        if (empty($destinations)) {
                            cpanelAddMigrationEntry($log, $logPath, "Skipping forwarder $email - no destinations found", 'warning');
                            continue;
                        }

                        // Parse destinations - can be comma or newline separated
                        $destArray = preg_split('/[\s,]+/', $destinations, -1, PREG_SPLIT_NO_EMPTY);
                        $destArray = array_filter($destArray, fn($d) => filter_var($d, FILTER_VALIDATE_EMAIL));

                        if (empty($destArray)) {
                            cpanelAddMigrationEntry($log, $logPath, "Skipping forwarder $email - no valid destinations", 'warning');
                            continue;
                        }
                        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
                            cpanelAddMigrationEntry($log, $logPath, "Skipping forwarder with invalid email: $email", 'warning');
                            continue;
                        }

                        // Create the forwarder
                        $result = emailForwarderCreate([
                            'username' => $username,
                            'email' => $email,
                            'destinations' => $destArray,
                        ]);

                        if ($result['success'] ?? false) {
                            cpanelAddMigrationEntry($log, $logPath, "Restored forwarder: $email -> $destinations", 'success');
                            $forwardersRestored++;
                        } else {
                            $error = $result['error'] ?? 'Unknown error';
                            if ($error === 'Forwarder already exists') {
                                $update = emailForwarderUpdate([
                                    'username' => $username,
                                    'email' => $email,
                                    'destinations' => $destArray,
                                ]);
                                if ($update['success'] ?? false) {
                                    cpanelAddMigrationEntry($log, $logPath, "Updated forwarder: $email -> $destinations", 'success');
                                    $forwardersRestored++;
                                    continue;
                                }
                            }
                            cpanelAddMigrationEntry($log, $logPath, "Failed to create forwarder: $email - " . $error, 'error');
                        }
                    }
                }

                if ($forwardersRestored > 0) {
                    logger("Restored $forwardersRestored email forwarders");
                }
            }

        }

        // Restore SSL certificates
        if ($restoreSsl) {
            cpanelAddMigrationEntry($log, $logPath, 'Checking for SSL certificates...', 'pending');

            $sslCerts = [];

            // cPanel stores SSL certificates in various locations - check all of them
            $keyPaths = [
                "$backupRoot/homedir/ssl/keys",
                "$backupRoot/ssl/keys",
                "$backupRoot/sslkeys",
                "$backupRoot/homedir/ssl",
                "$backupRoot/ssl",
            ];
            $certPaths = [
                "$backupRoot/homedir/ssl/certs",
                "$backupRoot/ssl/certs",
                "$backupRoot/sslcerts",
                "$backupRoot/homedir/ssl",
                "$backupRoot/ssl",
            ];

            // cPanel SSL naming convention:
            // Cert: domain_name_keyid_timestamp_hash.crt (e.g., laundry_sh_co_il_e3914_8256f_1758985760_abc123.crt)
            // Key: keyid_hash.key (e.g., e3914_8256f_abc123.key)
            // The keyid links certs to keys

            $keysByKeyId = [];  // keyid => path
            $certsByKeyId = []; // keyid => ['path' => path, 'domain' => domain]

            // Scan for private keys first - extract keyid from filename
            // Key format: {keyid}_{hash}.key where keyid is like e3914_8256f
            foreach ($keyPaths as $keyDir) {
                if (is_dir($keyDir)) {
                    foreach (scandir($keyDir) as $file) {
                        if (substr($file, -4) === '.key' && $file !== '.' && $file !== '..') {
                            // Key filename: e3914_8256f_abc123.key -> keyid is e3914_8256f
                            // Pattern: two hex groups separated by underscore, followed by underscore and hash
                            if (preg_match('/^([a-f0-9]+_[a-f0-9]+)_[a-f0-9]+\.key$/i', $file, $matches)) {
                                $keyId = $matches[1];
                                if (!isset($keysByKeyId[$keyId])) {
                                    $keysByKeyId[$keyId] = "$keyDir/$file";
                                    logger("Found SSL key with keyid $keyId: $file");
                                }
                            } else {
                                // Try simpler format: domain.key
                                $domain = str_replace('_', '.', substr($file, 0, -4));
                                if (!isset($sslCerts[$domain])) {
                                    $sslCerts[$domain] = [];
                                }
                                if (empty($sslCerts[$domain]['key'])) {
                                    $sslCerts[$domain]['key'] = "$keyDir/$file";
                                }
                            }
                        }
                    }
                }
            }

            // Scan for certificates - extract domain and keyid from filename
            // Cert format: {domain_with_underscores}_{keyid}_{timestamp}_{hash}.crt
            foreach ($certPaths as $certDir) {
                if (is_dir($certDir)) {
                    foreach (scandir($certDir) as $file) {
                        if (preg_match('/\.(crt|pem|cert)$/i', $file) && $file !== '.' && $file !== '..') {
                            // Try to match cPanel format: domain_keyid_timestamp_hash.crt
                            // e.g., laundry_sh_co_il_e3914_8256f_1758985760_abc123.crt
                            if (preg_match('/^(.+)_([a-f0-9]+_[a-f0-9]+)_\d+_[a-f0-9]+\.(crt|pem|cert)$/i', $file, $matches)) {
                                $domainWithUnderscores = $matches[1];
                                $keyId = $matches[2];
                                $domain = str_replace('_', '.', $domainWithUnderscores);

                                if (!isset($certsByKeyId[$keyId])) {
                                    $certsByKeyId[$keyId] = [
                                        'path' => "$certDir/$file",
                                        'domain' => $domain
                                    ];
                                    logger("Found SSL cert for $domain with keyid $keyId: $file");
                                }
                            } else {
                                // Try simpler format: domain.crt
                                $basename = preg_replace('/\.(crt|pem|cert)$/i', '', $file);
                                $domain = str_replace('_', '.', $basename);
                                if (!isset($sslCerts[$domain])) {
                                    $sslCerts[$domain] = [];
                                }
                                if (empty($sslCerts[$domain]['crt'])) {
                                    $sslCerts[$domain]['crt'] = "$certDir/$file";
                                }
                            }
                        }
                    }
                }
            }

            // Match certificates to keys by keyid
            foreach ($certsByKeyId as $keyId => $certInfo) {
                $domain = $certInfo['domain'];
                if (isset($keysByKeyId[$keyId])) {
                    // Found matching key for this cert
                    if (!isset($sslCerts[$domain])) {
                        $sslCerts[$domain] = [];
                    }
                    $sslCerts[$domain]['crt'] = $certInfo['path'];
                    $sslCerts[$domain]['key'] = $keysByKeyId[$keyId];
                    logger("Matched SSL cert and key for $domain using keyid $keyId");
                } else {
                    // No matching key found
                    logger("No matching key found for cert $domain (keyid: $keyId)", 'WARNING');
                    if (!isset($sslCerts[$domain])) {
                        $sslCerts[$domain] = [];
                    }
                    $sslCerts[$domain]['crt'] = $certInfo['path'];
                }
            }

            // Log found certificates for debugging
            if (!empty($sslCerts)) {
                logger("Found SSL certificates for domains: " . implode(', ', array_keys($sslCerts)));
            }

            // Build list of actual domain names being restored for matching
            // cPanel encodes both dots and hyphens as underscores in SSL filenames
            $actualDomains = array_column($domains, 'name');
            $domainMapping = []; // underscore_form => actual_domain
            foreach ($actualDomains as $actualDomain) {
                // Convert actual domain to underscore form for comparison
                $underscoreForm = str_replace(['.', '-'], '_', $actualDomain);
                $domainMapping[$underscoreForm] = $actualDomain;
                // Also add www variant
                $wwwForm = 'www_' . $underscoreForm;
                $domainMapping[$wwwForm] = $actualDomain;
            }
            logger("Domain mapping for SSL: " . json_encode($domainMapping));

            // Install certificates - try to match against actual domains
            $installedCount = 0;
            foreach ($sslCerts as $sslDomain => $files) {
                if (!empty($files['key']) && !empty($files['crt'])) {
                    // Convert parsed SSL domain back to underscore form for matching
                    $sslUnderscoreForm = str_replace('.', '_', $sslDomain);

                    // Try to find the actual domain this certificate belongs to
                    $targetDomain = null;

                    // Direct match against actual domains
                    if (in_array($sslDomain, $actualDomains)) {
                        $targetDomain = $sslDomain;
                    }
                    // Match via underscore form
                    elseif (isset($domainMapping[$sslUnderscoreForm])) {
                        $targetDomain = $domainMapping[$sslUnderscoreForm];
                        logger("Mapped SSL domain $sslDomain to actual domain $targetDomain via underscore form");
                    }
                    // Try partial matching (for addon domains with main domain suffix)
                    // cPanel addon SSL certs may have format: www.addon.main => www_addon_co_il_main_co_il
                    // We need to find the domain that appears EARLIEST in the SSL name (excluding www_)
                    else {
                        $bestMatch = null;
                        $bestPosition = PHP_INT_MAX;

                        foreach ($actualDomains as $actualDomain) {
                            $actualUnderscoreForm = str_replace(['.', '-'], '_', $actualDomain);

                            // Check for match starting at position 0 (domain.xxx.crt)
                            if (strpos($sslUnderscoreForm, $actualUnderscoreForm) === 0) {
                                $bestMatch = $actualDomain;
                                $bestPosition = 0;
                                break;
                            }

                            // Check for www_ prefix match (www.domain.xxx.crt)
                            if (strpos($sslUnderscoreForm, 'www_' . $actualUnderscoreForm) === 0) {
                                $bestMatch = $actualDomain;
                                $bestPosition = 0;
                                break;
                            }

                            // Find earliest occurrence of this domain in SSL name
                            $pos = strpos($sslUnderscoreForm, $actualUnderscoreForm);
                            if ($pos !== false && $pos < $bestPosition) {
                                $bestMatch = $actualDomain;
                                $bestPosition = $pos;
                            }
                        }

                        if ($bestMatch) {
                            $targetDomain = $bestMatch;
                            logger("Mapped SSL domain $sslDomain to actual domain $targetDomain via partial match (pos: $bestPosition)");
                        }
                    }

                    if (!$targetDomain) {
                        logger("No matching actual domain found for SSL domain: $sslDomain");
                        continue;
                    }

                    // Verify the key and cert files are readable
                    $keyContent = @file_get_contents($files['key']);
                    $certContent = @file_get_contents($files['crt']);

                    if ($keyContent && $certContent) {
                        try {
                            cpanelInstallSsl($username, $targetDomain, $files['crt'], $files['key']);
                            cpanelAddMigrationEntry($log, $logPath, "Installed SSL certificate for: $targetDomain", 'success');
                            $installedCount++;
                        } catch (Exception $e) {
                            cpanelAddMigrationEntry($log, $logPath, "Failed to install SSL for $targetDomain: " . $e->getMessage(), 'warning');
                        }
                    } else {
                        cpanelAddMigrationEntry($log, $logPath, "SSL files unreadable for $targetDomain", 'warning');
                    }
                } else {
                    $missing = [];
                    if (empty($files['key'])) $missing[] = 'key';
                    if (empty($files['crt'])) $missing[] = 'certificate';
                    cpanelAddMigrationEntry($log, $logPath, "Incomplete SSL for $sslDomain (missing: " . implode(', ', $missing) . ")", 'warning');
                }
            }

            if ($installedCount === 0) {
                cpanelAddMigrationEntry($log, $logPath, 'No complete SSL certificates found in backup', 'info');
            }
        }

        // Sync panel database records (domains, DNS, and optionally email)
        try {
            $panelMailboxes = $restoreEmails ? $mailboxes : [];
            $panelForwarders = $restoreEmails ? $forwarders : [];
            $panelData = cpanelBuildPanelEmailData($username, $domains, $panelMailboxes, $panelForwarders, $backupRoot);
            $hasPanelData = !empty($panelData['domains'])
                || !empty($panelData['email_domains'])
                || !empty($panelData['mailboxes'])
                || !empty($panelData['email_forwarders'])
                || !empty($panelData['dns_records']);

            if ($hasPanelData) {
                $importResult = importUserPanelData($username, $panelData);
                if ($importResult['success'] ?? false) {
                    cpanelAddMigrationEntry($log, $logPath, 'Synced panel records (domains, DNS, email)', 'success');
                } else {
                    cpanelAddMigrationEntry($log, $logPath, 'Failed to sync panel records: ' . ($importResult['error'] ?? 'Unknown error'), 'warning');
                }
            }
        } catch (Exception $e) {
            cpanelAddMigrationEntry($log, $logPath, 'Failed to sync panel records: ' . $e->getMessage(), 'warning');
        }

        // Clean up temp directory
        exec("rm -rf " . escapeshellarg($tempDir));

        logger("cPanel backup restore completed for user: $username");

        return [
            'success' => true,
            'log' => $log,
        ];

    } catch (Exception $e) {
        exec("rm -rf " . escapeshellarg($tempDir));
        logger("cPanel backup restore failed: " . $e->getMessage(), 'ERROR');
        return [
            'success' => false,
            'error' => $e->getMessage(),
            'log' => $log,
        ];
    }
}

/**
 * Fix permissions on a cPanel migration backup file so www-data can read it.
 * This is needed because SCP transfers create files owned by root.
 */
function cpanelFixBackupPermissions(array $params): array
{
    $backupPath = $params['backup_path'] ?? '';

    if (empty($backupPath)) {
        return ['success' => false, 'error' => 'Backup path is required'];
    }

    // Validate the path is within the allowed cpanel-migrations directory
    $allowedDir = '/var/backups/jabali/cpanel-migrations';
    $realPath = realpath($backupPath);

    // For new files that may not exist yet, check the parent directory
    if ($realPath === false) {
        $parentDir = dirname($backupPath);
        $realParent = realpath($parentDir);
        if ($realParent === false || strpos($realParent, $allowedDir) !== 0) {
            return ['success' => false, 'error' => 'Invalid backup path: must be within cpanel-migrations directory'];
        }
    } else {
        if (strpos($realPath, $allowedDir) !== 0) {
            return ['success' => false, 'error' => 'Invalid backup path: must be within cpanel-migrations directory'];
        }
    }

    if (!file_exists($backupPath)) {
        return ['success' => false, 'error' => 'Backup file not found: ' . $backupPath];
    }

    // Set permissions to 644 (owner read/write, group read, others read)
    if (!chmod($backupPath, 0644)) {
        return ['success' => false, 'error' => 'Failed to set permissions on backup file'];
    }

    // Optionally chown to www-data if needed
    $wwwData = posix_getpwnam('www-data');
    if ($wwwData) {
        chown($backupPath, $wwwData['uid']);
        chgrp($backupPath, $wwwData['gid']);
    }

    logger("Fixed permissions on cPanel backup: $backupPath");

    return ['success' => true];
}

/**
 * Download a backup from a WHM server via SCP.
 * Used for WHM migrations when HTTP download is not available.
 */
function whmDownloadBackupScp(array $params): array
{
    $remoteHost = $params['host'] ?? '';
    $remoteUser = $params['user'] ?? 'root';
    $remotePort = (int)($params['port'] ?? 22);
    $remotePath = $params['remote_path'] ?? '';
    $localPath = $params['local_path'] ?? '';
    $sshKeyPath = $params['key_path'] ?? '/root/.ssh/jabali_system_key';

    if (empty($remoteHost)) {
        return ['success' => false, 'error' => 'Remote host is required'];
    }

    if (empty($remotePath)) {
        return ['success' => false, 'error' => 'Remote path is required'];
    }

    if (empty($localPath)) {
        return ['success' => false, 'error' => 'Local path is required'];
    }

    // Validate local path is within allowed directories
    $allowedDirs = ['/var/backups/jabali/whm-migrations', '/var/backups/jabali/cpanel-migrations'];
    $realLocalDir = realpath(dirname($localPath));
    $isAllowed = false;

    foreach ($allowedDirs as $dir) {
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }
        $realDir = realpath($dir);
        if ($realLocalDir !== false && $realDir !== false && strpos($realLocalDir, $realDir) === 0) {
            $isAllowed = true;
            break;
        }
    }

    if (!$isAllowed) {
        return ['success' => false, 'error' => 'Invalid local path: must be within allowed backup directories'];
    }

    // Check if SSH key exists
    if (!file_exists($sshKeyPath)) {
        return ['success' => false, 'error' => 'SSH key not found: ' . $sshKeyPath];
    }

    logger("WHM: Downloading backup via SCP from $remoteUser@$remoteHost:$remotePath to $localPath");

    // Build SCP command
    $scpCmd = sprintf(
        'scp -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=30 -P %d -i %s %s@%s:%s %s 2>&1',
        $remotePort,
        escapeshellarg($sshKeyPath),
        escapeshellarg($remoteUser),
        escapeshellarg($remoteHost),
        escapeshellarg($remotePath),
        escapeshellarg($localPath)
    );

    exec($scpCmd, $output, $returnCode);

    if ($returnCode !== 0) {
        $errorMsg = implode("\n", $output);
        logger("WHM: SCP download failed: $errorMsg", 'ERROR');
        return ['success' => false, 'error' => 'SCP download failed: ' . $errorMsg];
    }

    // Verify file was downloaded
    if (!file_exists($localPath)) {
        return ['success' => false, 'error' => 'Download failed - file not found after SCP'];
    }

    $fileSize = filesize($localPath);
    if ($fileSize === 0) {
        @unlink($localPath);
        return ['success' => false, 'error' => 'Downloaded file is empty'];
    }

    // Verify it's a valid gzip file
    $handle = fopen($localPath, 'rb');
    $magic = $handle ? fread($handle, 2) : '';
    if ($handle) {
        fclose($handle);
    }

    if ($magic !== "\x1f\x8b") {
        @unlink($localPath);
        return ['success' => false, 'error' => 'Downloaded file is not a valid gzip archive'];
    }

    // Fix permissions
    chmod($localPath, 0644);
    $wwwData = posix_getpwnam('www-data');
    if ($wwwData) {
        chown($localPath, $wwwData['uid']);
        chgrp($localPath, $wwwData['gid']);
    }

    logger("WHM: Backup downloaded successfully: $localPath (" . formatBytes($fileSize) . ")");

    return [
        'success' => true,
        'path' => $localPath,
        'size' => $fileSize,
    ];
}

/**
 * Register a domain in Jabali's database for the given user.
 */
function cpanelRegisterDomain(string $username, string $domain): bool
{
    $pdo = getJabaliPdo();
    if (!$pdo) {
        logger("Failed to connect to Jabali database", 'ERROR');
        return false;
    }

    try {
        // Get user ID
        $stmt = $pdo->prepare("SELECT id FROM users WHERE username = ?");
        $stmt->execute([$username]);
        $user = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$user) {
            logger("User not found in database: $username", 'ERROR');
            return false;
        }

        // Check if domain already exists
        $stmt = $pdo->prepare("SELECT id FROM domains WHERE domain = ?");
        $stmt->execute([$domain]);
        if ($stmt->fetch()) {
            logger("Domain already exists in database: $domain");
            return true;
        }

        // Create domain record
        $stmt = $pdo->prepare("
            INSERT INTO domains (user_id, domain, document_root, is_active, ssl_enabled, created_at, updated_at)
            VALUES (?, ?, ?, 1, 0, datetime('now'), datetime('now'))
        ");
        $stmt->execute([
            $user['id'],
            $domain,
            "/home/$username/domains/$domain/public_html",
        ]);

        logger("Domain registered in Jabali: $domain for user $username");

        // Create nginx vhost config
        $result = domainCreate(['username' => $username, 'domain' => $domain]);
        if (!($result['success'] ?? false)) {
            logger("Warning: Failed to create vhost for $domain: " . ($result['error'] ?? 'Unknown error'), 'WARNING');
        }

        return true;

    } catch (Exception $e) {
        logger("Error registering domain: " . $e->getMessage(), 'ERROR');
        return false;
    }
}

/**
 * Create a MySQL database for cPanel migration.
 */
function cpanelCreateDatabase(string $username, string $dbName): bool
{
    // Use existing mysql.create_database function
    $result = mysqlCreateDatabase([
        'username' => $username,
        'database' => $dbName,
    ]);

    return $result['success'] ?? false;
}

/**
 * Store MySQL credentials in Jabali's database for the user.
 */
function cpanelStoreMysqlCredential(string $username, string $mysqlUsername, string $password): bool
{
    try {
        $pdo = getJabaliPdo();
        if (!$pdo) {
            return false;
        }

        // Get user ID
        $stmt = $pdo->prepare("SELECT id FROM users WHERE username = ?");
        $stmt->execute([$username]);
        $user = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$user) {
            logger("User not found for MySQL credential storage: $username", 'ERROR');
            return false;
        }

        // Check if credential already exists
        $stmt = $pdo->prepare("SELECT id FROM mysql_credentials WHERE user_id = ? AND mysql_username = ?");
        $stmt->execute([$user['id'], $mysqlUsername]);
        if ($stmt->fetch()) {
            // Update existing
            $stmt = $pdo->prepare("UPDATE mysql_credentials SET mysql_password_encrypted = ?, updated_at = datetime('now') WHERE user_id = ? AND mysql_username = ?");
            $stmt->execute([base64_encode($password), $user['id'], $mysqlUsername]);
        } else {
            // Insert new
            $stmt = $pdo->prepare("INSERT INTO mysql_credentials (user_id, mysql_username, mysql_password_encrypted, created_at, updated_at) VALUES (?, ?, ?, datetime('now'), datetime('now'))");
            $stmt->execute([$user['id'], $mysqlUsername, base64_encode($password)]);
        }

        logger("Stored MySQL credential for user $username: $mysqlUsername");
        return true;

    } catch (Exception $e) {
        logger("Error storing MySQL credential: " . $e->getMessage(), 'ERROR');
        return false;
    }
}

function cpanelStripZoneComments(string $line): string
{
    $result = '';
    $inQuotes = false;
    $length = strlen($line);

    for ($i = 0; $i < $length; $i++) {
        $char = $line[$i];
        if ($char === '"') {
            $inQuotes = ! $inQuotes;
            $result .= $char;
            continue;
        }

        if ($char === ';' && ! $inQuotes) {
            break;
        }

        $result .= $char;
    }

    return trim($result);
}

function cpanelNormalizeTxtContent(string $content): string
{
    $content = trim($content);
    if ($content === '') {
        return $content;
    }

    if (str_starts_with($content, '(') && str_ends_with($content, ')')) {
        $content = trim(substr($content, 1, -1));
    }

    if (preg_match_all('/"([^"]*)"/', $content, $matches) && ! empty($matches[1])) {
        return implode('', $matches[1]);
    }

    return trim($content, '"');
}

function cpanelNormalizeDnsName(string $name, string $domain): string
{
    $name = trim($name);
    if ($name === '') {
        return '@';
    }

    if ($name === '@') {
        return '@';
    }

    $name = rtrim($name, '.');
    $domain = rtrim($domain, '.');

    if ($name === $domain) {
        return '@';
    }

    $suffix = '.' . $domain;
    if (str_ends_with($name, $suffix)) {
        return substr($name, 0, -strlen($suffix));
    }

    return $name;
}

function cpanelFindZoneFile(string $backupRoot, string $domain): ?string
{
    $candidates = [
        "{$backupRoot}/dnszones/{$domain}.db",
        "{$backupRoot}/dnszones/{$domain}",
        "{$backupRoot}/dns/{$domain}.db",
        "{$backupRoot}/dns/{$domain}",
        "{$backupRoot}/var/named/{$domain}.db",
        "{$backupRoot}/var/named/{$domain}",
        "{$backupRoot}/named/{$domain}.db",
        "{$backupRoot}/named/{$domain}",
    ];

    foreach ($candidates as $path) {
        if (is_file($path)) {
            return $path;
        }
    }

    return null;
}

function cpanelParseZoneFileRecords(string $zoneFile, string $domain): array
{
    $records = [];
    $origin = rtrim($domain, '.') . '.';
    $currentName = '@';
    $currentTtl = null;

    $lines = file($zoneFile, FILE_IGNORE_NEW_LINES);
    $buffer = '';
    $parenLevel = 0;

    foreach ($lines as $line) {
        $clean = cpanelStripZoneComments($line);
        if ($clean === '') {
            continue;
        }

        $parenLevel += substr_count($clean, '(') - substr_count($clean, ')');
        $buffer = trim($buffer . ' ' . $clean);

        if ($parenLevel > 0) {
            continue;
        }

        $statement = trim($buffer);
        $buffer = '';

        if (stripos($statement, '$ORIGIN') === 0) {
            $parts = preg_split('/\s+/', $statement);
            if (! empty($parts[1])) {
                $origin = rtrim($parts[1], '.') . '.';
            }
            continue;
        }

        if (stripos($statement, '$TTL') === 0) {
            $parts = preg_split('/\s+/', $statement);
            if (! empty($parts[1]) && is_numeric($parts[1])) {
                $currentTtl = (int) $parts[1];
            }
            continue;
        }

        if (! preg_match('/^(?:(\S+)\s+)?(?:(\d+)\s+)?(?:IN\s+)?(A|AAAA|CNAME|MX|TXT|SRV|CAA)\s+(.+)$/i', $statement, $matches)) {
            continue;
        }

        $name = $matches[1] ?? $currentName;
        $ttl = $matches[2] ?? null;
        $type = strtoupper($matches[3]);
        $content = trim($matches[4]);

        if ($name !== null && is_numeric($name) && $ttl === null) {
            $ttl = $name;
            $name = $currentName;
        }

        if ($name !== null && strtoupper($name) === 'IN') {
            $name = $currentName;
        }

        $currentName = $name ?: $currentName;

        $priority = null;

        if ($type === 'TXT') {
            $content = cpanelNormalizeTxtContent($content);
        } elseif (in_array($type, ['MX', 'SRV'], true)) {
            $parts = preg_split('/\s+/', $content);
            if (! empty($parts[0]) && is_numeric($parts[0])) {
                $priority = (int) $parts[0];
                $parts = array_slice($parts, 1);
            }
            if ($type === 'SRV' && ! empty($parts)) {
                $last = array_pop($parts);
                $last = rtrim($last, '.');
                $parts[] = $last;
            }
            $content = trim(implode(' ', $parts));
        } elseif (in_array($type, ['CNAME', 'MX', 'CAA'], true)) {
            $content = rtrim($content, '.');
        }

        $name = cpanelNormalizeDnsName($name ?: '@', $origin);
        $recordTtl = $ttl !== null ? (int) $ttl : ($currentTtl ?? 3600);

        $records[] = [
            'name' => $name,
            'type' => $type,
            'content' => $content,
            'ttl' => $recordTtl,
            'priority' => $priority,
        ];
    }

    return $records;
}

function cpanelIsDefaultDnsRecord(string $domain, array $record): bool
{
    $name = strtolower($record['name'] ?? '');
    $type = strtoupper($record['type'] ?? '');
    $content = strtolower($record['content'] ?? '');

    if ($type === 'SOA' || $type === 'NS') {
        return true;
    }

    if ($name === '_dmarc' || str_contains($name, '_domainkey')) {
        return true;
    }

    if ($type === 'TXT') {
        if (str_contains($content, 'v=spf1')
            || str_contains($content, 'v=spf2.0')
            || str_contains($content, 'v=dmarc1')
            || str_contains($content, 'v=dkim1')) {
            return true;
        }
    }

    $defaultNames = [
        'www',
        'mail',
        'ftp',
        'cpanel',
        'whm',
        'webmail',
        'webdisk',
        'cpcontacts',
        'cpcalendars',
        'autoconfig',
        'autodiscover',
        'ns1',
        'ns2',
    ];

    if ($name === '@' && in_array($type, ['A', 'AAAA'], true)) {
        return true;
    }

    if (in_array($name, $defaultNames, true) && in_array($type, ['A', 'AAAA', 'CNAME'], true)) {
        return true;
    }

    if ($name === '@' && $type === 'MX') {
        $domain = rtrim(strtolower($domain), '.');
        $target = rtrim($content, '.');
        if ($target === "mail.{$domain}") {
            return true;
        }
    }

    return false;
}

function cpanelExtractCustomDnsRecords(string $backupRoot, array $domains): array
{
    $customRecords = [];

    foreach ($domains as $domain) {
        $domainName = $domain['name'] ?? $domain['domain'] ?? '';
        $domainName = strtolower(trim($domainName));
        if (empty($domainName)) {
            continue;
        }

        $zoneFile = cpanelFindZoneFile($backupRoot, $domainName);
        if (! $zoneFile) {
            continue;
        }

        $records = cpanelParseZoneFileRecords($zoneFile, $domainName);
        foreach ($records as $record) {
            if (cpanelIsDefaultDnsRecord($domainName, $record)) {
                continue;
            }
            $customRecords[$domainName][] = $record;
        }
    }

    return $customRecords;
}

function cpanelBuildPanelEmailData(string $username, array $domains, array $mailboxes, array $forwarders, string $backupRoot): array
{
    $panelData = [
        'username' => $username,
        'domains' => [],
        'email_domains' => [],
        'mailboxes' => [],
        'email_forwarders' => [],
        'dns_records' => [],
    ];

    $domainIdMap = [];
    $domainId = 1;

    foreach ($domains as $domain) {
        $domainName = $domain['name'] ?? $domain['domain'] ?? '';
        $domainName = strtolower(preg_replace('/[\x00-\x1F\x7F]/', '', trim($domainName)));
        if (empty($domainName)) {
            continue;
        }

        if (!isset($domainIdMap[$domainName])) {
            $domainIdMap[$domainName] = $domainId;
            $panelData['domains'][] = [
                'id' => $domainId,
                'domain' => $domainName,
                'document_root' => "/home/$username/domains/$domainName/public_html",
                'is_active' => 1,
                'ssl_enabled' => 0,
                'directory_index' => 'index.php index.html',
                'page_cache_enabled' => 0,
            ];
            $domainId++;
        }
    }

    $dnsRecordsByDomain = cpanelExtractCustomDnsRecords($backupRoot, $domains);
    if (! empty($dnsRecordsByDomain)) {
        foreach ($dnsRecordsByDomain as $domainName => $records) {
            $domainId = $domainIdMap[$domainName] ?? null;
            if (! $domainId) {
                continue;
            }

            foreach ($records as $record) {
                $panelData['dns_records'][] = array_merge(['domain_id' => $domainId], $record);
            }
        }
    }

    $emailDomainIdMap = [];
    $emailDomainId = 1;

    $emailDomains = [];
    foreach ($mailboxes as $mailbox) {
        $emailDomains[] = $mailbox['domain'] ?? '';
    }
    foreach ($forwarders as $forwarder) {
        $emailDomains[] = $forwarder['domain'] ?? '';
    }

    foreach (array_unique($emailDomains) as $domainName) {
        $domainName = strtolower(preg_replace('/[\x00-\x1F\x7F]/', '', trim($domainName)));
        if (empty($domainName)) {
            continue;
        }

        $emailDomainIdMap[$domainName] = $emailDomainId;
        $panelData['email_domains'][] = [
            'id' => $emailDomainId,
            'domain_id' => $domainIdMap[$domainName] ?? null,
            'domain_name' => $domainName,
            'is_active' => 1,
            'dkim_selector' => 'default',
            'catch_all_enabled' => 0,
            'max_mailboxes' => 10,
            'max_quota_bytes' => 5368709120,
        ];
        $emailDomainId++;
    }

    foreach ($mailboxes as $mailbox) {
        $localPart = preg_replace('/[\x00-\x1F\x7F]/', '', trim($mailbox['local_part'] ?? ''));
        $domainName = strtolower(preg_replace('/[\x00-\x1F\x7F]/', '', trim($mailbox['domain'] ?? '')));

        if (empty($localPart) || empty($domainName)) {
            continue;
        }

        $email = "{$localPart}@{$domainName}";
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            continue;
        }

        $emailDomainId = $emailDomainIdMap[$domainName] ?? null;
        if (!$emailDomainId) {
            continue;
        }

        $panelData['mailboxes'][] = [
            'email_domain_id' => $emailDomainId,
            'local_part' => $localPart,
            'domain_name' => $domainName,
            'name' => $localPart,
            'quota_bytes' => 1073741824,
            'maildir_path' => "/home/$username/mail/$domainName/$localPart/",
            'is_active' => 1,
        ];
    }

    foreach ($forwarders as $forwarder) {
        $localPart = preg_replace('/[\x00-\x1F\x7F]/', '', trim($forwarder['local_part'] ?? ''));
        $domainName = strtolower(preg_replace('/[\x00-\x1F\x7F]/', '', trim($forwarder['domain'] ?? '')));

        if (empty($localPart) || empty($domainName)) {
            continue;
        }

        $email = "{$localPart}@{$domainName}";
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            continue;
        }

        $emailDomainId = $emailDomainIdMap[$domainName] ?? null;
        if (!$emailDomainId) {
            continue;
        }

        $destinations = cpanelParseForwarderDestinations($forwarder, $backupRoot);
        if (empty($destinations)) {
            continue;
        }

        $panelData['email_forwarders'][] = [
            'email_domain_id' => $emailDomainId,
            'local_part' => $localPart,
            'destinations' => json_encode($destinations),
            'is_active' => 1,
        ];
    }

    return $panelData;
}

function cpanelParseForwarderDestinations(array $forwarder, string $backupRoot): array
{
    $destinations = '';

    if (!empty($forwarder['destinations'])) {
        $destinations = is_array($forwarder['destinations'])
            ? implode(', ', $forwarder['destinations'])
            : trim($forwarder['destinations']);
    }

    if (empty($destinations)) {
        $file = $forwarder['file'] ?? null;
        $domain = $forwarder['domain'] ?? '';
        $localPart = $forwarder['local_part'] ?? '';

        if ($file && file_exists($file)) {
            $destinations = trim(file_get_contents($file));
        } else {
            $possiblePaths = [
                "$backupRoot/homedir/etc/$domain/va/$localPart",
                "$backupRoot/etc/$domain/va/$localPart",
            ];
            foreach ($possiblePaths as $path) {
                if (file_exists($path)) {
                    $destinations = trim(file_get_contents($path));
                    break;
                }
            }
        }
    }

    $destArray = preg_split('/[\s,]+/', (string) $destinations, -1, PREG_SPLIT_NO_EMPTY);
    $destArray = array_filter($destArray, fn($d) => filter_var($d, FILTER_VALIDATE_EMAIL));

    return array_values($destArray);
}

function loadJabaliEnv(): void
{
    static $loaded = false;
    if ($loaded) {
        return;
    }

    $envPath = '/var/www/jabali/.env';
    if (!file_exists($envPath)) {
        $loaded = true;
        return;
    }

    $lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    foreach ($lines as $line) {
        $line = trim($line);
        if ($line === '' || str_starts_with($line, '#') || !str_contains($line, '=')) {
            continue;
        }

        [$key, $value] = explode('=', $line, 2);
        $key = trim($key);
        $value = trim($value);

        if ($key === '') {
            continue;
        }

        if ((str_starts_with($value, '"') && str_ends_with($value, '"')) ||
            (str_starts_with($value, "'") && str_ends_with($value, "'"))) {
            $value = substr($value, 1, -1);
        }

        if (!array_key_exists($key, $_ENV)) {
            $_ENV[$key] = $value;
            putenv("{$key}={$value}");
        }
    }

    $loaded = true;
}

function getJabaliPdo(): ?PDO
{
    loadJabaliEnv();

    $dbDriver = $_ENV['DB_CONNECTION'] ?? 'sqlite';
    $dbPath = $_ENV['DB_DATABASE'] ?? '/var/www/jabali/database/database.sqlite';

    try {
        if ($dbDriver === 'sqlite') {
            if (!file_exists($dbPath)) {
                return null;
            }
            $pdo = new PDO("sqlite:$dbPath");
        } else {
            $mysqlHost = $_ENV['DB_HOST'] ?? 'localhost';
            $mysqlPort = $_ENV['DB_PORT'] ?? '3306';
            $mysqlDb = $_ENV['DB_DATABASE'] ?? 'jabali';
            $mysqlUser = $_ENV['DB_USERNAME'] ?? 'root';
            $mysqlPass = $_ENV['DB_PASSWORD'] ?? '';
            $pdo = new PDO(
                "mysql:host={$mysqlHost};port={$mysqlPort};dbname={$mysqlDb};charset=utf8mb4",
                $mysqlUser,
                $mysqlPass
            );
        }

        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        return $pdo;
    } catch (Exception $e) {
        logger("Failed to connect to Jabali DB: " . $e->getMessage(), 'ERROR');
        return null;
    }
}

function cpanelGetUserId(PDO $pdo, string $username): ?int
{
    $stmt = $pdo->prepare("SELECT id FROM users WHERE username = ?");
    $stmt->execute([$username]);
    $user = $stmt->fetch(PDO::FETCH_ASSOC);

    if (!$user) {
        return null;
    }

    return (int) $user['id'];
}

function cpanelGetDomainId(PDO $pdo, string $domain): ?int
{
    $stmt = $pdo->prepare("SELECT id FROM domains WHERE domain = ?");
    $stmt->execute([$domain]);
    $row = $stmt->fetch(PDO::FETCH_ASSOC);

    if (!$row) {
        return null;
    }

    return (int) $row['id'];
}

function cpanelEnsureEmailDomainRecord(PDO $pdo, int $domainId): ?int
{
    $stmt = $pdo->prepare("SELECT id FROM email_domains WHERE domain_id = ?");
    $stmt->execute([$domainId]);
    $row = $stmt->fetch(PDO::FETCH_ASSOC);

    if ($row) {
        return (int) $row['id'];
    }

    $stmt = $pdo->prepare("
        INSERT INTO email_domains (domain_id, is_active, dkim_selector, catch_all_enabled, max_mailboxes, max_quota_bytes, created_at, updated_at)
        VALUES (?, 1, 'default', 0, 10, 5368709120, datetime('now'), datetime('now'))
    ");
    $stmt->execute([$domainId]);

    return (int) $pdo->lastInsertId();
}

function cpanelEnsureMailboxRecord(
    PDO $pdo,
    int $emailDomainId,
    int $userId,
    string $localPart,
    string $passwordHash,
    ?string $maildirPath,
    ?int $systemUid,
    ?int $systemGid,
    ?string $name
): bool {
    if ($passwordHash === '') {
        return false;
    }

    $stmt = $pdo->prepare("SELECT id FROM mailboxes WHERE email_domain_id = ? AND local_part = ?");
    $stmt->execute([$emailDomainId, $localPart]);
    $existing = $stmt->fetch(PDO::FETCH_ASSOC);

    if ($existing) {
        $stmt = $pdo->prepare("
            UPDATE mailboxes
            SET password_hash = ?,
                maildir_path = ?,
                system_uid = ?,
                system_gid = ?,
                name = COALESCE(?, name),
                updated_at = datetime('now')
            WHERE id = ?
        ");
        $stmt->execute([
            $passwordHash,
            $maildirPath,
            $systemUid,
            $systemGid,
            $name,
            $existing['id'],
        ]);

        return true;
    }

    $stmt = $pdo->prepare("
        INSERT INTO mailboxes
            (email_domain_id, user_id, local_part, password_hash, name, quota_bytes, quota_used_bytes, is_active, imap_enabled, pop3_enabled, smtp_enabled, maildir_path, system_uid, system_gid, created_at, updated_at)
        VALUES
            (?, ?, ?, ?, ?, 1073741824, 0, 1, 1, 1, 1, ?, ?, ?, datetime('now'), datetime('now'))
    ");
    $stmt->execute([
        $emailDomainId,
        $userId,
        $localPart,
        $passwordHash,
        $name,
        $maildirPath,
        $systemUid,
        $systemGid,
    ]);

    return true;
}

function cpanelNormalizeMailboxHash(?string $hash): ?string
{
    $hash = trim((string) $hash);

    if ($hash === '' || $hash === '!' || $hash === '!!' || $hash === '*') {
        return null;
    }

    if (str_starts_with($hash, '{')) {
        return $hash;
    }

    if (str_starts_with($hash, '$')) {
        return '{CRYPT}' . $hash;
    }

    return $hash;
}

function cpanelFindMailboxPasswordHash(string $backupRoot, string $domain, string $localPart): ?string
{
    $domain = trim($domain);
    $localPart = trim($localPart);

    if ($domain === '' || $localPart === '') {
        return null;
    }

    $paths = [
        "{$backupRoot}/homedir/etc/{$domain}/shadow",
        "{$backupRoot}/etc/{$domain}/shadow",
        "{$backupRoot}/homedir/etc/{$domain}/passwd",
        "{$backupRoot}/etc/{$domain}/passwd",
    ];

    foreach ($paths as $path) {
        if (!is_file($path)) {
            continue;
        }

        $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        foreach ($lines as $line) {
            $line = trim($line);
            if ($line === '' || str_starts_with($line, '#')) {
                continue;
            }

            $parts = explode(':', $line);
            if (count($parts) < 2) {
                continue;
            }

            if ($parts[0] !== $localPart) {
                continue;
            }

            return cpanelNormalizeMailboxHash($parts[1]);
        }
    }

    return null;
}

/**
 * Enable email for a domain during cPanel migration.
 */
function cpanelEnableEmailDomain(string $username, string $domain): bool
{
    try {
        $result = emailEnableDomain(['username' => $username, 'domain' => $domain]);
        if (!($result['success'] ?? false)) {
            return false;
        }

        $pdo = getJabaliPdo();
        if (!$pdo) {
            return true;
        }

        $domainId = cpanelGetDomainId($pdo, $domain);
        if (!$domainId) {
            cpanelRegisterDomain($username, $domain);
            $domainId = cpanelGetDomainId($pdo, $domain);
        }

        if (!$domainId) {
            return true;
        }

        cpanelEnsureEmailDomainRecord($pdo, $domainId);
        return true;

    } catch (Exception $e) {
        logger("Error enabling email domain: " . $e->getMessage(), 'ERROR');
        return false;
    }
}

/**
 * Create a mailbox during cPanel migration.
 */
function cpanelCreateMailbox(string $username, string $domain, string $localPart, string $password): array
{
    // Use existing email mailbox create function
    return emailMailboxCreate([
        'username' => $username,
        'email' => "{$localPart}@{$domain}",
        'password' => $password,
        'quota_bytes' => 1073741824, // 1GB
        'name' => $localPart,
    ]);
}

/**
 * Install SSL certificate during cPanel migration.
 */
function cpanelInstallSsl(string $username, string $domain, string $certFile, string $keyFile): bool
{
    // Read certificate and key files
    $cert = file_get_contents($certFile);
    $key = file_get_contents($keyFile);

    if (empty($cert) || empty($key)) {
        logger("SSL install failed for $domain: empty cert or key");
        return false;
    }

    // Create SSL directory for this domain
    $sslDir = "/etc/letsencrypt/live/$domain";
    if (!is_dir($sslDir)) {
        mkdir($sslDir, 0755, true);
    }

    // Save certificate and key files
    $certPath = "$sslDir/fullchain.pem";
    $keyPath = "$sslDir/privkey.pem";

    file_put_contents($certPath, $cert);
    chmod($certPath, 0644);

    file_put_contents($keyPath, $key);
    chmod($keyPath, 0600);

    // Use existing sslInstallCertificate function
    $result = sslInstallCertificate($domain, $username, $certPath, $keyPath);

    if ($result['success'] ?? false) {
        // Update database to mark SSL as active
        try {
            $pdo = getJabaliPdo();
            if ($pdo) {

                // Get domain ID
                $stmt = $pdo->prepare("SELECT id FROM domains WHERE domain = ?");
                $stmt->execute([$domain]);
                $domainRow = $stmt->fetch(PDO::FETCH_ASSOC);

                if ($domainRow) {
                    // Update or insert SSL certificate record
                    $stmt = $pdo->prepare("SELECT id FROM ssl_certificates WHERE domain_id = ?");
                    $stmt->execute([$domainRow['id']]);
                    $existing = $stmt->fetch(PDO::FETCH_ASSOC);

                    $now = date('Y-m-d H:i:s');
                    // Try to get expiry from certificate
                    $certInfo = openssl_x509_parse($cert);
                    $expiresAt = isset($certInfo['validTo_time_t']) ? date('Y-m-d H:i:s', $certInfo['validTo_time_t']) : null;
                    $issuer = $certInfo['issuer']['O'] ?? 'Unknown';

                    if ($existing) {
                        $stmt = $pdo->prepare("UPDATE ssl_certificates SET type = 'custom', status = 'active', certificate = ?, private_key = ?, issuer = ?, expires_at = ?, updated_at = ? WHERE id = ?");
                        $stmt->execute([$cert, $key, $issuer, $expiresAt, $now, $existing['id']]);
                    } else {
                        $stmt = $pdo->prepare("INSERT INTO ssl_certificates (domain_id, type, status, certificate, private_key, issuer, expires_at, created_at, updated_at) VALUES (?, 'custom', 'active', ?, ?, ?, ?, ?, ?)");
                        $stmt->execute([$domainRow['id'], $cert, $key, $issuer, $expiresAt, $now, $now]);
                    }

                    // Update domain ssl_enabled flag
                    $stmt = $pdo->prepare("UPDATE domains SET ssl_enabled = 1 WHERE id = ?");
                    $stmt->execute([$domainRow['id']]);

                    logger("SSL certificate installed and database updated for $domain");
                }
            }
        } catch (Exception $e) {
            logger("Failed to update SSL database records: " . $e->getMessage(), 'WARNING');
        }
        return true;
    }

    logger("SSL install failed for $domain: " . ($result['error'] ?? 'unknown error'));
    return false;
}

function toolExists(string $binary): bool
{
    $output = [];
    $code = 1;
    exec("command -v " . escapeshellarg($binary) . " 2>/dev/null", $output, $code);
    return $code === 0;
}

function validateDbIdentifier(string $name): bool
{
    return (bool) preg_match('/^[a-zA-Z0-9_]+$/', $name);
}

function gitGenerateKey(array $params): array
{
    $username = $params['username'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }

    $home = $userInfo['dir'];
    $sshDir = $home . '/.ssh';
    $keyPath = $sshDir . '/jabali_git_deploy';
    $pubPath = $keyPath . '.pub';

    if (!is_dir($sshDir)) {
        mkdir($sshDir, 0700, true);
        chown($sshDir, $userInfo['uid']);
        chgrp($sshDir, $userInfo['gid']);
    }
    chmod($sshDir, 0700);

    if (!file_exists($keyPath) || !file_exists($pubPath)) {
        $cmd = "ssh-keygen -t ed25519 -N '' -f " . escapeshellarg($keyPath) . " -C " . escapeshellarg("jabali-deploy-{$username}") . " 2>&1";
        exec($cmd, $output, $code);
        if ($code !== 0) {
            return ['success' => false, 'error' => 'Failed to generate deploy key: ' . implode("\n", $output)];
        }
        chmod($keyPath, 0600);
        chmod($pubPath, 0644);
        chown($keyPath, $userInfo['uid']);
        chgrp($keyPath, $userInfo['gid']);
        chown($pubPath, $userInfo['uid']);
        chgrp($pubPath, $userInfo['gid']);
    }

    $publicKey = trim((string) @file_get_contents($pubPath));
    if ($publicKey === '') {
        return ['success' => false, 'error' => 'Public key not found'];
    }

    return [
        'success' => true,
        'public_key' => $publicKey,
        'private_key_path' => $keyPath,
    ];
}

function gitDeploy(array $params): array
{
    $username = $params['username'] ?? '';
    $repoUrl = trim((string) ($params['repo_url'] ?? ''));
    $branch = trim((string) ($params['branch'] ?? 'main'));
    $deployPath = trim((string) ($params['deploy_path'] ?? ''));
    $deployScript = $params['deploy_script'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    if ($repoUrl === '' || $deployPath === '') {
        return ['success' => false, 'error' => 'Repository URL and deploy path are required'];
    }

    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }

    $home = $userInfo['dir'];
    if (!str_starts_with($deployPath, $home)) {
        return ['success' => false, 'error' => 'Deploy path must be inside user home directory'];
    }

    if (!is_dir($deployPath)) {
        mkdir($deployPath, 0755, true);
        chown($deployPath, $userInfo['uid']);
        chgrp($deployPath, $userInfo['gid']);
    }

    $sshKey = $home . '/.ssh/jabali_git_deploy';
    $env = 'HOME=' . escapeshellarg($home);
    if (file_exists($sshKey)) {
        $sshCmd = 'ssh -i ' . escapeshellarg($sshKey) . ' -o StrictHostKeyChecking=accept-new';
        $env .= ' GIT_SSH_COMMAND=' . escapeshellarg($sshCmd);
    }

    $output = [];
    $code = 0;

    if (!is_dir($deployPath . '/.git')) {
        $cmd = "sudo -u " . escapeshellarg($username) . " env {$env} git clone --branch " . escapeshellarg($branch)
            . " " . escapeshellarg($repoUrl) . " " . escapeshellarg($deployPath) . " 2>&1";
        exec($cmd, $output, $code);
    } else {
        $cmdFetch = "sudo -u " . escapeshellarg($username) . " env {$env} bash -lc "
            . escapeshellarg("cd {$deployPath} && git fetch --all --prune");
        exec($cmdFetch . " 2>&1", $fetchOutput, $fetchCode);
        $output = array_merge($output, $fetchOutput);

        $cmdReset = "sudo -u " . escapeshellarg($username) . " env {$env} bash -lc "
            . escapeshellarg("cd {$deployPath} && git checkout " . escapeshellarg($branch) . " && git reset --hard origin/{$branch}");
        exec($cmdReset . " 2>&1", $resetOutput, $code);
        $output = array_merge($output, $resetOutput);
    }

    if ($code !== 0) {
        return ['success' => false, 'error' => implode("\n", $output)];
    }

    exec("chown -R {$userInfo['uid']}:{$userInfo['gid']} " . escapeshellarg($deployPath));

    $scriptOutput = [];
    if (is_string($deployScript) && trim($deployScript) !== '') {
        $tmpScript = "/tmp/jabali-deploy-{$username}-" . time() . ".sh";
        $scriptBody = "#!/usr/bin/env bash\nset -e\ncd " . escapeshellarg($deployPath) . "\n" . $deployScript . "\n";
        file_put_contents($tmpScript, $scriptBody);
        chmod($tmpScript, 0700);

        $cmdScript = "sudo -u " . escapeshellarg($username) . " env HOME=" . escapeshellarg($home) . " bash " . escapeshellarg($tmpScript) . " 2>&1";
        exec($cmdScript, $scriptOutput, $scriptCode);
        @unlink($tmpScript);

        if ($scriptCode !== 0) {
            return ['success' => false, 'error' => implode("\n", $scriptOutput)];
        }
    }

    return [
        'success' => true,
        'output' => $output,
        'script_output' => $scriptOutput,
    ];
}

function rspamdUserSettings(array $params): array
{
    $username = $params['username'] ?? '';
    $whitelist = $params['whitelist'] ?? [];
    $blacklist = $params['blacklist'] ?? [];
    $score = $params['score'] ?? null;

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $whitelist = array_values(array_filter(array_map('trim', (array) $whitelist)));
    $blacklist = array_values(array_filter(array_map('trim', (array) $blacklist)));

    $confFile = '/etc/rspamd/local.d/users.conf';
    $content = file_exists($confFile) ? (string) file_get_contents($confFile) : '';
    $pattern = "/# BEGIN JABALI USER {$username}.*?# END JABALI USER {$username}\\n?/s";
    $content = preg_replace($pattern, '', $content) ?? $content;

    $lines = [];
    $lines[] = "# BEGIN JABALI USER {$username}";
    $lines[] = "{$username} {";
    if (!empty($whitelist)) {
        $lines[] = "    whitelist_from = [" . implode(', ', array_map(fn ($item) => '\"' . addslashes($item) . '\"', $whitelist)) . "];";
    }
    if (!empty($blacklist)) {
        $lines[] = "    blacklist_from = [" . implode(', ', array_map(fn ($item) => '\"' . addslashes($item) . '\"', $blacklist)) . "];";
    }
    if ($score !== null && $score !== '') {
        $lines[] = "    score = " . (float) $score . ";";
    }
    $lines[] = "}";
    $lines[] = "# END JABALI USER {$username}";

    $content = rtrim($content) . "\n\n" . implode("\n", $lines) . "\n";
    if (file_put_contents($confFile, $content) === false) {
        return ['success' => false, 'error' => 'Failed to write Rspamd config'];
    }

    exec("systemctl reload rspamd 2>&1", $reloadOutput, $reloadCode);
    if ($reloadCode !== 0) {
        return ['success' => false, 'error' => 'Failed to reload Rspamd: ' . implode("\n", $reloadOutput)];
    }

    return ['success' => true];
}

function usageBandwidthTotal(array $params): array
{
    $username = $params['username'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }

    $userHome = $userInfo['dir'];
    $total = 0;

    foreach (glob($userHome . '/domains/*/logs/access.log') as $logFile) {
        if (!is_readable($logFile)) {
            continue;
        }
        $cmd = "awk '{if ($10 ~ /^[0-9]+$/) sum+=$10} END {print sum}' " . escapeshellarg($logFile);
        exec($cmd, $out, $code);
        if ($code === 0 && isset($out[0])) {
            $total += (int) trim($out[0]);
        }
    }

    return ['success' => true, 'total_bytes' => $total];
}

function usageUserResources(array $params): array
{
    $username = $params['username'] ?? '';

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }

    $uid = (int) $userInfo['uid'];

    $cpuPercent = 0.0;
    $cpuUsageUsec = null;
    $memoryBytes = 0;
    $diskIoTotal = 0;
    $diskRead = 0;
    $diskWrite = 0;

    if ($cpuUsageUsec === null || $memoryBytes === 0) {
        $cpuTotal = 0.0;
        $rssTotal = 0;

        exec("ps -u " . escapeshellarg($username) . " -o %cpu=,rss= 2>/dev/null", $psOut, $psCode);
        if ($psCode === 0) {
            foreach ($psOut as $line) {
                $parts = preg_split('/\\s+/', trim($line));
                if (count($parts) < 2) {
                    continue;
                }
                $cpuTotal += (float) $parts[0];
                $rssTotal += (int) $parts[1] * 1024;
            }
        }

        $cpuPercent = round($cpuTotal, 2);
        if ($memoryBytes === 0) {
            $memoryBytes = $rssTotal;
        }
    }

    if ($diskIoTotal === 0) {

        foreach (glob('/proc/[0-9]*') as $procPath) {
            $statusFile = $procPath . '/status';
            if (!is_readable($statusFile)) {
                continue;
            }
            $status = file($statusFile, FILE_IGNORE_NEW_LINES);
            if (!$status) {
                continue;
            }

            $matchesUid = false;
            foreach ($status as $line) {
                if (str_starts_with($line, 'Uid:')) {
                    $parts = preg_split('/\\s+/', trim($line));
                    $matchesUid = isset($parts[1]) && (int) $parts[1] === $uid;
                    break;
                }
            }

            if (!$matchesUid) {
                continue;
            }

            $ioFile = $procPath . '/io';
            if (!is_readable($ioFile)) {
                continue;
            }

            $ioLines = file($ioFile, FILE_IGNORE_NEW_LINES);
            if (!$ioLines) {
                continue;
            }

            foreach ($ioLines as $line) {
                if (str_starts_with($line, 'read_bytes:')) {
                    $diskRead += (int) trim(substr($line, 11));
                } elseif (str_starts_with($line, 'write_bytes:')) {
                    $diskWrite += (int) trim(substr($line, 12));
                }
            }
        }

        $diskIoTotal = $diskRead + $diskWrite;
    }

    return [
        'success' => true,
        'cpu_percent' => $cpuPercent,
        'cpu_usage_usec_total' => $cpuUsageUsec,
        'memory_bytes' => $memoryBytes,
        'disk_io_total_bytes' => $diskIoTotal,
        'disk_io_read_bytes_total' => $diskRead,
        'disk_io_write_bytes_total' => $diskWrite,
    ];
}

function imageOptimize(array $params): array
{
    $username = $params['username'] ?? '';
    $path = $params['path'] ?? '';
    $convertWebp = (bool) ($params['convert_webp'] ?? false);
    $quality = (int) ($params['quality'] ?? 82);

    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        return ['success' => false, 'error' => 'User not found'];
    }

    $userHome = $userInfo['dir'];
    $path = $path ?: $userHome . '/domains';

    if (!str_starts_with($path, $userHome)) {
        return ['success' => false, 'error' => 'Path must be inside user home'];
    }

    if (!is_dir($path)) {
        return ['success' => false, 'error' => 'Path not found'];
    }

    $quality = max(40, min(95, $quality));

    $optimized = [
        'jpg' => 0,
        'png' => 0,
        'webp' => 0,
    ];

    if (toolExists('jpegoptim')) {
        $cmd = "find " . escapeshellarg($path) . " -type f \\( -iname '*.jpg' -o -iname '*.jpeg' \\) -print0 | xargs -0 -r jpegoptim --strip-all --max={$quality} 2>&1";
        exec($cmd, $out, $code);
        if ($code === 0) {
            $optimized['jpg'] = count($out) > 0 ? count($out) : 0;
        }
    }

    if (toolExists('optipng')) {
        $cmd = "find " . escapeshellarg($path) . " -type f -iname '*.png' -print0 | xargs -0 -r optipng -o2 2>&1";
        exec($cmd, $out, $code);
        if ($code === 0) {
            $optimized['png'] = count($out) > 0 ? count($out) : 0;
        }
    }

    if ($convertWebp && toolExists('cwebp')) {
        $cmd = "find " . escapeshellarg($path) . " -type f \\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' \\) -print0 | xargs -0 -r -I{} cwebp -q {$quality} {} -o {}.webp 2>&1";
        exec($cmd, $out, $code);
        if ($code === 0) {
            $optimized['webp'] = count($out) > 0 ? count($out) : 0;
        }
    }

    return ['success' => true, 'optimized' => $optimized];
}

function postgresListDatabases(array $params): array
{
    $username = $params['username'] ?? '';
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $sql = "SELECT datname, pg_database_size(datname) FROM pg_database WHERE datistemplate = false AND datname LIKE '" . addslashes($username) . "\\_%'";
    exec("sudo -u postgres psql -At -c " . escapeshellarg($sql) . " 2>&1", $out, $code);
    if ($code !== 0) {
        return ['success' => false, 'error' => implode("\n", $out)];
    }

    $databases = [];
    foreach ($out as $line) {
        if (!str_contains($line, '|')) {
            continue;
        }
        [$name, $size] = array_map('trim', explode('|', $line, 2));
        $databases[] = ['name' => $name, 'size_bytes' => (int) $size];
    }

    return ['success' => true, 'databases' => $databases];
}

function postgresListUsers(array $params): array
{
    $username = $params['username'] ?? '';
    if (!validateUsername($username)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $sql = "SELECT usename FROM pg_user WHERE usename LIKE '" . addslashes($username) . "\\_%'";
    exec("sudo -u postgres psql -At -c " . escapeshellarg($sql) . " 2>&1", $out, $code);
    if ($code !== 0) {
        return ['success' => false, 'error' => implode("\n", $out)];
    }

    $users = [];
    foreach ($out as $line) {
        $line = trim($line);
        if ($line !== '') {
            $users[] = ['username' => $line];
        }
    }

    return ['success' => true, 'users' => $users];
}

function postgresCreateUser(array $params): array
{
    $username = $params['username'] ?? '';
    $dbUser = $params['db_user'] ?? '';
    $password = $params['password'] ?? '';

    if (!validateUsername($username) || !validateDbIdentifier($dbUser)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $sql = "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '" . addslashes($dbUser) . "') THEN CREATE ROLE \"" . addslashes($dbUser) . "\" LOGIN PASSWORD '" . addslashes($password) . "'; END IF; END $$;";
    exec("sudo -u postgres psql -At -c " . escapeshellarg($sql) . " 2>&1", $out, $code);
    if ($code !== 0) {
        return ['success' => false, 'error' => implode("\n", $out)];
    }

    return ['success' => true];
}

function postgresDeleteUser(array $params): array
{
    $username = $params['username'] ?? '';
    $dbUser = $params['db_user'] ?? '';

    if (!validateUsername($username) || !validateDbIdentifier($dbUser)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $sql = "DROP ROLE IF EXISTS \"" . addslashes($dbUser) . "\"";
    exec("sudo -u postgres psql -At -c " . escapeshellarg($sql) . " 2>&1", $out, $code);
    if ($code !== 0) {
        return ['success' => false, 'error' => implode("\n", $out)];
    }

    return ['success' => true];
}

function postgresChangePassword(array $params): array
{
    $username = $params['username'] ?? '';
    $dbUser = $params['db_user'] ?? '';
    $password = $params['password'] ?? '';

    if (!validateUsername($username) || !validateDbIdentifier($dbUser)) {
        return ['success' => false, 'error' => 'Invalid username'];
    }

    $sql = "ALTER ROLE \"" . addslashes($dbUser) . "\" WITH PASSWORD '" . addslashes($password) . "'";
    exec("sudo -u postgres psql -At -c " . escapeshellarg($sql) . " 2>&1", $out, $code);
    if ($code !== 0) {
        return ['success' => false, 'error' => implode("\n", $out)];
    }

    return ['success' => true];
}

function postgresCreateDatabase(array $params): array
{
    $username = $params['username'] ?? '';
    $database = $params['database'] ?? '';
    $owner = $params['owner'] ?? '';

    if (!validateUsername($username) || !validateDbIdentifier($database) || !validateDbIdentifier($owner)) {
        return ['success' => false, 'error' => 'Invalid database name'];
    }

    $sql = "CREATE DATABASE \"" . addslashes($database) . "\" OWNER \"" . addslashes($owner) . "\"";
    exec("sudo -u postgres psql -At -c " . escapeshellarg($sql) . " 2>&1", $out, $code);
    if ($code !== 0) {
        return ['success' => false, 'error' => implode("\n", $out)];
    }

    return ['success' => true];
}

function postgresDeleteDatabase(array $params): array
{
    $username = $params['username'] ?? '';
    $database = $params['database'] ?? '';

    if (!validateUsername($username) || !validateDbIdentifier($database)) {
        return ['success' => false, 'error' => 'Invalid database name'];
    }

    $sql = "DROP DATABASE IF EXISTS \"" . addslashes($database) . "\"";
    exec("sudo -u postgres psql -At -c " . escapeshellarg($sql) . " 2>&1", $out, $code);
    if ($code !== 0) {
        return ['success' => false, 'error' => implode("\n", $out)];
    }

    return ['success' => true];
}

function postgresGrantPrivileges(array $params): array
{
    $username = $params['username'] ?? '';
    $database = $params['database'] ?? '';
    $dbUser = $params['db_user'] ?? '';

    if (!validateUsername($username) || !validateDbIdentifier($database) || !validateDbIdentifier($dbUser)) {
        return ['success' => false, 'error' => 'Invalid database name'];
    }

    $sql = "GRANT ALL PRIVILEGES ON DATABASE \"" . addslashes($database) . "\" TO \"" . addslashes($dbUser) . "\"";
    exec("sudo -u postgres psql -At -c " . escapeshellarg($sql) . " 2>&1", $out, $code);
    if ($code !== 0) {
        return ['success' => false, 'error' => implode("\n", $out)];
    }

    return ['success' => true];
}

function mailQueueList(array $params): array
{
    $queue = [];

    // Try JSON output if available (Postfix 3.1+)
    $jsonOutput = [];
    exec('postqueue -j 2>/dev/null', $jsonOutput, $jsonCode);
    if ($jsonCode === 0 && !empty($jsonOutput)) {
        foreach ($jsonOutput as $line) {
            $entry = json_decode($line, true);
            if (!is_array($entry)) {
                continue;
            }
            $queue[] = [
                'id' => $entry['queue_id'] ?? '',
                'arrival' => isset($entry['arrival_time']) ? date('Y-m-d H:i:s', (int) $entry['arrival_time']) : '',
                'sender' => $entry['sender'] ?? '',
                'recipients' => $entry['recipients'] ?? [],
                'size' => isset($entry['message_size']) ? formatBytes((int) $entry['message_size']) : '',
                'status' => $entry['status'] ?? '',
            ];
        }

        return ['success' => true, 'queue' => $queue];
    }

    $output = [];
    exec('postqueue -p 2>/dev/null', $output, $code);
    if ($code !== 0) {
        return ['success' => false, 'error' => 'Failed to read mail queue'];
    }

    if (empty($output)) {
        return ['success' => true, 'queue' => []];
    }

    $current = null;
    foreach ($output as $line) {
        $trim = trim($line);
        if ($trim === '' || str_contains($trim, 'Mail queue is empty')) {
            if ($current) {
                $queue[] = $current;
                $current = null;
            }
            continue;
        }

        if (preg_match('/^([A-F0-9]{5,}|[A-Za-z0-9]{5,})[\*\!\-]?\s+(\d+)\s+(.+?)\s+([^\s]+)$/', $line, $matches)) {
            if ($current) {
                $queue[] = $current;
            }
            $current = [
                'id' => $matches[1],
                'size' => formatBytes((int) $matches[2]),
                'arrival' => trim($matches[3]),
                'sender' => trim($matches[4]),
                'recipients' => [],
                'status' => '',
            ];
            continue;
        }

        if ($current) {
            if ($trim !== '') {
                if (preg_match('/^\((.+)\)$/', $trim, $statusMatch)) {
                    $current['status'] = $statusMatch[1];
                } else {
                    $current['recipients'][] = $trim;
                }
            }
        }
    }

    if ($current) {
        $queue[] = $current;
    }

    return ['success' => true, 'queue' => $queue];
}

function mailQueueRetry(array $params): array
{
    $id = $params['id'] ?? '';
    if (empty($id) || !preg_match('/^[A-Za-z0-9]{5,}$/', $id)) {
        return ['success' => false, 'error' => 'Invalid queue ID'];
    }

    exec('postqueue -i ' . escapeshellarg($id) . ' 2>&1', $output, $code);
    if ($code !== 0) {
        return ['success' => false, 'error' => implode("\n", $output)];
    }

    return ['success' => true];
}

function mailQueueDelete(array $params): array
{
    $id = $params['id'] ?? '';
    if (empty($id) || !preg_match('/^[A-Za-z0-9]{5,}$/', $id)) {
        return ['success' => false, 'error' => 'Invalid queue ID'];
    }

    exec('postsuper -d ' . escapeshellarg($id) . ' 2>&1', $output, $code);
    if ($code !== 0) {
        return ['success' => false, 'error' => implode("\n", $output)];
    }

    return ['success' => true];
}

function fail2banLogs(array $params): array
{
    $limit = (int) ($params['limit'] ?? 200);
    $limit = max(20, min(500, $limit));
    $path = '/var/log/fail2ban.log';

    if (!file_exists($path)) {
        return ['success' => true, 'logs' => []];
    }

    $output = [];
    exec('tail -n ' . $limit . ' ' . escapeshellarg($path) . ' 2>/dev/null', $output);

    $logs = [];
    foreach ($output as $line) {
        if (preg_match('/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}),\d+\s+\w+\s+\[([^\]]+)\]\s+(.+)$/', $line, $matches)) {
            $time = $matches[1];
            $jail = $matches[2];
            $message = $matches[3];
            $action = '';
            $ip = '';

            if (preg_match('/\b(Ban|Unban|Found)\s+([0-9a-fA-F:.]+)/', $message, $actionMatch)) {
                $action = $actionMatch[1];
                $ip = $actionMatch[2];
            }

            $logs[] = [
                'time' => $time,
                'jail' => $jail,
                'action' => $action ?: 'Info',
                'ip' => $ip,
                'message' => $message,
            ];
        }
    }

    return ['success' => true, 'logs' => $logs];
}

function updatesList(array $params): array
{
    $warnings = [];
    $refresh = (bool) ($params['refresh'] ?? false);
    $refreshOutput = [];
    $refreshSuccess = null;
    if ($refresh) {
        exec('apt-get update -y 2>&1', $refreshOutput, $codeUpdate);
        $refreshSuccess = $codeUpdate === 0;
        if ($codeUpdate !== 0) {
            $warnings[] = implode("\n", $refreshOutput);
        }
    }

    $output = [];
    exec('apt list --upgradable 2>/dev/null', $output, $code);
    if ($code !== 0) {
        return ['success' => false, 'error' => 'Failed to list updates'];
    }

    $packages = [];
    foreach ($output as $line) {
        if (str_starts_with($line, 'Listing') || trim($line) === '') {
            continue;
        }

        if (preg_match('/^([^\/]+)\/\S+\s+(\S+)\s+\S+(?:\s+\S+)?\s+\[upgradable from: ([^\]]+)\]/', $line, $matches)) {
            $packages[] = [
                'name' => $matches[1],
                'new_version' => $matches[2],
                'current_version' => $matches[3],
            ];
        }
    }

    return [
        'success' => true,
        'packages' => $packages,
        'warnings' => $warnings,
        'refresh_output' => $refreshOutput,
        'refresh_success' => $refreshSuccess,
    ];
}

function updatesRun(array $params): array
{
    $output = [];
    exec('apt-get update -y 2>&1', $output, $codeUpdate);
    if ($codeUpdate !== 0) {
        return ['success' => false, 'error' => implode("\n", $output)];
    }

    $upgradeOutput = [];
    exec('DEBIAN_FRONTEND=noninteractive apt-get upgrade -y 2>&1', $upgradeOutput, $codeUpgrade);
    if ($codeUpgrade !== 0) {
        return ['success' => false, 'error' => implode("\n", $upgradeOutput)];
    }

    return ['success' => true, 'output' => array_merge($output, $upgradeOutput)];
}
