<?php
session_start();

require_once __DIR__ . '/../common/version.php';

$lockFile = __DIR__ . '/install.lock';
if (file_exists($lockFile)) {
    header('Location: index.php');
    exit;
}

function redirectWithError($step, $message, $sessionKey = null, $payload = null) {
    $_SESSION['install_error'] = $message;
    if ($sessionKey !== null) {
        $_SESSION[$sessionKey] = $payload;
    }
    header('Location: index.php?step=' . (int) $step);
    exit;
}

function hasRequiredSg16Extension() {
    return function_exists('sg_load')
        || extension_loaded('sg16')
        || extension_loaded('sourceguardian')
        || extension_loaded('SourceGuardian');
}

function installDbColumnExists(PDO $pdo, $table, $column) {
    $stmt = $pdo->prepare("SHOW COLUMNS FROM `$table` LIKE ?");
    $stmt->execute([(string) $column]);

    return (bool) $stmt->fetch();
}

function installDbUniqueIndexExists(PDO $pdo, $table, $column) {
    $stmt = $pdo->prepare("SHOW INDEX FROM `$table` WHERE Column_name = ? AND Non_unique = 0");
    $stmt->execute([(string) $column]);

    return (bool) $stmt->fetch();
}

function installDbColumns(PDO $pdo, $table) {
    $columns = [];
    $stmt = $pdo->query("SHOW COLUMNS FROM `$table`");
    foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
        $field = (string) ($row['Field'] ?? '');
        if ($field !== '') {
            $columns[$field] = $row;
        }
    }

    return $columns;
}

function installQuoteIdentifier($identifier) {
    return '`' . str_replace('`', '``', (string) $identifier) . '`';
}

function installRelaxedColumnDefinition($field, array $column) {
    $type = trim((string) ($column['Type'] ?? ''));
    if ($type === '') {
        return '';
    }

    $field = strtolower((string) $field);
    $lowerType = strtolower($type);
    $isDateTime = strpos($lowerType, 'datetime') !== false || strpos($lowerType, 'timestamp') !== false;

    if ($isDateTime && $field === 'updated_at') {
        return $type . ' NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP';
    }
    if ($isDateTime && in_array($field, ['created_at', 'last_failed_at'], true)) {
        return $type . ' NOT NULL DEFAULT CURRENT_TIMESTAMP';
    }
    if (strpos($lowerType, 'text') !== false || strpos($lowerType, 'blob') !== false || strpos($lowerType, 'json') !== false) {
        return $type . ' NULL';
    }

    return $type . ' NULL DEFAULT NULL';
}

function installRelaxLegacyRequiredColumns(PDO $pdo, $table, array $protectedColumns = []) {
    $protected = array_fill_keys(array_map('strtolower', $protectedColumns), true);
    foreach (installDbColumns($pdo, $table) as $field => $column) {
        $fieldName = (string) $field;
        $fieldKey = strtolower($fieldName);
        $extra = strtolower((string) ($column['Extra'] ?? ''));

        if (isset($protected[$fieldKey])) {
            continue;
        }
        if (strtoupper((string) ($column['Null'] ?? '')) !== 'NO') {
            continue;
        }
        if (array_key_exists('Default', $column) && $column['Default'] !== null) {
            continue;
        }
        if (strpos($extra, 'auto_increment') !== false || strpos($extra, 'generated') !== false) {
            continue;
        }

        $definition = installRelaxedColumnDefinition($fieldName, $column);
        if ($definition === '') {
            continue;
        }

        try {
            $pdo->exec("ALTER TABLE " . installQuoteIdentifier($table) . " MODIFY COLUMN " . installQuoteIdentifier($fieldName) . " " . $definition);
        } catch (Throwable $e) {
        }
    }
}

