<?php
error_reporting(E_ALL);
ini_set('display_errors', '1');

if (version_compare(PHP_VERSION, '8.3.0', '<')) {
    header('HTTP/1.1 500 Internal Server Error');
    echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>PHP 版本过低</title></head><body style="display:flex;justify-content:center;align-items:center;height:100vh;margin:0;font-family:sans-serif;background:#f5f5f5"><div style="text-align:center;padding:40px;background:#fff;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,0.1)"><h1 style="color:#F53F3F">PHP 版本过低</h1><p>CmsPro 需要 PHP 8.3.0 或更高版本，当前版本：<strong>' . PHP_VERSION . '</strong></p></div></body></html>';
    exit;
}

define('ROOT_PATH', dirname(__DIR__));
define('PUBLIC_PATH', __DIR__);
define('STORAGE_PATH', ROOT_PATH . '/storage');
define('ARTISAN_PATH', ROOT_PATH . '/artisan');
define('LOCK_FILE', STORAGE_PATH . '/installed.lock');
define('LOG_PATH', STORAGE_PATH . '/logs');
define('DEBUG_LOG', PUBLIC_PATH . '/install_debug.log');
define('PROGRESS_FILE', STORAGE_PATH . '/install_progress.json');

if (!is_dir(LOG_PATH)) {
    mkdir(LOG_PATH, 0755, true);
}

// ==================== mode=worker：后台安装进程入口（必须在 lock 检查之前） ====================
$isWorker = (isset($_GET['mode']) && $_GET['mode'] === 'worker');
if (!$isWorker && in_array(PHP_SAPI, ['cli', 'phpdbg'], true)) {
    $isWorker = (isset($_SERVER['argv'][1]) && $_SERVER['argv'][1] === 'mode=worker');
}
if ($isWorker) {
    runAsWorker();
    exit;
}

// ==================== get_progress：安装进度轮询（必须在 lock 检查之前） ====================
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['get_progress'])) {
    header('Content-Type: application/json; charset=utf-8');
    header('Cache-Control: no-cache, no-store');
    if (!file_exists(PROGRESS_FILE)) {
        echo json_encode(null);
        exit;
    }
    $content = file_get_contents(PROGRESS_FILE);
    if ($content === false) { echo json_encode(null); exit; }
    $data = json_decode($content, true);
    if (!is_array($data)) { echo json_encode(null); exit; }
    if (time() - ($data['timestamp'] ?? 0) > 600) { echo json_encode(null); exit; }
    echo json_encode($data);
    exit;
}

if (file_exists(LOCK_FILE)) {
    header('HTTP/1.1 403 Forbidden');
    echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>系统已安装</title></head><body style="display:flex;justify-content:center;align-items:center;height:100vh;margin:0;font-family:sans-serif;background:#f5f5f5"><div style="text-align:center;padding:40px;background:#fff;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,0.1)"><h1 style="color:#165DFF">系统已安装</h1><p>CmsPro 系统已经安装完成，如需重新安装，请删除 <code>storage/installed.lock</code> 文件后重试。</p></div></body></html>';
    exit;
}

function log_message(string $message, string $level = 'info')
{
    $timestamp = date('Y-m-d H:i:s');
    $entry = "[{$timestamp}] [{$level}] {$message}\n";
    file_put_contents(LOG_PATH . '/install_' . date('Y-m-d') . '.log', $entry, FILE_APPEND);
    file_put_contents(DEBUG_LOG, $entry, FILE_APPEND);
}

/**
 * 写入安装进度到 JSON 文件，供前端轮询读取。
 *
 * @param array $steps     步骤列表 [[name, status, message?], ...]
 * @param int   $current   当前步骤索引（0-based）
 * @param bool  $done      安装是否完成
 * @param bool  $success   安装是否成功
 */
function writeProgress(array $steps, int $current, bool $done = false, bool $success = false): void
{
    $data = [
        'steps' => $steps,
        'current' => $current,
        'total' => count($steps),
        'done' => $done,
        'success' => $success,
        'timestamp' => time(),
    ];
    file_put_contents(PROGRESS_FILE, json_encode($data, JSON_UNESCAPED_UNICODE));
}

/**
 * 读取安装进度。
 *
 * @return array|null 进度数据，文件不存在或已过期返回 null
 */
function readProgress(): ?array
{
    if (!file_exists(PROGRESS_FILE)) {
        return null;
    }
    $content = file_get_contents(PROGRESS_FILE);
    if ($content === false) {
        return null;
    }
    $data = json_decode($content, true);
    if (!is_array($data)) {
        return null;
    }
    // 超过 600 秒视为过期（安装不会超过 10 分钟）
    if (time() - ($data['timestamp'] ?? 0) > 600) {
        return null;
    }
    return $data;
}

/**
 * 清理进度文件。
 */
function clearProgress(): void
{
    if (file_exists(PROGRESS_FILE)) {
        unlink(PROGRESS_FILE);
    }
}

function checkEnvironment()
{
    $errors = [];
    $warnings = [];
    $items = [];

    if (version_compare(PHP_VERSION, '8.3.0', '<')) {
        $errors[] = 'PHP 版本过低，需要 PHP 8.3.0 或更高版本（当前：' . PHP_VERSION . '）';
        $items[] = ['name' => 'PHP 版本', 'require' => '≥ 8.3.0', 'current' => PHP_VERSION, 'status' => 'error'];
    } else {
        $items[] = ['name' => 'PHP 版本', 'require' => '≥ 8.3.0', 'current' => PHP_VERSION, 'status' => 'success'];
    }

    $requiredExtensions = [
        'ctype' => 'Ctype 字符类型检测',
        'curl' => 'cURL HTTP 客户端',
        'fileinfo' => '文件信息检测',
        'filter' => '数据过滤',
        'gd' => 'GD 图像处理',
        'hash' => '哈希算法',
        'json' => 'JSON 解析',
        'mbstring' => '多字节字符串处理',
        'openssl' => 'OpenSSL 加密',
        'pdo' => 'PDO 数据库抽象层',
        'pdo_mysql' => 'PDO MySQL 驱动',
        'session' => 'Session 会话管理',
        'tokenizer' => 'PHP Tokenizer',
        'zip' => 'ZIP 压缩/解压',
    ];

    foreach ($requiredExtensions as $ext => $desc) {
        if (!extension_loaded($ext)) {
            $errors[] = "缺少必要扩展：{$ext}（{$desc}）";
            $items[] = ['name' => "扩展 {$ext}", 'require' => '已加载', 'current' => '未加载', 'status' => 'error'];
        } else {
            $items[] = ['name' => "扩展 {$ext}", 'require' => '已加载', 'current' => '已加载', 'status' => 'success'];
        }
    }

    // 可选扩展（缺失时警告，不阻断安装）
    $optionalExtensions = [
        'redis' => 'Redis 缓存驱动（推荐安装：可显著提升 Session/缓存/队列性能，未安装将自动降级为文件驱动）',
    ];

    foreach ($optionalExtensions as $ext => $desc) {
        if (!extension_loaded($ext)) {
            $warnings[] = "可选扩展未安装：{$ext}（{$desc}）";
            $items[] = ['name' => "扩展 {$ext}", 'require' => '建议安装', 'current' => '未安装', 'status' => 'warning'];
        } else {
            $items[] = ['name' => "扩展 {$ext}", 'require' => '建议安装', 'current' => '已安装', 'status' => 'success'];
        }
    }

    // 检测 Composer 运行必需的 PHP 函数
    $disabledFunctions = array_map('trim', explode(',', ini_get('disable_functions')));
    $disabledFunctions = array_filter($disabledFunctions);

    // 核心必用函数（缺失则 Composer/Laravel 无法运行）
    $criticalFunctions = [
        'putenv' => '环境变量设置（Composer/Laravel 必需）',
        'getenv' => '环境变量读取（Composer/Laravel 必需）',
        'file_get_contents' => '文件读取（Composer 读写依赖文件）',
        'file_put_contents' => '文件写入（Composer 生成缓存/锁文件）',
        'fopen' => '文件打开（Composer 下载/读写包文件）',
        'fread' => '文件读取（Composer 读取流数据）',
        'fwrite' => '文件写入（Composer 写入流数据）',
        'mkdir' => '创建目录（Composer 解压依赖必备）',
        'rmdir' => '删除目录（Composer 清理缓存）',
        'unlink' => '删除文件（Composer 清理临时文件）',
        'rename' => '重命名文件（Composer 原子操作）',
        'json_decode' => 'JSON 解析（Composer 解析配置文件）',
        'json_encode' => 'JSON 生成（Composer 生成锁文件）',
        'stream_socket_client' => '网络连接（Composer 访问 Packagist 仓库）',
        'gzopen' => 'GZ 解压（Composer 解压压缩包）',
        'gzdecode' => 'GZ 解码（Composer 解压 .tar.gz）',
    ];

    foreach ($criticalFunctions as $func => $desc) {
        if (in_array($func, $disabledFunctions)) {
            $errors[] = "核心函数被禁用：{$func}（{$desc}），请在 php.ini 的 disable_functions 中移除";
            $items[] = ['name' => "函数 {$func}()", 'require' => '可用', 'current' => '已禁用（' . $desc . '）', 'status' => 'error'];
        } else {
            $items[] = ['name' => "函数 {$func}()", 'require' => '可用', 'current' => '可用', 'status' => 'success'];
        }
    }

    // 强烈建议可用的函数（缺失会警告/部分功能失效）
    $recommendedFunctions = [
        'proc_open' => '进程调用（Composer 执行 git/hg/svn 拉取源码）',
        'popen' => '进程调用（Composer 备用进程通道）',
        'shell_exec' => 'Shell 执行（Composer 调用系统命令）',
        'exec' => '命令执行（Composer 执行外部程序）',
        'symlink' => '软链接（Laravel storage/public 链接）',
        'stat' => '文件状态（Composer 缓存校验）',
        'filemtime' => '修改时间（Composer 缓存判断）',
        'filesize' => '文件大小（Composer 包完整性检查）',
        'curl_exec' => 'cURL 请求（Composer 备用网络通道）',
        'tempnam' => '临时文件（Composer 缓存目录）',
        'sys_get_temp_dir' => '临时目录（Composer 缓存路径）',
        'hash' => '哈希计算（Composer 包完整性校验）',
        'md5_file' => 'MD5 校验（Composer 包文件校验）',
        'sha1_file' => 'SHA1 校验（Composer 包文件校验）',
        'fsockopen' => '网络连接（Composer 备用网络通道）',
        'parse_url' => 'URL 解析（Composer 仓库地址解析）',
        'basename' => '路径处理（Composer 文件路径解析）',
        'dirname' => '目录处理（Composer 路径解析）',
        'realpath' => '真实路径（Composer 路径规范化）',
        'Phar::mapPhar' => 'Phar 支持（Composer 本体为 phar 格式）',
    ];

    foreach ($recommendedFunctions as $func => $desc) {
        // Phar::mapPhar 是类方法，特殊处理
        if ($func === 'Phar::mapPhar') {
            $isDisabled = !class_exists('Phar') || !method_exists('Phar', 'mapPhar');
        } else {
            $isDisabled = in_array($func, $disabledFunctions);
        }
        if ($isDisabled) {
            $warnings[] = "建议函数不可用：{$func}（{$desc}），部分功能可能受限";
            $items[] = ['name' => "函数 {$func}()", 'require' => '可用', 'current' => '不可用（' . $desc . '）', 'status' => 'warning'];
        } else {
            $items[] = ['name' => "函数 {$func}()", 'require' => '可用', 'current' => '可用', 'status' => 'success'];
        }
    }

    // 检测 Composer 运行必需的 PHP 函数 --END

    $writableDirs = [
        '项目根目录（.env 写入）' => ROOT_PATH,
        'storage/' => STORAGE_PATH,
        'bootstrap/cache/' => ROOT_PATH . '/bootstrap/cache',
    ];

    foreach ($writableDirs as $label => $dir) {
        if (!is_writable($dir)) {
            $errors[] = "目录不可写：{$label}";
            $items[] = ['name' => "目录权限 {$label}", 'require' => '可写', 'current' => '不可写', 'status' => 'error'];
        } else {
            $items[] = ['name' => "目录权限 {$label}", 'require' => '可写', 'current' => '可写', 'status' => 'success'];
        }
    }

    $envPath = ROOT_PATH . '/.env';
    if (file_exists($envPath)) {
        $warnings[] = '.env 文件已存在，安装将覆盖该文件';
        $items[] = ['name' => '.env 文件', 'require' => '不存在', 'current' => '已存在（将被覆盖）', 'status' => 'warning'];
    } else {
        $items[] = ['name' => '.env 文件', 'require' => '不存在', 'current' => '不存在', 'status' => 'success'];
    }

    $vendorAutoload = ROOT_PATH . '/vendor/autoload.php';
    if (!file_exists($vendorAutoload)) {
        $errors[] = 'Composer 依赖未安装，请在服务器上执行 composer install 后重新检测';
        $items[] = ['name' => 'Composer 依赖', 'require' => '已安装', 'current' => '未安装（请在服务器执行 composer install）', 'status' => 'error'];
    } else {
        $pailProvider = ROOT_PATH . '/vendor/laravel/pail/src/PailServiceProvider.php';
        if (!file_exists($pailProvider)) {
            $errors[] = '开发依赖不完整（Pail 缺失），请在服务器上执行 composer install 后重新检测';
            $items[] = ['name' => '开发依赖(Pail)', 'require' => '已安装', 'current' => '未安装（请在服务器执行 composer install）', 'status' => 'error'];
        } else {
            $items[] = ['name' => 'Composer 依赖', 'require' => '已安装', 'current' => '已安装', 'status' => 'success'];
        }
    } // 关闭 vendorAutoload else 分支

    // 检测包自动发现缓存（composer install 后应自动生成）
        $packagesCache = ROOT_PATH . '/bootstrap/cache/packages.php';
        if (!file_exists($packagesCache)) {
            $warnings[] = 'Laravel 包自动发现缓存缺失（bootstrap/cache/packages.php），请确认 composer install 已完整执行';
            $items[] = ['name' => '包自动发现缓存', 'require' => '存在', 'current' => '缺失', 'status' => 'warning'];
        } else {
            $items[] = ['name' => '包自动发现缓存', 'require' => '存在', 'current' => '存在', 'status' => 'success'];
        }

    return ['errors' => $errors, 'warnings' => $warnings, 'items' => $items];
}

