<?php

namespace WHP;

require_once('/docker/whp/web/libs/mysqlmgmt.php');
require_once('BackupEngine.php');
require_once(__DIR__ . '/smtp_mailer.php');

use PDO;
use Exception;
use WHPBackup\BackupEngine;
use SmtpMailer;

class WordPressManager {
    /**
     * Source dir for files we drop into customer sites (mu-plugin, magic-login
     * create script). Mirrors the runtime layout described in CLAUDE.md:
     * the repo's web-files/ is rsynced to /docker/whp/web/ on the server.
     */
    const WHP_TEMPLATES_DIR = '/docker/whp/web/templates';

    private $db;
    private $backupEngine;

    public function __construct() {
        $mysql = new \mysqlmgmt();
        $this->db = $mysql->getMySQLConnection();
        $this->db->exec("USE whp");
        $this->backupEngine = new BackupEngine();
    }
    
    /**
     * Execute WP-CLI command in a container
     */
    private function execWpCli($container, $user, $command, $workDir = null) {
        // Validate input parameters to prevent command injection
        if (!preg_match('/^[a-zA-Z0-9_-]+$/', $user)) {
            throw new Exception("Invalid user format: $user");
        }
        if (!preg_match('/^[a-zA-Z0-9.-]+-[0-9]+$/', $container)) {
            throw new Exception("Invalid container format: $container");
        }
        if (strlen($user) > 32 || strlen($container) > 64) {
            throw new Exception("User or container name too long");
        }
        
        if ($workDir === null) {
            $workDir = "/home/{$user}/public_html";
        }
        
        // Validate work directory path
        $workDir = realpath($workDir) ?: $workDir;
        if (!str_starts_with($workDir, '/home/') && !str_starts_with($workDir, '/docker/users/')) {
            throw new Exception("Invalid work directory: $workDir");
        }
        
        // Build the docker exec command
        $dockerCmd = sprintf(
            "docker exec --user %s -w %s %s %s",
            escapeshellarg($user),
            escapeshellarg($workDir),
            escapeshellarg($container),
            $command
        );
        
        // Log the command for debugging
        error_log("Executing WP-CLI: $dockerCmd");
        
        $output = [];
        $returnCode = 0;
        exec($dockerCmd . " 2>&1", $output, $returnCode);
        
        $result = [
            'success' => $returnCode === 0,
            'output' => implode("\n", $output),
            'return_code' => $returnCode
        ];
        
        // Log errors
        if (!$result['success']) {
            error_log("WP-CLI command failed: " . $result['output']);
        }
        
        return $result;
    }
    
    /**
     * Drop a file from the WHP templates dir into a site's container.
     *
     * Idempotent: if the target file already exists with a matching sha256,
     * does nothing and returns ['installed' => true, 'fresh' => false].
     *
     * @param string $container container name (validated via existing regex)
     * @param string $user      site user (validated via existing regex)
     * @param string $srcHost   absolute host path to the template file
     * @param string $dstInside absolute path inside the container
     * @param string $mode      octal mode string, e.g. '0644'
     * @return array{installed: bool, fresh: bool}
     */
    private function dropTemplateFile($container, $user, $srcHost, $dstInside, $mode) {
        if (!preg_match('/^[a-zA-Z0-9_-]+$/', $user)) {
            throw new Exception("Invalid user format: $user");
        }
        if (!preg_match('/^[a-zA-Z0-9.-]+-[0-9]+$/', $container)) {
            throw new Exception("Invalid container format: $container");
        }
        if (!is_file($srcHost)) {
            throw new Exception("WHP template missing: $srcHost");
        }
        // Disallow shell metacharacters in destination path - we only ever
        // call this with internally-built paths but defense in depth.
        if (strpbrk($dstInside, " \t\n\r\"'\\$|;&<>`") !== false) {
            throw new Exception("Invalid destination path: $dstInside");
        }
        if (!preg_match('/^[0-7]{3,4}$/', $mode)) {
            throw new Exception("Invalid mode: $mode");
        }

        $dstDir = dirname($dstInside);
        $expectedHash = hash_file('sha256', $srcHost);

        // Cheap presence-and-hash check before any docker cp.
        $hashCmd = sprintf(
            "docker exec --user %s %s /bin/sh -c %s",
            escapeshellarg($user),
            escapeshellarg($container),
            escapeshellarg("test -f " . escapeshellarg($dstInside) . " && sha256sum " . escapeshellarg($dstInside) . " 2>/dev/null | awk '{print \$1}'")
        );
        $hashOutput = [];
        $hashRc = 0;
        \exec($hashCmd . " 2>/dev/null", $hashOutput, $hashRc);
        $currentHash = trim(implode('', $hashOutput));
        if ($currentHash === $expectedHash) {
            return ['installed' => true, 'fresh' => false];
        }

        // Ensure parent dir exists (as the site user, so dir is owned correctly).
        $mkdirCmd = sprintf(
            "docker exec --user %s %s /bin/sh -c %s",
            escapeshellarg($user),
            escapeshellarg($container),
            escapeshellarg("mkdir -p " . escapeshellarg($dstDir))
        );
        $out = [];
        $rc = 0;
        \exec($mkdirCmd . " 2>&1", $out, $rc);
        if ($rc !== 0) {
            throw new Exception("Failed to create $dstDir in $container: " . implode("\n", $out));
        }

        // docker cp the body. docker cp creates the file owned by root inside
        // the container, so we chown afterward.
        $cpCmd = sprintf(
            "docker cp %s %s:%s",
            escapeshellarg($srcHost),
            escapeshellarg($container),
            escapeshellarg($dstInside)
        );
        $out = [];
        $rc = 0;
        \exec($cpCmd . " 2>&1", $out, $rc);
        if ($rc !== 0) {
            throw new Exception("Failed to copy $srcHost into $container:$dstInside: " . implode("\n", $out));
        }

        $finalizeCmd = sprintf(
            "docker exec --user root %s /bin/sh -c %s",
            escapeshellarg($container),
            escapeshellarg(sprintf(
                "chown %s:%s %s && chmod %s %s",
                escapeshellarg($user),
                escapeshellarg($user),
                escapeshellarg($dstInside),
                escapeshellarg($mode),
                escapeshellarg($dstInside)
            ))
        );
        $out = [];
        $rc = 0;
        \exec($finalizeCmd . " 2>&1", $out, $rc);
        if ($rc !== 0) {
            throw new Exception("Failed to chown/chmod $dstInside in $container: " . implode("\n", $out));
        }

        return ['installed' => true, 'fresh' => true];
    }

    /**
     * Drop the WHP-patched magic-login mu-plugin into a site.
     */
    private function dropPatchedMuPlugin($container, $user, $sitePath) {
        return $this->dropTemplateFile(
            $container,
            $user,
            self::WHP_TEMPLATES_DIR . '/mu-plugins/wp-cli-login-server.php',
            $sitePath . '/wp-content/mu-plugins/wp-cli-login-server.php',
            '0644'
        );
    }

    /**
     * Drop the native magic-login token-creation script into a site.
     *
     * Lives in wp-content/.whp/ behind a deny-all .htaccess so it cannot be
     * invoked over HTTP. Only meant to be run by docker exec.
     */
    private function dropMagicLoginCreateScript($container, $user, $sitePath) {
        $whpDir = $sitePath . '/wp-content/.whp';

        // Ensure the directory has a deny-all .htaccess. We write it via a
        // here-string echo to avoid shipping yet another template file.
        $htaccessCmd = sprintf(
            "docker exec --user %s %s /bin/sh -c %s",
            escapeshellarg($user),
            escapeshellarg($container),
            escapeshellarg(
                "mkdir -p " . escapeshellarg($whpDir) . " && " .
                "test -f " . escapeshellarg($whpDir . '/.htaccess') . " || " .
                "printf 'Require all denied\\n' > " . escapeshellarg($whpDir . '/.htaccess')
            )
        );
        $out = [];
        $rc = 0;
        \exec($htaccessCmd . " 2>&1", $out, $rc);
        if ($rc !== 0) {
            error_log("Warning: failed to seed $whpDir/.htaccess: " . implode("\n", $out));
        }

        return $this->dropTemplateFile(
            $container,
            $user,
            self::WHP_TEMPLATES_DIR . '/site-private/whp-magic-login-create.php',
            $whpDir . '/whp-magic-login-create.php',
            '0640'
        );
    }

    /**
     * Check if WordPress is installed
     */
    public function isWordPressInstalled($container, $user, $path = null) {
        if ($path === null) {
            $path = "/home/{$user}/public_html";
        }
        
        $result = $this->execWpCli($container, $user, "wp core is-installed", $path);
        return $result['success'];
    }
    
    /**
     * Get WordPress version
     */
    public function getWordPressVersion($container, $user, $path = null) {
        if ($path === null) {
            $path = "/home/{$user}/public_html";
        }
        
        $result = $this->execWpCli($container, $user, "wp core version", $path);
        if ($result['success']) {
            return trim($result['output']);
        }
        return null;
    }
    
    /**
     * Install WordPress
     */
    public function installWordPress($data) {
        // For site-based installation, extract data from site
        if (!empty($data['site_id'])) {
            $stmt = $this->db->prepare("
                SELECT s.*, 
                       ct.name as container_type_name,
                       sd.fqdn as primary_fqdn
                FROM sites s
                INNER JOIN container_types ct ON s.container_type_id = ct.id
                INNER JOIN site_domains sd ON s.id = sd.site_id AND sd.is_primary = 1
                WHERE s.id = ?
            ");
            $stmt->execute([$data['site_id']]);
            $site = $stmt->fetch(PDO::FETCH_ASSOC);
            
            if (!$site) {
                throw new Exception("Site not found");
            }
            
            // Use site data
            $data['user'] = $site['username'];
            $data['domain'] = $site['primary_fqdn'];
            // Container name format: domain-01 (matching existing containers)
            $data['container'] = $site['primary_fqdn'] . '-01';
            
            // Log for debugging
            error_log("WordPress Install - Site ID: {$data['site_id']}, User: {$data['user']}, Domain: {$data['domain']}, Container: {$data['container']}");
        }
        
        $required = ['container', 'user', 'domain', 'title', 'admin_username', 'admin_password', 'admin_email'];
        foreach ($required as $field) {
            if (empty($data[$field])) {
                throw new Exception("Missing required field: $field");
            }
        }
        
        $path = "/home/{$data['user']}/public_html";
        
        // Check if already installed
        if ($this->isWordPressInstalled($data['container'], $data['user'], $path)) {
            throw new Exception("WordPress is already installed at this location");
        }
        
        // Download WordPress core (skip if already present)
        $result = $this->execWpCli($data['container'], $data['user'], "wp core download --skip-content", $path);
        if (!$result['success']) {
            // If files already exist, that's okay
            if (strpos($result['output'], 'already be present') === false) {
                throw new Exception("Failed to download WordPress: " . $result['output']);
            }
            error_log("WordPress Install: WordPress files already present, continuing with installation");
        }
        
        // Create wp-config.php with unique database and user names
        $randomSuffix = substr(bin2hex(random_bytes(4)), 0, 8); // 8 random characters
        $dbName = $data['db_name'] ?? $data['user'] . '_' . str_replace('.', '_', $data['domain']) . '_wp_' . $randomSuffix;
        $dbUser = $data['db_user'] ?? $data['user'] . '_wp_' . $randomSuffix;
        $dbPass = $data['db_password'] ?? $this->generatePassword();
        $dbHost = $data['db_host'] ?? 'mysql';
        
        // Create database if needed
        if ($data['create_database'] ?? true) {
            $mysqlMgmt = new \mysqlmgmt();
            
            error_log("WordPress Install: Creating DB user '$dbUser' and database '$dbName'");
            
            // First ensure the MySQL user exists
            $userResult = $mysqlMgmt->createMySQLUser($dbUser, $dbPass);
            if ($userResult['status'] !== '0' && strpos($userResult['msg'], 'already exists') === false) {
                throw new Exception("Failed to create MySQL user: " . $userResult['msg']);
            }
            
            // Then create the database 
            $dbResult = $mysqlMgmt->createDatabase($dbName, $dbUser);
            if ($dbResult['status'] !== '0') {
                // If database already exists, that's okay for WordPress installation
                if (strpos($dbResult['msg'], 'already exists') === false) {
                    throw new Exception("Failed to create database: " . $dbResult['msg']);
                }
                error_log("WordPress Install: Database $dbName already exists, continuing with installation");
            }
        }
        
        // Create wp-config
        $configCmd = sprintf(
            "wp config create --dbname=%s --dbuser=%s --dbpass=%s --dbhost=%s",
            escapeshellarg($dbName),
            escapeshellarg($dbUser),
            escapeshellarg($dbPass),
            escapeshellarg($dbHost)
        );
        
        $result = $this->execWpCli($data['container'], $data['user'], $configCmd, $path);
        if (!$result['success']) {
            throw new Exception("Failed to create wp-config.php: " . $result['output']);
        }
        
        // Install WordPress
        $installCmd = sprintf(
            "wp core install --url=%s --title=%s --admin_user=%s --admin_password=%s --admin_email=%s",
            escapeshellarg($data['domain']),
            escapeshellarg($data['title']),
            escapeshellarg($data['admin_username']),
            escapeshellarg($data['admin_password']),
            escapeshellarg($data['admin_email'])
        );
        
        $result = $this->execWpCli($data['container'], $data['user'], $installCmd, $path);
        if (!$result['success']) {
            throw new Exception("Failed to install WordPress: " . $result['output']);
        }
        
        // Drop WHP's patched magic-login mu-plugin and native create script.
        // Replaces the old `wp package install aaemnnosttv/wp-cli-login-command`
        // (whose mu-plugin fatals on PHP 8.5+ with `WP_CLI_Login\is_admin()`).
        try {
            $this->dropPatchedMuPlugin($data['container'], $data['user'], $path);
            $this->dropMagicLoginCreateScript($data['container'], $data['user'], $path);
        } catch (Exception $e) {
            // Don't fail the install if magic-login files can't be dropped.
            error_log("Warning: Failed to drop magic-login files: " . $e->getMessage());
        }
        
        // Install and activate default theme (WordPress core install doesn't include themes)
        $themeCmd = "wp theme install twentytwentyfour --activate";
        $themeResult = $this->execWpCli($data['container'], $data['user'], $themeCmd, $path);
        if (!$themeResult['success']) {
            // Try an older theme if the latest fails
            $themeCmd = "wp theme install twentytwentythree --activate";
            $themeResult = $this->execWpCli($data['container'], $data['user'], $themeCmd, $path);
            if (!$themeResult['success']) {
                error_log("Warning: Failed to install default theme: " . $themeResult['output']);
            }
        }
        
        // Create wp-cli.yml file to enable mod_rewrite (required for .htaccess generation)
        $wpCliYmlContent = "apache_modules:\n  - mod_rewrite\n";
        $wpCliYmlPath = $path . '/wp-cli.yml';
        
        // Write wp-cli.yml file inside container
        $createYmlCmd = "echo " . escapeshellarg($wpCliYmlContent) . " > " . escapeshellarg($wpCliYmlPath);
        $dockerCmd = "docker exec --user " . escapeshellarg($data['user']) . " -w " . escapeshellarg($path) . " " . 
                    escapeshellarg($data['container']) . " /bin/bash -c " . escapeshellarg($createYmlCmd);
        
        exec($dockerCmd . " 2>&1", $output, $returnCode);
        if ($returnCode !== 0) {
            error_log("Warning: Failed to create wp-cli.yml: " . implode("\n", $output));
        } else {
            error_log("Created wp-cli.yml file for mod_rewrite support");
        }
        
        // Set up proper permalink structure with .htaccess
        $permalinkCmd = "wp rewrite structure '/%postname%/'";
        $permalinkResult = $this->execWpCli($data['container'], $data['user'], $permalinkCmd, $path);
        if (!$permalinkResult['success']) {
            error_log("Warning: Failed to set up permalinks: " . $permalinkResult['output']);
        }
        
        // Flush rewrite rules to ensure .htaccess is created (now with mod_rewrite enabled)
        $flushCmd = "wp rewrite flush --hard";
        $flushResult = $this->execWpCli($data['container'], $data['user'], $flushCmd, $path);
        if (!$flushResult['success']) {
            error_log("Warning: Failed to flush rewrite rules: " . $flushResult['output']);
        }
        
        // Save site information to database
        $stmt = $this->db->prepare("
            INSERT INTO wordpress_sites 
            (user, domain, container_name, admin_email, admin_username, preferred_backup_target_id) 
            VALUES (?, ?, ?, ?, ?, ?)
        ");
        
        // Handle backup target ID - convert empty string to null
        $backupTargetId = null;
        if (!empty($data['backup_target_id']) && is_numeric($data['backup_target_id'])) {
            $backupTargetId = (int)$data['backup_target_id'];
        }
        
        $stmt->execute([
            $data['user'],
            $data['domain'],
            $data['container'],
            $data['admin_email'],
            $data['admin_username'],
            $backupTargetId
        ]);
        
        $siteId = $this->db->lastInsertId();

        // Cache the freshly-installed WP version on the row so the WP page
        // doesn't have to fetch it live next time.
        $this->setCachedVersion($siteId);

        // Set up basic security settings
        $this->applySecurityHardening($siteId, $data['container'], $data['user']);
        
        return [
            'success' => true,
            'site_id' => $siteId,
            'db_name' => $dbName,
            'db_user' => $dbUser,
            'db_password' => $dbPass
        ];
    }
    
    /**
     * Update WordPress core
     * @param int $siteId Site ID
     * @param bool $createBackup Whether to create backup before update
     * @return array Result array
     */
    public function updateCore($siteId, $createBackup = null) {
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("Site not found");
        }

        // If createBackup is not specified, use site's auto_backup_before_update setting
        if ($createBackup === null) {
            $createBackup = $site['auto_backup_before_update'] ?? false;
        }

        // Record update start
        $updateId = $this->recordUpdateStart($siteId, 'core', 'WordPress Core', $site['wp_version']);

        try {
            if ($createBackup) {
                // Queue backup first, then queue update to wait for backup completion
                $result = $this->queueUpdateWithBackup($siteId, $updateId, 'core');

                return [
                    'success' => true,
                    'message' => 'WordPress core update and backup queued successfully',
                    'update_id' => $updateId,
                    'status' => 'queued',
                    'backup_id' => $result['backup_id'] ?? null,
                    'queue_mode' => true
                ];
            } else {
                // Perform immediate update without backup
                $result = $this->performCoreUpdateImmediate($siteId, $updateId);

                return [
                    'success' => true,
                    'message' => 'WordPress core update completed successfully',
                    'update_id' => $updateId,
                    'status' => 'completed',
                    'new_version' => $result['new_version'] ?? null,
                    'backup_results' => $result['backup_results'] ?? null,
                    'queue_mode' => false
                ];
            }

        } catch (Exception $e) {
            // Record failure
            $this->recordUpdateFailed($updateId, $e->getMessage());
            $this->sendSiteNotification($siteId, 'update_failed', [
                'error' => $e->getMessage(),
            ]);

            return [
                'success' => false,
                'message' => 'WordPress core update failed: ' . $e->getMessage(),
                'update_id' => $updateId,
                'status' => 'failed'
            ];
        }
    }

    /**
     * Update WordPress core, plugins, and themes
     * @param int $siteId Site ID
     * @param bool $createBackup Whether to create backup before update
     * @return array Result array
     */
    public function updateAll($siteId, $createBackup = null) {
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("Site not found");
        }

        // If createBackup is not specified, use site's auto_backup_before_update setting
        if ($createBackup === null) {
            $createBackup = $site['auto_backup_before_update'] ?? false;
        }

        // Record update start for comprehensive update
        $updateId = $this->recordUpdateStart($siteId, 'all', 'WordPress Complete Update', $site['wp_version']);

        try {
            if ($createBackup) {
                // Queue backup first, then queue update to wait for backup completion
                $result = $this->queueUpdateWithBackup($siteId, $updateId, 'all');

                return [
                    'success' => true,
                    'message' => 'WordPress update and backup queued successfully',
                    'update_id' => $updateId,
                    'status' => 'queued',
                    'backup_id' => $result['backup_id'] ?? null,
                    'queue_mode' => true
                ];
            } else {
                // Perform immediate update without backup
                $result = $this->performCompleteUpdateImmediate($siteId, $updateId);

                return [
                    'success' => true,
                    'message' => 'WordPress complete update finished successfully',
                    'update_id' => $updateId,
                    'status' => 'completed',
                    'results' => $result,
                    'queue_mode' => false
                ];
            }

        } catch (Exception $e) {
            // Record failure
            $this->recordUpdateFailed($updateId, $e->getMessage());
            $this->sendSiteNotification($siteId, 'update_failed', [
                'error' => $e->getMessage(),
            ]);

            return [
                'success' => false,
                'message' => 'WordPress complete update failed: ' . $e->getMessage(),
                'update_id' => $updateId,
                'status' => 'failed'
            ];
        }
    }