function installEnsureSettingsSchema(PDO $pdo, $table) {
    if (!installDbColumnExists($pdo, $table, 'setting_key')) {
        $pdo->exec("ALTER TABLE `$table` ADD COLUMN `setting_key` VARCHAR(100) DEFAULT NULL");
    }
    if (!installDbColumnExists($pdo, $table, 'setting_value')) {
        $pdo->exec("ALTER TABLE `$table` ADD COLUMN `setting_value` TEXT NULL");
    }

    $columns = installDbColumns($pdo, $table);
    if (isset($columns['key_name'])) {
        try {
            $pdo->exec("ALTER TABLE `$table` MODIFY COLUMN `key_name` VARCHAR(100) NULL DEFAULT NULL");
        } catch (Throwable $e) {
        }
        try {
            $pdo->exec("UPDATE `$table` SET `setting_key` = `key_name` WHERE (`setting_key` IS NULL OR `setting_key` = '') AND `key_name` IS NOT NULL AND `key_name` <> ''");
        } catch (Throwable $e) {
        }
    }

    foreach (['key_value', 'value'] as $legacyValueColumn) {
        if (!isset($columns[$legacyValueColumn])) {
            continue;
        }
        try {
            $pdo->exec("ALTER TABLE `$table` MODIFY COLUMN `$legacyValueColumn` TEXT NULL");
        } catch (Throwable $e) {
        }
        try {
            $pdo->exec("UPDATE `$table` SET `setting_value` = `$legacyValueColumn` WHERE `setting_value` IS NULL AND `$legacyValueColumn` IS NOT NULL");
        } catch (Throwable $e) {
        }
        break;
    }

    if (!installDbUniqueIndexExists($pdo, $table, 'setting_key')) {
        try {
            $pdo->exec("ALTER TABLE `$table` ADD UNIQUE KEY `uniq_setting_key` (`setting_key`)");
        } catch (Throwable $e) {
        }
    }

    installRelaxLegacyRequiredColumns($pdo, $table, ['setting_key']);
}

if (isset($_POST['step']) && $_POST['step'] === 'finalize') {
    $payload = $_SESSION['install_finalize_payload'] ?? null;
    $probePassed = !empty($_SESSION['install_runtime_probe_passed']);

    if (!is_array($payload) || !$payload) {
        redirectWithError(1, '安装会话不存在，请重新执行安装。');
    }

    if (!$probePassed) {
        redirectWithError(4, '当前 Web 环境尚未通过加密核心自检，请先完成运行环境检测。', 'install_site', $_SESSION['install_site'] ?? []);
    }

    if (file_put_contents($lockFile, json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) === false) {
        redirectWithError(4, '无法写入 install/install.lock 文件。', 'install_site', $_SESSION['install_site'] ?? []);
    }

    unset($_SESSION['install_db'], $_SESSION['install_site'], $_SESSION['install_finalize_payload'], $_SESSION['install_runtime_probe_passed']);
    $_SESSION['install_success'] = '安装完成，系统已经可以使用。';
    header('Location: index.php?completed=1');
    exit;
}