function testDatabaseConnection(string $host, string $port, string $database, string $username, string $password)
{
    try {
        $dsn = "mysql:host={$host};port={$port};dbname={$database}";
        $pdo = new PDO($dsn, $username, $password, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_TIMEOUT => 5,
        ]);
        $pdo = null;
        return true;
    } catch (PDOException $e) {
        return $e->getMessage();
    }
}

/**
 * 检查数据库是否已有数据（表或记录）
 * @return array{has_data: bool, tables: array<string,int>, table_count: int, total_rows: int}
 */
function checkDatabaseData(string $host, string $port, string $database, string $username, string $password)
{
    try {
        $dsn = "mysql:host={$host};port={$port};dbname={$database}";
        $pdo = new PDO($dsn, $username, $password, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_TIMEOUT => 5,
        ]);

        // 获取所有用户创建的表（排除系统临时表）
        $stmt = $pdo->prepare(
            "SELECT TABLE_NAME, TABLE_ROWS FROM information_schema.TABLES "
            . "WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE' "
            . "ORDER BY TABLE_NAME"
        );
        $stmt->execute([$database]);
        $tables = [];
        $totalRows = 0;

        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $tableName = $row['TABLE_NAME'];
            $rowCount = intval($row['TABLE_ROWS']);
            $tables[$tableName] = $rowCount;
            $totalRows += $rowCount;
        }

        $tableCount = count($tables);
        // 存在任何数据表即视为有数据（无论表中是否有记录）
        $hasData = $tableCount > 0;

        $pdo = null;

        return [
            'has_data' => $hasData,
            'tables' => $tables,
            'table_count' => $tableCount,
            'total_rows' => $totalRows,
        ];
    } catch (PDOException $e) {
        return [
            'has_data' => false,
            'tables' => [],
            'table_count' => 0,
            'total_rows' => 0,
            'error' => $e->getMessage(),
        ];
    }
}

function importSchemaSql(string $host, string $port, string $database, string $username, string $password, bool $cleanReinstall = false)
{
    $sqlFile = ROOT_PATH . '/public/install.sql';
    if (!file_exists($sqlFile)) {
        log_message('建表脚本文件不存在：' . $sqlFile, 'error');
        return ['success' => false, 'message' => '建表脚本文件不存在：install.sql'];
    }

    $sqlContent = file_get_contents($sqlFile);
    if ($sqlContent === false) {
        log_message('无法读取建表脚本文件', 'error');
        return ['success' => false, 'message' => '无法读取建表脚本文件'];
    }

    log_message('开始执行建表脚本，文件大小：' . strlen($sqlContent) . ' 字节');

    try {
        $dsn = "mysql:host={$host};port={$port};dbname={$database}";
        $pdo = new PDO($dsn, $username, $password, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4",
        ]);
        log_message('PDO 连接数据库成功：' . $database);

        // 清空重装模式：先删除所有现有表
        if ($cleanReinstall) {
            log_message('清空重装模式：开始批量删除所有现有表');
            $pdo->exec('SET FOREIGN_KEY_CHECKS = 0');
            $stmt = $pdo->prepare(
                "SELECT TABLE_NAME FROM information_schema.TABLES "
                . "WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE'"
            );
            $stmt->execute([$database]);
            $tablesToDrop = $stmt->fetchAll(PDO::FETCH_COLUMN);
            if (!empty($tablesToDrop)) {
                // 批量删表优化：单条 DROP TABLE 一次删除所有表
                // 原逐张删除 33 张表耗时约 10 秒，合并后 < 0.5 秒
                $quoted = array_map(fn($t) => '`' . str_replace('`', '', $t) . '`', $tablesToDrop);
                $pdo->exec('DROP TABLE IF EXISTS ' . implode(',', $quoted));
                foreach ($tablesToDrop as $table) {
                    log_message("已删除表：{$table}");
                }
            }
            $droppedCount = count($tablesToDrop);
            log_message("清空重装完成：共删除 {$droppedCount} 张表");
            $pdo->exec('SET FOREIGN_KEY_CHECKS = 1');
        }

        $sqlContent = str_replace(["\r\n", "\r"], "\n", $sqlContent);
        $parts = explode(";\n", $sqlContent);
        $total = 0;
        $executed = 0;
        $skipped = 0;

        foreach ($parts as $part) {
            $lines = explode("\n", $part);
            $sqlLines = [];
            foreach ($lines as $line) {
                $trimmed = trim($line);
                if ($trimmed === '' || preg_match('/^(--|\/\*)/', $trimmed)) {
                    continue;
                }
                $sqlLines[] = $trimmed;
            }
            $statement = implode(' ', $sqlLines);
            if (strlen($statement) < 10) {
                $skipped++;
                continue;
            }
            if (preg_match('/^(SET|USE)\s/i', $statement)) {
                try {
                    $pdo->exec($statement);
                    $skipped++;
                } catch (PDOException $ex) {
                    log_message("跳过 SET/USE 语句失败（可忽略）：" . substr($statement, 0, 80), 'warning');
                    $skipped++;
                }
                continue;
            }
            $total++;
            $preview = substr($statement, 0, 100);
            try {
                $pdo->exec($statement);
                $executed++;
                log_message("SQL[{$total}] OK: {$preview}...");
            } catch (PDOException $ex) {
                log_message("SQL[{$total}] FAIL: {$preview}... 错误：" . $ex->getMessage(), 'error');
                throw $ex;
            }
        }

        $pdo->exec('SET FOREIGN_KEY_CHECKS = 1');
        $pdo = null;

        log_message("建表脚本执行完成：共解析 {$total} 条有效SQL，全部成功，跳过 {$skipped} 条");
        return ['success' => true];
    } catch (PDOException $e) {
        log_message('建表脚本执行异常：' . $e->getMessage(), 'error');
        return ['success' => false, 'message' => $e->getMessage()];
    }
}

function generateEnvFile(array $config)
{
    $envPath = ROOT_PATH . '/.env.example';
    $content = '';

    if (file_exists($envPath)) {
        $content = file_get_contents($envPath);
    }

    // 根据用户选择决定是否使用 Redis 驱动
    $useRedis = !($config['skip_redis'] ?? false);
    $sessionDriver = $useRedis ? 'redis' : 'file';
    $cacheStore = $useRedis ? 'redis' : 'file';
    $queueConnection = $useRedis ? 'redis' : 'sync';

    // 根据 APP_URL 提取域名，生成唯一的 Session Cookie 名称
    // 避免同一主域名的多站点（HTTP/HTTPS混合）之间 Cookie 冲突导致无法登录
    $appUrlHost = parse_url($config['app_url'], PHP_URL_HOST) ?: 'localhost';
    $cookieSuffix = str_replace(['.', '-'], '_', $appUrlHost);
    $sessionCookie = 'cmspro_session_' . $cookieSuffix;

    // 从 config/app.php 读取版本号，写入 .env 供 system_version() 辅助函数使用
    $appVersion = '5.0.1';
    $appConfigPath = ROOT_PATH . '/config/app.php';
    if (file_exists($appConfigPath)) {
        $appConfigContent = file_get_contents($appConfigPath);
        if (preg_match("/'version'\s*=>\s*(?:env\s*\(\s*'[^']+'\s*,\s*)?'([^']+)'(?:\s*\))?/", $appConfigContent, $m)) {
            $appVersion = $m[1];
        }
    }

    $replacements = [
        'APP_NAME' => $config['app_name'],
        'APP_ENV' => 'production',
        'APP_DEBUG' => 'false',
        'APP_URL' => $config['app_url'],
        'APP_VERSION' => $appVersion,
        'DB_CONNECTION' => 'mysql',
        'DB_HOST' => $config['db_host'],
        'DB_PORT' => $config['db_port'],
        'DB_DATABASE' => $config['db_database'],
        'DB_USERNAME' => $config['db_username'],
        'DB_PASSWORD' => $config['db_password'],
        'REDIS_HOST' => $config['redis_host'],
        'REDIS_PASSWORD' => $config['redis_password'] ?: 'null',
        'REDIS_PORT' => $config['redis_port'],
        'CACHE_STORE' => $cacheStore,
        'SESSION_DRIVER' => $sessionDriver,
        'SESSION_COOKIE' => $sessionCookie,
        'QUEUE_CONNECTION' => $queueConnection,
        'MAIL_MAILER' => 'log',
        'LOG_CHANNEL' => 'stack',
        'LOG_LEVEL' => 'error',
    ];

    foreach ($replacements as $key => $value) {
        $quoted = '"' . addcslashes($value, '"\\$') . '"';
        $content = preg_replace(
            "/^{$key}=.*/m",
            "{$key}={$quoted}",
            $content
        );
    }

    if (empty($content)) {
        $q = function ($v) { return '"' . addcslashes($v, '"\\$') . '"'; };
        // Redis 密码为空时写 null（不带引号），Laravel 会识别为 PHP null
        $redisPass = empty($config['redis_password']) ? 'null' : $q($config['redis_password']);
        $content = "APP_NAME={$q($config['app_name'])}\n"
            . "APP_ENV=\"production\"\n"
            . "APP_KEY=\n"
            . "APP_DEBUG=\"false\"\n"
            . "APP_URL={$q($config['app_url'])}\n"
            . "APP_VERSION={$q($appVersion)}\n\n"
            . "APP_LOCALE=\"zh_CN\"\n"
            . "APP_FALLBACK_LOCALE=\"zh_CN\"\n"
            . "APP_MAINTENANCE_DRIVER=file\n\n"
            . "BCRYPT_ROUNDS=12\n\n"
            . "DB_CONNECTION=\"mysql\"\n"
            . "DB_HOST={$q($config['db_host'])}\n"
            . "DB_PORT={$q($config['db_port'])}\n"
            . "DB_DATABASE={$q($config['db_database'])}\n"
            . "DB_USERNAME={$q($config['db_username'])}\n"
            . "DB_PASSWORD={$q($config['db_password'])}\n\n"
            . "SESSION_DRIVER=\"{$sessionDriver}\"\n"
            . "SESSION_LIFETIME=120\n"
            . "SESSION_ENCRYPT=false\n"
            . "SESSION_PATH=/\n"
            . "SESSION_DOMAIN=null\n"
            . "SESSION_COOKIE=\"{$sessionCookie}\"\n\n"
            . "BROADCAST_CONNECTION=log\n"
            . "FILESYSTEM_DISK=local\n"
            . "QUEUE_CONNECTION=\"{$queueConnection}\"\n\n"
            . "CACHE_STORE=\"{$cacheStore}\"\n"
            . "CACHE_PREFIX=\"cmspro_{$config['db_database']}_\"\n\n"
            . "REDIS_CLIENT=\"phpredis\"\n"
            . "REDIS_HOST={$q($config['redis_host'])}\n"
            . "REDIS_PASSWORD={$redisPass}\n"
            . "REDIS_PORT={$q($config['redis_port'])}\n\n"
            . "MAIL_MAILER=\"log\"\n"
            . "LOG_CHANNEL=\"stack\"\n"
            . "LOG_STACK=daily\n"
            . "LOG_LEVEL=\"error\"\n\n"
            . "APP_MARKET_ENABLED=true\n"
            . "APP_MARKET_URL=\"https://v5.cmspro.cn\"\n"
            . "APP_MARKET_KEY=\n";
    }

    $envFilePath = ROOT_PATH . '/.env';
    $result = file_put_contents($envFilePath, $content);
    if ($result === false) {
        throw new \RuntimeException('无法写入 .env 文件（' . $envFilePath . '），请检查项目根目录写权限');
    }
}

/**
 * 初始化 Laravel 应用（用于内部调用 Artisan 命令，无需 shell 执行）
 */
function bootstrapLaravel()
{
    static $bootstrapped = false;
    if ($bootstrapped) {
        return;
    }

    $autoloadPath = ROOT_PATH . '/vendor/autoload.php';
    if (!file_exists($autoloadPath)) {
        throw new \RuntimeException('Composer 依赖未安装，请先执行 composer install');
    }

    // 清除可能存在的配置缓存，确保读取最新 .env
    $configCache = ROOT_PATH . '/bootstrap/cache/config.php';
    if (file_exists($configCache)) {
        unlink($configCache);
        log_message('已清除配置缓存：bootstrap/cache/config.php');
    }

    // 清除包自动发现缓存，避免已缓存的 ServiceProvider（如 Octane）在对应包未安装时导致引导失败
    // 这发生在安装包中的 bootstrap/cache/packages.php 缓存了构建环境已安装的包，但目标环境未安装时
    $packagesCache = ROOT_PATH . '/bootstrap/cache/packages.php';
    if (file_exists($packagesCache)) {
        // 检查是否存在已缓存但 vendor 中缺失的包，重新生成缓存
        $cachedPackages = require $packagesCache;
        $needRebuild = false;
        foreach (array_keys($cachedPackages) as $pkgName) {
            $vendorDir = ROOT_PATH . '/vendor/' . $pkgName;
            if (!is_dir($vendorDir)) {
                $needRebuild = true;
                log_message("检测到已缓存但未安装的包：{$pkgName}，将重新生成包自动发现缓存");
                break;
            }
        }
        if ($needRebuild) {
            unlink($packagesCache);
            log_message('已清除包自动发现缓存：bootstrap/cache/packages.php，将在 Laravel 引导时重新生成');
        }
    }

    // 清除编译后的服务提供者缓存，避免引用已缓存的 ServiceProvider（如 Octane）在对应包未安装时导致引导失败
    $servicesCache = ROOT_PATH . '/bootstrap/cache/services.php';
    if (file_exists($servicesCache)) {
        unlink($servicesCache);
        log_message('已清除服务提供者缓存：bootstrap/cache/services.php，将在 Laravel 引导时重新生成');
    }

    require $autoloadPath;
    $app = require ROOT_PATH . '/bootstrap/app.php';
    $app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
    $bootstrapped = true;
}