    /**
     * Perform comprehensive WordPress update (core, plugins, themes) immediately without backup
     */
    public function performCompleteUpdateImmediate($siteId, $updateId) {
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("Site not found");
        }

        $container = $site['domain'] . '-01';
        $user = $site['user'];
        $results = [
            'core' => null,
            'plugins' => null,
            'themes' => null,
            'backup_results' => null
        ];

        // No backup - this is an immediate update
        error_log("WordPress Update: Starting immediate update for site {$siteId} (no backup)");
        
        // Update WordPress core
        try {
            $coreResult = $this->execWpCli($container, $user, "wp core update");
            if ($coreResult['success']) {
                $results['core'] = ['success' => true, 'message' => 'Core updated successfully'];
                // WordPress core updated successfully
            } else {
                $results['core'] = ['success' => false, 'message' => $coreResult['error']];
                // Core update failed
            }
        } catch (Exception $e) {
            $results['core'] = ['success' => false, 'message' => $e->getMessage()];
            // Core update exception
        }
        
        // Update all plugins
        // Update plugins
        try {
            $pluginResult = $this->execWpCli($container, $user, "wp plugin update --all");
            if ($pluginResult['success']) {
                $results['plugins'] = ['success' => true, 'message' => 'Plugins updated successfully'];
                // All plugins updated successfully
            } else {
                $results['plugins'] = ['success' => false, 'message' => $pluginResult['error']];
                // Plugin updates failed
            }
        } catch (Exception $e) {
            $results['plugins'] = ['success' => false, 'message' => $e->getMessage()];
            // Plugin update exception
        }
        
        // Update all themes
        // Update themes
        try {
            $themeResult = $this->execWpCli($container, $user, "wp theme update --all");
            if ($themeResult['success']) {
                $results['themes'] = ['success' => true, 'message' => 'Themes updated successfully'];
                // All themes updated successfully
            } else {
                $results['themes'] = ['success' => false, 'message' => $themeResult['error']];
                // Theme updates failed
            }
        } catch (Exception $e) {
            $results['themes'] = ['success' => false, 'message' => $e->getMessage()];
            // Theme update exception
        }
        
        // Get new WordPress version
        $versionResult = $this->execWpCli($container, $user, "wp core version");
        $newVersion = $versionResult['success'] ? trim($versionResult['output']) : null;

        // Add new version to results for response
        if ($results['core']['success'] && $newVersion) {
            $results['new_version'] = $newVersion;
        }

        // Record completion + refresh cached row version
        $this->recordUpdateComplete($updateId, $newVersion);
        if ($newVersion) {
            $this->setCachedVersion($siteId, $newVersion);
        }

        // Notify per the site's per-event flags. We classify success vs
        // failure on whether the core update succeeded — even if a stray
        // plugin/theme update failed, customers care most about core's state.
        $allOk = !empty($results['core']['success'])
              && !empty($results['plugins']['success'])
              && !empty($results['themes']['success']);
        if ($allOk) {
            $this->sendSiteNotification($siteId, 'update_complete', [
                'new_version' => $newVersion,
            ]);
        } else {
            $errs = [];
            foreach (['core', 'plugins', 'themes'] as $part) {
                if (!empty($results[$part]) && empty($results[$part]['success'])) {
                    $errs[] = $part . ': ' . ($results[$part]['message'] ?? 'unknown');
                }
            }
            $this->sendSiteNotification($siteId, 'update_failed', [
                'error' => $errs ? implode(' | ', $errs) : 'unknown',
                'new_version' => $newVersion,
            ]);
        }
        // Complete update finished
        