function buildConfigContent(array $db, $siteUrl) {
    $export = function ($value) {
        return var_export($value, true);
    };

    return "<?php\n"
        . "if (!defined('BTBLOG_COMMON_PATH')) {\n"
        . "    define('BTBLOG_COMMON_PATH', __DIR__);\n"
        . "}\n\n"
        . "if (!defined('BTBLOG_ROOT_PATH')) {\n"
        . "    define('BTBLOG_ROOT_PATH', dirname(__DIR__));\n"
        . "}\n\n"
        . "if (!function_exists('btblogRootPath')) {\n"
        . "    function btblogRootPath(\$relativePath = '') {\n"
        . "        \$root = rtrim((string) BTBLOG_ROOT_PATH, '/\\\\');\n"
        . "        if (\$relativePath === '' || \$relativePath === null) {\n"
        . "            return \$root;\n"
        . "        }\n\n"
        . "        return \$root . '/' . ltrim(str_replace('\\\\', '/', (string) \$relativePath), '/');\n"
        . "    }\n"
        . "}\n\n"
        . "if (!function_exists('btblogCommonPath')) {\n"
        . "    function btblogCommonPath(\$relativePath = '') {\n"
        . "        \$common = rtrim((string) BTBLOG_COMMON_PATH, '/\\\\');\n"
        . "        if (\$relativePath === '' || \$relativePath === null) {\n"
        . "            return \$common;\n"
        . "        }\n\n"
        . "        return \$common . '/' . ltrim(str_replace('\\\\', '/', (string) \$relativePath), '/');\n"
        . "    }\n"
        . "}\n\n"
        . "define('DB_HOST', " . $export($db['db_host']) . ");\n"
        . "define('DB_PORT', " . $export($db['db_port']) . ");\n"
        . "define('DB_NAME', " . $export($db['db_name']) . ");\n"
        . "define('DB_USER', " . $export($db['db_user']) . ");\n"
        . "define('DB_PASS', " . $export($db['db_pass']) . ");\n"
        . "define('DB_CHARSET', 'utf8mb4');\n"
        . "define('DB_PREFIX', " . $export($db['db_prefix']) . ");\n"
        . "define('SITE_URL', " . $export($siteUrl) . ");\n"
        . "define('UPLOAD_DIR', btblogRootPath('uploads') . '/');\n"
        . "define('UPLOAD_URL', SITE_URL . '/uploads/');\n\n"
        . "function getDB() {\n"
        . "    static \$pdo = null;\n"
        . "    if (\$pdo === null) {\n"
        . "        try {\n"
        . "            \$dsn = 'mysql:host=' . DB_HOST . ';port=' . DB_PORT . ';dbname=' . DB_NAME . ';charset=' . DB_CHARSET;\n"
        . "            \$options = [\n"
        . "                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,\n"
        . "                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,\n"
        . "            ];\n"
        . "            if (defined('PDO::MYSQL_ATTR_INIT_COMMAND')) {\n"
        . "                \$options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES ' . DB_CHARSET;\n"
        . "            }\n"
        . "            \$pdo = new PDO(\$dsn, DB_USER, DB_PASS, \$options);\n"
        . "        } catch (PDOException \$e) {\n"
        . "            die('数据库连接失败: ' . \$e->getMessage());\n"
        . "        }\n"
        . "    }\n"
        . "    return \$pdo;\n"
        . "}\n";
}