/**
 * 通过 Laravel Artisan 内部调用执行命令（无需 shell，不受 disable_functions 限制）
 * @param string $command 命令名，如 'db:seed'、'app:install'
 * @param array $parameters 命令参数，如 ['--force' => true] 或 ['app_id' => 'finance']
 */
function executeArtisanCommand(string $command, array $parameters = [])
{
    try {
        bootstrapLaravel();
    } catch (\Throwable $e) {
        log_message('Laravel 引导失败：' . $e->getMessage(), 'error');
        return ['output' => 'Laravel 引导失败：' . $e->getMessage(), 'return_code' => -1];
    }

    log_message('Artisan 内部调用：' . $command . ' 参数：' . json_encode($parameters, JSON_UNESCAPED_UNICODE));

    try {
        $exitCode = \Illuminate\Support\Facades\Artisan::call($command, $parameters);
        $output = \Illuminate\Support\Facades\Artisan::output();
        log_message('Artisan 返回码：' . $exitCode . ' 输出：' . substr($output, 0, 500));
        return [
            'output' => $output,
            'return_code' => $exitCode,
        ];
    } catch (\Throwable $e) {
        log_message('Artisan 调用异常：' . $e->getMessage(), 'error');
        return ['output' => $e->getMessage(), 'return_code' => -1];
    }
}

/**
 * 更新步骤状态并写入进度文件。
 *
 * @param array  $steps    步骤引用
 * @param int    $index    当前步骤索引
 * @param string $status   状态：running/success/error/warning
 * @param string $message  可选消息
 */
function updateStep(array &$steps, int $index, string $status, string $message = ''): void
{
    $steps[$index]['status'] = $status;
    if ($message !== '') {
        $steps[$index]['message'] = $message;
    }
    $done = ($status === 'error');
    $success = false;
    writeProgress($steps, $index, $done, $success);
}