        return $results;
    }
    
    /**
     * Perform WordPress core update immediately without backup
     */
    public function performCoreUpdateImmediate($siteId, $updateId) {
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("Site not found");
        }
        
        // No backup - this is an immediate update
        $backupResults = [];
        error_log("WordPress Update: Starting immediate core update for site {$siteId} (no backup)");
        
        try {
            // Update WordPress core
            $result = $this->execWpCli($site['container_name'], $site['user'], "wp core update");
            if (!$result['success']) {
                throw new Exception("Core update failed: " . $result['output']);
            }
            
            // Update database if needed
            $result = $this->execWpCli($site['container_name'], $site['user'], "wp core update-db");
            
            // Get new version for response and update the cached column.
            $newVersion = $this->getWordPressVersion($site['container_name'], $site['user']);

            // Record success
            $this->recordUpdateComplete($updateId, $newVersion);
            if ($newVersion) {
                $this->setCachedVersion($siteId, $newVersion);
            }
            $this->sendSiteNotification($siteId, 'update_complete', [
                'new_version' => $newVersion,
            ]);

            return ['success' => true, 'new_version' => $newVersion, 'backup_results' => $backupResults];

        } catch (Exception $e) {
            // Record failure
            $this->recordUpdateFailed($updateId, $e->getMessage());
            $this->sendSiteNotification($siteId, 'update_failed', [
                'error' => $e->getMessage(),
            ]);
            
            // Check if site is broken
            if ($this->isSiteBroken($site)) {
                // Attempt rollback if backup exists
                if (!empty($backupResults)) {
                    error_log("WordPress Update: Site appears broken, attempting rollback for site {$siteId}");
                    $this->rollbackFromBackup($site, $backupResults);
                }
            }
            
            throw $e;
        }
    }
    
    /**
     * Get update status
     */
    public function getUpdateStatus($updateId) {
        $stmt = $this->db->prepare("
            SELECT * FROM wp_update_history 
            WHERE id = ?
        ");
        $stmt->execute([$updateId]);
        $update = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if (!$update) {
            return ['error' => 'Update not found'];
        }
        
        return [
            'success' => true,
            'status' => $update['status'],
            'old_version' => $update['old_version'],
            'new_version' => $update['new_version'],
            'error_message' => $update['error_message'],
            'started_at' => $update['started_at'],
            'completed_at' => $update['completed_at']
        ];
    }
    
    /**
     * Apply security hardening
     */
    public function applySecurityHardening($siteId, $container = null, $user = null) {
        if (!$container || !$user) {
            $site = $this->getSite($siteId);
            $container = $site['container_name'];
            $user = $site['user'];
            $domain = $site['domain'];
        } else {
            // If container and user are provided, we still need domain
            $site = $this->getSite($siteId);
            $domain = $site['domain'];
        }
        
        // Get or create security settings
        $stmt = $this->db->prepare("
            INSERT INTO wp_security_settings (site_id) VALUES (?)
            ON DUPLICATE KEY UPDATE updated_at = NOW()
        ");
        $stmt->execute([$siteId]);
        
        // Disable file editor
        $this->execWpCli($container, $user, 
            "wp config set DISALLOW_FILE_EDIT true --raw"
        );
        
        // Set secure keys
        $this->execWpCli($container, $user, "wp config shuffle-salts");
        
        // Disable XML-RPC
        $htaccessRules = '
# Block XML-RPC
<Files xmlrpc.php>
Order Allow,Deny
Deny from all
</Files>

# Disable directory browsing
Options -Indexes

# Block PHP in uploads
<Directory "wp-content/uploads">
  <Files "*.php">
    Order Allow,Deny
    Deny from all
  </Files>
</Directory>
';
        
        // Add .htaccess rules
        $this->addHtaccessRules($container, $user, $domain, $htaccessRules);
        
        return ['success' => true];
    }
    
    /**
     * Import existing WordPress installation
     */
    public function importWordPress($data) {
        if (empty($data['site_id'])) {
            throw new Exception('Site ID is required');
        }
        
        $siteId = $data['site_id'];
        $site = $this->getRegularSite($siteId);
        if (!$site) {
            throw new Exception('Site not found');
        }
        
        // Check permissions
        if (AUTH_USER !== 'root' && $site['username'] !== AUTH_USER) {
            throw new Exception('Permission denied: You can only import WordPress for your own sites');
        }
        
        $container = $site['primary_fqdn'] . '-01';
        $user = $site['username'];
        $domain = $site['primary_fqdn'];
        $sitePath = "/home/{$user}/public_html";
        
        // Check if WordPress is installed
        $wpCheckResult = $this->execWpCli($container, $user, "wp core is-installed", $sitePath);
        if (!$wpCheckResult['success']) {
            throw new Exception('WordPress installation not found at ' . $sitePath);
        }
        
        // Get WordPress information
        $wpInfo = $this->getWordPressInfo($container, $user, $sitePath);
        
        // Get database information from wp-config.php
        $dbInfo = $this->getWordPressDatabaseInfo($container, $user, $sitePath);
        
        // Check if this site is already managed
        $existingWpSite = $this->getSiteByDomain($domain);
        if ($existingWpSite) {
            throw new Exception('WordPress site ' . $domain . ' is already being managed');
        }
        
        // Handle backup target ID - convert empty string to null
        $backupTargetId = null;
        if (!empty($data['import_backup_target_id']) && is_numeric($data['import_backup_target_id'])) {
            $backupTargetId = (int)$data['import_backup_target_id'];
        }
        
        // Create WordPress site record
        $stmt = $this->db->prepare("
            INSERT INTO wordpress_sites 
            (user, domain, container_name, site_path, admin_email, admin_username, database_name, preferred_backup_target_id, status, created_at)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'active', NOW())
        ");
        
        $stmt->execute([
            $user,
            $domain, 
            $container,
            $sitePath,
            $wpInfo['admin_email'] ?? '',
            $wpInfo['admin_user'] ?? '',
            $dbInfo['name'] ?? '',
            $backupTargetId
        ]);
        
        $wpSiteId = $this->db->lastInsertId();

        // Cache the imported site's WP version. We already have it in $wpInfo
        // from getWordPressInfo() above; pass it through so we don't pay for
        // a redundant wp-cli call.
        if (!empty($wpInfo['version'])) {
            $this->setCachedVersion($wpSiteId, $wpInfo['version']);
        }

        // Create default security settings
        $this->createDefaultSecuritySettings($wpSiteId);
        
        // Drop WHP's patched magic-login mu-plugin and native create script.
        // Replaces the old `wp package install aaemnnosttv/wp-cli-login-command`.
        try {
            $this->dropPatchedMuPlugin($container, $user, $sitePath);
            $this->dropMagicLoginCreateScript($container, $user, $sitePath);
        } catch (Exception $e) {
            // Don't fail the import if magic-login files can't be dropped.
            error_log("Warning: Failed to drop magic-login files during import: " . $e->getMessage());
        }
        
        return [
            'success' => true,
            'message' => 'WordPress site imported successfully',
            'site_info' => $wpInfo,
            'database_info' => $dbInfo
        ];
    }
    
    /**
     * Get WordPress installation information
     */
    public function getWordPressInfo($container, $user, $sitePath = null) {
        // Default site path if not provided (for backward compatibility)
        if ($sitePath === null) {
            $sitePath = "/home/{$user}/public_html";
        }
        $info = [];
        
        // Get WordPress version
        $versionResult = $this->execWpCli($container, $user, "wp core version", $sitePath);
        if ($versionResult['success']) {
            $info['version'] = trim($versionResult['output']);
        }
        
        // Get site URL
        $urlResult = $this->execWpCli($container, $user, "wp option get siteurl", $sitePath);
        if ($urlResult['success']) {
            $info['site_url'] = trim($urlResult['output']);
        }
        
        // Get site title
        $titleResult = $this->execWpCli($container, $user, "wp option get blogname", $sitePath);
        if ($titleResult['success']) {
            $info['site_title'] = trim($titleResult['output']);
        }
        
        // Get admin email
        $emailResult = $this->execWpCli($container, $user, "wp option get admin_email", $sitePath);
        if ($emailResult['success']) {
            $info['admin_email'] = trim($emailResult['output']);
        }
        
        // Get admin user
        $adminResult = $this->execWpCli($container, $user, "wp user list --role=administrator --field=user_login --format=csv", $sitePath);
        if ($adminResult['success']) {
            $adminUsers = array_filter(explode("\n", trim($adminResult['output'])));
            if (!empty($adminUsers)) {
                $info['admin_user'] = $adminUsers[0];
            }
        }
        
        // Get theme info
        $themeResult = $this->execWpCli($container, $user, "wp theme list --status=active --format=json", $sitePath);
        if ($themeResult['success']) {
            $themes = json_decode($themeResult['output'], true);
            if (!empty($themes)) {
                $info['active_theme'] = $themes[0]['name'];
            }
        }
        
        // Get plugin count
        $pluginResult = $this->execWpCli($container, $user, "wp plugin list --status=active --format=count", $sitePath);
        if ($pluginResult['success']) {
            $info['active_plugins'] = intval(trim($pluginResult['output']));
        }
        
        // Get database configuration (for backward compatibility)
        $dbNameResult = $this->execWpCli($container, $user, "wp config get DB_NAME", $sitePath);
        if ($dbNameResult['success']) {
            $info['db_name'] = trim($dbNameResult['output']);
        }
        
        $dbUserResult = $this->execWpCli($container, $user, "wp config get DB_USER", $sitePath);
        if ($dbUserResult['success']) {
            $info['db_user'] = trim($dbUserResult['output']);
        }
        
        return $info;
    }
    
    /**
     * Get WordPress database information from wp-config.php
     */
    private function getWordPressDatabaseInfo($container, $user, $sitePath) {
        $info = [];
        
        // Get database name
        $dbNameResult = $this->execWpCli($container, $user, "wp config get DB_NAME", $sitePath);
        if ($dbNameResult['success']) {
            $info['name'] = trim($dbNameResult['output']);
        }
        
        // Get database user
        $dbUserResult = $this->execWpCli($container, $user, "wp config get DB_USER", $sitePath);
        if ($dbUserResult['success']) {
            $info['user'] = trim($dbUserResult['output']);
        }
        
        // Get database host
        $dbHostResult = $this->execWpCli($container, $user, "wp config get DB_HOST", $sitePath);
        if ($dbHostResult['success']) {
            $info['host'] = trim($dbHostResult['output']);
        }
        
        // Get table prefix
        $prefixResult = $this->execWpCli($container, $user, "wp config get table_prefix", $sitePath);
        if ($prefixResult['success']) {
            $info['prefix'] = trim($prefixResult['output']);
        }
        
        return $info;
    }
    
    /**
     * Get site by domain
     */
    private function getSiteByDomain($domain) {
        $stmt = $this->db->prepare("SELECT * FROM wordpress_sites WHERE domain = ?");
        $stmt->execute([$domain]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }
    
    /**
     * Remove WordPress site from management
     */
    public function removeFromManagement($siteId) {
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("WordPress site not found");
        }
        
        // Check permissions
        if (AUTH_USER !== 'root' && $site['user'] !== AUTH_USER) {
            throw new Exception("Permission denied: You can only remove your own WordPress sites from management");
        }
        
        // Remove the WordPress site record and all related data
        $this->db->beginTransaction();
        
        try {
            // Remove security settings
            $stmt = $this->db->prepare("DELETE FROM wp_security_settings WHERE site_id = ?");
            $stmt->execute([$siteId]);
            
            // Remove login tokens
            $stmt = $this->db->prepare("DELETE FROM wp_login_tokens WHERE site_id = ?");
            $stmt->execute([$siteId]);
            
            // Remove site plugins
            $stmt = $this->db->prepare("DELETE FROM wp_site_plugins WHERE site_id = ?");
            $stmt->execute([$siteId]);
            
            // Remove update history
            $stmt = $this->db->prepare("DELETE FROM wp_update_history WHERE site_id = ?");
            $stmt->execute([$siteId]);
            
            // Remove the main WordPress site record
            $stmt = $this->db->prepare("DELETE FROM wordpress_sites WHERE id = ?");
            $stmt->execute([$siteId]);
            
            $this->db->commit();
            
            return [
                'success' => true,
                'message' => 'WordPress site removed from management successfully. Files and databases remain unchanged.'
            ];
            
        } catch (Exception $e) {
            $this->db->rollback();
            throw new Exception("Failed to remove WordPress site from management: " . $e->getMessage());
        }
    }
    
    /**
     * Reset admin password
     */
    public function resetAdminPassword($siteId, $newPassword = null) {
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("Site not found");
        }
        
        if (!$newPassword) {
            $newPassword = $this->generatePassword();
        }
        
        $result = $this->execWpCli(
            $site['container_name'], 
            $site['user'],
            sprintf("wp user update %s --user_pass=%s", 
                escapeshellarg($site['admin_username']),
                escapeshellarg($newPassword)
            )
        );
        
        if (!$result['success']) {
            throw new Exception("Failed to reset password: " . $result['output']);
        }
        
        return ['success' => true, 'password' => $newPassword];
    }
    
    /**
     * Create one-time login token
     */
    public function createLoginToken($siteId) {
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("Site not found");
        }
        
        // Check permissions
        if (AUTH_USER !== 'root' && $site['user'] !== AUTH_USER) {
            throw new Exception("Permission denied: You can only create login tokens for your own WordPress sites");
        }
        
        $path = "/home/{$site['user']}/public_html";
        
        // Use the admin username from the database
        $adminUsername = $site['admin_username'];
        if (empty($adminUsername)) {
            throw new Exception("No admin username found for this WordPress site");
        }
        
        // Drop the patched mu-plugin and the native create-script (idempotent;
        // sha256 short-circuits when already current).
        try {
            $this->dropPatchedMuPlugin($site['container_name'], $site['user'], $path);
            $this->dropMagicLoginCreateScript($site['container_name'], $site['user'], $path);
        } catch (Exception $e) {
            throw new Exception("Failed to provision magic-login files: " . $e->getMessage());
        }

        // Run the native create-script directly inside the container. No wp-cli
        // boot, no `wp login install`, no `wp login toggle on`, no `wp login
        // create`. Mirrors the existing docker-exec pattern used elsewhere in
        // this file.
        $createScript = $path . '/wp-content/.whp/whp-magic-login-create.php';
        $innerCmd = 'php ' . escapeshellarg($createScript) . ' ' . escapeshellarg($adminUsername);
        $dockerCmd = sprintf(
            "docker exec --user %s -w %s %s /bin/sh -c %s",
            escapeshellarg($site['user']),
            escapeshellarg($path),
            escapeshellarg($site['container_name']),
            escapeshellarg($innerCmd)
        );
        $output = [];
        $returnCode = 0;
        \exec($dockerCmd . " 2>&1", $output, $returnCode);
        $stdout = implode("\n", $output);
        if ($returnCode !== 0) {
            throw new Exception("Failed to create login link: " . $stdout);
        }

        // The script's only stdout is the URL line, but be defensive against
        // stray notices/warnings PHP might emit before our echo.
        $loginUrl = null;
        foreach (explode("\n", $stdout) as $line) {
            $line = trim($line);
            if (preg_match('/^https?:\/\//', $line)) {
                $loginUrl = $line;
                break;
            }
        }
        if (!$loginUrl) {
            throw new Exception("Could not extract login URL from create-script output: " . $stdout);
        }
        
        return [
            'success' => true,
            'login_url' => $loginUrl,
            'expires_at' => date('Y-m-d H:i:s', time() + 900), // 15 min — matches the create-script's set_transient TTL
            'message' => 'Magic login link created successfully. Link expires in 15 minutes or after first use.'
        ];
    }
    
    /**
     * Clone site to staging
     */
    public function cloneToStaging($siteId, $stagingDomain, $stagingContainer) {
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("Site not found");
        }
        
        // Check if user has enough resources
        if (!$this->checkUserResources($site['user'], 'staging')) {
            throw new Exception("Insufficient resources for staging site");
        }
        
        // Create staging database
        $stagingDb = str_replace('.', '_', $stagingDomain) . '_staging';
        $mysqlMgmt = new \mysqlmgmt();
        $dbResult = $mysqlMgmt->createDatabase($stagingDb, $site['user'], $this->generatePassword());
        
        if ($dbResult['status'] !== '0') {
            throw new Exception("Failed to create staging database: " . $dbResult['msg']);
        }
        
        // Export production database
        $exportResult = $this->execWpCli(
            $site['container_name'],
            $site['user'],
            "wp db export --add-drop-table /tmp/staging-export.sql"
        );
        
        if (!$exportResult['success']) {
            throw new Exception("Failed to export database: " . $exportResult['output']);
        }
        
        // Copy files to staging
        $prodPath = "/home/{$site['user']}/public_html";
        $stagingPath = "/home/{$site['user']}/staging";
        
        $copyCmd = sprintf(
            "docker exec %s cp -a %s %s",
            escapeshellarg($site['container_name']),
            escapeshellarg($prodPath),
            escapeshellarg($stagingPath)
        );
        
        exec($copyCmd, $output, $returnCode);
        if ($returnCode !== 0) {
            throw new Exception("Failed to copy files to staging");
        }
        
        // Update wp-config for staging
        $configResult = $this->execWpCli(
            $stagingContainer,
            $site['user'],
            sprintf("wp config set DB_NAME %s", escapeshellarg($stagingDb)),
            $stagingPath
        );
        
        // Import database to staging
        $importResult = $this->execWpCli(
            $stagingContainer,
            $site['user'],
            "wp db import /tmp/staging-export.sql",
            $stagingPath
        );
        
        // Search and replace URLs
        $this->searchReplace(
            $stagingContainer,
            $site['user'],
            $site['domain'],
            $stagingDomain,
            $stagingPath
        );
        
        // Create staging site record
        $stmt = $this->db->prepare("
            INSERT INTO wordpress_sites 
            (user, domain, container_name, site_path, is_staging, parent_site_id, 
             admin_email, admin_username) 
            VALUES (?, ?, ?, ?, 1, ?, ?, ?)
        ");
        
        $stmt->execute([
            $site['user'],
            $stagingDomain,
            $stagingContainer,
            $stagingPath,
            $siteId,
            $site['admin_email'],
            $site['admin_username']
        ]);
        
        return [
            'success' => true,
            'staging_id' => $this->db->lastInsertId(),
            'staging_url' => "https://{$stagingDomain}"
        ];
    }
    
    /**
     * Search and replace in database
     */
    public function searchReplace($container, $user, $search, $replace, $path = null) {
        if ($path === null) {
            $path = "/home/{$user}/public_html";
        }
        
        $cmd = sprintf(
            "wp search-replace %s %s --all-tables --precise",
            escapeshellarg($search),
            escapeshellarg($replace)
        );
        
        $result = $this->execWpCli($container, $user, $cmd, $path);
        
        if (!$result['success']) {
            throw new Exception("Search and replace failed: " . $result['output']);
        }
        
        return ['success' => true, 'output' => $result['output']];
    }
    
    /**
     * Get list of WP-CLI enabled container types
     */
    public function getWpCliContainerTypes() {
        $stmt = $this->db->query("
            SELECT ct.*, wcc.is_active as wp_cli_enabled 
            FROM container_types ct
            LEFT JOIN wp_cli_containers wcc ON ct.name = wcc.container_image
            WHERE ct.active = 1
            ORDER BY ct.name
        ");
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    
    /**
     * Update WP-CLI support for container types (root only)
     */
    public function updateWpCliSupport($containerTypeName, $enabled) {
        if ($enabled) {
            $stmt = $this->db->prepare("
                INSERT INTO wp_cli_containers (container_image, is_active) 
                VALUES (?, 1)
                ON DUPLICATE KEY UPDATE is_active = 1
            ");
            $stmt->execute([$containerTypeName]);
        } else {
            $stmt = $this->db->prepare("
                UPDATE wp_cli_containers SET is_active = 0 
                WHERE container_image = ?
            ");
            $stmt->execute([$containerTypeName]);
        }
        return ['success' => true];
    }
    
    /**
     * Get user sites that use WP-CLI enabled containers
     */
    public function getUserWordPressSites($user) {
        $userFilter = $user === 'root' ? '' : 'AND s.username = :user';
        
        $sql = "
            SELECT s.*, 
                   ct.name as container_type_name, 
                   ct.description as container_description,
                   sd.fqdn as primary_fqdn
            FROM sites s
            INNER JOIN container_types ct ON s.container_type_id = ct.id
            INNER JOIN site_domains sd ON s.id = sd.site_id AND sd.is_primary = 1
            INNER JOIN wp_cli_containers wcc ON ct.name = wcc.container_image
            LEFT JOIN wordpress_sites ws ON sd.fqdn = ws.domain
            WHERE wcc.is_active = 1 
            AND s.active = 1
            AND ws.id IS NULL
            {$userFilter}
            ORDER BY s.username, s.site_name
        ";
        
        $stmt = $this->db->prepare($sql);
        if ($user !== 'root') {
            $stmt->bindParam(':user', $user);
        }
        $stmt->execute();
        
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    
    /**
     * Check if WordPress is already installed on a site
     */
    public function checkWordPressInstallation($siteId) {
        // Get site info
        $stmt = $this->db->prepare("
            SELECT s.*, 
                   ct.name as container_type_name,
                   sd.fqdn as primary_fqdn
            FROM sites s
            INNER JOIN container_types ct ON s.container_type_id = ct.id
            INNER JOIN site_domains sd ON s.id = sd.site_id AND sd.is_primary = 1
            WHERE s.id = ?
        ");
        $stmt->execute([$siteId]);
        $site = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if (!$site) {
            return ['exists' => false, 'error' => 'Site not found'];
        }
        
        $containerName = $site['primary_fqdn'] . '-01';
        $isInstalled = $this->isWordPressInstalled($containerName, $site['username']);
        
        $result = ['exists' => $isInstalled, 'site' => $site];
        
        if ($isInstalled) {
            // Get WordPress info
            $result['wp_version'] = $this->getWordPressVersion($containerName, $site['username']);
            
            // Try to get database info, admin user, etc.
            $wpInfo = $this->getWordPressInfo($containerName, $site['username']);
            $result = array_merge($result, $wpInfo);
        }
        
        return $result;
    }
    
    /**
     * Update backup settings for a WordPress site
     */
    public function updateBackupSettings($siteId, $backupTargetId, $autoBackup) {
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("Site not found");
        }
        
        // Update backup settings
        $stmt = $this->db->prepare("
            UPDATE wordpress_sites 
            SET preferred_backup_target_id = ?, 
                auto_backup_before_update = ? 
            WHERE id = ?
        ");
        
        $stmt->execute([
            $backupTargetId ?: null,
            $autoBackup ? 1 : 0,
            $siteId
        ]);
        
        return ['success' => true];
    }
    
    /**
     * Get recommended plugins
     */
    public function getRecommendedPlugins() {
        $stmt = $this->db->query("
            SELECT * FROM wp_recommended_plugins 
            WHERE is_active = 1 
            ORDER BY sort_order, name
        ");
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    
    /**
     * Install plugin from recommendation
     */
    public function installRecommendedPlugin($siteId, $pluginSlug) {
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("Site not found");
        }
        
        // Check if plugin exists in recommendations
        $stmt = $this->db->prepare("
            SELECT * FROM wp_recommended_plugins 
            WHERE slug = ? AND is_active = 1
        ");
        $stmt->execute([$pluginSlug]);
        $plugin = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if (!$plugin) {
            throw new Exception("Plugin not found in recommendations");
        }
        
        // Install plugin
        $result = $this->execWpCli(
            $site['container_name'],
            $site['user'],
            sprintf("wp plugin install %s --activate", escapeshellarg($plugin['install_url']))
        );
        
        if (!$result['success']) {
            throw new Exception("Failed to install plugin: " . $result['output']);
        }
        
        // Record installation
        $stmt = $this->db->prepare("
            INSERT INTO wp_site_plugins 
            (site_id, plugin_slug, plugin_name, is_active) 
            VALUES (?, ?, ?, 1)
            ON DUPLICATE KEY UPDATE 
            is_active = 1,
            last_updated = NOW()
        ");
        $stmt->execute([$siteId, $pluginSlug, $plugin['name']]);
        
        return ['success' => true];
    }
    
    // Helper methods
    
    public function getSite($siteId) {
        $stmt = $this->db->prepare("SELECT * FROM wordpress_sites WHERE id = ?");
        $stmt->execute([$siteId]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }
    
    private function getRegularSite($siteId) {
        $stmt = $this->db->prepare("
            SELECT s.*, 
                   ct.name as container_type_name,
                   sd.fqdn as primary_fqdn,
                   sd.fqdn as domain
            FROM sites s
            INNER JOIN container_types ct ON s.container_type_id = ct.id
            INNER JOIN site_domains sd ON s.id = sd.site_id AND sd.is_primary = 1
            WHERE s.id = ?
        ");
        $stmt->execute([$siteId]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }
    
    private function createDefaultSecuritySettings($siteId) {
        $stmt = $this->db->prepare("
            INSERT INTO wp_security_settings (
                site_id, 
                disable_file_editor, 
                disable_xmlrpc, 
                hide_wp_version, 
                disable_directory_browsing, 
                limit_login_attempts,
                database_prefix
            ) VALUES (?, TRUE, TRUE, FALSE, TRUE, TRUE, 'wp_')
        ");
        $stmt->execute([$siteId]);
    }
    
    private function generatePassword($length = 16) {
        // Use MySQL-safe characters: alphanumeric plus safe special characters ! # - _
        $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#-_';
        $password = '';
        for ($i = 0; $i < $length; $i++) {
            $password .= $chars[random_int(0, strlen($chars) - 1)];
        }
        return $password;
    }
    
    private function addHtaccessRules($container, $user, $domain, $rules) {
        $path = "/home/{$user}/{$domain}/public_html";
        $htaccessPath = "{$path}/.htaccess";
        
        // Use cat with here-document for multi-line content
        $cmd = sprintf(
            "cat >> %s << 'EOF'\n%s\nEOF",
            escapeshellarg($htaccessPath),
            $rules
        );
        
        $result = $this->execWpCli($container, $user, $cmd, $path);
        return $result['success'];
    }
    
    private function recordUpdateStart($siteId, $type, $itemName, $oldVersion) {
        $stmt = $this->db->prepare("
            INSERT INTO wp_update_history 
            (site_id, update_type, item_name, old_version, status) 
            VALUES (?, ?, ?, ?, 'in_progress')
        ");
        $stmt->execute([$siteId, $type, $itemName, $oldVersion]);
        return $this->db->lastInsertId();
    }
    
    private function recordUpdateComplete($updateId, $newVersion) {
        $stmt = $this->db->prepare("
            UPDATE wp_update_history
            SET status = 'completed', new_version = ?, completed_at = NOW()
            WHERE id = ?
        ");
        $stmt->execute([$newVersion, $updateId]);
    }

    /**
     * Send a per-site notification email for a backup or update event.
     *
     * No-ops silently when the site has no notification_email, the relevant
     * notify_* flag is off, or SMTP isn't configured. Errors during send are
     * logged and swallowed — a notification problem must never break the
     * underlying operation.
     *
     * @param int    $siteId  wordpress_sites.id
     * @param string $event   one of: backup_complete, backup_failed,
     *                        update_complete, update_failed
     * @param array  $context optional fields to include in the body
     *                        (e.g. ['error' => '...', 'new_version' => '6.9.4',
     *                        'backup_name' => '...']).
     */
    public function sendSiteNotification($siteId, $event, array $context = []) {
        try {
            $flagColumn = [
                'backup_complete' => 'notify_backup_complete',
                'backup_failed'   => 'notify_backup_failed',
                'update_complete' => 'notify_update_complete',
                'update_failed'   => 'notify_update_failed',
            ][$event] ?? null;

            if ($flagColumn === null) {
                error_log("sendSiteNotification: unknown event '$event'");
                return;
            }

            $site = $this->getSite($siteId);
            if (!$site) return;

            // Per-event opt-in.
            if (empty($site[$flagColumn])) return;

            $to = trim((string)($site['notification_email'] ?? ''));
            if (!$to || !filter_var($to, FILTER_VALIDATE_EMAIL)) return;

            $config = SmtpMailer::loadConfig();
            if (!$config) {
                error_log("sendSiteNotification: SMTP not configured; skipping $event for site $siteId");
                return;
            }

            list($subject, $bodyHtml, $bodyText) = $this->buildNotificationBody($site, $event, $context);

            $mailer = new SmtpMailer($config);
            $result = $mailer->send($to, $subject, $bodyHtml, $bodyText);
            if (empty($result['success'])) {
                error_log("sendSiteNotification: send failed for site $siteId event $event: " . ($result['error'] ?? 'unknown'));
            }
        } catch (Exception $e) {
            error_log("sendSiteNotification: exception for site $siteId event $event: " . $e->getMessage());
        }
    }

    /**
     * Build [$subject, $bodyHtml, $bodyText] for a notification. Pure function —
     * doesn't read DB or send. Pulled out so it's easy to unit-test by eye and
     * to keep sendSiteNotification readable.
     */
    private function buildNotificationBody(array $site, $event, array $context) {
        $domain = $site['domain'];
        $when = date('Y-m-d H:i:s T');

        $titles = [
            'backup_complete' => "[WHP] Backup completed for {$domain}",
            'backup_failed'   => "[WHP] Backup FAILED for {$domain}",
            'update_complete' => "[WHP] WordPress update completed for {$domain}",
            'update_failed'   => "[WHP] WordPress update FAILED for {$domain}",
        ];
        $subject = $titles[$event];

        $detailLines = [];
        $detailLines[] = "Site: {$domain}";
        $detailLines[] = "Event: {$event}";
        $detailLines[] = "Time: {$when}";

        if ($event === 'update_complete' && !empty($context['new_version'])) {
            $detailLines[] = "WordPress version: " . $context['new_version'];
        }
        if (!empty($context['backup_name'])) {
            $detailLines[] = "Backup: " . $context['backup_name'];
        }
        if (in_array($event, ['backup_failed', 'update_failed'], true) && !empty($context['error'])) {
            $detailLines[] = "Error: " . $context['error'];
        }

        $textBody = implode("\n", $detailLines)
            . "\n\n--\nThis is an automated notification from your WHP-managed WordPress site."
            . "\nTo change which events trigger emails, sign in to WHP and edit the site's notification settings.";

        // Minimal HTML — escape every interpolation.
        $rows = '';
        foreach ($detailLines as $line) {
            $rows .= '<tr><td style="padding:4px 12px;">' . htmlspecialchars($line) . '</td></tr>';
        }
        $htmlBody = '<table style="font-family:sans-serif;font-size:14px;border-collapse:collapse;">'
            . $rows . '</table>'
            . '<p style="font-family:sans-serif;font-size:12px;color:#666;margin-top:16px;">'
            . 'Automated notification from your WHP-managed WordPress site. '
            . 'Sign in to WHP to change which events trigger emails.'
            . '</p>';

        return [$subject, $htmlBody, $textBody];
    }

    /**
     * Public on-demand version refresh used by the panel's per-row refresh
     * button. Permission-checked: only root or the site's owning user can
     * trigger it. Returns the freshly-cached value.
     *
     * @return array{version: ?string, checked_at: string}
     */
    public function refreshCachedVersion($siteId) {
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("Site not found");
        }
        if (AUTH_USER !== 'root' && $site['user'] !== AUTH_USER) {
            throw new Exception("Permission denied");
        }
        $version = $this->setCachedVersion($siteId);
        return [
            'version'    => $version,
            'checked_at' => date('Y-m-d H:i:s'),
        ];
    }

    /**
     * Cache the WordPress version on the wordpress_sites row.
     *
     * The panel's WP-management page reads from these cached columns instead
     * of fetching wp core version live for every site on every page load.
     * Callers that already have the version (e.g., right after a core update)
     * pass it in to avoid an extra wp-cli boot.
     */
    private function setCachedVersion($siteId, $version = null) {
        if ($version === null) {
            $site = $this->getSite($siteId);
            if (!$site) return null;
            $version = $this->getWordPressVersion($site['container_name'], $site['user']);
        }
        if (!$version) return null;
        $stmt = $this->db->prepare(
            "UPDATE wordpress_sites SET wp_version = ?, wp_version_checked_at = NOW() WHERE id = ?"
        );
        $stmt->execute([$version, $siteId]);
        return $version;
    }
    
    private function recordUpdateFailed($updateId, $error) {
        $stmt = $this->db->prepare("
            UPDATE wp_update_history 
            SET status = 'failed', error_message = ?, completed_at = NOW() 
            WHERE id = ?
        ");
        $stmt->execute([$error, $updateId]);
    }
    
    private function isSiteBroken($site) {
        // Check if site returns 500 error or white screen
        $url = "https://{$site['domain']}";
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 10);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        
        curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        return $httpCode >= 500;
    }
    
    private function createBackup($site, $type = 'manual') {
        // Use existing backup system - need to get target ID first
        $mysql = new \mysqlmgmt();
        $db = $mysql->getMySQLConnection();
        $db->exec("USE whp");
        
        $targetId = null;
        
        // Check if site has a preferred backup target
        if (!empty($site['preferred_backup_target_id'])) {
            // Verify the user has access to this target
            $stmt = $db->prepare("
                SELECT id, name, type 
                FROM backup_targets 
                WHERE id = ? 
                AND (owner = ? OR is_global = 1)
                LIMIT 1
            ");
            $stmt->execute([$site['preferred_backup_target_id'], $site['user']]);
            $target = $stmt->fetch(PDO::FETCH_ASSOC);
            
            if ($target) {
                $targetId = $target['id'];
                error_log("WordPress Backup: Using preferred backup target '{$target['name']}' (ID: {$targetId}, Type: {$target['type']}) for user {$site['user']}");
            }
        }
        
        // If no preferred target or preferred target not accessible, fall back to any available backup target  
        if (!$targetId) {
                $stmt = $db->prepare("
                    SELECT id, name, type 
                    FROM backup_targets 
                    WHERE (owner = ? OR is_global = 1) 
                    AND type != 'local'
                    ORDER BY 
                        CASE WHEN owner = ? THEN 0 ELSE 1 END,  -- Prefer user's own targets
                        id ASC
                    LIMIT 1
                ");
                $stmt->execute([$site['user'], $site['user']]);
                $target = $stmt->fetch(PDO::FETCH_ASSOC);
                
                if (!$target) {
                    // No backup targets available for this user
                    throw new Exception("No backup targets configured. Please configure a backup target in the Backups section first.");
                }
                
            $targetId = $target['id'];
            error_log("WordPress Backup: Using backup target '{$target['name']}' (ID: {$targetId}, Type: {$target['type']}) for user {$site['user']}");
        }
        
        // Queue site backup (files)
        $this->queueBackup($targetId, $site['user'], 'site', $site['domain']);
        
        // Also queue the WordPress database backup
        try {
            // First try to get database name from our site record
            $dbName = !empty($site['database_name']) ? $site['database_name'] : null;
            
            // If not in database, get it from wp-config.php and update our record
            if (!$dbName) {
                $dbName = $this->getWordPressDatabaseName($site['container_name'], $site['user']);
                if ($dbName) {
                    // Update the site record with the database name for future use
                    $stmt = $this->db->prepare("UPDATE wordpress_sites SET database_name = ? WHERE id = ?");
                    $stmt->execute([$dbName, $site['id']]);
                }
            }
            
            if ($dbName) {
                $this->queueBackup($targetId, $site['user'], 'database', $dbName);
                
                return [
                    'success' => true,
                    'message' => "Queued WordPress site and database backups"
                ];
            } else {
                error_log("WordPress Backup: Could not determine database name for site {$site['domain']}");
            }
        } catch (Exception $e) {
            error_log("WordPress Backup: Could not queue database backup: " . $e->getMessage());
        }
        
        return [
            'success' => true,
            'message' => "Queued WordPress site backup"
        ];
    }
    
    private function queueBackup($targetId, $user, $backupType, $backupName) {
        // Add backup to queue for processing by the cron job
        $stmt = $this->db->prepare("
            INSERT INTO backup_queue 
            (target_id, user, backup_type, backup_name, priority, status) 
            VALUES (?, ?, ?, ?, 'high', 'queued')
        ");
        
        $stmt->execute([$targetId, $user, $backupType, $backupName]);
        
        error_log("WordPress Backup: Queued $backupType backup '$backupName' for user $user (target: $targetId)");
        
        return $this->db->lastInsertId();
    }
    
    private function getWordPressDatabaseName($container, $user) {
        // Get database name from wp-config.php
        $result = $this->execWpCli($container, $user, "wp config get DB_NAME");
        if ($result['success']) {
            return trim($result['output']);
        }
        return null;
    }
    
    private function rollbackFromBackup($site, $backupId) {
        // Implement backup restoration
        // This would restore files and database from backup
        error_log("Attempting to rollback site {$site['id']} from backup {$backupId}");
        // TODO: Implement actual rollback
    }
    
    private function checkUserResources($user, $resourceType) {
        // Check if user has enough disk space, sites allowed, etc.
        // For now, return true
        return true;
    }
    
    
    /**
     * Update auto-update settings for a WordPress site
     */
    public function updateAutoUpdateSettings($siteId, $updateType, $enabled) {
        // Validate inputs
        $validTypes = ['core', 'plugins', 'themes'];
        if (!in_array($updateType, $validTypes)) {
            throw new Exception("Invalid update type. Must be: " . implode(', ', $validTypes));
        }
        
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("Site not found");
        }
        
        // Check if user has permission to manage this site
        if (AUTH_USER !== 'root' && $site['user'] !== AUTH_USER) {
            throw new Exception("Permission denied: You can only manage your own WordPress sites");
        }
        
        // Update the database with safe column mapping to prevent SQL injection
        $validColumns = [
            'core' => 'auto_update_core',
            'plugins' => 'auto_update_plugins', 
            'themes' => 'auto_update_themes'
        ];
        $columnName = $validColumns[$updateType];
        $enabledValue = $enabled ? 1 : 0; // Ensure it's explicitly 1 or 0
        $stmt = $this->db->prepare("UPDATE wordpress_sites SET `{$columnName}` = ? WHERE id = ?");
        $stmt->execute([$enabledValue, $siteId]);
        
        if ($stmt->rowCount() === 0) {
            throw new Exception("Failed to update auto-update settings");
        }
        
        return ['success' => true, 'message' => "Auto-update setting for {$updateType} updated successfully"];
    }
    
    /**
     * Trigger an immediate (background-queued) site backup for a WordPress
     * site. Uses the site's preferred_backup_target_id when set, otherwise
     * backup_target_id. The actual archive work happens in the backup worker
     * — we just record the pending row and kick the queue.
     *
     * Returns the underlying BackupEngine::createSiteBackup result on success
     * (with backup_id, backup_name, status). Throws on permission errors and
     * when no backup target is configured.
     */
    public function createImmediateBackup($siteId) {
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("Site not found");
        }
        if (AUTH_USER !== 'root' && $site['user'] !== AUTH_USER) {
            throw new Exception("Permission denied");
        }

        $targetId = !empty($site['preferred_backup_target_id'])
            ? (int)$site['preferred_backup_target_id']
            : (int)($site['backup_target_id'] ?? 0);
        if (!$targetId) {
            throw new Exception(
                "No backup target configured for this site. Set one in Backup Settings first."
            );
        }

        $result = $this->backupEngine->createSiteBackup(
            $site['user'],
            $site['domain'],
            $targetId
        );

        // BackupEngine returns a structured result with success=false on
        // recoverable errors — surface those as exceptions so the AJAX layer
        // returns a clean error to the UI.
        if (empty($result['success'])) {
            throw new Exception($result['error'] ?? 'Backup creation failed');
        }
        return $result;
    }

    /**
     * Read the per-site notification settings (recipient + 4 toggles).
     * Permission-checked.
     */
    public function getNotificationSettings($siteId) {
        $site = $this->getSite($siteId);
        if (!$site) throw new Exception("Site not found");
        if (AUTH_USER !== 'root' && $site['user'] !== AUTH_USER) {
            throw new Exception("Permission denied");
        }
        return [
            'notification_email'      => $site['notification_email'] ?? '',
            'notify_backup_complete'  => (int)($site['notify_backup_complete'] ?? 0),
            'notify_backup_failed'    => (int)($site['notify_backup_failed'] ?? 0),
            'notify_update_complete'  => (int)($site['notify_update_complete'] ?? 0),
            'notify_update_failed'    => (int)($site['notify_update_failed'] ?? 0),
        ];
    }

    /**
     * Persist the per-site notification settings.
     *
     * @param int   $siteId
     * @param array $settings keys: notification_email (string|null),
     *              notify_backup_complete, notify_backup_failed,
     *              notify_update_complete, notify_update_failed (truthy/0|1)
     */
    public function updateNotificationSettings($siteId, array $settings) {
        $site = $this->getSite($siteId);
        if (!$site) throw new Exception("Site not found");
        if (AUTH_USER !== 'root' && $site['user'] !== AUTH_USER) {
            throw new Exception("Permission denied");
        }

        $email = trim((string)($settings['notification_email'] ?? ''));
        if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new Exception("Invalid notification email address");
        }
        $emailValue = $email === '' ? null : $email;

        $flag = function ($v) { return !empty($v) ? 1 : 0; };

        $stmt = $this->db->prepare(
            "UPDATE wordpress_sites SET
                notification_email = ?,
                notify_backup_complete = ?,
                notify_backup_failed = ?,
                notify_update_complete = ?,
                notify_update_failed = ?
             WHERE id = ?"
        );
        $stmt->execute([
            $emailValue,
            $flag($settings['notify_backup_complete'] ?? 0),
            $flag($settings['notify_backup_failed'] ?? 0),
            $flag($settings['notify_update_complete'] ?? 0),
            $flag($settings['notify_update_failed'] ?? 0),
            $siteId,
        ]);

        return ['success' => true, 'message' => 'Notification settings saved'];
    }

    /**
     * Execute WP-CLI command (public wrapper for CLI script access)
     */
    public function executeWpCliCommand($container, $user, $command, $workDir = null) {
        return $this->execWpCli($container, $user, $command, $workDir);
    }
    
    /**
     * Get security settings for a WordPress site
     */
    public function getSecuritySettings($siteId) {
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("Site not found");
        }
        
        // Check permissions
        if (AUTH_USER !== 'root' && $site['user'] !== AUTH_USER) {
            throw new Exception("Permission denied: You can only manage your own WordPress sites");
        }
        
        // Get security settings
        $stmt = $this->db->prepare("SELECT * FROM wp_security_settings WHERE site_id = ?");
        $stmt->execute([$siteId]);
        $settings = $stmt->fetch(PDO::FETCH_ASSOC);
        
        // If no settings exist, create default ones
        if (!$settings) {
            $stmt = $this->db->prepare("
                INSERT INTO wp_security_settings (
                    site_id, disable_file_editor, disable_xmlrpc, hide_wp_version, 
                    disable_directory_browsing, limit_login_attempts, security_headers_enabled,
                    block_php_uploads, force_strong_passwords
                ) VALUES (?, TRUE, TRUE, FALSE, TRUE, TRUE, TRUE, TRUE, FALSE)
            ");
            $stmt->execute([$siteId]);
            
            // Fetch the newly created settings
            $stmt = $this->db->prepare("SELECT * FROM wp_security_settings WHERE site_id = ?");
            $stmt->execute([$siteId]);
            $settings = $stmt->fetch(PDO::FETCH_ASSOC);
        }
        
        return ['success' => true, 'settings' => $settings];
    }
    
    /**
     * Update security settings for a WordPress site
     */
    public function updateSecuritySettings($siteId, $data) {
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("Site not found");
        }
        
        // Check permissions
        if (AUTH_USER !== 'root' && $site['user'] !== AUTH_USER) {
            throw new Exception("Permission denied: You can only manage your own WordPress sites");
        }
        
        // Prepare security settings  
        $settings = [
            'disable_file_editor' => isset($data['disable_file_editor']) ? 1 : 0,
            'disable_xmlrpc' => isset($data['disable_xmlrpc']) ? 1 : 0,
            'hide_wp_version' => isset($data['hide_wp_version']) ? 1 : 0,
            'disable_directory_browsing' => isset($data['disable_directory_browsing']) ? 1 : 0,
            'limit_login_attempts' => isset($data['limit_login_attempts']) ? 1 : 0,
            'security_headers_enabled' => isset($data['security_headers_enabled']) ? 1 : 0,
            'block_php_uploads' => isset($data['block_php_uploads']) ? 1 : 0,
            'force_strong_passwords' => isset($data['force_strong_passwords']) ? 1 : 0
        ];
        
        // Update or insert security settings
        $stmt = $this->db->prepare("
            INSERT INTO wp_security_settings (
                site_id, disable_file_editor, disable_xmlrpc, hide_wp_version,
                disable_directory_browsing, limit_login_attempts, security_headers_enabled,
                block_php_uploads, force_strong_passwords
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
            ON DUPLICATE KEY UPDATE
                disable_file_editor = VALUES(disable_file_editor),
                disable_xmlrpc = VALUES(disable_xmlrpc),
                hide_wp_version = VALUES(hide_wp_version),
                disable_directory_browsing = VALUES(disable_directory_browsing),
                limit_login_attempts = VALUES(limit_login_attempts),
                security_headers_enabled = VALUES(security_headers_enabled),
                block_php_uploads = VALUES(block_php_uploads),
                force_strong_passwords = VALUES(force_strong_passwords),
                updated_at = NOW()
        ");
        
        $stmt->execute([
            $siteId,
            $settings['disable_file_editor'],
            $settings['disable_xmlrpc'],
            $settings['hide_wp_version'],
            $settings['disable_directory_browsing'],
            $settings['limit_login_attempts'],
            $settings['security_headers_enabled'],
            $settings['block_php_uploads'],
            $settings['force_strong_passwords']
        ]);
        
        // Apply security measures to WordPress
        $this->applySecurity($siteId, $site, $settings);
        
        return ['success' => true, 'message' => 'Security settings applied successfully'];
    }
    
    /**
     * Apply security measures based on settings
     */
    private function applySecurity($siteId, $site, $settings) {
        $container = $site['container_name'];
        $user = $site['user'];
        $domain = $site['domain'];
        
        // Disable file editor
        if ($settings['disable_file_editor']) {
            $result = $this->execWpCli($container, $user, "wp config set DISALLOW_FILE_EDIT true --raw");
            if (!$result['success']) {
                error_log("Failed to set DISALLOW_FILE_EDIT: " . $result['output']);
            }
        } else {
            $result = $this->execWpCli($container, $user, "wp config delete DISALLOW_FILE_EDIT");
            if (!$result['success']) {
                error_log("Failed to delete DISALLOW_FILE_EDIT: " . $result['output']);
            }
        }
        
        // Hide WordPress version
        if ($settings['hide_wp_version']) {
            $this->execWpCli($container, $user, "wp config set WP_HIDE_VERSION true --raw");
        } else {
            $this->execWpCli($container, $user, "wp config delete WP_HIDE_VERSION");
        }
        
        // Force strong passwords
        if ($settings['force_strong_passwords']) {
            $this->execWpCli($container, $user, "wp config set FORCE_SSL_ADMIN true --raw");
        }
        
        // Build .htaccess rules
        $htaccessRules = '';
        
        // XML-RPC blocking
        if ($settings['disable_xmlrpc']) {
            $htaccessRules .= '
# Block XML-RPC
<Files xmlrpc.php>
Order Allow,Deny
Deny from all
</Files>
';
        }
        
        // Directory browsing
        if ($settings['disable_directory_browsing']) {
            $htaccessRules .= '
# Disable directory browsing
Options -Indexes
';
        }
        
        // Block PHP in uploads
        if ($settings['block_php_uploads']) {
            $htaccessRules .= '
# Block PHP in uploads
<Directory "wp-content/uploads">
  <Files "*.php">
    Order Allow,Deny
    Deny from all
  </Files>
</Directory>
';
        }
        
        // Security headers
        if ($settings['security_headers_enabled']) {
            $htaccessRules .= '
# Security headers
<IfModule mod_headers.c>
    Header always set X-Content-Type-Options nosniff
    Header always set X-Frame-Options DENY
    Header always set X-XSS-Protection "1; mode=block"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
';
        }
        
        // Apply .htaccess rules if any
        if (!empty($htaccessRules)) {
            $this->addHtaccessRules($container, $user, $domain, $htaccessRules);
        }
    }
    
    /**
     * Shuffle WordPress security salts
     */
    public function shuffleSalts($siteId) {
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("Site not found");
        }
        
        // Check permissions
        if (AUTH_USER !== 'root' && $site['user'] !== AUTH_USER) {
            throw new Exception("Permission denied: You can only manage your own WordPress sites");
        }
        
        $result = $this->execWpCli($site['container_name'], $site['user'], "wp config shuffle-salts");
        
        if (!$result['success']) {
            throw new Exception("Failed to shuffle salts: " . $result['output']);
        }
        
        return ['success' => true, 'message' => 'Security keys regenerated successfully'];
    }
    
    /**
     * Update .htaccess rules
     */
    public function updateHtaccessRules($siteId) {
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("Site not found");
        }
        
        // Check permissions
        if (AUTH_USER !== 'root' && $site['user'] !== AUTH_USER) {
            throw new Exception("Permission denied: You can only manage your own WordPress sites");
        }
        
        // Get current security settings
        $stmt = $this->db->prepare("SELECT * FROM wp_security_settings WHERE site_id = ?");
        $stmt->execute([$siteId]);
        $settings = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if ($settings) {
            $this->applySecurity($siteId, $site, $settings);
        }
        
        return ['success' => true, 'message' => '.htaccess rules updated successfully'];
    }

    /**
     * Force HTTPS redirects in .htaccess
     */
    public function forceHttps($siteId) {
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("Site not found");
        }

        // Check permissions
        if (AUTH_USER !== 'root' && $site['user'] !== AUTH_USER) {
            throw new Exception("Permission denied: You can only manage your own WordPress sites");
        }

        $user = $site['user'];
        $domain = $site['domain'];

        // Direct file path on host filesystem (mounted into containers)
        $htaccessPath = "/docker/users/{$user}/{$domain}/public_html/.htaccess";

        // HTTPS redirect block marker and rules
        $httpsMarker = '# BEGIN Force HTTPS';
        $httpsMarkerEnd = '# END Force HTTPS';
        $httpsRules = <<<'EOT'
# BEGIN Force HTTPS
<IfModule mod_rewrite.c>
RewriteEngine On
# Only redirect if X-Forwarded-Proto header exists and is not https
# This prevents redirect loops when header is not set
RewriteCond %{HTTP:X-Forwarded-Proto} ^(.+)$ [NC]
RewriteCond %{HTTP:X-Forwarded-Proto} !^https$ [NC]
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</IfModule>
# END Force HTTPS
EOT;

        // Validate the path exists and is within allowed directories
        $baseDir = "/docker/users/{$user}/{$domain}/public_html";
        if (!is_dir($baseDir)) {
            throw new Exception("WordPress directory not found: {$baseDir}");
        }

        // Check if .htaccess exists and if HTTPS rules are already present
        if (file_exists($htaccessPath)) {
            $currentContent = file_get_contents($htaccessPath);
            if ($currentContent === false) {
                throw new Exception("Failed to read .htaccess file");
            }

            // Check if HTTPS redirect rules already exist
            if (strpos($currentContent, $httpsMarker) !== false) {
                return ['success' => true, 'message' => 'HTTPS redirect rules already exist in .htaccess'];
            }

            // Prepend HTTPS rules to existing content
            $newContent = $httpsRules . "\n\n" . $currentContent;
        } else {
            // Create new .htaccess with HTTPS rules
            $newContent = $httpsRules . "\n";
        }

        // Write the updated content to .htaccess
        $result = file_put_contents($htaccessPath, $newContent);
        if ($result === false) {
            throw new Exception("Failed to write HTTPS redirect rules to .htaccess");
        }

        // Set appropriate permissions (644 - readable by web server, writable by owner)
        chmod($htaccessPath, 0644);

        return ['success' => true, 'message' => 'HTTPS redirect rules added successfully to .htaccess'];
    }

    /**
     * Check if Force HTTPS is enabled by checking .htaccess
     */
    public function checkForceHttpsStatus($siteId) {
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("Site not found");
        }

        // Check permissions
        if (AUTH_USER !== 'root' && $site['user'] !== AUTH_USER) {
            throw new Exception("Permission denied: You can only manage your own WordPress sites");
        }

        $user = $site['user'];
        $domain = $site['domain'];
        $htaccessPath = "/docker/users/{$user}/{$domain}/public_html/.htaccess";

        // Check if .htaccess exists and contains HTTPS redirect rules
        $httpsEnabled = false;
        if (file_exists($htaccessPath)) {
            $currentContent = file_get_contents($htaccessPath);
            if ($currentContent !== false) {
                // Check for the Force HTTPS marker
                $httpsEnabled = strpos($currentContent, '# BEGIN Force HTTPS') !== false;
            }
        }

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

    /**
     * Disable Force HTTPS by removing redirect rules from .htaccess
     */
    public function disableForceHttps($siteId) {
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("Site not found");
        }

        // Check permissions
        if (AUTH_USER !== 'root' && $site['user'] !== AUTH_USER) {
            throw new Exception("Permission denied: You can only manage your own WordPress sites");
        }

        $user = $site['user'];
        $domain = $site['domain'];
        $htaccessPath = "/docker/users/{$user}/{$domain}/public_html/.htaccess";

        // Check if .htaccess exists
        if (!file_exists($htaccessPath)) {
            return ['success' => true, 'message' => 'HTTPS redirect rules are not present (no .htaccess file)'];
        }

        $currentContent = file_get_contents($htaccessPath);
        if ($currentContent === false) {
            throw new Exception("Failed to read .htaccess file");
        }

        // Check if HTTPS redirect rules exist
        if (strpos($currentContent, '# BEGIN Force HTTPS') === false) {
            return ['success' => true, 'message' => 'HTTPS redirect rules are not present in .htaccess'];
        }

        // Remove the Force HTTPS block
        $httpsMarker = '# BEGIN Force HTTPS';
        $httpsMarkerEnd = '# END Force HTTPS';

        $startPos = strpos($currentContent, $httpsMarker);
        $endPos = strpos($currentContent, $httpsMarkerEnd);

        if ($startPos !== false && $endPos !== false) {
            // Calculate end position (include the end marker line)
            $endPos = $endPos + strlen($httpsMarkerEnd);

            // Remove the block and any trailing newlines
            $before = substr($currentContent, 0, $startPos);
            $after = substr($currentContent, $endPos);

            // Clean up extra newlines
            $newContent = $before . ltrim($after, "\n");

            // If the file would be empty except for whitespace, just remove it
            if (trim($newContent) === '') {
                if (!unlink($htaccessPath)) {
                    throw new Exception("Failed to remove empty .htaccess file");
                }
                return ['success' => true, 'message' => 'HTTPS redirect rules removed and empty .htaccess deleted'];
            }

            // Write the updated content
            $result = file_put_contents($htaccessPath, $newContent);
            if ($result === false) {
                throw new Exception("Failed to update .htaccess file");
            }

            return ['success' => true, 'message' => 'HTTPS redirect rules removed successfully from .htaccess'];
        }

        return ['success' => true, 'message' => 'HTTPS redirect rules not found in expected format'];
    }

    /**
     * Perform basic security scan
     */
    public function performSecurityScan($siteId) {
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("Site not found");
        }
        
        // Check permissions
        if (AUTH_USER !== 'root' && $site['user'] !== AUTH_USER) {
            throw new Exception("Permission denied: You can only manage your own WordPress sites");
        }
        
        $issues = [];
        $container = $site['container_name'];
        $user = $site['user'];
        
        // Check WordPress version
        $versionResult = $this->execWpCli($container, $user, "wp core check-update --format=json");
        if ($versionResult['success']) {
            $updates = json_decode($versionResult['output'], true);
            if (!empty($updates)) {
                $issues[] = "WordPress core updates available";
            }
        }
        
        // Check plugin updates
        $pluginResult = $this->execWpCli($container, $user, "wp plugin list --update=available --format=count");
        if ($pluginResult['success'] && trim($pluginResult['output']) > 0) {
            $issues[] = trim($pluginResult['output']) . " plugin updates available";
        }
        
        // Check admin user strength
        $adminResult = $this->execWpCli($container, $user, "wp user list --role=administrator --format=json");
        if ($adminResult['success']) {
            $admins = json_decode($adminResult['output'], true);
            foreach ($admins as $admin) {
                if (in_array($admin['user_login'], ['admin', 'administrator', 'root'])) {
                    $issues[] = "Weak admin username detected: " . $admin['user_login'];
                }
            }
        }
        
        // Check file permissions
        $permResult = $this->execWpCli($container, $user, "wp config get DISALLOW_FILE_EDIT");
        error_log("DISALLOW_FILE_EDIT check - Success: " . ($permResult['success'] ? 'true' : 'false') . ", Output: '" . trim($permResult['output']) . "'");
        
        if (!$permResult['success']) {
            $issues[] = "File editor not disabled (DISALLOW_FILE_EDIT constant not found)";
        } else {
            $output = trim($permResult['output']);
            // Accept 'true', '1', or 'TRUE' as valid values
            if (!in_array($output, ['true', '1', 'TRUE'])) {
                $issues[] = "File editor not disabled (DISALLOW_FILE_EDIT is '" . $output . "', should be 'true')";
            }
        }
        
        // Check for debug mode
        $debugResult = $this->execWpCli($container, $user, "wp config get WP_DEBUG");
        if ($debugResult['success'] && trim($debugResult['output']) === 'true') {
            $issues[] = "Debug mode is enabled (should be disabled in production)";
        }
        
        return ['success' => true, 'issues' => $issues];
    }
    
    // ========================================
    // RECOMMENDED PLUGINS MANAGEMENT METHODS
    // ========================================
    
    /**
     * Get all recommended plugins (for management interface)
     */
    public function getRecommendedPluginsForManagement() {
        $stmt = $this->db->query("
            SELECT id, slug, name, description, category, install_url, is_active, sort_order, created_at
            FROM wp_recommended_plugins 
            ORDER BY sort_order ASC, name ASC
        ");
        
        $plugins = $stmt->fetchAll(PDO::FETCH_ASSOC);
        
        // Convert boolean fields
        foreach ($plugins as &$plugin) {
            $plugin['is_active'] = (bool)$plugin['is_active'];
            $plugin['sort_order'] = (int)$plugin['sort_order'];
        }
        
        return $plugins;
    }
    
    /**
     * Get a single recommended plugin by ID
     */
    public function getRecommendedPlugin($pluginId) {
        $stmt = $this->db->prepare("
            SELECT id, slug, name, description, category, install_url, is_active, sort_order, created_at
            FROM wp_recommended_plugins 
            WHERE id = ?
        ");
        $stmt->execute([$pluginId]);
        
        $plugin = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$plugin) {
            throw new Exception('Plugin not found');
        }
        
        // Convert boolean fields
        $plugin['is_active'] = (bool)$plugin['is_active'];
        $plugin['sort_order'] = (int)$plugin['sort_order'];
        
        return $plugin;
    }
    
    /**
     * Add a new recommended plugin
     */
    public function addRecommendedPlugin($pluginData) {
        // Validate required fields
        if (empty($pluginData['name']) || empty($pluginData['slug'])) {
            throw new Exception('Plugin name and slug are required');
        }
        
        // Check if slug already exists
        $stmt = $this->db->prepare("SELECT id FROM wp_recommended_plugins WHERE slug = ?");
        $stmt->execute([$pluginData['slug']]);
        if ($stmt->fetch()) {
            throw new Exception('A plugin with this slug already exists');
        }
        
        $stmt = $this->db->prepare("
            INSERT INTO wp_recommended_plugins 
            (slug, name, description, category, install_url, is_active, sort_order) 
            VALUES (?, ?, ?, ?, ?, ?, ?)
        ");
        
        $stmt->execute([
            $pluginData['slug'],
            $pluginData['name'],
            $pluginData['description'] ?? '',
            $pluginData['category'] ?? '',
            $pluginData['install_url'] ?? '',
            $pluginData['is_active'] ? 1 : 0,
            $pluginData['sort_order'] ?? 0
        ]);
        
        $pluginId = $this->db->lastInsertId();
        
        error_log("Recommended plugin added: ID={$pluginId}, Name={$pluginData['name']}, Slug={$pluginData['slug']}");
        
        return $pluginId;
    }
    
    /**
     * Update an existing recommended plugin
     */
    public function updateRecommendedPlugin($pluginId, $pluginData) {
        // Validate required fields
        if (empty($pluginData['name']) || empty($pluginData['slug'])) {
            throw new Exception('Plugin name and slug are required');
        }
        
        // Check if plugin exists
        $stmt = $this->db->prepare("SELECT id FROM wp_recommended_plugins WHERE id = ?");
        $stmt->execute([$pluginId]);
        if (!$stmt->fetch()) {
            throw new Exception('Plugin not found');
        }
        
        // Check if slug conflicts with another plugin
        $stmt = $this->db->prepare("SELECT id FROM wp_recommended_plugins WHERE slug = ? AND id != ?");
        $stmt->execute([$pluginData['slug'], $pluginId]);
        if ($stmt->fetch()) {
            throw new Exception('A plugin with this slug already exists');
        }
        
        $stmt = $this->db->prepare("
            UPDATE wp_recommended_plugins 
            SET slug = ?, name = ?, description = ?, category = ?, install_url = ?, is_active = ?, sort_order = ?
            WHERE id = ?
        ");
        
        $stmt->execute([
            $pluginData['slug'],
            $pluginData['name'],
            $pluginData['description'] ?? '',
            $pluginData['category'] ?? '',
            $pluginData['install_url'] ?? '',
            $pluginData['is_active'] ? 1 : 0,
            $pluginData['sort_order'] ?? 0,
            $pluginId
        ]);
        
        error_log("Recommended plugin updated: ID={$pluginId}, Name={$pluginData['name']}, Slug={$pluginData['slug']}");
        
        return true;
    }
    
    /**
     * Delete a recommended plugin
     */
    public function deleteRecommendedPlugin($pluginId) {
        // Check if plugin exists
        $stmt = $this->db->prepare("SELECT name, slug FROM wp_recommended_plugins WHERE id = ?");
        $stmt->execute([$pluginId]);
        $plugin = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if (!$plugin) {
            throw new Exception('Plugin not found');
        }
        
        // Delete the plugin
        $stmt = $this->db->prepare("DELETE FROM wp_recommended_plugins WHERE id = ?");
        $stmt->execute([$pluginId]);
        
        error_log("Recommended plugin deleted: ID={$pluginId}, Name={$plugin['name']}, Slug={$plugin['slug']}");
        
        return true;
    }
    
    /**
     * Get active recommended plugins for installation display
     */
    public function getActiveRecommendedPlugins() {
        $stmt = $this->db->query("
            SELECT id, slug, name, description, category, install_url, sort_order
            FROM wp_recommended_plugins 
            WHERE is_active = 1
            ORDER BY sort_order ASC, name ASC
        ");
        
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    
    /**
     * Toggle plugin active status
     */
    public function togglePluginStatus($pluginId, $isActive) {
        $stmt = $this->db->prepare("UPDATE wp_recommended_plugins SET is_active = ? WHERE id = ?");
        $stmt->execute([$isActive ? 1 : 0, $pluginId]);
        
        if ($stmt->rowCount() === 0) {
            throw new Exception('Plugin not found');
        }
        
        error_log("Recommended plugin status toggled: ID={$pluginId}, Active=" . ($isActive ? 'true' : 'false'));
        
        return true;
    }

    /**
     * Queue backup and update together (backup first, then update waits for completion)
     */
    private function queueUpdateWithBackup($siteId, $updateId, $updateType) {
        $site = $this->getSite($siteId);
        if (!$site) {
            throw new Exception("Site not found");
        }

        error_log("WordPress Update: Queuing backup + update for site {$siteId}, update type: {$updateType}");

        // Create the combined WordPress backup using BackupEngine (queued for background processing)
        $backupData = $this->backupEngine->createWordPressBackup(
            $siteId,
            null, // Let BackupEngine generate backup name
            $site['preferred_backup_target_id'] ?? null
        );

        if (!$backupData || !isset($backupData['backup_id'])) {
            throw new Exception("Failed to queue WordPress backup");
        }

        $backupId = $backupData['backup_id'];
        error_log("WordPress Update: Queued backup ID {$backupId}");

        // Queue the update to run after backup completes
        $this->queueWordPressUpdate($siteId, $updateId, $updateType, $backupId);

        return [
            'success' => true,
            'backup_id' => $backupId,
            'backup_name' => $backupData['backup_name'] ?? null,
            'update_id' => $updateId,
            'status' => 'queued',
            'message' => 'Backup and update queued for background processing'
        ];
    }

    /**
     * Queue WordPress update to run after backup completion
     */
    private function queueWordPressUpdate($siteId, $updateId, $updateType, $backupId) {
        // Add to wp_update_queue table to be processed by background worker
        $stmt = $this->db->prepare("
            INSERT INTO wp_update_queue
            (site_id, update_id, update_type, backup_id, status, created_at)
            VALUES (?, ?, ?, ?, 'waiting_for_backup', NOW())
        ");

        $stmt->execute([$siteId, $updateId, $updateType, $backupId]);
        $queueId = $this->db->lastInsertId();

        error_log("WordPress Update: Queued update ID {$updateId} (type: {$updateType}) to wait for backup {$backupId}, queue ID: {$queueId}");

        return $queueId;
    }

    /**
     * Create WordPress backup using the new combined backup system (background only)
     */
    private function createWordPressBackupForUpdate($site) {
        error_log("WordPress Update: Queuing combined WordPress backup for site {$site['domain']}");

        // Create the combined WordPress backup using BackupEngine (returns immediately)
        $backupData = $this->backupEngine->createWordPressBackup(
            $site['id'],
            null, // Let BackupEngine generate backup name
            $site['preferred_backup_target_id'] ?? null
        );

        if (!$backupData || !isset($backupData['backup_id'])) {
            throw new Exception("Failed to queue WordPress backup");
        }

        $backupId = $backupData['backup_id'];
        error_log("WordPress Update: Queued backup ID {$backupId} for background processing");

        return [
            'success' => true,
            'backup_id' => $backupId,
            'backup_name' => $backupData['backup_name'] ?? null,
            'status' => 'queued',
            'message' => 'Backup queued for background processing'
        ];
    }

    /**
     * Clone a WordPress site to a new site
     */
    public function cloneSite($sourceSiteId, $targetSiteId) {
        try {
            // Validate input parameters are positive integers
            if (!is_int($sourceSiteId) || !is_int($targetSiteId) || $sourceSiteId <= 0 || $targetSiteId <= 0) {
                throw new Exception("Invalid site ID parameters");
            }

            // Validate source WordPress site exists
            $sourceSite = $this->getWordPressSiteById($sourceSiteId);
            if (!$sourceSite) {
                throw new Exception("Source WordPress site not found");
            }

            // Check if source site has a database configured
            if (empty($sourceSite['database_name'])) {
                // Try to determine database name from wp-config.php if not in database
                // Use host server path for file access
                $sourcePath = "/docker/users/{$sourceSite['user']}/{$sourceSite['domain']}/public_html";

                // CRITICAL SECURITY: Validate path to prevent traversal attacks
                $sourcePath = $this->validateAndCanonicalizePath($sourcePath);
                $wpConfigPath = $sourcePath . '/wp-config.php';

                if (file_exists($wpConfigPath)) {
                    $wpConfig = file_get_contents($wpConfigPath);
                    if (preg_match("/define\s*\(\s*['\"]DB_NAME['\"]\s*,\s*['\"]([^'\"]+)['\"]\s*\)/", $wpConfig, $matches)) {
                        $extractedDbName = $matches[1];

                        // Validate the extracted database name
                        if ($this->isValidDatabaseName($extractedDbName)) {
                            $sourceSite['database_name'] = $extractedDbName;

                            // Update the database record with the discovered database name
                            // Use conditional update to avoid race conditions
                            $updateStmt = $this->db->prepare("UPDATE wordpress_sites SET database_name = ? WHERE id = ? AND (database_name IS NULL OR database_name = '')");
                            $updateStmt->execute([$extractedDbName, $sourceSiteId]);
                            error_log("WordPress Clone: Updated database_name for site ID $sourceSiteId");
                        } else {
                            throw new Exception("Invalid database name format found in wp-config.php");
                        }
                    }
                }

                if (empty($sourceSite['database_name'])) {
                    throw new Exception("Source WordPress site does not have a database configured. Cannot proceed with cloning.");
                }
            }

            // CRITICAL SECURITY: Validate user ownership of source site
            $currentUser = defined('AUTH_USER') ? AUTH_USER : '';
            $isRoot = $currentUser === 'root';

            if (!$isRoot && $sourceSite['user'] !== $currentUser) {
                throw new Exception("Access denied: You can only clone your own WordPress sites");
            }

            // Get target site info from sites table
            $stmt = $this->db->prepare("
                SELECT s.*,
                       ct.name as container_type_name,
                       sd.fqdn as primary_fqdn,
                       d.domain_name as domain_name
                FROM sites s
                INNER JOIN container_types ct ON s.container_type_id = ct.id
                INNER JOIN site_domains sd ON s.id = sd.site_id AND sd.is_primary = 1
                INNER JOIN domains d ON sd.domain_id = d.id
                WHERE s.id = ?
            ");
            $stmt->execute([$targetSiteId]);
            $targetSite = $stmt->fetch(PDO::FETCH_ASSOC);

            if (!$targetSite) {
                throw new Exception("Target site not found");
            }

            // CRITICAL SECURITY: Validate user ownership of target site
            if (!$isRoot && $targetSite['username'] !== $currentUser) {
                throw new Exception("Access denied: You do not own the target site");
            }

            // Check if target site already has WordPress installed
            $existingWpSite = $this->getWordPressSiteByContainer($targetSite['primary_fqdn'] . '-01');
            if ($existingWpSite) {
                throw new Exception("WordPress is already installed on the target site");
            }

            // Validate target site has at least one running container
            $stmt = $this->db->prepare("SELECT container_id FROM site_containers WHERE site_id = ? AND status = 'running' LIMIT 1");
            $stmt->execute([$targetSiteId]);
            $targetContainer = $stmt->fetchColumn();

            if (!$targetContainer) {
                throw new Exception("Target site has no running containers");
            }

            $targetContainer = $targetSite['primary_fqdn'] . '-01';
            // Use host server paths for file operations
            $sourcePath = "/docker/users/{$sourceSite['user']}/{$sourceSite['domain']}/public_html";
            $targetPath = "/docker/users/{$targetSite['username']}/{$targetSite['primary_fqdn']}/public_html";

            // EARLY VALIDATION: Check if target directory is empty before starting clone process
            if (is_dir($targetPath)) {
                $files = array_diff(scandir($targetPath), ['.', '..', 'index.html']);
                if (!empty($files)) {
                    throw new Exception("Target directory is not empty. Please ensure the target site directory is empty before cloning.");
                }
            }

            // Step 1: Create new database for the cloned site
            $dbCredentials = $this->createCloneDatabase($targetSite['username'], $targetSite['primary_fqdn']);

            // Step 2: Export source database
            $this->exportSourceDatabase($sourceSite['database_name'], $dbCredentials['database']);

            // Step 3: Copy files from source to target
            $this->copyWordPressFiles($sourceSite, $targetSite, $sourcePath, $targetPath);

            // Step 4: Update wp-config.php with new database credentials and security keys
            $this->updateWpConfig($targetSite['username'], $targetPath, $dbCredentials);

            // Step 5: Perform search and replace for domain/path updates
            $this->performCloneSearchReplace($targetContainer, $targetSite['username'], $targetPath, $sourceSite, $targetSite);

            // Step 6: Register the new WordPress site in our database
            $this->registerClonedWordPressSite($sourceSite, $targetSite, $dbCredentials['database'], $targetContainer, $targetPath);

            return [
                'success' => true,
                'message' => "WordPress site successfully cloned to {$targetSite['primary_fqdn']}",
                'target_site_id' => $targetSiteId,
                'target_domain' => $targetSite['primary_fqdn'],
                'database_name' => $dbCredentials['database']
            ];

        } catch (Exception $e) {
            error_log("WordPress Clone Error: " . $e->getMessage());
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
    }

    /**
     * Get WordPress site by ID
     */
    private function getWordPressSiteById($siteId) {
        $stmt = $this->db->prepare("SELECT * FROM wordpress_sites WHERE id = ?");
        $stmt->execute([$siteId]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }

    /**
     * Get WordPress site by container name
     */
    private function getWordPressSiteByContainer($containerName) {
        $stmt = $this->db->prepare("SELECT * FROM wordpress_sites WHERE container_name = ?");
        $stmt->execute([$containerName]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }

    /**
     * Create a new database for the cloned site
     */
    private function createCloneDatabase($targetUser, $targetDomain) {
        require_once('mysqlmgmt.php');
        $mysql = new \mysqlmgmt();

        // HIGH SECURITY: Generate secure database name with cryptographic component
        $domainPart = preg_replace('/[^a-zA-Z0-9]/', '', $targetDomain);
        $domainPart = substr($domainPart, 0, 20); // Limit domain part
        $randomPart = bin2hex(random_bytes(8)); // 16 character random hex string
        $dbName = substr($targetUser . '_' . $domainPart . '_' . $randomPart, 0, 64);

        // Generate database user and password
        $dbUser = substr($dbName, 0, 32); // MySQL username limit is 32 chars
        $dbPassword = $this->generatePassword();

        // Ensure unique database name with additional entropy if needed
        $counter = 1;
        $originalDbName = $dbName;
        $originalDbUser = $dbUser;
        $pdo = $mysql->getMySQLConnection();
        if (!$pdo) {
            throw new Exception("Failed to connect to MySQL server");
        }

        // Check if database exists using information_schema
        while (true) {
            $stmt = $pdo->prepare("SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = ?");
            $stmt->execute([$dbName]);
            if (!$stmt->fetch()) {
                // Database doesn't exist, we can use this name
                break;
            }

            // Database exists, generate a new name
            $extraRandom = bin2hex(random_bytes(2)); // 4 more random chars
            $dbName = substr($originalDbName, 0, 58) . '_' . $counter . $extraRandom;
            $dbUser = substr($originalDbUser, 0, 30) . '_' . $counter; // Keep under 32 char limit
            $counter++;
            if ($counter > 100) { // Prevent infinite loops
                throw new Exception("Unable to generate unique database name");
            }
        }

        error_log("WordPress Clone: Creating database user '$dbUser' and database '$dbName'");

        // First create the MySQL user
        $userResult = $mysql->createMySQLUser($dbUser, $dbPassword);
        if ($userResult['status'] !== '0' && strpos($userResult['msg'], 'already exists') === false) {
            throw new Exception("Failed to create MySQL user: " . $userResult['msg']);
        }

        // Then create the database and grant privileges
        $result = $mysql->createDatabase($dbName, $dbUser);
        if ($result['status'] !== '0') {
            throw new Exception("Failed to create database: " . $result['msg']);
        }

        // Return database credentials
        return [
            'database' => $dbName,
            'username' => $dbUser,
            'password' => $dbPassword
        ];
    }

    /**
     * Export source database and import to target database
     */
    private function exportSourceDatabase($sourceDbName, $targetDbName) {
        // Check for null or empty database names
        if (empty($sourceDbName)) {
            throw new Exception("Source database name is not available. The WordPress site may not have a database configured.");
        }
        if (empty($targetDbName)) {
            throw new Exception("Target database name is not available");
        }

        // CRITICAL SECURITY: Validate database names to prevent command injection
        if (!$this->isValidDatabaseName($sourceDbName) || !$this->isValidDatabaseName($targetDbName)) {
            throw new Exception("Invalid database name format");
        }

        // Use mysqldump to export and mysql to import
        $exportCmd = sprintf(
            "mysqldump --defaults-extra-file=/root/.my.cnf --single-transaction --routines --triggers %s",
            escapeshellarg($sourceDbName)
        );

        $importCmd = sprintf(
            "mysql --defaults-extra-file=/root/.my.cnf %s",
            escapeshellarg($targetDbName)
        );

        $fullCmd = $exportCmd . " | " . $importCmd;

        $output = [];
        $returnCode = 0;
        exec($fullCmd . " 2>&1", $output, $returnCode);

        if ($returnCode !== 0) {
            throw new Exception("Database export/import failed: " . implode("\n", $output));
        }

        error_log("WordPress Clone: Database exported from $sourceDbName to $targetDbName");
    }

    /**
     * Copy WordPress files from source to target
     */
    private function copyWordPressFiles($sourceSite, $targetSite, $sourcePath, $targetPath) {
        // CRITICAL SECURITY: Validate source path exists
        $sourcePath = $this->validateAndCanonicalizePath($sourcePath);

        // For target path, validate securely but allow creation
        // First validate the parent directory structure exists and is valid
        $targetParentDir = dirname($targetPath);
        if (!is_dir($targetParentDir)) {
            throw new Exception("Target parent directory does not exist - site may not be set up correctly");
        }

        // Validate parent directory is in allowed location using our security function
        $this->validateAndCanonicalizePath($targetParentDir);

        // Check for path traversal attempts in target path
        if (strpos($targetPath, '..') !== false) {
            throw new Exception("Path traversal attempt detected in target path");
        }

        // Ensure target path follows expected pattern
        if (!preg_match('#^/docker/users/[a-zA-Z0-9_-]+/[a-zA-Z0-9.-]+/public_html$#', $targetPath)) {
            throw new Exception("Target path does not follow expected format");
        }

        // Ensure target directory exists and is empty
        if (!is_dir($targetPath)) {
            if (!mkdir($targetPath, 0755, true)) {
                throw new Exception("Failed to create target directory: $targetPath");
            }
        } else {
            // Check if directory is empty (allow only index.html)
            $files = array_diff(scandir($targetPath), ['.', '..', 'index.html']);
            if (!empty($files)) {
                throw new Exception("Target directory is not empty");
            }
        }

        // Copy files using rsync with proper permissions
        $rsyncCmd = sprintf(
            "rsync -av --exclude='.git' --exclude='*.log' %s/ %s/",
            escapeshellarg($sourcePath),
            escapeshellarg($targetPath)
        );

        $output = [];
        $returnCode = 0;
        exec($rsyncCmd . " 2>&1", $output, $returnCode);

        if ($returnCode !== 0) {
            throw new Exception("File copy failed: " . implode("\n", $output));
        }

        // Set proper ownership
        $uid = trim(shell_exec("id -u " . escapeshellarg($targetSite['username'])));
        $gid = trim(shell_exec("id -g " . escapeshellarg($targetSite['username'])));

        if (is_numeric($uid) && is_numeric($gid)) {
            exec("chown -R {$uid}:{$gid} " . escapeshellarg($targetPath));
        }

        error_log("WordPress Clone: Files copied from $sourcePath to $targetPath");
    }

    /**
     * Update wp-config.php with new database credentials and rotate security keys
     */
    private function updateWpConfig($targetUser, $targetPath, $dbCredentials) {
        $wpConfigPath = $targetPath . '/wp-config.php';

        if (!file_exists($wpConfigPath)) {
            throw new Exception("wp-config.php not found at: $wpConfigPath");
        }

        $wpConfig = file_get_contents($wpConfigPath);

        // Update database credentials with proper escaping
        $wpConfig = preg_replace(
            "/define\s*\(\s*['\"]DB_NAME['\"]\s*,\s*['\"][^'\"]*['\"]\s*\)/",
            "define('DB_NAME', '" . addslashes($dbCredentials['database']) . "')",
            $wpConfig
        );

        $wpConfig = preg_replace(
            "/define\s*\(\s*['\"]DB_USER['\"]\s*,\s*['\"][^'\"]*['\"]\s*\)/",
            "define('DB_USER', '" . addslashes($dbCredentials['username']) . "')",
            $wpConfig
        );

        $wpConfig = preg_replace(
            "/define\s*\(\s*['\"]DB_PASSWORD['\"]\s*,\s*['\"][^'\"]*['\"]\s*\)/",
            "define('DB_PASSWORD', '" . addslashes($dbCredentials['password']) . "')",
            $wpConfig
        );

        // Generate new security keys
        $saltKeys = [
            'AUTH_KEY', 'SECURE_AUTH_KEY', 'LOGGED_IN_KEY', 'NONCE_KEY',
            'AUTH_SALT', 'SECURE_AUTH_SALT', 'LOGGED_IN_SALT', 'NONCE_SALT'
        ];

        foreach ($saltKeys as $key) {
            $newSalt = $this->generateWpSalt();
            // Use more robust pattern matching and proper escaping
            $pattern = "/define\s*\(\s*['\"]" . preg_quote($key, '/') . "['\"]\s*,\s*['\"][^'\"]*['\"]\s*\)/";
            $replacement = "define('" . $key . "', '" . addslashes($newSalt) . "')";
            $wpConfig = preg_replace($pattern, $replacement, $wpConfig);
        }

        if (!file_put_contents($wpConfigPath, $wpConfig)) {
            throw new Exception("Failed to update wp-config.php");
        }

        error_log("WordPress Clone: Updated wp-config.php with new database credentials and security keys");
    }

    /**
     * Generate WordPress salt
     */
    private function generateWpSalt($length = 64) {
        // Exclude quotes and backslashes to prevent PHP syntax errors
        $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+{}[]|;:,.<>?~';
        $salt = '';
        for ($i = 0; $i < $length; $i++) {
            $salt .= $chars[random_int(0, strlen($chars) - 1)];
        }

        // Double-check for any problematic characters that could break PHP syntax
        $salt = str_replace(["'", '"', '\\'], ['', '', ''], $salt);

        // Ensure we still have a good length after cleaning
        while (strlen($salt) < $length) {
            $salt .= $chars[random_int(0, strlen($chars) - 1)];
        }

        return substr($salt, 0, $length);
    }

    /**
     * Perform search and replace operations for the cloned site
     */
    private function performCloneSearchReplace($targetContainer, $targetUser, $targetPath, $sourceSite, $targetSite) {
        // HIGH SECURITY: Validate and sanitize domain names and paths for search/replace
        if (!$this->isValidDomainName($sourceSite['domain']) || !$this->isValidDomainName($targetSite['primary_fqdn'])) {
            throw new Exception("Invalid domain name format for search/replace");
        }

        // Container paths (different from host paths)
        $sourceContainerPath = "/home/{$sourceSite['user']}/public_html";
        $targetContainerPath = "/home/{$targetUser}/public_html";

        // Validate container paths contain only safe characters
        if (!$this->isValidPath($sourceContainerPath) || !$this->isValidPath($targetContainerPath)) {
            throw new Exception("Invalid path format for search/replace");
        }

        $replacements = [
            // Domain/URL replacements
            $sourceSite['domain'] => $targetSite['primary_fqdn'],
            'https://' . $sourceSite['domain'] => 'https://' . $targetSite['primary_fqdn'],
            'http://' . $sourceSite['domain'] => 'http://' . $targetSite['primary_fqdn'],
            '//' . $sourceSite['domain'] => '//' . $targetSite['primary_fqdn'],

            // Container path replacements (if different users)
            $sourceContainerPath => $targetContainerPath,
        ];

        foreach ($replacements as $search => $replace) {
            if ($search !== $replace) {
                // Database search/replace - let searchReplace method use default container path
                $result = $this->searchReplace($targetContainer, $targetUser, $search, $replace);
                if (!$result['success']) {
                    error_log("WordPress Clone: Search/replace warning: " . ($result['output'] ?? 'unknown error'));
                }
            }
        }

        error_log("WordPress Clone: Completed search/replace operations");
    }

    /**
     * Register the cloned WordPress site in our database
     */
    private function registerClonedWordPressSite($sourceSite, $targetSite, $newDbName, $targetContainer, $targetPath) {
        $stmt = $this->db->prepare("
            INSERT INTO wordpress_sites (
                user, domain, container_name, site_path, admin_email, admin_username,
                database_name, status, auto_update_core, auto_update_plugins, auto_update_themes,
                auto_backup_before_update, parent_site_id, is_staging, preferred_backup_target_id
            ) VALUES (?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, 0, ?)
        ");

        $stmt->execute([
            $targetSite['username'],
            $targetSite['primary_fqdn'],
            $targetContainer,
            $targetPath,
            $sourceSite['admin_email'],
            $sourceSite['admin_username'],
            $newDbName,
            $sourceSite['auto_update_core'] ?? 1,
            $sourceSite['auto_update_plugins'] ?? 1,
            $sourceSite['auto_update_themes'] ?? 1,
            $sourceSite['auto_backup_before_update'] ?? 1,
            $sourceSite['id'], // Set as parent site
            $sourceSite['preferred_backup_target_id']
        ]);

        $newSiteId = $this->db->lastInsertId();

        // Security settings will be created automatically when user first accesses
        // the WordPress management page - no need to copy during clone process

        error_log("WordPress Clone: Registered new WordPress site with ID $newSiteId");
        return $newSiteId;
    }

    /**
     * SECURITY: Validate database name format to prevent injection attacks
     */
    private function isValidDatabaseName($dbName) {
        // Database names must be alphanumeric with underscores only, max 64 chars
        return preg_match('/^[a-zA-Z0-9_]{1,64}$/', $dbName);
    }

    /**
     * SECURITY: Validate and canonicalize file paths to prevent traversal attacks
     */
    private function validateAndCanonicalizePath($path, $allowedPrefix = null) {
        // Resolve the canonical path
        $realPath = realpath($path);

        // Path must exist - if it doesn't, the site is not set up correctly
        if ($realPath === false) {
            throw new Exception("Path does not exist: $path - site may not be set up correctly");
        }

        // Ensure path is within allowed prefixes
        // Only allow /docker/users/ paths for hosting users
        $allowedPrefixes = ['/docker/users/'];
        if ($allowedPrefix) {
            $allowedPrefixes = [$allowedPrefix];
        }

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

        if (!$pathIsAllowed) {
            throw new Exception("Path not allowed: $realPath");
        }

        return $realPath;
    }

    /**
     * SECURITY: Validate domain name format
     */
    private function isValidDomainName($domain) {
        // Basic domain name validation - alphanumeric, dots, hyphens only
        return preg_match('/^[a-zA-Z0-9.-]{1,253}$/', $domain) &&
               strpos($domain, '..') === false && // No consecutive dots
               !str_starts_with($domain, '.') &&   // No leading dot
               !str_ends_with($domain, '.');       // No trailing dot
    }

    /**
     * SECURITY: Validate path format
     */
    private function isValidPath($path) {
        // Path should not contain dangerous characters
        return preg_match('/^[a-zA-Z0-9._\/-]{1,500}$/', $path) &&
               strpos($path, '..') === false && // No path traversal
               !str_contains($path, '//');     // No double slashes
    }

    /**
     * SECURITY: Validate container name format
     */
    private function isValidContainerName($containerName) {
        // Container names: alphanumeric, hyphens, underscores, periods, 1-255 chars
        return preg_match('/^[a-zA-Z0-9._-]{1,255}$/', $containerName);
    }

    /**
     * SECURITY: Validate username format
     */
    private function isValidUsername($username) {
        // Usernames: alphanumeric, hyphens, underscores, 1-50 chars
        return preg_match('/^[a-zA-Z0-9_-]{1,50}$/', $username);
    }

    /**
     * SECURITY: Validate and resolve path
     */
    private function validatePath($path) {
        // Resolve to canonical path
        $realPath = realpath($path);

        // If path doesn't exist, climb up directory tree to find an existing parent
        if ($realPath === false) {
            $checkPath = $path;
            $realParent = false;

            // Climb up until we find an existing directory (max 10 levels)
            for ($i = 0; $i < 10; $i++) {
                $checkPath = dirname($checkPath);
                $realParent = realpath($checkPath);

                if ($realParent !== false) {
                    break;
                }

                // Stop if we've reached root
                if ($checkPath === '/' || $checkPath === '.') {
                    break;
                }
            }

            if ($realParent === false) {
                throw new Exception("Invalid path: no valid parent directory found");
            }

            // Validate the existing parent is in allowed location
            if (!str_starts_with($realParent, '/docker/users/')) {
                throw new Exception("Path not in allowed location");
            }

            // Return the original path if an allowed parent was found
            return $path;
        }

        // Validate path is in allowed location
        if (!str_starts_with($realPath, '/docker/users/')) {
            throw new Exception("Path not in allowed location: $realPath");
        }

        return $realPath;
    }

    /**
     * Restore WordPress site from backup
     *
     * @param string $containerName Container name
     * @param string $user Username
     * @param string $domain Domain name
     * @param array $backupData Backup data from backup_history
     * @param PDO $db Database connection
     * @return array Result array with success status and message/error
     */
    public function restoreWordPressBackup($containerName, $user, $domain, $backupData, $db) {
        try {
            // Validate inputs
            if (!$this->isValidContainerName($containerName)) {
                return ['success' => false, 'error' => 'Invalid container name'];
            }
            if (!$this->isValidUsername($user)) {
                return ['success' => false, 'error' => 'Invalid username'];
            }
            if (!$this->isValidDomainName($domain)) {
                return ['success' => false, 'error' => 'Invalid domain'];
            }

            // Validate backup data structure and required fields
            if (!is_array($backupData)) {
                return ['success' => false, 'error' => 'Invalid backup data structure'];
            }

            // Check for required fields
            $requiredFields = ['id', 'backup_name', 'backup_type', 'status', 'user'];
            foreach ($requiredFields as $field) {
                if (!isset($backupData[$field]) || $backupData[$field] === '') {
                    return ['success' => false, 'error' => "Missing required backup field: $field"];
                }
            }

            // Validate backup type is in allowed list
            $allowedBackupTypes = ['wordpress', 'site', 'database', 'userfiles'];
            if (!in_array($backupData['backup_type'], $allowedBackupTypes, true)) {
                return ['success' => false, 'error' => 'Invalid backup type'];
            }

            // Validate backup status
            if ($backupData['status'] !== 'completed') {
                return ['success' => false, 'error' => 'Cannot restore incomplete backup'];
            }

            // Validate backup belongs to the user
            if ($backupData['user'] !== $user) {
                return ['success' => false, 'error' => 'Access denied: backup belongs to different user'];
            }

            // Sanitize backup_name to prevent path traversal
            $backupData['backup_name'] = str_replace(["\0", '/', '\\', '..'], '', $backupData['backup_name']);
            if (empty($backupData['backup_name'])) {
                return ['success' => false, 'error' => 'Invalid backup name'];
            }

            // Sanitize backup_path if present to prevent path traversal
            if (isset($backupData['backup_path'])) {
                // Remove null bytes and normalize path separators
                $backupData['backup_path'] = str_replace(["\0", '\\'], ['', '/'], $backupData['backup_path']);
                // Remove any ../ sequences
                $backupData['backup_path'] = preg_replace('#/+\.\./#', '/', $backupData['backup_path']);
                $backupData['backup_path'] = preg_replace('#^\.\./#', '', $backupData['backup_path']);
            }

            // Validate and sanitize metadata if present
            if (isset($backupData['metadata'])) {
                if (is_string($backupData['metadata'])) {
                    // Decode JSON metadata
                    $metadata = json_decode($backupData['metadata'], true);
                    if ($metadata === null && $backupData['metadata'] !== 'null') {
                        error_log("WordPress Restore: Invalid JSON in backup metadata for backup ID: " . $backupData['id']);
                        // Set to empty array rather than failing - metadata is optional
                        $backupData['metadata'] = [];
                    } else {
                        $backupData['metadata'] = $metadata;
                    }
                } elseif (!is_array($backupData['metadata'])) {
                    // Metadata must be array or null
                    $backupData['metadata'] = [];
                }
            }

            // Check if backup file is available
            $backupFilePath = null;

            // Check local storage first
            if (!empty($backupData['local_path']) && $backupData['local_deleted_at'] === null) {
                if (file_exists($backupData['local_path'])) {
                    $backupFilePath = $backupData['local_path'];
                }
            }

            // If not in local storage, check user's userfiles directory
            if (!$backupFilePath && !empty($backupData['backup_name'])) {
                $userfilesPath = "/docker/users/$user/userfiles/" . $backupData['backup_name'];

                // SECURITY: Validate the constructed path to prevent traversal
                // First, resolve any symbolic links and get canonical path
                $realUserfilesPath = realpath($userfilesPath);

                // Validate the path exists and is within the user's userfiles directory
                $expectedPrefix = "/docker/users/$user/userfiles";
                if ($realUserfilesPath &&
                    strpos($realUserfilesPath, $expectedPrefix) === 0 &&
                    file_exists($realUserfilesPath) &&
                    is_file($realUserfilesPath)) {
                    $backupFilePath = $realUserfilesPath;
                    error_log("WordPress Restore: Found backup in userfiles directory: $backupFilePath");
                }
            }

            // If not available locally or in userfiles, try to download from remote storage
            if (!$backupFilePath && $backupData['upload_status'] === 'uploaded') {
                require_once 'BackupTarget.php';
                require_once 'BackupStorage.php';

                $target = new \WHPBackup\BackupTarget($db, $backupData['target_id']);
                $storage = new \WHPBackup\BackupStorage($target->getCredentials());

                // Download to temporary location with sanitized filename
                $baseFilename = basename($backupData['backup_path']);
                // Additional sanitization: remove null bytes, path traversal attempts, and dangerous characters
                $sanitizedFilename = str_replace(["\0", '/', '\\', '..'], '', $baseFilename);
                $sanitizedFilename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $sanitizedFilename);
                // Ensure filename is not empty after sanitization
                if (empty($sanitizedFilename)) {
                    $sanitizedFilename = 'backup_' . uniqid() . '.tar.gz';
                }
                $tempFile = '/tmp/wp_restore_' . uniqid() . '_' . $sanitizedFilename;
                $downloadResult = $storage->downloadFile($backupData['backup_path'], $tempFile);

                if ($downloadResult['success']) {
                    $backupFilePath = $tempFile;
                } else {
                    return ['success' => false, 'error' => 'Failed to download backup from remote storage'];
                }
            }

            if (!$backupFilePath) {
                return ['success' => false, 'error' => 'Backup file not available'];
            }

            // Get site path
            $sitePath = "/docker/users/$user/$domain/public_html";
            $validatedPath = $this->validatePath($sitePath);

            // Extract backup to temporary directory with secure permissions
            $tempDir = '/tmp/wp_restore_extract_' . uniqid();
            if (!mkdir($tempDir, 0700, true)) {
                return ['success' => false, 'error' => 'Failed to create temporary directory'];
            }

            // Determine extraction method.
            //
            // BackupEngine produces a gzipped SQL dump for backup_type='database'
            // and a .tar.gz archive for everything else (wordpress/site/userfiles),
            // so backup_type is the authoritative signal — file detection alone
            // is unreliable because `file -b` on a .tar.gz returns "gzip
            // compressed data" without peeking inside, which used to cause
            // .tar.gz archives to be misclassified as gzipped SQL and then
            // "imported" as binary garbage into MySQL while the restore reported
            // success. We still run `file -bz` for diagnostic logs.
            $fileTypeCmd = "file -bz " . escapeshellarg($backupFilePath);
            \exec($fileTypeCmd, $fileTypeOutput, $fileTypeCode);
            $fileType = implode(' ', $fileTypeOutput);

            $isTarArchive = (stripos($fileType, 'tar archive') !== false ||
                           stripos($fileType, 'POSIX tar') !== false);
            $isGzippedData = (stripos($fileType, 'gzip compressed data') !== false);

            error_log("WordPress Restore: File type detected (file -bz): $fileType");
            error_log("WordPress Restore: Is tar archive: " . ($isTarArchive ? 'yes' : 'no') .
                     ", Is gzipped: " . ($isGzippedData ? 'yes' : 'no') .
                     ", Backup type: {$backupData['backup_type']}");

            $isDatabaseBackup = ($backupData['backup_type'] === 'database');
            if ($isDatabaseBackup && $isTarArchive) {
                error_log("WordPress Restore: WARNING — backup_type=database but file looks like a tar archive; trusting backup_type");
            }

            // Database backups: gzipped SQL dump → gunzip to .sql.
            // All other types: tar.gz → tar -xzf.
            if ($isDatabaseBackup) {
                // This is a gzipped SQL file, not a tar archive
                $sqlFilename = basename($backupFilePath, '.gz');
                if (!str_ends_with($sqlFilename, '.sql')) {
                    $sqlFilename .= '.sql';
                }
                $sqlPath = $tempDir . '/' . $sqlFilename;

                $decompressCmd = "gunzip -c " . escapeshellarg($backupFilePath) . " > " . escapeshellarg($sqlPath) . " 2>&1";
                exec($decompressCmd, $extractOutput, $extractCode);

                if ($extractCode !== 0 || !file_exists($sqlPath)) {
                    exec("rm -rf " . escapeshellarg($tempDir));
                    return ['success' => false, 'error' => 'Failed to decompress database backup: ' . implode("\n", $extractOutput)];
                }

                error_log("WordPress Restore: Decompressed database backup to: $sqlPath");
            } else {
                // For tar archives (site, wordpress, userfiles backups)
                $extractCmd = "cd " . escapeshellarg($tempDir) . " && tar -xzf " . escapeshellarg($backupFilePath) . " 2>&1";
                exec($extractCmd, $extractOutput, $extractCode);

                if ($extractCode !== 0) {
                    // Cleanup
                    exec("rm -rf " . escapeshellarg($tempDir));
                    return ['success' => false, 'error' => 'Failed to extract backup: ' . implode("\n", $extractOutput)];
                }
            }

            // Detect what's in the backup
            $wpDir = $this->findWordPressDirectory($tempDir);
            $sqlFiles = glob($tempDir . '/*.sql');
            if (empty($sqlFiles)) {
                // Check in subdirectories
                $sqlFiles = glob($tempDir . '/*/*.sql');
            }
            $hasSqlFile = !empty($sqlFiles);

            // Determine backup type by examining contents
            $hasWordPressFiles = ($wpDir !== null);
            $hasDatabase = $hasSqlFile;

            // Validate that we have something to restore
            if (!$hasWordPressFiles && !$hasDatabase) {
                exec("rm -rf " . escapeshellarg($tempDir));
                return ['success' => false, 'error' => 'Backup contains neither WordPress files nor database'];
            }

            // Route to appropriate restore method based on backup contents
            $result = null;

            if ($hasWordPressFiles && $hasDatabase) {
                // Full backup - restore both files and database
                error_log("WordPress Restore: Full backup detected (files + database)");

                // Stop any running processes that might have files open
                exec("docker exec " . escapeshellarg($containerName) . " sh -c 'pkill -f \"$domain\" 2>/dev/null || true'");

                // Create backup of current state (safety measure)
                $currentBackupDir = '/tmp/wp_current_backup_' . uniqid();
                if (file_exists($validatedPath)) {
                    exec("cp -a " . escapeshellarg($validatedPath) . " " . escapeshellarg($currentBackupDir) . " 2>&1", $output, $code);
                }

                // Remove current WordPress installation
                if (file_exists($validatedPath)) {
                    exec("rm -rf " . escapeshellarg($validatedPath) . "/* 2>&1", $output, $code);
                    if ($code !== 0) {
                        exec("rm -rf " . escapeshellarg($tempDir));
                        return ['success' => false, 'error' => 'Failed to remove current installation'];
                    }
                }

                // Copy restored files to site directory
                $copyCmd = "cp -a " . escapeshellarg($wpDir) . "/. " . escapeshellarg($validatedPath) . "/ 2>&1";
                exec($copyCmd, $copyOutput, $copyCode);

                if ($copyCode !== 0) {
                    // Try to restore from backup if available
                    if (isset($currentBackupDir) && file_exists($currentBackupDir)) {
                        exec("cp -a " . escapeshellarg($currentBackupDir) . "/. " . escapeshellarg($validatedPath) . "/");
                    }
                    exec("rm -rf " . escapeshellarg($tempDir));
                    return ['success' => false, 'error' => 'Failed to restore files: ' . implode("\n", $copyOutput)];
                }

                // Fix permissions
                exec("chown -R $user:$user " . escapeshellarg($validatedPath) . " 2>&1");
                exec("chmod -R 755 " . escapeshellarg($validatedPath) . " 2>&1");

                // Restore database. Note: we deliberately do NOT pass -h with the
                // host from wp-config.php — that value is the in-container Docker
                // network name (e.g. 'mysql') and does not resolve from the host.
                // /root/.my.cnf already supplies a host that works from the host
                // process; matches the working pattern at cloneDatabase().
                $sqlFile = !empty($sqlFiles) ? $sqlFiles[0] : null;
                $dbRestoreError = null;
                if ($sqlFile && file_exists($sqlFile)) {
                    $wpConfigPath = $validatedPath . '/wp-config.php';
                    if (!file_exists($wpConfigPath)) {
                        $dbRestoreError = 'wp-config.php missing after file restore — cannot import database';
                    } else {
                        $dbInfo = $this->extractDatabaseInfo($wpConfigPath);
                        if (!$dbInfo) {
                            $dbRestoreError = 'Could not parse database credentials from wp-config.php';
                        } else {
                            $importCmd = sprintf(
                                "mysql --defaults-extra-file=/root/.my.cnf %s < %s 2>&1",
                                escapeshellarg($dbInfo['name']),
                                escapeshellarg($sqlFile)
                            );
                            exec($importCmd, $dbOutput, $dbCode);

                            if ($dbCode !== 0) {
                                $dbRestoreError = trim(implode("\n", $dbOutput));
                                error_log("Database restore failed (rc=$dbCode): $dbRestoreError");
                            }
                        }
                    }
                }
                if ($dbRestoreError !== null) {
                    exec("rm -rf " . escapeshellarg($tempDir));
                    if (isset($currentBackupDir) && file_exists($currentBackupDir)) {
                        exec("rm -rf " . escapeshellarg($currentBackupDir));
                    }
                    return [
                        'success' => false,
                        'error'   => "Files restored but database import failed: $dbRestoreError",
                    ];
                }

                // Cleanup
                exec("rm -rf " . escapeshellarg($tempDir));
                if (isset($currentBackupDir) && file_exists($currentBackupDir)) {
                    exec("rm -rf " . escapeshellarg($currentBackupDir));
                }

                $result = ['success' => true, 'message' => 'WordPress restored successfully (files and database)'];

            } elseif ($hasWordPressFiles && !$hasDatabase) {
                // Files-only backup
                error_log("WordPress Restore: Files-only backup detected");

                // Stop any running processes
                exec("docker exec " . escapeshellarg($containerName) . " sh -c 'pkill -f \"$domain\" 2>/dev/null || true'");

                // Create backup of current state
                $currentBackupDir = '/tmp/wp_current_backup_' . uniqid();
                if (file_exists($validatedPath)) {
                    exec("cp -a " . escapeshellarg($validatedPath) . " " . escapeshellarg($currentBackupDir) . " 2>&1");
                }

                // Remove current files
                if (file_exists($validatedPath)) {
                    exec("rm -rf " . escapeshellarg($validatedPath) . "/* 2>&1", $output, $code);
                    if ($code !== 0) {
                        exec("rm -rf " . escapeshellarg($tempDir));
                        return ['success' => false, 'error' => 'Failed to remove current files'];
                    }
                }

                // Copy restored files
                $copyCmd = "cp -a " . escapeshellarg($wpDir) . "/. " . escapeshellarg($validatedPath) . "/ 2>&1";
                exec($copyCmd, $copyOutput, $copyCode);

                if ($copyCode !== 0) {
                    // Try to restore from backup
                    if (isset($currentBackupDir) && file_exists($currentBackupDir)) {
                        exec("cp -a " . escapeshellarg($currentBackupDir) . "/. " . escapeshellarg($validatedPath) . "/");
                    }
                    exec("rm -rf " . escapeshellarg($tempDir));
                    return ['success' => false, 'error' => 'Failed to restore files: ' . implode("\n", $copyOutput)];
                }

                // Fix permissions
                exec("chown -R $user:$user " . escapeshellarg($validatedPath) . " 2>&1");
                exec("chmod -R 755 " . escapeshellarg($validatedPath) . " 2>&1");

                // Cleanup
                exec("rm -rf " . escapeshellarg($tempDir));
                if (isset($currentBackupDir) && file_exists($currentBackupDir)) {
                    exec("rm -rf " . escapeshellarg($currentBackupDir));
                }

                $result = ['success' => true, 'message' => 'WordPress files restored successfully (database unchanged)'];

            } elseif (!$hasWordPressFiles && $hasDatabase) {
                // Database-only backup
                error_log("WordPress Restore: Database-only backup detected");

                // Get database credentials from existing wp-config.php
                $wpConfigPath = $validatedPath . '/wp-config.php';
                if (!file_exists($wpConfigPath)) {
                    exec("rm -rf " . escapeshellarg($tempDir));
                    return ['success' => false, 'error' => 'WordPress installation not found - cannot restore database-only backup'];
                }

                $dbInfo = $this->extractDatabaseInfo($wpConfigPath);
                if (!$dbInfo) {
                    exec("rm -rf " . escapeshellarg($tempDir));
                    return ['success' => false, 'error' => 'Failed to extract database credentials from wp-config.php'];
                }

                // Find and import SQL file
                $sqlFile = !empty($sqlFiles) ? $sqlFiles[0] : null;
                if (!$sqlFile || !file_exists($sqlFile)) {
                    exec("rm -rf " . escapeshellarg($tempDir));
                    return ['success' => false, 'error' => 'Database file not found in backup'];
                }

                // Import database. Do not pass -h from wp-config.php — its value
                // ('mysql') is a Docker network name unreachable from the host;
                // /root/.my.cnf provides a host-side endpoint.
                $importCmd = sprintf(
                    "mysql --defaults-extra-file=/root/.my.cnf %s < %s 2>&1",
                    escapeshellarg($dbInfo['name']),
                    escapeshellarg($sqlFile)
                );
                exec($importCmd, $dbOutput, $dbCode);

                // Cleanup
                exec("rm -rf " . escapeshellarg($tempDir));

                if ($dbCode !== 0) {
                    $dbErr = trim(implode("\n", $dbOutput));
                    error_log("Database restore failed (rc=$dbCode): $dbErr");
                    $result = ['success' => false, 'error' => "Database import failed: $dbErr"];
                } else {
                    $result = ['success' => true, 'message' => 'Database restored successfully (files unchanged)'];
                }
            }

            // If we downloaded a temp file, clean it up
            if (isset($tempFile) && file_exists($tempFile)) {
                unlink($tempFile);
            }

            return $result;

        } catch (Exception $e) {
            error_log("WordPress restore error: " . $e->getMessage());
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
    }

    /**
     * Find WordPress directory in extracted backup
     */
    private function findWordPressDirectory($baseDir) {
        if (file_exists($baseDir . '/wp-config.php')) {
            return $baseDir;
        }

        $commonDirs = ['public_html', 'htdocs', 'www', 'wordpress'];
        foreach ($commonDirs as $dir) {
            $path = $baseDir . '/' . $dir;
            if (file_exists($path . '/wp-config.php')) {
                return $path;
            }
        }

        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($baseDir, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::SELF_FIRST
        );
        $iterator->setMaxDepth(5);

        foreach ($iterator as $file) {
            if ($file->isFile() && $file->getFilename() === 'wp-config.php') {
                return dirname($file->getPathname());
            }
        }

        return null;
    }

    /**
     * Extract database information from wp-config.php
     */
    private function extractDatabaseInfo($wpConfigPath) {
        if (!file_exists($wpConfigPath)) {
            return null;
        }

        $content = file_get_contents($wpConfigPath);
        $dbInfo = [];

        if (preg_match("/define\s*\(\s*['\"]DB_NAME['\"]\s*,\s*['\"]([^'\"]+)['\"]\s*\)/", $content, $matches)) {
            $dbInfo['name'] = $matches[1];
        }
        if (preg_match("/define\s*\(\s*['\"]DB_USER['\"]\s*,\s*['\"]([^'\"]+)['\"]\s*\)/", $content, $matches)) {
            $dbInfo['user'] = $matches[1];
        }
        if (preg_match("/define\s*\(\s*['\"]DB_PASSWORD['\"]\s*,\s*['\"]([^'\"]+)['\"]\s*\)/", $content, $matches)) {
            $dbInfo['password'] = $matches[1];
        }
        if (preg_match("/define\s*\(\s*['\"]DB_HOST['\"]\s*,\s*['\"]([^'\"]+)['\"]\s*\)/", $content, $matches)) {
            $dbInfo['host'] = $matches[1];
        }

        if (isset($dbInfo['name']) && isset($dbInfo['user']) && isset($dbInfo['password']) && isset($dbInfo['host'])) {
            return $dbInfo;
        }

        return null;
    }

    /**
     * Restore database-only backup
     * @param string $containerName Container name
     * @param string $user Username
     * @param string $domain Domain name
     * @param array $backupData Backup data
     * @param PDO $db Database connection
     * @return array Result
     */
    private function restoreDatabaseBackup($containerName, $user, $domain, $backupData, $db) {
        try {
            // Get backup file path
            $fileResult = $this->getBackupFilePath($backupData, $db);
            if (!$fileResult['success']) {
                return $fileResult;
            }

            $backupFilePath = $fileResult['path'];
            $isTempFile = $fileResult['temp'] ?? false;

            // Get site path to find wp-config.php
            $sitePath = "/docker/users/$user/$domain/public_html";
            $validatedPath = $this->validatePath($sitePath);
            $wpConfigPath = $validatedPath . '/wp-config.php';

            if (!file_exists($wpConfigPath)) {
                if ($isTempFile && file_exists($backupFilePath)) {
                    unlink($backupFilePath);
                }
                return ['success' => false, 'error' => 'WordPress installation not found at this location'];
            }

            // Get database credentials
            $dbInfo = $this->extractDatabaseInfo($wpConfigPath);
            if (!$dbInfo) {
                if ($isTempFile && file_exists($backupFilePath)) {
                    unlink($backupFilePath);
                }
                return ['success' => false, 'error' => 'Failed to extract database credentials from wp-config.php'];
            }

            // Extract and restore database
            $tempDir = '/tmp/db_restore_' . uniqid();
            mkdir($tempDir, 0755, true);

            $extractCmd = "cd " . escapeshellarg($tempDir) . " && tar -xzf " . escapeshellarg($backupFilePath) . " 2>&1";
            exec($extractCmd, $output, $code);

            if ($code !== 0) {
                exec("rm -rf " . escapeshellarg($tempDir));
                if ($isTempFile && file_exists($backupFilePath)) {
                    unlink($backupFilePath);
                }
                return ['success' => false, 'error' => 'Failed to extract backup'];
            }

            // Find SQL file
            $sqlFile = $this->findSQLFile($tempDir);
            if (!$sqlFile) {
                exec("rm -rf " . escapeshellarg($tempDir));
                if ($isTempFile && file_exists($backupFilePath)) {
                    unlink($backupFilePath);
                }
                return ['success' => false, 'error' => 'Database file not found in backup'];
            }

            // Import database
            // Override host to 127.0.0.1 since we're running from host server, not container
            // The wp-config.php will have 'mysql' as host (container name), but we need localhost
            $mysqlHost = '127.0.0.1';

            $importCmd = sprintf(
                "mysql -h %s -u %s -p%s %s < %s 2>&1",
                escapeshellarg($mysqlHost),
                escapeshellarg($dbInfo['user']),
                escapeshellarg($dbInfo['password']),
                escapeshellarg($dbInfo['name']),
                escapeshellarg($sqlFile)
            );
            exec($importCmd, $dbOutput, $dbCode);

            // Cleanup
            exec("rm -rf " . escapeshellarg($tempDir));
            if ($isTempFile && file_exists($backupFilePath)) {
                unlink($backupFilePath);
            }

            if ($dbCode !== 0) {
                error_log("Database restore warnings: " . implode("\n", $dbOutput));
                return ['success' => true, 'message' => 'Database restored with warnings - check error log'];
            }

            return ['success' => true, 'message' => 'Database restored successfully'];

        } catch (Exception $e) {
            error_log("Database restore error: " . $e->getMessage());
            return ['success' => false, 'error' => $e->getMessage()];
        }
    }

    /**
     * Restore files-only backup
     * @param string $containerName Container name
     * @param string $user Username
     * @param string $domain Domain name
     * @param array $backupData Backup data
     * @param PDO $db Database connection
     * @return array Result
     */
    private function restoreFilesBackup($containerName, $user, $domain, $backupData, $db) {
        try {
            // Get backup file path
            $fileResult = $this->getBackupFilePath($backupData, $db);
            if (!$fileResult['success']) {
                return $fileResult;
            }

            $backupFilePath = $fileResult['path'];
            $isTempFile = $fileResult['temp'] ?? false;

            // Get site path
            $sitePath = "/docker/users/$user/$domain/public_html";
            $validatedPath = $this->validatePath($sitePath);

            // Extract backup to temporary directory
            $tempDir = '/tmp/files_restore_' . uniqid();
            mkdir($tempDir, 0755, true);

            $extractCmd = "cd " . escapeshellarg($tempDir) . " && tar -xzf " . escapeshellarg($backupFilePath) . " 2>&1";
            exec($extractCmd, $extractOutput, $extractCode);

            if ($extractCode !== 0) {
                exec("rm -rf " . escapeshellarg($tempDir));
                if ($isTempFile && file_exists($backupFilePath)) {
                    unlink($backupFilePath);
                }
                return ['success' => false, 'error' => 'Failed to extract backup'];
            }

            // Find the WordPress directory in the extracted backup
            $wpDir = $this->findWordPressDirectory($tempDir);
            if (!$wpDir) {
                exec("rm -rf " . escapeshellarg($tempDir));
                if ($isTempFile && file_exists($backupFilePath)) {
                    unlink($backupFilePath);
                }
                return ['success' => false, 'error' => 'WordPress files not found in backup'];
            }

            // Create backup of current state
            $currentBackupDir = '/tmp/wp_current_backup_' . uniqid();
            if (file_exists($validatedPath)) {
                exec("cp -a " . escapeshellarg($validatedPath) . " " . escapeshellarg($currentBackupDir) . " 2>&1");
            }

            // Remove current files
            if (file_exists($validatedPath)) {
                exec("rm -rf " . escapeshellarg($validatedPath) . "/* 2>&1", $output, $code);
                if ($code !== 0) {
                    exec("rm -rf " . escapeshellarg($tempDir));
                    if ($isTempFile && file_exists($backupFilePath)) {
                        unlink($backupFilePath);
                    }
                    return ['success' => false, 'error' => 'Failed to remove current files'];
                }
            }

            // Copy restored files
            $copyCmd = "cp -a " . escapeshellarg($wpDir) . "/. " . escapeshellarg($validatedPath) . "/ 2>&1";
            exec($copyCmd, $copyOutput, $copyCode);

            if ($copyCode !== 0) {
                // Try to restore from backup
                if (isset($currentBackupDir) && file_exists($currentBackupDir)) {
                    exec("cp -a " . escapeshellarg($currentBackupDir) . "/. " . escapeshellarg($validatedPath) . "/");
                }
                exec("rm -rf " . escapeshellarg($tempDir));
                if ($isTempFile && file_exists($backupFilePath)) {
                    unlink($backupFilePath);
                }
                return ['success' => false, 'error' => 'Failed to restore files'];
            }

            // Fix permissions
            exec("chown -R $user:$user " . escapeshellarg($validatedPath) . " 2>&1");
            exec("chmod -R 755 " . escapeshellarg($validatedPath) . " 2>&1");

            // Cleanup
            exec("rm -rf " . escapeshellarg($tempDir));
            if (isset($currentBackupDir) && file_exists($currentBackupDir)) {
                exec("rm -rf " . escapeshellarg($currentBackupDir));
            }
            if ($isTempFile && file_exists($backupFilePath)) {
                unlink($backupFilePath);
            }

            return ['success' => true, 'message' => 'Files restored successfully'];

        } catch (Exception $e) {
            error_log("Files restore error: " . $e->getMessage());
            return ['success' => false, 'error' => $e->getMessage()];
        }
    }

    /**
     * Restore full backup (original method refactored)
     * @param string $containerName Container name
     * @param string $user Username
     * @param string $domain Domain name
     * @param array $backupData Backup data
     * @param PDO $db Database connection
     * @return array Result
     */
    private function restoreFullBackup($containerName, $user, $domain, $backupData, $db) {
        try {
            // Get backup file path
            $fileResult = $this->getBackupFilePath($backupData, $db);
            if (!$fileResult['success']) {
                return $fileResult;
            }

            $backupFilePath = $fileResult['path'];
            $isTempFile = $fileResult['temp'] ?? false;

            // Get site path
            $sitePath = "/docker/users/$user/$domain/public_html";
            $validatedPath = $this->validatePath($sitePath);

            // Extract backup to temporary directory
            $tempDir = '/tmp/wp_restore_extract_' . uniqid();
            if (!mkdir($tempDir, 0755, true)) {
                if ($isTempFile && file_exists($backupFilePath)) {
                    unlink($backupFilePath);
                }
                return ['success' => false, 'error' => 'Failed to create temporary directory'];
            }

            // Extract the backup
            $extractCmd = "cd " . escapeshellarg($tempDir) . " && tar -xzf " . escapeshellarg($backupFilePath) . " 2>&1";
            exec($extractCmd, $extractOutput, $extractCode);

            if ($extractCode !== 0) {
                // Cleanup
                exec("rm -rf " . escapeshellarg($tempDir));
                if ($isTempFile && file_exists($backupFilePath)) {
                    unlink($backupFilePath);
                }
                return ['success' => false, 'error' => 'Failed to extract backup: ' . implode("\n", $extractOutput)];
            }

            // Find the WordPress directory in the extracted backup
            $wpDir = $this->findWordPressDirectory($tempDir);
            if (!$wpDir) {
                exec("rm -rf " . escapeshellarg($tempDir));
                if ($isTempFile && file_exists($backupFilePath)) {
                    unlink($backupFilePath);
                }
                return ['success' => false, 'error' => 'WordPress installation not found in backup'];
            }

            // Stop any running processes that might have files open
            exec("docker exec " . escapeshellarg($containerName) . " sh -c 'pkill -f \"$domain\" 2>/dev/null || true'");

            // Create backup of current state (safety measure)
            $currentBackupDir = '/tmp/wp_current_backup_' . uniqid();
            if (file_exists($validatedPath)) {
                exec("cp -a " . escapeshellarg($validatedPath) . " " . escapeshellarg($currentBackupDir) . " 2>&1", $output, $code);
            }

            // Remove current WordPress installation
            if (file_exists($validatedPath)) {
                exec("rm -rf " . escapeshellarg($validatedPath) . "/* 2>&1", $output, $code);
                if ($code !== 0) {
                    exec("rm -rf " . escapeshellarg($tempDir));
                    if ($isTempFile && file_exists($backupFilePath)) {
                        unlink($backupFilePath);
                    }
                    return ['success' => false, 'error' => 'Failed to remove current installation'];
                }
            }

            // Copy restored files to site directory
            $copyCmd = "cp -a " . escapeshellarg($wpDir) . "/. " . escapeshellarg($validatedPath) . "/ 2>&1";
            exec($copyCmd, $copyOutput, $copyCode);

            if ($copyCode !== 0) {
                // Try to restore from backup if available
                if (isset($currentBackupDir) && file_exists($currentBackupDir)) {
                    exec("cp -a " . escapeshellarg($currentBackupDir) . "/. " . escapeshellarg($validatedPath) . "/");
                }
                exec("rm -rf " . escapeshellarg($tempDir));
                if ($isTempFile && file_exists($backupFilePath)) {
                    unlink($backupFilePath);
                }
                return ['success' => false, 'error' => 'Failed to restore files: ' . implode("\n", $copyOutput)];
            }

            // Fix permissions
            exec("chown -R $user:$user " . escapeshellarg($validatedPath) . " 2>&1");
            exec("chmod -R 755 " . escapeshellarg($validatedPath) . " 2>&1");

            // Restore database if included in backup
            $sqlFile = $this->findSQLFile($tempDir);
            if ($sqlFile) {
                // Get database credentials from wp-config.php
                $wpConfigPath = $validatedPath . '/wp-config.php';
                if (file_exists($wpConfigPath)) {
                    $dbInfo = $this->extractDatabaseInfo($wpConfigPath);

                    if ($dbInfo) {
                        // Import database
                        // Override host to 127.0.0.1 since we're running from host server, not container
                        $mysqlHost = '127.0.0.1';

                        $importCmd = sprintf(
                            "mysql -h %s -u %s -p%s %s < %s 2>&1",
                            escapeshellarg($mysqlHost),
                            escapeshellarg($dbInfo['user']),
                            escapeshellarg($dbInfo['password']),
                            escapeshellarg($dbInfo['name']),
                            escapeshellarg($sqlFile)
                        );
                        exec($importCmd, $dbOutput, $dbCode);

                        if ($dbCode !== 0) {
                            error_log("Database restore had warnings: " . implode("\n", $dbOutput));
                        }
                    }
                }
            }

            // Cleanup
            exec("rm -rf " . escapeshellarg($tempDir));
            if (isset($currentBackupDir) && file_exists($currentBackupDir)) {
                exec("rm -rf " . escapeshellarg($currentBackupDir));
            }
            if ($isTempFile && file_exists($backupFilePath)) {
                unlink($backupFilePath);
            }

            return [
                'success' => true,
                'message' => 'WordPress restored successfully from backup'
            ];

        } catch (Exception $e) {
            error_log("WordPress restore error: " . $e->getMessage());
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
    }

    /**
     * Get backup file path (download from remote if necessary)
     * @param array $backupData Backup data
     * @param PDO $db Database connection
     * @return array Result with 'success', 'path', and optional 'temp' flag
     */
    private function getBackupFilePath($backupData, $db) {
        // Check local storage first
        if (!empty($backupData['local_path']) && $backupData['local_deleted_at'] === null) {
            if (file_exists($backupData['local_path'])) {
                return ['success' => true, 'path' => $backupData['local_path'], 'temp' => false];
            }
        }

        // If not available locally, try to download from remote storage
        if ($backupData['upload_status'] === 'uploaded') {
            require_once 'BackupTarget.php';
            require_once 'BackupStorage.php';

            $target = new \WHPBackup\BackupTarget($db, $backupData['target_id']);
            $storage = new \WHPBackup\BackupStorage($target->getCredentials());

            // Download to temporary location
            $tempFile = '/tmp/wp_restore_' . uniqid() . '_' . basename($backupData['backup_path']);
            $downloadResult = $storage->downloadFile($backupData['backup_path'], $tempFile);

            if ($downloadResult['success']) {
                return ['success' => true, 'path' => $tempFile, 'temp' => true];
            } else {
                return ['success' => false, 'error' => 'Failed to download backup from remote storage'];
            }
        }

        return ['success' => false, 'error' => 'Backup file not available'];
    }

    /**
     * Find SQL file in extracted backup directory
     * @param string $dir Directory to search
     * @return string|null Path to SQL file or null
     */
    private function findSQLFile($dir) {
        // Check common locations
        $commonNames = ['database.sql', 'backup.sql', 'dump.sql', 'mysql.sql'];
        foreach ($commonNames as $name) {
            $path = $dir . '/' . $name;
            if (file_exists($path)) {
                return $path;
            }
        }

        // Search recursively
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
            RecursiveIteratorIterator::SELF_FIRST
        );
        $iterator->setMaxDepth(3);

        foreach ($iterator as $file) {
            if ($file->isFile() && pathinfo($file->getFilename(), PATHINFO_EXTENSION) === 'sql') {
                return $file->getPathname();
            }
        }

        return null;
    }
}