if (isset($_POST['step']) && $_POST['step'] === 'test') {
    header('Content-Type: application/json; charset=utf-8');

    if (!hasRequiredSg16Extension()) {
        echo json_encode([
            'success' => false,
            'message' => '当前 PHP 环境未安装 SG16 / SourceGuardian Loader，请先安装扩展后再继续安装。',
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    $host = trim((string) ($_POST['db_host'] ?? 'localhost'));
    $port = trim((string) ($_POST['db_port'] ?? '3306'));
    $name = trim((string) ($_POST['db_name'] ?? ''));
    $user = trim((string) ($_POST['db_user'] ?? 'root'));
    $pass = (string) ($_POST['db_pass'] ?? '');

    try {
        $dsn = "mysql:host=$host;port=$port;charset=utf8mb4";
        $pdo = new PDO($dsn, $user, $pass, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_TIMEOUT => 5,
        ]);
        $version = $pdo->query('SELECT VERSION()')->fetchColumn();
        $existsStmt = $pdo->prepare('SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?');
        $existsStmt->execute([$name]);
        $dbExists = (bool) $existsStmt->fetchColumn();
        $message = '连接成功，MySQL 版本：' . $version;
        if ($name !== '') {
            $message .= $dbExists ? '，目标数据库已存在。' : '，目标数据库不存在，安装时会自动创建。';
        }
        echo json_encode(['success' => true, 'message' => $message], JSON_UNESCAPED_UNICODE);
    } catch (PDOException $e) {
        $message = $e->getMessage();
        if (stripos($message, 'access denied') !== false) {
            $message = '用户名或密码错误。';
        } elseif (stripos($message, 'connection refused') !== false) {
            $message = '无法连接到数据库服务。';
        }
        echo json_encode(['success' => false, 'message' => '连接失败：' . $message], JSON_UNESCAPED_UNICODE);
    }
    exit;
}

if (isset($_POST['step']) && $_POST['step'] === '2') {
    if (!hasRequiredSg16Extension()) {
        redirectWithError(1, '当前 PHP 环境未安装 SG16 / SourceGuardian Loader，请先安装扩展后再继续安装。');
    }

    $db = [
        'db_host' => trim((string) ($_POST['db_host'] ?? 'localhost')),
        'db_port' => trim((string) ($_POST['db_port'] ?? '3306')),
        'db_name' => trim((string) ($_POST['db_name'] ?? 'glass_blog')),
        'db_user' => trim((string) ($_POST['db_user'] ?? 'root')),
        'db_pass' => (string) ($_POST['db_pass'] ?? ''),
        'db_prefix' => preg_replace('/[^A-Za-z0-9_]/', '', (string) ($_POST['db_prefix'] ?? '')),
    ];

    if ($db['db_host'] === '' || $db['db_name'] === '' || $db['db_user'] === '') {
        redirectWithError(2, '请填写完整的数据库连接信息。', 'install_db', $db);
    }
    if (!preg_match('/^[A-Za-z0-9_]+$/', $db['db_name'])) {
        redirectWithError(2, '数据库名仅支持字母、数字和下划线。', 'install_db', $db);
    }

    try {
        $dsn = "mysql:host={$db['db_host']};port={$db['db_port']};charset=utf8mb4";
        new PDO($dsn, $db['db_user'], $db['db_pass'], [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_TIMEOUT => 5,
        ]);
    } catch (PDOException $e) {
        redirectWithError(2, '数据库连接失败：' . $e->getMessage(), 'install_db', $db);
    }

    $_SESSION['install_db'] = $db;
    $_SESSION['install_success'] = '数据库连接验证通过。';
    header('Location: index.php?step=3');
    exit;
}

if (isset($_POST['step']) && $_POST['step'] === '3') {
    if (!hasRequiredSg16Extension()) {
        redirectWithError(1, '当前 PHP 环境未安装 SG16 / SourceGuardian Loader，请先安装扩展后再继续安装。');
    }

    $site = [
        'site_title' => trim((string) ($_POST['site_title'] ?? 'Beita Blog')),
        'site_subtitle' => trim((string) ($_POST['site_subtitle'] ?? '')),
        'site_description' => trim((string) ($_POST['site_description'] ?? '')),
        'site_url' => rtrim(trim((string) ($_POST['site_url'] ?? '')), '/'),
        'admin_user' => trim((string) ($_POST['admin_user'] ?? 'admin')),
        'admin_pass' => (string) ($_POST['admin_pass'] ?? ''),
        'admin_pass2' => (string) ($_POST['admin_pass2'] ?? ''),
        'theme_color' => trim((string) ($_POST['theme_color'] ?? '#66CCFF')),
    ];

    if ($site['site_title'] === '') {
        redirectWithError(3, '网站标题不能为空。', 'install_site', $site);
    }
    if ($site['site_url'] === '' || !filter_var($site['site_url'], FILTER_VALIDATE_URL)) {
        redirectWithError(3, '请填写有效的站点 URL。', 'install_site', $site);
    }
    if (strlen($site['admin_user']) < 3) {
        redirectWithError(3, '管理员用户名至少需要 3 个字符。', 'install_site', $site);
    }
    if (strlen($site['admin_pass']) < 6) {
        redirectWithError(3, '管理员密码至少需要 6 个字符。', 'install_site', $site);
    }
    if ($site['admin_pass'] !== $site['admin_pass2']) {
        redirectWithError(3, '两次输入的密码不一致。', 'install_site', $site);
    }
    if (!preg_match('/^[A-Za-z0-9_\-\.]+$/', $site['admin_user'])) {
        redirectWithError(3, '管理员用户名仅支持字母、数字、下划线、中划线和点。', 'install_site', $site);
    }
    if (!preg_match('/^#[0-9a-fA-F]{6}$/', $site['theme_color'])) {
        $site['theme_color'] = '#66CCFF';
    }

    $db = $_SESSION['install_db'] ?? null;
    if (!$db) {
        redirectWithError(2, '数据库配置不存在，请重新填写。');
    }

    $prefix = $db['db_prefix'];

    try {
        $pdo = new PDO(
            "mysql:host={$db['db_host']};port={$db['db_port']};charset=utf8mb4",
            $db['db_user'],
            $db['db_pass'],
            [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
        );

        $pdo->exec("CREATE DATABASE IF NOT EXISTS `{$db['db_name']}` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
        $pdo->exec("USE `{$db['db_name']}`");

        $pdo->exec("CREATE TABLE IF NOT EXISTS `{$prefix}settings` (
            `id` INT AUTO_INCREMENT PRIMARY KEY,
            `setting_key` VARCHAR(100) NOT NULL UNIQUE,
            `setting_value` TEXT
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        installEnsureSettingsSchema($pdo, "{$prefix}settings");

        $pdo->exec("CREATE TABLE IF NOT EXISTS `{$prefix}admin_users` (
            `id` INT AUTO_INCREMENT PRIMARY KEY,
            `username` VARCHAR(50) NOT NULL UNIQUE,
            `password` VARCHAR(255) NOT NULL,
            `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

        $pdo->exec("CREATE TABLE IF NOT EXISTS `{$prefix}navigations` (
            `id` INT AUTO_INCREMENT PRIMARY KEY,
            `title` VARCHAR(100) NOT NULL,
            `url` VARCHAR(255) NOT NULL,
            `target` VARCHAR(20) DEFAULT '_self',
            `sort_order` INT DEFAULT 0,
            `is_active` TINYINT(1) DEFAULT 1
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

        $pdo->exec("CREATE TABLE IF NOT EXISTS `{$prefix}categories` (
            `id` INT AUTO_INCREMENT PRIMARY KEY,
            `name` VARCHAR(100) NOT NULL,
            `parent_id` INT DEFAULT 0,
            `sort_order` INT DEFAULT 0,
            `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

        $pdo->exec("CREATE TABLE IF NOT EXISTS `{$prefix}articles` (
            `id` INT AUTO_INCREMENT PRIMARY KEY,
            `title` VARCHAR(255) NOT NULL,
            `content` LONGTEXT,
            `summary` VARCHAR(500) DEFAULT NULL,
            `thumbnail` VARCHAR(500) DEFAULT NULL,
            `category_id` INT DEFAULT NULL,
            `word_count` INT DEFAULT 0,
            `views` INT DEFAULT 0,
            `status` TINYINT(1) DEFAULT 1,
            `access_type` TINYINT(1) NOT NULL DEFAULT 0,
            `access_password` VARCHAR(255) DEFAULT NULL,
            `access_price` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
            `comments_enabled` TINYINT(1) NOT NULL DEFAULT 1,
            `content_type` VARCHAR(20) NOT NULL DEFAULT 'article',
            `page_slug` VARCHAR(120) DEFAULT NULL,
            `page_template` VARCHAR(40) NOT NULL DEFAULT '',
            `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
            `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            INDEX `idx_status_created` (`status`, `created_at`),
            INDEX `idx_category` (`category_id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

        $pdo->exec("CREATE TABLE IF NOT EXISTS `{$prefix}comments` (
            `id` INT AUTO_INCREMENT PRIMARY KEY,
            `article_id` INT NOT NULL,
            `parent_id` INT NOT NULL DEFAULT 0,
            `author_name` VARCHAR(60) NOT NULL,
            `email` VARCHAR(120) NOT NULL,
            `qq` VARCHAR(20) NOT NULL,
            `website` VARCHAR(255) DEFAULT NULL,
            `content` TEXT NOT NULL,
            `client_ip` VARCHAR(64) DEFAULT '',
            `user_agent` VARCHAR(255) DEFAULT '',
            `status` TINYINT(1) DEFAULT 1,
            `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
            INDEX `idx_article_status_created` (`article_id`, `status`, `created_at`),
            INDEX `idx_parent_created` (`parent_id`, `created_at`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

        $pdo->exec("CREATE TABLE IF NOT EXISTS `{$prefix}article_orders` (
            `id` INT AUTO_INCREMENT PRIMARY KEY,
            `order_no` VARCHAR(64) NOT NULL UNIQUE,
            `article_id` INT NOT NULL,
            `article_title` VARCHAR(255) NOT NULL,
            `buyer_email` VARCHAR(120) DEFAULT '',
            `amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
            `pay_type` VARCHAR(20) DEFAULT '',
            `trade_no` VARCHAR(64) DEFAULT NULL,
            `access_token` VARCHAR(64) NOT NULL,
            `client_ip` VARCHAR(64) DEFAULT '',
            `status` TINYINT(1) DEFAULT 0,
            `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
            `paid_at` DATETIME DEFAULT NULL,
            `notified_at` DATETIME DEFAULT NULL,
            `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            INDEX `idx_article_status` (`article_id`, `status`),
            INDEX `idx_trade_no` (`trade_no`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

        $pdo->exec("CREATE TABLE IF NOT EXISTS `{$prefix}novel_volumes` (
            `id` INT AUTO_INCREMENT PRIMARY KEY,
            `article_id` INT NOT NULL,
            `title` VARCHAR(255) NOT NULL,
            `access_price` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
            `sort_order` INT NOT NULL DEFAULT 0,
            `status` TINYINT(1) NOT NULL DEFAULT 1,
            `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
            `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            INDEX `idx_article_sort` (`article_id`, `sort_order`, `id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

        $pdo->exec("CREATE TABLE IF NOT EXISTS `{$prefix}novel_chapters` (
            `id` INT AUTO_INCREMENT PRIMARY KEY,
            `article_id` INT NOT NULL,
            `volume_id` INT NOT NULL DEFAULT 0,
            `title` VARCHAR(255) NOT NULL,
            `summary` VARCHAR(500) DEFAULT '',
            `content` LONGTEXT NULL,
            `word_count` INT NOT NULL DEFAULT 0,
            `views` INT NOT NULL DEFAULT 0,
            `access_price` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
            `sort_order` INT NOT NULL DEFAULT 0,
            `status` TINYINT(1) NOT NULL DEFAULT 1,
            `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
            `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            INDEX `idx_article_volume_sort` (`article_id`, `volume_id`, `sort_order`, `id`),
            INDEX `idx_article_status` (`article_id`, `status`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

        $pdo->exec("CREATE TABLE IF NOT EXISTS `{$prefix}novel_chapter_orders` (
            `id` INT AUTO_INCREMENT PRIMARY KEY,
            `order_no` VARCHAR(64) NOT NULL UNIQUE,
            `article_id` INT NOT NULL,
            `chapter_id` INT NOT NULL DEFAULT 0,
            `volume_id` INT NOT NULL DEFAULT 0,
            `purchase_scope` VARCHAR(20) NOT NULL DEFAULT 'chapter',
            `chapter_title` VARCHAR(255) NOT NULL,
            `buyer_email` VARCHAR(120) DEFAULT '',
            `amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
            `pay_type` VARCHAR(20) DEFAULT '',
            `trade_no` VARCHAR(64) DEFAULT NULL,
            `access_token` VARCHAR(64) NOT NULL,
            `client_ip` VARCHAR(64) DEFAULT '',
            `status` TINYINT(1) DEFAULT 0,
            `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
            `paid_at` DATETIME DEFAULT NULL,
            `notified_at` DATETIME DEFAULT NULL,
            `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            INDEX `idx_chapter_status` (`chapter_id`, `status`),
            INDEX `idx_volume_status` (`volume_id`, `status`),
            INDEX `idx_article_status` (`article_id`, `status`),
            INDEX `idx_trade_no` (`trade_no`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

        $pdo->exec("CREATE TABLE IF NOT EXISTS `{$prefix}tags` (
            `id` INT AUTO_INCREMENT PRIMARY KEY,
            `name` VARCHAR(50) NOT NULL UNIQUE,
            `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

        $pdo->exec("CREATE TABLE IF NOT EXISTS `{$prefix}article_tags` (
            `article_id` INT NOT NULL,
            `tag_id` INT NOT NULL,
            PRIMARY KEY (`article_id`, `tag_id`),
            INDEX `idx_tag_id` (`tag_id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

        $pdo->exec("CREATE TABLE IF NOT EXISTS `{$prefix}uploaded_files` (
            `id` INT AUTO_INCREMENT PRIMARY KEY,
            `article_id` INT DEFAULT NULL,
            `uploader_name` VARCHAR(120) DEFAULT '',
            `original_name` VARCHAR(255) NOT NULL,
            `stored_name` VARCHAR(255) NOT NULL,
            `relative_path` VARCHAR(255) NOT NULL,
            `file_url` VARCHAR(500) NOT NULL,
            `file_ext` VARCHAR(20) DEFAULT '',
            `mime_type` VARCHAR(120) DEFAULT '',
            `file_size` BIGINT NOT NULL DEFAULT 0,
            `file_kind` VARCHAR(20) DEFAULT 'file',
            `source_context` VARCHAR(50) DEFAULT '',
            `download_count` INT NOT NULL DEFAULT 0,
            `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
            `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            UNIQUE KEY `uniq_relative_path` (`relative_path`),
            INDEX `idx_article_id` (`article_id`),
            INDEX `idx_created_at` (`created_at`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

        $protectedRequiredColumns = [
            "{$prefix}settings" => ['setting_key'],
            "{$prefix}admin_users" => ['username', 'password'],
            "{$prefix}navigations" => ['title', 'url'],
            "{$prefix}categories" => ['name'],
            "{$prefix}articles" => ['title'],
            "{$prefix}comments" => ['article_id', 'author_name', 'email', 'qq', 'content'],
            "{$prefix}article_orders" => ['order_no', 'article_id', 'article_title', 'access_token'],
            "{$prefix}novel_volumes" => ['article_id', 'title'],
            "{$prefix}novel_chapters" => ['article_id', 'title'],
            "{$prefix}novel_chapter_orders" => ['order_no', 'article_id', 'chapter_title', 'access_token'],
            "{$prefix}tags" => ['name'],
            "{$prefix}article_tags" => ['article_id', 'tag_id'],
            "{$prefix}uploaded_files" => ['original_name', 'stored_name', 'relative_path', 'file_url'],
        ];
        foreach ($protectedRequiredColumns as $tableName => $columns) {
            installRelaxLegacyRequiredColumns($pdo, $tableName, $columns);
        }

        $adminStmt = $pdo->prepare(
            "INSERT INTO `{$prefix}admin_users` (`username`, `password`)
             VALUES (?, ?)
             ON DUPLICATE KEY UPDATE `password` = VALUES(`password`)"
        );
        $adminStmt->execute([$site['admin_user'], password_hash($site['admin_pass'], PASSWORD_DEFAULT)]);

        $defaults = [
            'app_version' => defined('BTBLOG_VERSION') ? BTBLOG_VERSION : '2.0',
            'site_title' => $site['site_title'],
            'site_subtitle' => $site['site_subtitle'],
            'site_description' => $site['site_description'],
            'site_keywords' => '博客,PHP,技术,生活',
            'icp_number' => '',
            'police_number' => '',
            'hero_image' => '',
            'hero_title' => $site['site_title'],
            'hero_subtitle' => $site['site_subtitle'] !== '' ? $site['site_subtitle'] : '在这里记录每一个值得分享的瞬间。',
            'hero_use_api' => '0',
            'hero_api_url' => 'https://v1.hitokoto.cn/?encode=text',
            'hero_height' => '85vh',
            'hero_min_height' => '500px',
            'html_player' => '',
            'font_family' => "'Microsoft YaHei', 'PingFang SC', sans-serif",
            'custom_font_url' => '',
            'articles_per_page' => '12',
            'home_articles_limit' => '6',
            'home_pagination_mode' => 'pages',
            'frontend_style' => 'aurora',
            'admin_style' => 'panel',
            'site_theme' => 'default',
            'default_theme_mode' => 'light',
            'theme_color' => $site['theme_color'],
            'theme_color_secondary' => '',
            'bg_darkness' => '50',
            'card_opacity' => '8',
            'card_blur' => '24',
            'card_radius' => '24',
            'glow_intensity' => '14',
            'hero_overlay' => '55',
            'max_width' => '1200',
            'card_thumb_height' => '220',
            'navbar_blur' => '30',
            'enable_animations' => '1',
            'text_glow' => '1',
            'site_comment_enabled' => '1',
            'comment_submit_interval' => '60',
            'comment_notify_enabled' => '0',
            'comment_notify_email' => '',
            'comment_require_geetest' => '0',
            'geetest_captcha_id' => '',
            'geetest_captcha_key' => '',
            'epay_gateway_url' => '',
            'epay_pid' => '',
            'epay_key' => '',
            'smtp_host' => '',
            'smtp_port' => '465',
            'smtp_secure' => 'ssl',
            'smtp_user' => '',
            'smtp_pass' => '',
            'smtp_from_email' => '',
            'smtp_from_name' => $site['site_title'],
            'article_copyright_owner' => $site['site_title'],
            'article_license_name' => '知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议',
            'article_license_url' => 'https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh-hans',
            'default_article_thumbnails' => '',
            'admin_login_max_attempts' => '5',
            'admin_login_block_minutes' => '600',
            'sitemap_auto_enabled' => '0',
            'sitemap_auto_interval_hours' => '24',
            'sitemap_last_generated_at' => '',
        ];

        $settingsTable = "{$prefix}settings";
        $settingsColumns = installDbColumns($pdo, $settingsTable);
        $legacyKeyColumn = isset($settingsColumns['key_name']) ? 'key_name' : '';
        $legacyValueColumn = '';
        foreach (['key_value', 'value'] as $candidateColumn) {
            if (isset($settingsColumns[$candidateColumn])) {
                $legacyValueColumn = $candidateColumn;
                break;
            }
        }

        $insertColumns = ['`setting_key`', '`setting_value`'];
        $insertPlaceholders = ['?', '?'];
        if ($legacyKeyColumn !== '') {
            $insertColumns[] = '`' . $legacyKeyColumn . '`';
            $insertPlaceholders[] = '?';
        }
        if ($legacyValueColumn !== '') {
            $insertColumns[] = '`' . $legacyValueColumn . '`';
            $insertPlaceholders[] = '?';
        }

        $updateParts = ['`setting_value` = ?'];
        if ($legacyValueColumn !== '') {
            $updateParts[] = '`' . $legacyValueColumn . '` = ?';
        }

        $settingLookupStmt = $pdo->prepare("SELECT COUNT(*) FROM `$settingsTable` WHERE `setting_key` = ?");
        $settingInsertStmt = $pdo->prepare(
            "INSERT INTO `$settingsTable` (" . implode(', ', $insertColumns) . ")
             VALUES (" . implode(', ', $insertPlaceholders) . ")"
        );
        $settingUpdateStmt = $pdo->prepare("UPDATE `$settingsTable` SET " . implode(', ', $updateParts) . " WHERE `setting_key` = ?");
        foreach ($defaults as $key => $value) {
            $settingLookupStmt->execute([$key]);
            if ((int) $settingLookupStmt->fetchColumn() > 0) {
                $updateValues = [$value];
                if ($legacyValueColumn !== '') {
                    $updateValues[] = $value;
                }
                $updateValues[] = $key;
                $settingUpdateStmt->execute($updateValues);
            } else {
                $insertValues = [$key, $value];
                if ($legacyKeyColumn !== '') {
                    $insertValues[] = $key;
                }
                if ($legacyValueColumn !== '') {
                    $insertValues[] = $value;
                }
                $settingInsertStmt->execute($insertValues);
            }
        }

        $commonPath = dirname(__DIR__) . '/common';
        if (!is_dir($commonPath)) {
            mkdir($commonPath, 0755, true);
        }

        $configPath = $commonPath . '/config.php';
        if (file_put_contents($configPath, buildConfigContent($db, $site['site_url'])) === false) {
            throw new RuntimeException('无法写入 common/config.php 文件。');
        }

        foreach ([
            dirname(__DIR__) . '/uploads/',
            dirname(__DIR__) . '/uploads/images/',
            dirname(__DIR__) . '/uploads/audio/',
            dirname(__DIR__) . '/uploads/video/',
            dirname(__DIR__) . '/uploads/files/',
            dirname(__DIR__) . '/common/storage/',
            dirname(__DIR__) . '/common/storage/backups/',
        ] as $dir) {
            if (!is_dir($dir)) {
                mkdir($dir, 0755, true);
            }
        }

        $_SESSION['install_finalize_payload'] = [
            'installed_at' => date('Y-m-d H:i:s'),
            'admin_user' => $site['admin_user'],
            'db_name' => $db['db_name'],
            'site_url' => $site['site_url'],
        ];
        unset($_SESSION['install_runtime_probe_passed']);

        $_SESSION['install_site'] = $site;
        $_SESSION['install_success'] = '基础安装已完成，正在检查加密后的运行环境。';
        header('Location: index.php?step=4');
        exit;
    } catch (Throwable $e) {
        redirectWithError(3, '安装失败：' . $e->getMessage(), 'install_site', $site);
    }
}

header('Location: index.php');
exit;