function runInstallation(array $config): array
{
    $steps = [];
    $systemApps = ['versionmgr'];
    $cleanReinstall = !empty($config['clean_reinstall']);

    // 清理旧的进度文件
    clearProgress();

    log_message('========== 开始安装 CmsPro 系统 ==========');
    log_message('安装配置：DB=' . $config['db_host'] . ':' . $config['db_port'] . '/' . $config['db_database'] . ' User=' . $config['db_username'] . ' Admin=' . $config['admin_username']);

    $steps[] = ['name' => '生成 .env 配置文件', 'status' => 'running'];
    writeProgress($steps, count($steps) - 1);
    try {
        generateEnvFile($config);
        updateStep($steps, 0, 'success');
        log_message('生成 .env 配置文件成功');
    } catch (\Throwable $e) {
        updateStep($steps, 0, 'error', $e->getMessage());
        log_message('生成 .env 配置文件失败：' . $e->getMessage(), 'error');
        writeProgress($steps, 0, true, false);
        return ['success' => false, 'steps' => $steps];
    }

    $steps[] = ['name' => '生成 APP_KEY', 'status' => 'running'];
    writeProgress($steps, count($steps) - 1);
    try {
        $appKey = 'base64:' . base64_encode(random_bytes(32));
        $envPath = ROOT_PATH . '/.env';
        if (file_exists($envPath)) {
            $envContent = file_get_contents($envPath);
            $envContent = preg_replace('/^APP_KEY=.*$/m', 'APP_KEY=' . $appKey, $envContent);
            $writeResult = file_put_contents($envPath, $envContent);
            if ($writeResult === false) {
                throw new \RuntimeException('无法写入 APP_KEY 到 .env 文件，请检查文件权限');
            }
        }
        updateStep($steps, 1, 'success');
        log_message('生成 APP_KEY 成功');
    } catch (\Throwable $e) {
        updateStep($steps, 1, 'error', $e->getMessage());
        log_message('生成 APP_KEY 失败：' . $e->getMessage(), 'error');
        writeProgress($steps, 1, true, false);
        return ['success' => false, 'steps' => $steps];
    }

    // 生成 CMSPRO_SITE_KEY（站点唯一识别码，用于应用市场通讯验证）
    $steps[] = ['name' => '生成站点识别码', 'status' => 'running'];
    writeProgress($steps, count($steps) - 1);
    try {
        $siteKey = bin2hex(random_bytes(16));
        $envPath = ROOT_PATH . '/.env';
        if (file_exists($envPath)) {
            $envContent = file_get_contents($envPath);
            if (preg_match('/^CMSPRO_SITE_KEY=/m', $envContent)) {
                $envContent = preg_replace('/^CMSPRO_SITE_KEY=.*$/m', 'CMSPRO_SITE_KEY=' . $siteKey, $envContent);
            } else {
                $envContent = rtrim($envContent) . "\nCMSPRO_SITE_KEY=" . $siteKey . "\n";
            }
            $writeResult = file_put_contents($envPath, $envContent);
            if ($writeResult === false) {
                throw new \RuntimeException('无法写入 CMSPRO_SITE_KEY 到 .env 文件');
            }
        }
        updateStep($steps, 2, 'success');
        log_message('生成 CMSPRO_SITE_KEY 成功');
    } catch (\Throwable $e) {
        updateStep($steps, 2, 'error', $e->getMessage());
        log_message('生成 CMSPRO_SITE_KEY 失败：' . $e->getMessage(), 'error');
        writeProgress($steps, 2, true, false);
        return ['success' => false, 'steps' => $steps];
    }

    $steps[] = ['name' => '测试数据库连接', 'status' => 'running'];
    writeProgress($steps, count($steps) - 1);
    $dbTest = testDatabaseConnection(
        $config['db_host'],
        $config['db_port'],
        $config['db_database'],
        $config['db_username'],
        $config['db_password']
    );
    if ($dbTest === true) {
        updateStep($steps, 3, 'success');
        log_message('数据库连接测试成功');
    } else {
        updateStep($steps, 3, 'error', $dbTest);
        log_message('数据库连接测试失败：' . $dbTest, 'error');
        writeProgress($steps, 3, true, false);
        return ['success' => false, 'steps' => $steps];
    }

    $steps[] = ['name' => '初始化数据库表结构', 'status' => 'running'];
    writeProgress($steps, count($steps) - 1);
    $schemaResult = importSchemaSql(
        $config['db_host'],
        $config['db_port'],
        $config['db_database'],
        $config['db_username'],
        $config['db_password'],
        $cleanReinstall
    );
    if ($schemaResult['success']) {
        updateStep($steps, 4, 'success');
        log_message('初始化数据库表结构成功');
    } else {
        updateStep($steps, 4, 'error', $schemaResult['message']);
        log_message('初始化数据库表结构失败：' . $schemaResult['message'], 'error');
        writeProgress($steps, 4, true, false);
        return ['success' => false, 'steps' => $steps];
    }

    $steps[] = ['name' => '执行数据填充', 'status' => 'running'];
    writeProgress($steps, count($steps) - 1);
    $result = executeArtisanCommand('db:seed', ['--force' => true]);
    log_message('db:seed --force 返回码：' . $result['return_code'] . ' 输出：' . substr($result['output'], 0, 500));
    if ($result['return_code'] === 0) {
        updateStep($steps, 5, 'success');
        log_message('数据填充执行成功');
    } else {
        updateStep($steps, 5, 'error', $result['output']);
        log_message('数据填充执行失败：' . $result['output'], 'error');
        writeProgress($steps, 5, true, false);
        return ['success' => false, 'steps' => $steps];
    }

    $steps[] = ['name' => '同步数据库结构', 'status' => 'running'];
    writeProgress($steps, count($steps) - 1);
    $migrateResult = executeArtisanCommand('migrate', ['--force' => true]);
    log_message('migrate --force 返回码：' . $migrateResult['return_code'] . ' 输出：' . substr($migrateResult['output'], 0, 500));
    if ($migrateResult['return_code'] === 0) {
        updateStep($steps, 6, 'success');
        log_message('同步数据库结构成功');
    } else {
        updateStep($steps, 6, 'error', '数据库迁移失败：' . mb_substr($migrateResult['output'], 0, 200));
        log_message('同步数据库结构失败：' . $migrateResult['output'], 'error');
        writeProgress($steps, 6, true, false);
        return ['success' => false, 'steps' => $steps];
    }

    $steps[] = ['name' => '安装系统应用', 'status' => 'running'];
    writeProgress($steps, count($steps) - 1);
    $appResults = [];
    $allAppSuccess = true;
    $installPdo = null;
    foreach ($systemApps as $appId) {
        $appResult = executeArtisanCommand('app:install', ['app_id' => $appId]);
        $appResults[$appId] = $appResult;
        log_message("app:install {$appId} 返回码：{$appResult['return_code']} 输出：" . substr($appResult['output'], 0, 200));

        // 已安装（50005）视为成功，避免 db:seed 预写入导致重复安装误判
        $isAlreadyInstalled = ($appResult['return_code'] !== 0)
            && strpos($appResult['output'], '应用已安装') !== false;

        if ($appResult['return_code'] !== 0 && !$isAlreadyInstalled) {
            $allAppSuccess = false;
            log_message("系统应用 {$appId} 安装失败：" . $appResult['output'], 'warning');
        } else {
            log_message("系统应用 {$appId} 安装成功");
            try {
                if (!$installPdo) {
                    $dsn = "mysql:host={$config['db_host']};port={$config['db_port']};dbname={$config['db_database']}";
                    $installPdo = new PDO($dsn, $config['db_username'], $config['db_password'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
                }
                $installPdo->prepare("UPDATE apps SET status = 1 WHERE app_id = ?")->execute([$appId]);
                log_message("已启用应用：{$appId}");
            } catch (\Throwable $e) {
                log_message("启用应用 {$appId} 失败（可忽略）：" . $e->getMessage(), 'warning');
            }
        }
    }
    if ($allAppSuccess) {
        updateStep($steps, 7, 'success');
    } else {
        $failedApps = implode(', ', array_keys(array_filter($appResults, fn($r) => $r['return_code'] !== 0)));
        updateStep($steps, 7, 'warning', "部分应用安装失败：{$failedApps}");
    }

    $steps[] = ['name' => '标记系统应用', 'status' => 'running'];
    writeProgress($steps, count($steps) - 1);
    try {
        $dsn = "mysql:host={$config['db_host']};port={$config['db_port']};dbname={$config['db_database']}";
        $pdo = new PDO($dsn, $config['db_username'], $config['db_password'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
        $placeholders = implode(',', array_fill(0, count($systemApps), '?'));
        $sql = "UPDATE apps SET is_system = 1 WHERE app_id IN ({$placeholders})";
        log_message('标记系统应用 SQL：' . $sql);
        log_message('标记系统应用参数：' . json_encode($systemApps));
        $stmt = $pdo->prepare($sql);
        $stmt->execute(array_values($systemApps));
        $affectedRows = $stmt->rowCount();
        updateStep($steps, 8, 'success');
        log_message('标记系统应用成功，影响行数：' . $affectedRows);
    } catch (\Throwable $e) {
        updateStep($steps, 8, 'warning', '标记系统应用失败，请手动设置');
        log_message('标记系统应用失败：' . $e->getMessage(), 'warning');
    }

    $steps[] = ['name' => '初始化终端菜单', 'status' => 'running'];
    writeProgress($steps, count($steps) - 1);
    try {
        if (!$installPdo) {
            $dsn = "mysql:host={$config['db_host']};port={$config['db_port']};dbname={$config['db_database']}";
            $installPdo = new PDO($dsn, $config['db_username'], $config['db_password'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
        }

        // 初始化终端类型（确保 admin_menu_terminals 表有完整数据）
        $terminalTypes = [
            ['type' => 'admin', 'name' => '后台', 'description' => '后台管理终端', 'sort' => 1],
            ['type' => 'user', 'name' => '用户', 'description' => '前台用户管理终端', 'sort' => 2],
            ['type' => 'home', 'name' => '前端', 'description' => '官网前端导航菜单', 'sort' => 3],
        ];
        foreach ($terminalTypes as $t) {
            $exists = $installPdo->prepare("SELECT id FROM admin_menu_terminals WHERE type = ?");
            $exists->execute([$t['type']]);
            if (!$exists->fetchColumn()) {
                $stmt = $installPdo->prepare("INSERT INTO admin_menu_terminals (type, name, description, sort, status, create_time, update_time) VALUES (?, ?, ?, ?, 1, NOW(), NOW())");
                $stmt->execute([$t['type'], $t['name'], $t['description'], $t['sort']]);
                log_message("已初始化终端类型：{$t['type']}（{$t['name']}）");
            }
        }

        // 初始化用户中心（user）终端菜单
        $userParentExists = $installPdo->prepare("SELECT id FROM admin_menus WHERE terminal_type = 'user' AND parent_id = 0 AND name = '用户中心'");
        $userParentExists->execute();
        $userParentId = $userParentExists->fetchColumn();

        if (!$userParentId) {
            $installPdo->prepare("INSERT INTO admin_menus (parent_id, name, icon, path, component, permission_code, type, visible, sort, status, terminal_type, app_id, create_time, update_time) VALUES (0, '用户中心', 'layui-icon-user', '', '', '', 1, 1, 1, 1, 'user', NULL, NOW(), NOW())")->execute();
            $userParentId = $installPdo->lastInsertId();
            log_message("已创建用户中心目录菜单，ID：{$userParentId}");
        }

        // 用户中心子菜单
        $userMenus = [
            ['name' => '我的首页', 'icon' => 'layui-icon-home', 'path' => '/user/dashboard', 'sort' => 1],
            ['name' => '账号管理', 'icon' => 'layui-icon-username', 'path' => '/user/account', 'sort' => 2],
        ];
        foreach ($userMenus as $menu) {
            $exists = $installPdo->prepare("SELECT id FROM admin_menus WHERE terminal_type = 'user' AND path = ?");
            $exists->execute([$menu['path']]);
            if (!$exists->fetchColumn()) {
                $stmt = $installPdo->prepare("INSERT INTO admin_menus (parent_id, name, icon, path, component, permission_code, type, visible, sort, status, terminal_type, app_id, create_time, update_time) VALUES (?, ?, ?, ?, '', '', 2, 1, ?, 1, 'user', NULL, NOW(), NOW())");
                $stmt->execute([$userParentId, $menu['name'], $menu['icon'], $menu['path'], $menu['sort']]);
                log_message("已创建用户端菜单：{$menu['name']}");
            }
        }

        updateStep($steps, 9, 'success');
    } catch (\Throwable $e) {
        updateStep($steps, 9, 'warning', '终端菜单初始化失败（可忽略）');
        log_message('终端菜单初始化失败：' . $e->getMessage(), 'warning');
    }

    $steps[] = ['name' => '创建管理员账号', 'status' => 'running'];
    writeProgress($steps, count($steps) - 1);
    $result = executeArtisanCommand('admin:create', [
        '--username' => $config['admin_username'],
        '--email' => $config['admin_email'],
        '--password' => $config['admin_password'],
        '--name' => '超级管理员',
    ]);
    if ($result['return_code'] === 0) {
        updateStep($steps, 10, 'success');
        log_message('创建管理员账号成功');
    } else {
        updateStep($steps, 10, 'error', $result['output']);
        log_message('创建管理员账号失败：' . $result['output'], 'error');
        writeProgress($steps, 10, true, false);
        return ['success' => false, 'steps' => $steps];
    }

    $steps[] = ['name' => '生成安装锁', 'status' => 'running'];
    writeProgress($steps, count($steps) - 1);
    try {
        $lockResult = file_put_contents(LOCK_FILE, date('Y-m-d H:i:s'));
        if ($lockResult === false) {
            throw new \RuntimeException('无法写入安装锁文件（' . LOCK_FILE . '），请检查 storage 目录写权限');
        }
        updateStep($steps, 11, 'success');
        log_message('生成安装锁成功');
    } catch (\Throwable $e) {
        updateStep($steps, 11, 'error', $e->getMessage());
        log_message('生成安装锁失败：' . $e->getMessage(), 'error');
        writeProgress($steps, 11, true, false);
        return ['success' => false, 'steps' => $steps];
    }

    log_message('CmsPro 系统安装完成');
    writeProgress($steps, count($steps) - 1, true, true);

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

$envCheck = checkEnvironment();
$installResult = null;
$version = '5.0.1';

$appConfigPath = ROOT_PATH . '/config/app.php';
if (file_exists($appConfigPath)) {
    $appConfigContent = file_get_contents($appConfigPath);
    if (preg_match("/'version'\s*=>\s*(?:env\s*\(\s*'[^']+'\s*,\s*)?'([^']+)'(?:\s*\))?/", $appConfigContent, $matches)) {
        $version = $matches[1];
    }
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    log_message('收到 POST 请求，action=' . (isset($_POST['test_db']) ? 'test_db' : (isset($_POST['env_check']) ? 'env_check' : (isset($_POST['check_db_data']) ? 'check_db_data' : (isset($_POST['start_install']) ? 'start_install' : (isset($_POST['admin_username']) ? 'install' : 'unknown'))))));

    // 进度轮询接口
    if (isset($_POST['get_progress'])) {
        header('Content-Type: application/json; charset=utf-8');
        $progress = readProgress();
        echo json_encode($progress);
        exit;
    }

    // AJAX 启动安装接口
    if (isset($_POST['start_install'])) {
        header('Content-Type: application/json; charset=utf-8');

        $formErrors = [];
        if (empty($_POST['app_name'])) $formErrors[] = '站点名称不能为空';
        if (empty($_POST['app_url'])) $formErrors[] = '站点 URL 不能为空';
        if (empty($_POST['db_host'])) $formErrors[] = '数据库主机不能为空';
        if (empty($_POST['db_port'])) $formErrors[] = '数据库端口不能为空';
        if (empty($_POST['db_database'])) $formErrors[] = '数据库名称不能为空';
        if (empty($_POST['db_username'])) $formErrors[] = '数据库用户名不能为空';
        if (empty($_POST['admin_username'])) $formErrors[] = '管理员用户名不能为空';
        if (empty($_POST['admin_email'])) $formErrors[] = '管理员邮箱不能为空';
        if (!empty($_POST['admin_email']) && !filter_var($_POST['admin_email'], FILTER_VALIDATE_EMAIL)) {
            $formErrors[] = '管理员邮箱格式不正确';
        }
        if (empty($_POST['admin_password'])) $formErrors[] = '管理员密码不能为空';
        if (strlen($_POST['admin_password'] ?? '') < 6) $formErrors[] = '管理员密码至少6位';
        if (($_POST['admin_password'] ?? '') !== ($_POST['admin_password_confirm'] ?? '')) {
            $formErrors[] = '两次输入的密码不一致';
        }

        if (!empty($formErrors)) {
            echo json_encode(['success' => false, 'message' => implode('；', $formErrors)]);
            exit;
        }

        // 构建安装配置
        $installConfig = [
            'app_name' => $_POST['app_name'],
            'app_url' => rtrim(str_replace('`', '', $_POST['app_url']), '/'),
            'db_host' => $_POST['db_host'],
            'db_port' => $_POST['db_port'],
            'db_database' => $_POST['db_database'],
            'db_username' => $_POST['db_username'],
            'db_password' => $_POST['db_password'],
            'redis_host' => $_POST['redis_host'] ?? '127.0.0.1',
            'redis_password' => $_POST['redis_password'] ?? '',
            'redis_port' => $_POST['redis_port'] ?? '6379',
            'skip_redis' => !empty($_POST['skip_redis']),
            'admin_username' => $_POST['admin_username'],
            'admin_email' => $_POST['admin_email'],
            'admin_password' => $_POST['admin_password'],
            'clean_reinstall' => !empty($_POST['clean_reinstall']),
        ];

        // 将配置写入固定路径，供后台进程读取（不传 CLI 参数，规避 Windows start /B 参数传递问题）
        $configFile = STORAGE_PATH . '/install_config.json';
        file_put_contents($configFile, json_encode($installConfig, JSON_UNESCAPED_UNICODE));

        // 清理旧进度文件
        clearProgress();

        // 初始化进度文件（标记安装即将开始）
        writeProgress([['name' => '准备启动安装...', 'status' => 'running']], 0);

        // 启动后台 PHP 进程执行安装，当前请求立即返回
        // 复用 install.php?mode=worker 作为工作进程入口（不传额外参数，从固定路径读配置）
        $phpBin = PHP_BINARY ?: 'php';
        $selfPath = PUBLIC_PATH . '/install.php';

        if (DIRECTORY_SEPARATOR === '\\') {
            pclose(popen('start /B "" "' . $phpBin . '" "' . $selfPath . '" mode=worker', 'r'));
        } else {
            exec('nohup "' . $phpBin . '" "' . $selfPath . '" mode=worker > /dev/null 2>&1 &');
        }

        echo json_encode(['success' => true]);
        exit;
    }

    if (isset($_POST['test_db'])) {
        header('Content-Type: application/json; charset=utf-8');
        $dbResult = testDatabaseConnection(
            $_POST['db_host'] ?? '',
            $_POST['db_port'] ?? '3306',
            $_POST['db_database'] ?? '',
            $_POST['db_username'] ?? '',
            $_POST['db_password'] ?? ''
        );
        if ($dbResult === true) {
            echo json_encode(['success' => true, 'message' => '数据库连接成功']);
        } else {
            log_message('数据库连接测试失败：' . $dbResult, 'error');
            echo json_encode(['success' => false, 'message' => '数据库连接失败，请检查配置是否正确']);
        }
        exit;
    }

    if (isset($_POST['check_db_data'])) {
        header('Content-Type: application/json; charset=utf-8');
        $dataInfo = checkDatabaseData(
            $_POST['db_host'] ?? '',
            $_POST['db_port'] ?? '3306',
            $_POST['db_database'] ?? '',
            $_POST['db_username'] ?? '',
            $_POST['db_password'] ?? ''
        );
        echo json_encode($dataInfo);
        exit;
    }

    if (isset($_POST['test_redis'])) {
        header('Content-Type: application/json; charset=utf-8');
        $redisHost = $_POST['redis_host'] ?? '127.0.0.1';
        $redisPort = intval($_POST['redis_port'] ?? 6379);
        $redisPass = $_POST['redis_password'] ?? '';

        if (!extension_loaded('redis')) {
            echo json_encode(['success' => false, 'message' => 'PHP 未安装 Redis 扩展']);
            exit;
        }

        try {
            $redis = new \Redis();
            $connected = $redis->connect($redisHost, $redisPort, 2);
            if (!$connected) {
                echo json_encode(['success' => false, 'message' => "连接失败：无法连接到 {$redisHost}:{$redisPort}"]);
                exit;
            }
            if (!empty($redisPass)) {
                $authResult = $redis->auth($redisPass);
                if (!$authResult) {
                    $redis->close();
                    echo json_encode(['success' => false, 'message' => '认证失败：密码错误']);
                    exit;
                }
            }
            $redis->ping();
            $info = $redis->info();
            $version = $info['redis_version'] ?? 'unknown';
            $redis->close();
            echo json_encode(['success' => true, 'message' => "Redis 连接成功（{$redisHost}:{$redisPort}, v{$version})"]);
        } catch (\Throwable $e) {
            echo json_encode(['success' => false, 'message' => '连接异常：' . $e->getMessage()]);
        }
        exit;
    }

    if (isset($_POST['env_check'])) {
        header('Content-Type: application/json; charset=utf-8');
        $result = checkEnvironment();
        echo json_encode($result);
        exit;
    }

    $formErrors = [];
    if (empty($_POST['app_name'])) $formErrors[] = '站点名称不能为空';
    if (empty($_POST['app_url'])) $formErrors[] = '站点 URL 不能为空';
    if (empty($_POST['db_host'])) $formErrors[] = '数据库主机不能为空';
    if (empty($_POST['db_port'])) $formErrors[] = '数据库端口不能为空';
    if (empty($_POST['db_database'])) $formErrors[] = '数据库名称不能为空';
    if (empty($_POST['db_username'])) $formErrors[] = '数据库用户名不能为空';
    if (empty($_POST['admin_username'])) $formErrors[] = '管理员用户名不能为空';
    if (empty($_POST['admin_email'])) $formErrors[] = '管理员邮箱不能为空';
    if (!empty($_POST['admin_email']) && !filter_var($_POST['admin_email'], FILTER_VALIDATE_EMAIL)) {
        $formErrors[] = '管理员邮箱格式不正确';
    }
    if (empty($_POST['admin_password'])) $formErrors[] = '管理员密码不能为空';
    if (strlen($_POST['admin_password'] ?? '') < 6) $formErrors[] = '管理员密码至少6位';
    if (($_POST['admin_password'] ?? '') !== ($_POST['admin_password_confirm'] ?? '')) {
        $formErrors[] = '两次输入的密码不一致';
    }

    $dbPort = intval($_POST['db_port'] ?? 0);
    if ($dbPort < 1 || $dbPort > 65535) {
        $formErrors[] = '数据库端口无效（1-65535）';
    }
    $redisPort = intval($_POST['redis_port'] ?? 0);
    if ($redisPort < 1 || $redisPort > 65535) {
        $formErrors[] = 'Redis端口无效（1-65535）';
    }

    if (empty($formErrors)) {
        $installResult = runInstallation([
            'app_name' => $_POST['app_name'],
            'app_url' => rtrim(str_replace('`', '', $_POST['app_url']), '/'),
            'db_host' => $_POST['db_host'],
            'db_port' => $_POST['db_port'],
            'db_database' => $_POST['db_database'],
            'db_username' => $_POST['db_username'],
            'db_password' => $_POST['db_password'],
            'redis_host' => $_POST['redis_host'] ?? '127.0.0.1',
            'redis_password' => $_POST['redis_password'] ?? '',
            'redis_port' => $_POST['redis_port'] ?? '6379',
            'skip_redis' => !empty($_POST['skip_redis']),
            'admin_username' => $_POST['admin_username'],
            'admin_email' => $_POST['admin_email'],
            'admin_password' => $_POST['admin_password'],
            'clean_reinstall' => !empty($_POST['clean_reinstall']),
        ]);
    }
}

$installMd = '';
$installMdPath = PUBLIC_PATH . '/install.md';
if (file_exists($installMdPath)) {
    $md = file_get_contents($installMdPath);
    $md = preg_replace('/^# (.*)$/m', '<h2>$1</h2>', $md);
    $md = preg_replace('/^## (.*)$/m', '<h3>$1</h3>', $md);
    $md = preg_replace('/^### (.*)$/m', '<h4>$1</h4>', $md);
    $md = preg_replace('/^- (.*)$/m', '<li>$1</li>', $md);
    $md = preg_replace('/(\n<li>.*<\/li>)+/s', '<ul>$0</ul>', $md);
    $md = preg_replace('/\*\*(.*?)\*\*/', '<strong>$1</strong>', $md);
    $md = preg_replace_callback('/\|(.+)\|\n\|[-| :]+\|\n((?:\|.+\|\n?)+)/', function ($m) {
        $header = array_map('trim', explode('|', trim($m[1], '|')));
        $rows = explode("\n", trim($m[2]));
        $html = '<table><thead><tr>';
        foreach ($header as $h) $html .= '<th>' . $h . '</th>';
        $html .= '</tr></thead><tbody>';
        foreach ($rows as $row) {
            $cells = array_map('trim', explode('|', trim($row, '|')));
            $html .= '<tr>';
            foreach ($cells as $c) $html .= '<td>' . $c . '</td>';
            $html .= '</tr>';
        }
        $html .= '</tbody></table>';
        return $html;
    }, $md);
    $md = preg_replace('/\n{2,}/', '<br><br>', $md);
    $installMd = $md;
}

$currentStep = 1;
if ($installResult !== null) {
    $currentStep = 4;
} elseif (isset($_GET['step'])) {
    $currentStep = max(1, min(4, (int)$_GET['step']));
}

$detectedUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http')
    . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost');

$statusIcons = [
    'success' => '<span style="color:#00B42A;font-size:18px">&#10004;</span>',
    'error' => '<span style="color:#F53F3F;font-size:18px">&#10008;</span>',
    'warning' => '<span style="color:#FF7D00;font-size:18px">&#9888;</span>',
    'running' => '<span style="color:#165DFF;font-size:18px">&#9679;</span>',
];
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>CmsPro v<?php echo htmlspecialchars($version); ?> 安装向导</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#f2f3f5;color:#1d2129;line-height:1.6;display:flex;align-items:center;justify-content:center;padding:20px}
.container{width:100%;max-width:900px;background:#fff;border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,.08);overflow:hidden}
.header{background:linear-gradient(135deg,#165DFF,#0E42B4);color:#fff;padding:30px 40px;text-align:center}
.header h1{font-size:22px;font-weight:600;margin-bottom:4px}
.header p{font-size:14px;opacity:.85}
.body{padding:30px 40px}
.steps{display:flex;justify-content:space-between;margin-bottom:36px;position:relative;padding:0 10px}
.steps::before{content:'';position:absolute;top:16px;left:40px;right:40px;height:2px;background:#e5e6eb;z-index:1}
.steps::after{content:'';position:absolute;top:16px;left:40px;height:2px;background:#165DFF;z-index:2;transition:width .3s}
.step-1 .steps::after{width:0}
.step-2 .steps::after{width:calc(33.3% - 20px)}
.step-3 .steps::after{width:calc(66.6% - 20px)}
.step-4 .steps::after{width:calc(100% - 0px)}
.step-item{position:relative;z-index:3;display:flex;flex-direction:column;align-items:center;flex:1}
.step-num{width:34px;height:34px;border-radius:50%;background:#e5e6eb;color:#86909c;display:flex;align-items:center;justify-content:center;font-weight:600;font-size:14px;margin-bottom:8px;transition:all .3s}
.step-item.active .step-num{background:#165DFF;color:#fff}
.step-item.done .step-num{background:#00B42A;color:#fff}
.step-label{font-size:12px;color:#86909c;text-align:center}
.step-item.active .step-label{color:#165DFF;font-weight:500}
.step-item.done .step-label{color:#00B42A}
.panel{display:none}
.panel.active{display:block}
.section-title{font-size:16px;font-weight:600;color:#1d2129;margin-bottom:20px;padding-bottom:10px;border-bottom:2px solid #e5e6eb}
.intro-content{max-height:400px;overflow-y:auto;border:1px solid #e5e6eb;border-radius:6px;padding:20px;margin-bottom:20px;font-size:14px;line-height:1.8}
.intro-content h2{font-size:18px;margin-bottom:12px;color:#1d2129}
.intro-content h3{font-size:15px;margin:16px 0 8px;color:#272e3b}
.intro-content ul{padding-left:20px;margin-bottom:12px}
.intro-content li{margin-bottom:4px}
.intro-content table{width:100%;border-collapse:collapse;margin-bottom:12px}
.intro-content th,.intro-content td{padding:8px 12px;border:1px solid #e5e6eb;text-align:left;font-size:13px}
.intro-content th{background:#f7f8fa;font-weight:500}
.env-table{width:100%;border-collapse:collapse;margin-bottom:20px;border:1px solid #e5e6eb;border-radius:4px}
.env-table thead,.env-table tbody,.env-table tr{display:table;width:100%;table-layout:fixed}
.env-table thead{display:table;width:calc(100% - 0px)}
.env-table tbody{display:block;height:300px;overflow-y:auto}
.env-table th,.env-table td{padding:10px 14px;border:1px solid #e5e6eb;text-align:left;font-size:13px}
.env-table th{background:#f7f8fa;font-weight:500}
.env-table td.pass{color:#00B42A;font-weight:500}
.env-table td.fail{color:#F53F3F;font-weight:500}
.form-group{margin-bottom:18px}
.form-group label{display:block;font-weight:500;color:#272e3b;margin-bottom:6px;font-size:14px}
.form-group label .req{color:#F53F3F;margin-left:2px}
.form-control{width:100%;height:38px;padding:0 12px;border:1px solid #e5e6eb;border-radius:4px;font-size:14px;transition:border-color .2s;outline:none}
.form-control:focus{border-color:#165DFF;box-shadow:0 0 0 2px rgba(22,93,255,.1)}
.form-hint{font-size:12px;color:#86909c;margin-top:4px}
.form-row{display:flex;gap:16px}
.form-row .form-group{flex:1}
.form-section{margin-bottom:28px}
.form-section-title{font-size:14px;font-weight:600;color:#165DFF;margin-bottom:14px;padding-left:10px;border-left:3px solid #165DFF}
.btn{display:inline-block;padding:8px 24px;border:1px solid transparent;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s;text-align:center;text-decoration:none;line-height:1.6}
.btn-primary{background:#165DFF;color:#fff;border-color:#165DFF}
.btn-primary:hover{background:#0E42B4;border-color:#0E42B4}
.btn-primary:disabled{background:#94bfff;border-color:#94bfff;cursor:not-allowed}
.btn-default{background:#fff;color:#4e5969;border-color:#e5e6eb}
.btn-default:hover{color:#165DFF;border-color:#165DFF}
.btn-test{background:#fff;color:#165DFF;border-color:#165DFF;padding:6px 16px;font-size:13px}
.btn-test:hover{background:#f2f3ff}
.actions{display:flex;justify-content:space-between;align-items:center;margin-top:30px;padding-top:20px;border-top:1px solid #e5e6eb}
.actions-right{display:flex;gap:12px}
.result-list{list-style:none;margin-bottom:20px}
.result-item{display:flex;align-items:center;padding:12px 16px;border:1px solid #e5e6eb;border-radius:4px;margin-bottom:8px;background:#fafafa}
.result-item .icon{margin-right:12px;flex-shrink:0}
.result-item .name{flex:1;font-size:14px}
.result-item .msg{font-size:12px;color:#86909c;margin-left:12px;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.result-item.success{border-left:3px solid #00B42A}
.result-item.error{border-left:3px solid #F53F3F}
.result-item.warning{border-left:3px solid #FF7D00}
.success-box{background:rgba(0,180,42,.06);border:1px solid rgba(0,180,42,.2);border-radius:6px;padding:20px;margin-bottom:20px;text-align:center}
.success-box h3{color:#00B42A;font-size:18px;margin-bottom:8px}
.success-box p{color:#4e5969;font-size:14px}
.warning-box{background:rgba(255,125,0,.06);border:1px solid rgba(255,125,0,.2);border-radius:6px;padding:16px;margin-bottom:20px;font-size:13px;color:#7a4a00}
.link-list{display:flex;gap:16px;justify-content:center;margin-top:16px}
.link-list a{color:#165DFF;text-decoration:none;font-size:14px}
.link-list a:hover{text-decoration:underline}
.db-test-result{margin-top:8px;font-size:13px;padding:6px 10px;border-radius:4px;display:none}
.db-test-result.show{display:block}
.db-test-result.success{background:rgba(0,180,42,.06);color:#00B42A}
.db-test-result.fail{background:rgba(245,63,63,.06);color:#F53F3F}
.db-data-warning{margin-top:10px;padding:12px 14px;border-radius:6px;background:rgba(255,125,0,.04);border:1px solid rgba(255,125,0,.2)}
.db-data-warning-title{font-size:14px;font-weight:600;color:#ff7d00;margin-bottom:8px}
.db-data-warning-detail{font-size:13px;color:#86909c;margin-bottom:10px;line-height:1.6}
.db-data-checkbox{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:#4e5969;padding:8px 0;border-top:1px dashed rgba(255,125,0,.15)}
.db-data-checkbox input[type=checkbox]{width:16px;height:16px;cursor:pointer;accent-color:#165DFF}
.db-data-backup-notice{font-size:12px;color:#d46b08;background:rgba(255,125,0,.06);border-radius:4px;padding:8px 10px;margin-bottom:8px;line-height:1.6}
.overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.6);display:flex;justify-content:center;align-items:center;z-index:9999;opacity:0;visibility:hidden;transition:all .3s}
.overlay.active{opacity:1;visibility:visible}
.overlay-box{background:#fff;padding:40px;border-radius:8px;text-align:center;box-shadow:0 4px 20px rgba(0,0,0,.2)}
.spinner{border:4px solid #e5e6eb;border-top:4px solid #165DFF;border-radius:50%;width:40px;height:40px;animation:spin 1s linear infinite;margin:0 auto 16px}
@keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}
.overlay-text{font-size:15px;color:#1d2129;font-weight:500}
.progress-bar{width:100%;height:10px;background:#e5e6eb;padding:0;margin-bottom:16px;box-shadow:inset 0 1px 2px rgba(0,0,0,.1);border-radius:10px;overflow:hidden}
.progress-fill{height:100%;background:linear-gradient(90deg,#165DFF,#0E42B4);border-radius:10px;transition:width .4s ease}
.progress-info{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;font-size:13px;color:#86909c}
.progress-percent{font-weight:600;color:#165DFF}
.step-spinner{display:inline-block;width:16px;height:16px;border:2px solid #e5e6eb;border-top:2px solid #165DFF;border-radius:50%;animation:spin .8s linear infinite;vertical-align:middle}
.result-item.running{border-left:3px solid #165DFF}
.result-item.running .icon{color:#165DFF}
.install-result-area{margin-top:16px}
</style>
</head>
<body>
<div class="container step-<?php echo $currentStep; ?>">
<div class="header">
<h1>CmsPro v<?php echo htmlspecialchars($version); ?> 安装向导</h1>
<p>欢迎使用 CmsPro 企业级内容管理系统</p>
</div>
<div class="body">
<div class="steps">
<div class="step-item <?php echo $currentStep >= 1 ? ($currentStep > 1 ? 'done' : 'active') : ''; ?>">
<div class="step-num">1</div><div class="step-label">系统介绍</div>
</div>
<div class="step-item <?php echo $currentStep >= 2 ? ($currentStep > 2 ? 'done' : 'active') : ''; ?>">
<div class="step-num">2</div><div class="step-label">环境检测</div>
</div>
<div class="step-item <?php echo $currentStep >= 3 ? ($currentStep > 3 ? 'done' : 'active') : ''; ?>">
<div class="step-num">3</div><div class="step-label">安装配置</div>
</div>
<div class="step-item <?php echo $currentStep >= 4 ? 'done active' : ''; ?>">
<div class="step-num">4</div><div class="step-label">安装进度</div>
</div>
</div>

<div class="panel <?php echo $currentStep === 1 ? 'active' : ''; ?>" id="step1">
<div class="section-title">系统介绍</div>
<div class="intro-content">
<?php echo $installMd ?: '<p>欢迎使用 CmsPro 企业级内容管理系统。</p>'; ?>
</div>
<div class="actions">
<div></div>
<a href="?step=2" class="btn btn-primary">开始安装</a>
</div>
</div>

<div class="panel <?php echo $currentStep === 2 ? 'active' : ''; ?>" id="step2">
<div class="section-title">环境检测</div>
<table class="env-table" id="envCheckTable">
<thead><tr><th style=" width: 200px;">检测项目</th><th>要求</th><th>当前</th><th>状态</th></tr></thead>
<tbody>
<?php foreach ($envCheck['items'] as $item):
    $statusClass = $item['status'] === 'error' ? 'fail' : ($item['status'] === 'warning' ? 'warn' : 'pass');
    $statusText = $item['status'] === 'error' ? '&#10008; 未通过' : ($item['status'] === 'warning' ? '! 警告' : '&#10004; 通过');
?>
<tr class="env-row-<?php echo $item['status']; ?>">
<td><?php echo htmlspecialchars($item['name']); ?></td>
<td><?php echo htmlspecialchars($item['require']); ?></td>
<td><?php echo htmlspecialchars($item['current']); ?></td>
<td class="<?php echo $statusClass; ?>">
<?php echo $statusText; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if (!empty($envCheck['errors'])): ?>
<div class="warning-box" style="background:rgba(245,63,63,.06);border-color:rgba(245,63,63,.2);color:#cb2634" id="envErrorBox">
<strong>环境检测未通过：</strong>
<ul style="margin:8px 0 0 16px">
<?php foreach ($envCheck['errors'] as $err): ?>
<li><?php echo htmlspecialchars($err); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php else: ?>
<div id="envErrorBox" style="display:none"></div>
<?php endif; ?>
<?php if (!empty($envCheck['warnings'])): ?>
<div class="warning-box" id="envWarningBox">
<strong>提示：</strong>
<ul style="margin:8px 0 0 16px">
<?php foreach ($envCheck['warnings'] as $w): ?>
<li><?php echo htmlspecialchars($w); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php else: ?>
<div id="envWarningBox" style="display:none"></div>
<?php endif; ?>
<div class="actions">
<a href="?step=1" class="btn btn-default">上一步</a>
<div class="actions-right">
<?php
$vendorAutoload = ROOT_PATH . '/vendor/autoload.php';
$composerMissing = !file_exists($vendorAutoload);
?>
<?php if ($composerMissing || !empty($envCheck['errors'])): ?>
<button class="btn btn-primary disabled">环境检测未通过</button>
<button type="button" class="btn btn-default" style="margin-left:8px" onclick="recheckEnv()">重新检测</button>
<?php else: ?>
<a href="?step=3" class="btn btn-primary">下一步</a>
<?php endif; ?>
</div>
</div>
</div>

<div class="panel <?php echo $currentStep === 3 ? 'active' : ''; ?>" id="step3">
<form method="post" action="install.php" id="installForm" onsubmit="return onSubmit()">
<div class="section-title">安装配置</div>

<div class="form-section">
<div class="form-section-title">站点配置</div>
<div class="form-row">
<div class="form-group">
<label>站点名称 <span class="req">*</span></label>
<input type="text" name="app_name" class="form-control" value="CmsPro" required>
</div>
<div class="form-group">
<label>站点 URL <span class="req">*</span></label>
<input type="text" name="app_url" class="form-control" value="<?php echo htmlspecialchars($detectedUrl); ?>" required>
<div class="form-hint">请输入完整 URL，如：https://www.example.com</div>
</div>
</div>
</div>

<div class="form-section">
<div class="form-section-title">数据库配置</div>
<div class="form-row">
<div class="form-group">
<label>主机地址 <span class="req">*</span></label>
<input type="text" name="db_host" class="form-control" value="127.0.0.1" required>
</div>
<div class="form-group" style="max-width:120px">
<label>端口 <span class="req">*</span></label>
<input type="text" name="db_port" class="form-control" value="3306" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>数据库名称 <span class="req">*</span></label>
<input type="text" name="db_database" class="form-control" value="cmspro" required>
</div>
<div class="form-group">
<label>用户名 <span class="req">*</span></label>
<input type="text" name="db_username" class="form-control" value="root" required>
</div>
<div class="form-group">
<label>密码</label>
<input type="password" name="db_password" class="form-control" value="">
</div>
<div class="form-group" style="display:flex;align-items:flex-end;padding-bottom:1px">
<button type="button" class="btn btn-test" onclick="testDb()">测试连接</button>
</div>
</div>
<div id="dbTestResult" class="db-test-result"></div>
<div id="dbDataWarning" class="db-data-warning" style="display:none">
<div class="db-data-warning-title">检测到数据库中已有数据</div>
<div class="db-data-warning-detail" id="dbDataDetail"></div>
<div class="db-data-backup-notice">
<svg viewBox="0 0 16 16" fill="currentColor" style="vertical-align:-3px;margin-right:4px;width:14px;height:14px"><path d="M8 1a5 5 0 00-5 5v3.5L1.5 12h13L13 9.5V6a5 5 0 00-5-5zM5.5 13a2.5 2.5 0 005 0h-5z"/></svg>
覆盖安装将<strong>清空并删除所有现有数据表和数据</strong>，请务必先备份重要数据！
</div>
<label class="db-data-checkbox">
<input type="checkbox" name="clean_reinstall" id="cleanReinstall" value="1">
<span>我已备份数据，确认清空现有数据并重新安装</span>
</label>
</div>
</div>

<div class="form-section">
<div class="form-section-title">Redis 配置 <span style="font-weight:400;color:#86909c;font-size:12px">（可选，不使用将自动切换为文件驱动）</span></div>
<div class="form-row">
<div class="form-group">
<label>主机地址</label>
<input type="text" name="redis_host" id="redisHost" class="form-control" value="127.0.0.1">
</div>
<div class="form-group" style="max-width:120px">
<label>端口</label>
<input type="text" name="redis_port" id="redisPort" class="form-control" value="6379">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" name="redis_password" id="redisPassword" class="form-control" value="" placeholder="无密码留空">
</div>
<div class="form-group" style="display:flex;align-items:flex-end;padding-bottom:1px">
<button type="button" class="btn btn-test" onclick="testRedis()">测试连接</button>
</div>
</div>
<div id="redisTestResult" class="db-test-result"></div>
<label class="db-data-checkbox" style="margin-top:8px">
<input type="checkbox" name="skip_redis" id="skipRedis" value="1"<?php echo !extension_loaded('redis') ? ' checked' : ''; ?>>
<span>不使用 Redis（Session/Cache/Queue 将使用文件或同步驱动）</span>
</label>
</div>

<div class="form-section">
<div class="form-section-title">管理员账号</div>
<div class="form-row">
<div class="form-group">
<label>用户名 <span class="req">*</span></label>
<input type="text" name="admin_username" class="form-control" value="admin" required>
</div>
<div class="form-group">
<label>邮箱 <span class="req">*</span></label>
<input type="email" name="admin_email" class="form-control" value="admin@example.com" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>密码 <span class="req">*</span></label>
<input type="password" name="admin_password" class="form-control" required minlength="6">
<div class="form-hint">至少 6 位字符</div>
</div>
<div class="form-group">
<label>确认密码 <span class="req">*</span></label>
<input type="password" name="admin_password_confirm" class="form-control" required minlength="6">
</div>
</div>
</div>

<div class="actions">
<a href="?step=2" class="btn btn-default">上一步</a>
<button type="submit" class="btn btn-primary">开始安装</button>
</div>
</form>
</div>

<div class="panel <?php echo $currentStep === 4 ? 'active' : ''; ?>" id="step4">
<div class="section-title">安装进度</div>

<!-- AJAX 动态进度面板 -->
<div id="installProgressPanel" style="display:none">
<div class="progress-info">
<span id="installProgressLabel">正在安装...</span>
<span class="progress-percent" id="installProgressText">0%</span>
</div>
<div class="progress-bar"><div class="progress-fill" id="installProgressBar" style="width:0%"></div></div>
<ul class="result-list" id="installStepList"></ul>
<div class="install-result-area" id="installResultArea"></div>
</div>

<!-- 非 AJAX 降级：PHP 直接渲染的安装结果 -->
<div id="installFallbackPanel" style="<?php echo $installResult !== null ? '' : 'display:none'; ?>">
<?php if ($installResult !== null): ?>
<?php if ($installResult['success']): ?>
<div class="success-box">
<h3>&#10004; 安装成功</h3>
<p>CmsPro 系统已成功安装，请尽快删除安装文件以确保安全。</p>
</div>
<?php else: ?>
<div class="warning-box" style="background:rgba(245,63,63,.06);border-color:rgba(245,63,63,.2);color:#cb2634">
<strong>&#10008; 安装过程中出现错误</strong>
</div>
<?php endif; ?>

<ul class="result-list">
<?php foreach ($installResult['steps'] as $step): ?>
<li class="result-item <?php echo $step['status']; ?>">
<div class="icon"><?php echo $statusIcons[$step['status']] ?? ''; ?></div>
<div class="name"><?php echo htmlspecialchars($step['name']); ?></div>
<?php if (!empty($step['message'])): ?>
<div class="msg" title="<?php echo htmlspecialchars($step['message']); ?>">
<?php echo htmlspecialchars(mb_substr($step['message'], 0, 60)); ?>
</div>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>

<?php if ($installResult['success']): ?>
<div class="warning-box">
<strong>安全提示：</strong>请手动删除以下文件：<code>public/install.php</code> 和 <code>public/install.md</code>
</div>
<div class="link-list">
<a href="<?php echo htmlspecialchars($installResult['siteUrl'] ?? $detectedUrl); ?>/admin">访问后台</a>
<a href="<?php echo htmlspecialchars($installResult['siteUrl'] ?? $detectedUrl); ?>">访问首页</a>
</div>
<?php endif; ?>
<?php else: ?>
<div class="warning-box">未获取到安装结果，请返回重新安装。</div>
<div class="actions">
<a href="?step=3" class="btn btn-default">返回配置</a>
<div></div>
</div>
<?php endif; ?>
</div>
</div>

</div>
</div>

<div class="overlay" id="overlay">
<div class="overlay-box">
<div class="spinner"></div>
<div class="overlay-text">正在安装中，请稍候...</div>
</div>
</div>

<script>
function testDb() {
    var result = document.getElementById('dbTestResult');
    result.className = 'db-test-result show';
    result.style.background = 'rgba(22,93,255,.06)';
    result.style.color = '#165DFF';
    result.textContent = '正在测试连接...';

    // 隐藏之前的数据检测警告
    document.getElementById('dbDataWarning').style.display = 'none';
    document.getElementById('cleanReinstall').checked = false;

    var params = new URLSearchParams();
    params.append('test_db', '1');
    params.append('db_host', document.querySelector('[name="db_host"]').value);
    params.append('db_port', document.querySelector('[name="db_port"]').value);
    params.append('db_database', document.querySelector('[name="db_database"]').value);
    params.append('db_username', document.querySelector('[name="db_username"]').value);
    params.append('db_password', document.querySelector('[name="db_password"]').value);

    fetch('install.php', {method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:params})
    .then(function(r){return r.json()})
    .then(function(d){
        if(d.success){
            result.className='db-test-result show success';
            result.textContent=d.message;
            checkDbData();
        }else{
            result.className='db-test-result show fail';
            result.textContent=d.message;
        }
    })
    .catch(function(e){
        result.className='db-test-result show fail';
        result.textContent='请求失败：'+e.message;
    });
}

function checkDbData() {
    var warningEl = document.getElementById('dbDataWarning');
    var detailEl = document.getElementById('dbDataDetail');

    var params = new URLSearchParams();
    params.append('check_db_data', '1');
    params.append('db_host', document.querySelector('[name="db_host"]').value);
    params.append('db_port', document.querySelector('[name="db_port"]').value);
    params.append('db_database', document.querySelector('[name="db_database"]').value);
    params.append('db_username', document.querySelector('[name="db_username"]').value);
    params.append('db_password', document.querySelector('[name="db_password"]').value);

    fetch('install.php', {method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:params})
    .then(function(r){return r.json()})
    .then(function(d){
        if (d.error) return;
        if (d.has_data && d.table_count > 0) {
            var tableList = [];
            for (var tableName in d.tables) {
                tableList.push('<span style="color:#4e5969">' + escHtml(tableName) + '</span>（<span style="color:#86909c">' + d.tables[tableName] + '</span> 条记录）');
            }
            detailEl.innerHTML = '<div>共 <strong>' + d.table_count + '</strong> 张表，约 <strong>' + d.total_rows + '</strong> 条记录</div>'
                + '<div style="margin-top:6px;font-size:12px;color:#c9cdd4;max-height:120px;overflow-y:auto">'
                + tableList.join('<br>')
                + '</div>';
            warningEl.style.display = 'block';
        } else {
            warningEl.style.display = 'none';
        }
    })
    .catch(function(){});
}

function testRedis() {
    var result = document.getElementById('redisTestResult');
    result.className = 'db-test-result show';
    result.style.background = 'rgba(22,93,255,.06)';
    result.style.color = '#165DFF';
    result.textContent = '正在测试连接...';

    var params = new URLSearchParams();
    params.append('test_redis', '1');
    params.append('redis_host', document.getElementById('redisHost').value);
    params.append('redis_port', document.getElementById('redisPort').value);
    params.append('redis_password', document.getElementById('redisPassword').value);

    fetch('install.php', {method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:params})
    .then(function(r){return r.json()})
    .then(function(d){
        if(d.success){
            result.className='db-test-result show success';
            result.textContent=d.message;
        }else{
            result.className='db-test-result show fail';
            result.textContent=d.message;
        }
    })
    .catch(function(e){
        result.className='db-test-result show fail';
        result.textContent='请求失败：'+e.message;
    });
}

function escHtml(str) {
    var div = document.createElement('div');
    div.appendChild(document.createTextNode(str || ''));
    return div.innerHTML;
}

function recheckEnv() {
    var btn = event.target;
    btn.disabled = true;
    btn.textContent = '检测中...';

    var params = new URLSearchParams();
    params.append('env_check', '1');

    fetch('install.php', {method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:params})
    .then(function(r){return r.json()})
    .then(function(d){
        var html = '<thead><tr><th style="width:200px">检测项目</th><th>要求</th><th>当前</th><th>状态</th></tr></thead><tbody>';
        var hasError = false;

        (d.items || []).forEach(function(item) {
            var rowClass = 'env-row-' + item.status;
            var tdClass = item.status === 'error' ? 'fail' : (item.status === 'warning' ? 'warn' : 'pass');
            var statusText = item.status === 'error' ? '&#10008; 未通过' : (item.status === 'warning' ? '! 警告' : '&#10004; 通过');
            html += '<tr class="' + rowClass + '"><td>' + escHtml(item.name) + '</td><td>' + escHtml(item.require) + '</td><td>' + escHtml(item.current) + '</td><td class="' + tdClass + '">' + statusText + '</td></tr>';
            if (item.status === 'error') hasError = true;
        });

        html += '</tbody>';
        document.getElementById('envCheckTable').innerHTML = html;

        var errorBox = document.getElementById('envErrorBox');
        var errors = d.errors || [];
        if (errors.length > 0) {
            errorBox.className = 'warning-box';
            errorBox.style.background = 'rgba(245,63,63,.06)';
            errorBox.style.borderColor = 'rgba(245,63,63,.2)';
            errorBox.style.color = '#cb2634';
            errorBox.style.display = 'block';
            var errHtml = '<strong>环境检测未通过：</strong><ul style="margin:8px 0 0 16px">';
            errors.forEach(function(e) { errHtml += '<li>' + escHtml(e) + '</li>'; });
            errHtml += '</ul>';
            errorBox.innerHTML = errHtml;
        } else {
            errorBox.style.display = 'none';
        }

        var warningBox = document.getElementById('envWarningBox');
        var warnings = d.warnings || [];
        if (warnings.length > 0) {
            warningBox.className = 'warning-box';
            warningBox.style.display = 'block';
            var warnHtml = '<strong>提示：</strong><ul style="margin:8px 0 0 16px">';
            warnings.forEach(function(w) { warnHtml += '<li>' + escHtml(w) + '</li>'; });
            warnHtml += '</ul>';
            warningBox.innerHTML = warnHtml;
        } else {
            warningBox.style.display = 'none';
        }

        var actionsDiv = btn.parentElement;
        if (hasError) {
            actionsDiv.innerHTML = '<button class="btn btn-primary disabled">环境检测未通过</button><button type="button" class="btn btn-default" style="margin-left:8px" onclick="recheckEnv()">重新检测</button>';
        } else {
            actionsDiv.innerHTML = '<a href="?step=3" class="btn btn-primary">下一步</a>';
        }
    })
    .catch(function(e){
        btn.disabled = false;
        btn.textContent = '重新检测';
        alert('检测失败：' + e.message);
    });
}

var pollTimer = null;

function onSubmit() {
    var pwd = document.querySelector('[name="admin_password"]').value;
    var confirmPwd = document.querySelector('[name="admin_password_confirm"]').value;
    if (pwd !== confirmPwd) {
        alert('两次输入的密码不一致');
        return false;
    }
    if (pwd.length < 6) {
        alert('管理员密码至少6位');
        return false;
    }

    var warningEl = document.getElementById('dbDataWarning');
    var cleanCheckbox = document.getElementById('cleanReinstall');

    // 如果警告已显示，直接根据状态判断
    if (warningEl.style.display === 'block') {
        if (!cleanCheckbox.checked) {
            alert('检测到数据库中已有数据表，如需覆盖安装请先勾选"我已备份数据，确认清空现有数据并重新安装"');
            return false;
        }
        var msg = '警告：即将清空数据库中所有现有数据表和数据！\n\n此操作不可恢复，请确认您已备份重要数据。';
        if (!confirm(msg)) {
            return false;
        }
        startAjaxInstall();
        return false;
    }

    // 警告未显示，先异步检查数据库
    var params = new URLSearchParams();
    params.append('check_db_data', '1');
    params.append('db_host', document.querySelector('[name="db_host"]').value);
    params.append('db_port', document.querySelector('[name="db_port"]').value);
    params.append('db_database', document.querySelector('[name="db_database"]').value);
    params.append('db_username', document.querySelector('[name="db_username"]').value);
    params.append('db_password', document.querySelector('[name="db_password"]').value);

    fetch('install.php', {method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:params})
    .then(function(r){return r.json()})
    .then(function(d){
        if (d.error || !d.has_data || d.table_count === 0) {
            startAjaxInstall();
            return;
        }
        var detailEl = document.getElementById('dbDataDetail');
        var tableList = [];
        for (var tn in d.tables) {
            tableList.push(tn + '（' + d.tables[tn] + ' 条）');
        }
        detailEl.innerHTML = '<div>共 <strong>' + d.table_count + '</strong> 张表，约 <strong>' + d.total_rows + '</strong> 条记录</div>'
            + '<div style="margin-top:6px;font-size:12px;color:#c9cdd4;max-height:120px;overflow-y:auto">'
            + tableList.join('<br>') + '</div>';
        warningEl.style.display = 'block';

        var msg1 = '检测到数据库中已有 ' + d.table_count + ' 张数据表（约 ' + d.total_rows + ' 条记录）。\n\n'
            + '覆盖安装将删除所有现有数据，请确认您已备份重要数据。\n\n是否继续安装？';
        if (!confirm(msg1)) return;
        cleanCheckbox.checked = true;
        var msg2 = '最终确认：即将清空数据库所有现有数据并重新安装！\n\n此操作不可恢复。';
        if (!confirm(msg2)) return;
        startAjaxInstall();
    })
    .catch(function(){
        startAjaxInstall();
    });

    return false;
}

function startAjaxInstall() {
    // 禁用提交按钮
    var submitBtn = document.querySelector('#installForm button[type="submit"]');
    if (submitBtn) {
        submitBtn.disabled = true;
        submitBtn.textContent = '安装中...';
    }

    // 切换到进度面板
    document.getElementById('installProgressPanel').style.display = 'block';
    document.getElementById('installFallbackPanel').style.display = 'none';
    document.getElementById('installResultArea').innerHTML = '';

    // 切换到 step4 视图
    var panels = document.querySelectorAll('.panel');
    for (var i = 0; i < panels.length; i++) {
        panels[i].classList.remove('active');
    }
    document.getElementById('step4').classList.add('active');
    document.querySelector('.container').className = 'container step-4';

    // 更新步骤导航指示器为 step4 高亮
    var stepItems = document.querySelectorAll('.step-item');
    for (var i = 0; i < stepItems.length; i++) {
        stepItems[i].classList.remove('active', 'done');
    }
    // step1~3 = done, step4 = active
    if (stepItems.length >= 4) {
        stepItems[0].classList.add('done');
        stepItems[1].classList.add('done');
        stepItems[2].classList.add('done');
        stepItems[3].classList.add('done', 'active');
    }

    // 收集表单数据并发起 AJAX 安装请求
    var formData = new FormData(document.getElementById('installForm'));
    formData.append('start_install', '1');
    var params = new URLSearchParams();
    formData.forEach(function(value, key) {
        params.append(key, value);
    });

    // 立即启动轮询（不等 AJAX 响应，后台进程模式安装任务独立运行）
    pollProgress();

    fetch('install.php', {method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:params})
    .then(function(r){return r.json()})
    .then(function(d){
        if (!d.success && d.message) {
            // 表单验证失败，停止轮询
            if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
            document.getElementById('installResultArea').innerHTML =
                '<div class="warning-box" style="background:rgba(245,63,63,.06);border-color:rgba(245,63,63,.2);color:#cb2634"><strong>安装启动失败：</strong>' + escHtml(d.message) + '</div>';
            if (submitBtn) {
                submitBtn.disabled = false;
                submitBtn.textContent = '开始安装';
            }
        }
    })
    .catch(function(e){
        // fetch 失败不中断，轮询已在运行
    });
}

function pollProgress() {
    if (pollTimer) return;

    var pollCount = 0;
    var maxPolls = 600; // 最多轮询 600 次 × 2秒 = 20分钟

    pollTimer = setInterval(function() {
        pollCount++;
        if (pollCount > maxPolls) {
            clearInterval(pollTimer);
            pollTimer = null;
            document.getElementById('installResultArea').innerHTML =
                '<div class="warning-box" style="background:rgba(245,63,63,.06);border-color:rgba(245,63,63,.2);color:#cb2634"><strong>安装超时</strong>，请检查服务器日志或刷新页面重试。</div>';
            return;
        }

        // 轮询 install.php + get_progress（轻量处理，位于文件顶部，跳过 lock 检查）
        var pollParams = new URLSearchParams();
        pollParams.append('get_progress', '1');
        fetch('install.php', {method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:pollParams})
        .then(function(r){return r.json()})
        .then(function(d) {
            if (!d) return;
            renderProgress(d);
            if (d.done) {
                clearInterval(pollTimer);
                pollTimer = null;
                showFinalResult(d);
            }
        })
        .catch(function(){
            // 轮询失败不中断，继续轮询
        });
    }, 2000);
}

function renderProgress(data) {
    var steps = data.steps || [];
    var total = data.total || 1;

    // 计算完成百分比
    var completedCount = 0;
    for (var i = 0; i < steps.length; i++) {
        if (steps[i].status === 'success' || steps[i].status === 'warning' || steps[i].status === 'error') {
            completedCount++;
        }
    }
    var percent = Math.round(completedCount / total * 100);

    // 更新进度条
    document.getElementById('installProgressBar').style.width = percent + '%';
    document.getElementById('installProgressText').textContent = percent + '%';

    // 更新标签
    if (data.done) {
        document.getElementById('installProgressLabel').textContent = '安装完成';
    } else {
        document.getElementById('installProgressLabel').textContent = '正在安装...';
    }

    // 更新步骤列表
    var listEl = document.getElementById('installStepList');
    var html = '';
    for (var i = 0; i < steps.length; i++) {
        var s = steps[i];
        var icon = '';
        var cls = '';
        if (s.status === 'success') { icon = '&#10004;'; cls = 'success'; }
        else if (s.status === 'error') { icon = '&#10008;'; cls = 'error'; }
        else if (s.status === 'warning') { icon = '&#9888;'; cls = 'warning'; }
        else if (s.status === 'running') { icon = '<span class="step-spinner"></span>'; cls = 'running'; }
        else { icon = '&#9679;'; cls = ''; }

        html += '<li class="result-item ' + cls + '">';
        html += '<div class="icon">' + icon + '</div>';
        html += '<div class="name">' + escHtml(s.name) + '</div>';
        if (s.message) {
            html += '<div class="msg" title="' + escHtml(s.message) + '">' + escHtml(s.message.substring(0, 60)) + '</div>';
        }
        html += '</li>';
    }
    listEl.innerHTML = html;
}

function showFinalResult(data) {
    var resultEl = document.getElementById('installResultArea');
    if (data.success) {
        resultEl.innerHTML = '<div class="success-box"><h3>&#10004; 安装成功</h3><p>CmsPro 系统已成功安装，请尽快删除安装文件以确保安全。</p></div>'
            + '<div class="warning-box"><strong>安全提示：</strong>请手动删除以下文件：<code>public/install.php</code> 和 <code>public/install.md</code></div>'
            + '<div class="link-list"><a href="/admin">访问后台</a><a href="/">访问首页</a></div>';
    } else {
        resultEl.innerHTML = '<div class="warning-box" style="background:rgba(245,63,63,.06);border-color:rgba(245,63,63,.2);color:#cb2634"><strong>&#10008; 安装过程中出现错误</strong></div>';
    }
}
</script>
</body>
</html>
<?php

// ==================== mode=worker 后台安装进程入口 ====================

function runAsWorker(): void
{
    $systemApps = ['versionmgr'];
    if (!is_dir(STORAGE_PATH . '/logs')) { mkdir(STORAGE_PATH . '/logs', 0755, true); }

    $configFile = STORAGE_PATH . '/install_config.json';
    if (!file_exists($configFile)) {
        worker_log('Worker 启动失败：配置文件不存在 ' . $configFile, 'error');
        worker_write([['name' => '安装启动失败', 'status' => 'error', 'message' => '配置文件不存在']], 0, true, false);
        exit(1);
    }
    $config = json_decode(file_get_contents($configFile), true);
    if (!is_array($config)) {
        worker_log('Worker 启动失败：配置文件格式错误', 'error');
        worker_write([['name' => '安装启动失败', 'status' => 'error', 'message' => '配置文件格式错误']], 0, true, false);
        exit(1);
    }

    worker_log('Worker 进程启动，开始执行安装任务');
    chdir(ROOT_PATH);

    $cache = ROOT_PATH . '/bootstrap/cache/services.php';
    if (file_exists($cache)) { unlink($cache); }

    $steps = [];
    $cleanReinstall = !empty($config['clean_reinstall']);

    // --- 步骤 0：生成 .env ---
    $steps[] = ['name' => '生成 .env 配置文件', 'status' => 'running'];
    worker_write($steps, 0);
    try {
        generateEnvFile($config);
        worker_ok($steps, 0);
        worker_log('生成 .env 配置文件成功');
    } catch (\Throwable $e) {
        worker_fail($steps, 0, $e->getMessage()); worker_cleanup(); exit(1);
    }

    // --- 步骤 1：生成 APP_KEY ---
    $steps[] = ['name' => '生成 APP_KEY', 'status' => 'running'];
    worker_write($steps, 1);
    try {
        $envPath = ROOT_PATH . '/.env';
        $appKey = 'base64:' . base64_encode(random_bytes(32));
        $content = file_get_contents($envPath);
        $content = preg_replace('/^APP_KEY=.*$/m', 'APP_KEY=' . $appKey, $content);
        file_put_contents($envPath, $content);
        worker_ok($steps, 1);
        worker_log('生成 APP_KEY 成功');
    } catch (\Throwable $e) {
        worker_fail($steps, 1, $e->getMessage()); worker_cleanup(); exit(1);
    }

    // --- 步骤 2：生成 CMSPRO_SITE_KEY ---
    $steps[] = ['name' => '生成站点识别码', 'status' => 'running'];
    worker_write($steps, 2);
    try {
        $envPath = ROOT_PATH . '/.env';
        $siteKey = bin2hex(random_bytes(16));
        $content = file_get_contents($envPath);
        if (preg_match('/^CMSPRO_SITE_KEY=/m', $content)) {
            $content = preg_replace('/^CMSPRO_SITE_KEY=.*$/m', 'CMSPRO_SITE_KEY=' . $siteKey, $content);
        } else {
            $content = rtrim($content) . "\nCMSPRO_SITE_KEY=" . $siteKey . "\n";
        }
        file_put_contents($envPath, $content);
        worker_ok($steps, 2);
        worker_log('生成站点识别码成功');
    } catch (\Throwable $e) {
        worker_fail($steps, 2, $e->getMessage()); worker_cleanup(); exit(1);
    }

    // --- 步骤 3：测试数据库连接 ---
    $steps[] = ['name' => '测试数据库连接', 'status' => 'running'];
    worker_write($steps, 3);
    $dbTest = worker_testDB($config['db_host'], $config['db_port'], $config['db_database'], $config['db_username'], $config['db_password']);
    if ($dbTest === true) {
        worker_ok($steps, 3);
        worker_log('数据库连接测试成功');
    } else {
        worker_fail($steps, 3, $dbTest); worker_cleanup(); exit(1);
    }

    // --- 步骤 4：初始化数据库表结构 ---
    $steps[] = ['name' => '初始化数据库表结构', 'status' => 'running'];
    worker_write($steps, 4);
    try {
        $r = worker_initDB($config, $cleanReinstall);
        if ($r['success']) {
            worker_ok($steps, 4);
            worker_log('初始化数据库表结构成功');
        } else {
            worker_fail($steps, 4, $r['message'] ?? '未知错误'); worker_cleanup(); exit(1);
        }
    } catch (\Throwable $e) {
        worker_fail($steps, 4, $e->getMessage()); worker_cleanup(); exit(1);
    }

    // --- 步骤 5：数据填充 ---
    $steps[] = ['name' => '执行数据填充', 'status' => 'running'];
    worker_write($steps, 5);
    $r = worker_artisan('db:seed', ['--force' => true]);
    if ($r['rc'] === 0) { worker_ok($steps, 5); worker_log('数据填充成功'); }
    else { worker_fail($steps, 5, '返回码：' . $r['rc']); worker_cleanup(); exit(1); }

    // --- 步骤 6：运行迁移 ---
    $steps[] = ['name' => '同步数据库结构', 'status' => 'running'];
    worker_write($steps, 6);
    $r = worker_artisan('migrate', ['--force' => true]);
    if ($r['rc'] === 0) { worker_ok($steps, 6); worker_log('同步数据库结构成功'); }
    else { worker_fail($steps, 6, '返回码：' . $r['rc']); worker_cleanup(); exit(1); }

    // --- 步骤 7：安装系统应用 ---
    $steps[] = ['name' => '安装系统应用', 'status' => 'running'];
    worker_write($steps, 7);
    $allOk = true; $msgs = [];
    $installPdo7 = null;
    foreach ($systemApps as $appId) {
        $r = worker_artisan('app:install', ['app_id' => $appId]);

        // 已安装（50005）视为成功，避免 db:seed 预写入导致重复安装误判
        $isAlreadyInstalled = ($r['rc'] !== 0)
            && strpos($r['out'], '应用已安装') !== false;

        if ($r['rc'] !== 0 && !$isAlreadyInstalled) {
            $allOk = false;
            $msgs[] = $appId . ' 失败';
            worker_log("{$appId} 安装失败：" . $r['out'], 'error');
        } else {
            $msgs[] = $appId . ' 已安装';
            worker_log("{$appId} 安装成功");
            // 启用应用
            try {
                if (!$installPdo7) {
                    $dsn = "mysql:host={$config['db_host']};port={$config['db_port']};dbname={$config['db_database']}";
                    $installPdo7 = new \PDO($dsn, $config['db_username'], $config['db_password'], [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
                }
                $installPdo7->prepare("UPDATE apps SET status = 1 WHERE app_id = ?")->execute([$appId]);
                worker_log("已启用应用：{$appId}");
            } catch (\Throwable $e) {
                worker_log("启用应用 {$appId} 失败（可忽略）：" . $e->getMessage(), 'warning');
            }
        }
    }
    if (!$allOk) { worker_update($steps, 7, 'warning', implode('；', $msgs)); }
    else { worker_ok($steps, 7); }

    // --- 步骤 8：标记系统应用（直接 SQL，不存在 Artisan 命令） ---
    $steps[] = ['name' => '标记系统应用', 'status' => 'running'];
    worker_write($steps, 8);
    try {
        $dsn = "mysql:host={$config['db_host']};port={$config['db_port']};dbname={$config['db_database']}";
        $pdo8 = new \PDO($dsn, $config['db_username'], $config['db_password'], [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
        $placeholders = implode(',', array_fill(0, count($systemApps), '?'));
        $sql = "UPDATE apps SET is_system = 1 WHERE app_id IN ({$placeholders})";
        $stmt = $pdo8->prepare($sql);
        $stmt->execute(array_values($systemApps));
        worker_ok($steps, 8);
        worker_log('标记系统应用成功，影响行数：' . $stmt->rowCount());
    } catch (\Throwable $e) {
        worker_update($steps, 8, 'warning', '标记系统应用失败，请手动设置');
        worker_log('标记系统应用失败：' . $e->getMessage(), 'warning');
    }

    // --- 步骤 9：初始化终端菜单（直接 SQL，不存在 Artisan 命令） ---
    $steps[] = ['name' => '初始化终端菜单', 'status' => 'running'];
    worker_write($steps, 9);
    try {
        $dsn = "mysql:host={$config['db_host']};port={$config['db_port']};dbname={$config['db_database']}";
        $pdo9 = new \PDO($dsn, $config['db_username'], $config['db_password'], [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);

        // 初始化终端类型
        $terminalTypes = [
            ['type' => 'admin', 'name' => '后台', 'description' => '后台管理终端', 'sort' => 1],
            ['type' => 'user', 'name' => '用户', 'description' => '前台用户管理终端', 'sort' => 2],
            ['type' => 'home', 'name' => '前端', 'description' => '官网前端导航菜单', 'sort' => 3],
        ];
        foreach ($terminalTypes as $t) {
            $exists = $pdo9->prepare("SELECT id FROM admin_menu_terminals WHERE type = ?");
            $exists->execute([$t['type']]);
            if ($exists->fetchColumn()) continue;
            $pdo9->prepare("INSERT INTO admin_menu_terminals (type, name, description, sort, status, create_time, update_time) VALUES (?, ?, ?, ?, 1, NOW(), NOW())")
                ->execute([$t['type'], $t['name'], $t['description'], $t['sort']]);
            worker_log("已创建终端类型：{$t['name']}");
        }

        // 初始化用户中心（user）终端菜单
        $userExists = $pdo9->prepare("SELECT id FROM admin_menus WHERE terminal_type = 'user' AND parent_id = 0 AND name = '用户中心'");
        $userExists->execute();
        $userParentId = $userExists->fetchColumn();
        if (!$userParentId) {
            $pdo9->prepare("INSERT INTO admin_menus (parent_id, name, icon, path, component, permission_code, type, visible, sort, status, terminal_type, app_id, create_time, update_time) VALUES (0, '用户中心', 'layui-icon-user', '', '', '', 1, 1, 1, 1, 'user', NULL, NOW(), NOW())")->execute();
            $userParentId = $pdo9->lastInsertId();
            worker_log("已创建用户中心目录菜单，ID：{$userParentId}");
        }

        $userMenus = [
            ['name' => '我的首页', 'icon' => 'layui-icon-home', 'path' => '/user/dashboard', 'sort' => 1],
            ['name' => '账号管理', 'icon' => 'layui-icon-username', 'path' => '/user/account', 'sort' => 2],
        ];
        foreach ($userMenus as $m) {
            $childExists = $pdo9->prepare("SELECT id FROM admin_menus WHERE parent_id = ? AND path = ? AND terminal_type = 'user'");
            $childExists->execute([$userParentId, $m['path']]);
            if ($childExists->fetchColumn()) continue;
            $pdo9->prepare("INSERT INTO admin_menus (parent_id, name, icon, path, component, permission_code, type, visible, sort, status, terminal_type, app_id, create_time, update_time) VALUES (?, ?, ?, '', '', '', 2, 1, ?, 1, 'user', NULL, NOW(), NOW())")
                ->execute([$userParentId, $m['name'], $m['icon'], $m['path'], $m['sort']]);
            worker_log("已创建用户子菜单：{$m['name']}");
        }

        worker_ok($steps, 9);
        worker_log('终端菜单初始化成功');
    } catch (\Throwable $e) {
        worker_update($steps, 9, 'warning', '终端菜单初始化失败（可忽略）');
        worker_log('终端菜单初始化失败：' . $e->getMessage(), 'warning');
    }

    // --- 步骤 10：创建管理员（--username 是 option 参数，需带 -- 前缀） ---
    $steps[] = ['name' => '创建管理员账号', 'status' => 'running'];
    worker_write($steps, 10);
    $r = worker_artisan('admin:create', ['--username' => $config['admin_username'], '--email' => $config['admin_email'], '--password' => $config['admin_password'], '--name' => '超级管理员']);
    if ($r['rc'] === 0) { worker_ok($steps, 10); worker_log('管理员账号创建成功'); }
    else { worker_fail($steps, 10, $r['out']); worker_cleanup(); exit(1); }

    // --- 步骤 11：生成安装锁 ---
    $steps[] = ['name' => '生成安装锁', 'status' => 'running'];
    worker_write($steps, 11);
    try {
        file_put_contents(LOCK_FILE, date('Y-m-d H:i:s'));
        worker_ok($steps, 11);
        worker_log('安装锁生成成功');
    } catch (\Throwable $e) {
        worker_fail($steps, 11, $e->getMessage()); worker_cleanup(); exit(1);
    }

    worker_write($steps, count($steps) - 1, true, true);
    worker_log('========== CmsPro 系统安装完成 ==========');
    worker_cleanup();
    exit(0);
}

// ============ Worker 辅助函数 ============

function worker_log(string $msg, string $level = 'info'): void
{
    $t = date('Y-m-d H:i:s');
    $entry = "[{$t}] [{$level}] [worker] {$msg}\n";
    file_put_contents(LOG_PATH . '/install_' . date('Y-m-d') . '.log', $entry, FILE_APPEND);
    file_put_contents(DEBUG_LOG, $entry, FILE_APPEND);
}

function worker_write(array $steps, int $idx, bool $done = false, bool $success = false): void
{
    file_put_contents(PROGRESS_FILE, json_encode([
        'steps' => $steps, 'current' => $idx, 'total' => count($steps),
        'done' => $done, 'success' => $success, 'timestamp' => time(),
    ], JSON_UNESCAPED_UNICODE));
}

function worker_update(array &$steps, int $idx, string $status, string $msg = ''): void
{
    $steps[$idx]['status'] = $status;
    if ($msg) { $steps[$idx]['message'] = $msg; }
    worker_write($steps, $idx, $status === 'error', false);
}

function worker_ok(array &$steps, int $idx): void
{
    $steps[$idx]['status'] = 'success';
    worker_write($steps, $idx);
}

function worker_fail(array &$steps, int $idx, string $err): void
{
    $steps[$idx]['status'] = 'error';
    $steps[$idx]['message'] = $err;
    worker_write($steps, $idx, true, false);
}

function worker_cleanup(): void
{
    $f = STORAGE_PATH . '/install_config.json';
    if (file_exists($f)) { unlink($f); }
}

function worker_testDB(string $host, string $port, string $db, string $user, string $pass)
{
    try {
        new \PDO("mysql:host={$host};port={$port};dbname={$db}", $user, $pass,
            [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_TIMEOUT => 5]);
        return true;
    } catch (\PDOException $e) { return $e->getMessage(); }
}

function worker_initDB(array $config, bool $clean): array
{
    $sqlFile = PUBLIC_PATH . '/install.sql';
    if (!file_exists($sqlFile)) { return ['success' => false, 'message' => 'install.sql 不存在']; }

    $sql = file_get_contents($sqlFile);
    worker_log('开始执行建表脚本，文件大小：' . strlen($sql) . ' 字节');

    try {
        $pdo = new \PDO("mysql:host={$config['db_host']};port={$config['db_port']};dbname={$config['db_database']}", $config['db_username'], $config['db_password'],
            [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
        if ($clean) {
            worker_log('清空重装模式：开始批量删除所有现有表');
            $tables = $pdo->query('SHOW TABLES')->fetchAll(\PDO::FETCH_COLUMN);
            if (!empty($tables)) {
                // 批量删表优化：关闭外键检查 + 单条 DROP TABLE 一次删除所有表
                // 原逐张删除 33 张表耗时约 10 秒（每张 0.3 秒含日志 IO），合并后 < 0.5 秒
                $pdo->exec('SET foreign_key_checks=0');
                $quoted = array_map(fn($t) => '`' . $t . '`', $tables);
                $pdo->exec('DROP TABLE IF EXISTS ' . implode(',', $quoted));
                $pdo->exec('SET foreign_key_checks=1');
                foreach ($tables as $t) { worker_log('已删除表：' . $t); }
            }
            worker_log('清空重装完成：共删除 ' . count($tables) . ' 张表');
        }
        $stmts = worker_parseSQL($sql);
        $ok = 0; $skip = 0;
        foreach ($stmts as $s) {
            $s = trim($s); if (!$s) continue;
            try { $pdo->exec($s); $ok++; }
            catch (\PDOException $e) {
                if (str_contains($e->getMessage(), 'already exists') || str_contains($e->getMessage(), 'Duplicate')) { $skip++; }
                else { throw $e; }
            }
        }
        worker_log("建表完成：{$ok} 条成功，{$skip} 条跳过");
        return ['success' => true, 'message' => "成功执行 {$ok} 条SQL"];
    } catch (\Throwable $e) { return ['success' => false, 'message' => $e->getMessage()]; }
}

/**
 * 解析 install.sql 为独立的 SQL 语句
 * 使用 explode(";\n") 拆分 + 逐行过滤注释，与存档版本 importSchemaSql 逻辑一致
 */
function worker_parseSQL(string $content): array
{
    $stmts = [];
    // 统一换行符
    $content = str_replace(["\r\n", "\r"], "\n", $content);
    $parts = explode(";\n", $content);

    foreach ($parts as $part) {
        $lines = explode("\n", $part);
        $sqlLines = [];
        foreach ($lines as $line) {
            $trimmed = trim($line);
            // 跳过空行、SQL注释行
            if ($trimmed === '' || preg_match('/^(--|\/\*)/', $trimmed)) {
                continue;
            }
            $sqlLines[] = $trimmed;
        }
        $statement = trim(implode(' ', $sqlLines));
        // 跳过 SET 语句（如 SET NAMES, SET FOREIGN_KEY_CHECKS）
        if (preg_match('/^(SET|USE)\s/i', $statement)) {
            continue;
        }
        // 跳过过短的语句
        if (strlen($statement) < 10) {
            continue;
        }
        $stmts[] = $statement;
    }

    return $stmts;
}

/**
 * Worker 模式下的 Artisan 调用（使用 bootstrapLaravel + Artisan::call，不可用 exec）
 * exec() 在 Windows 上命令行参数解析有问题，导致 Artisan 只输出帮助信息
 */
function worker_artisan(string $cmd, array $params = []): array
{
    try {
        bootstrapLaravel();
    } catch (\Throwable $e) {
        $msg = 'Laravel 引导失败：' . $e->getMessage();
        worker_log($msg, 'error');
        return ['out' => $msg, 'rc' => -1];
    }

    worker_log("Artisan 调用：{$cmd} " . json_encode($params, JSON_UNESCAPED_UNICODE));

    try {
        $exitCode = \Illuminate\Support\Facades\Artisan::call($cmd, $params);
        $output = \Illuminate\Support\Facades\Artisan::output();
        worker_log("返回码：{$exitCode} 输出：" . mb_substr($output, 0, 500));
        return ['out' => $output, 'rc' => $exitCode];
    } catch (\Throwable $e) {
        worker_log("Artisan 调用异常：{$cmd} " . $e->getMessage(), 'error');
        return ['out' => $e->getMessage(), 'rc' => -1];
    }
}
