<?php
/**
 * 模板套件系统 (Template Pack)
 * 每套模板以独立文件夹存放在 templates/ 目录下，
 * 包含 manifest.json + homepage.html + article.html + archive.html + header.html + footer.html + style.css + script.js
 */

function template_packs_base_dir()
{
    return dirname(__DIR__) . DIRECTORY_SEPARATOR . 'templates';
}

function template_pack_dir($packCode, $packType = null)
{
    $packCode = trim((string)$packCode);
    // 支持复合格式 "pack_type:code"（如 "variation:fangchan01"）
    if ($packType === null && strpos($packCode, ':') !== false) {
        $parts = explode(':', $packCode, 2);
        if (in_array($parts[0], array('variation', 'standard', 'page'))) {
            $packType = $parts[0];
            $packCode = $parts[1];
        }
    }
    if ($packCode === '' || preg_match('/[\/\\\\\.]{2,}/', $packCode)) {
        return '';
    }
    $baseDir = template_packs_base_dir();
    // 指定类型时直接返回子目录路径
    if ($packType !== null && in_array($packType, array('variation', 'standard', 'page'))) {
        return $baseDir . DIRECTORY_SEPARATOR . $packType . DIRECTORY_SEPARATOR . $packCode;
    }
    // 未指定类型时搜索三个子目录
    foreach (array('variation', 'standard', 'page') as $_t) {
        $dir = $baseDir . DIRECTORY_SEPARATOR . $_t . DIRECTORY_SEPARATOR . $packCode;
        if (is_dir($dir)) {
            return $dir;
        }
    }
    // 向后兼容：检查平铺目录（迁移前）
    $flatDir = $baseDir . DIRECTORY_SEPARATOR . $packCode;
    if (is_dir($flatDir)) {
        return $flatDir;
    }
    // 未找到时默认返回 variation 子目录路径
    return $baseDir . DIRECTORY_SEPARATOR . 'variation' . DIRECTORY_SEPARATOR . $packCode;
}

function template_pack_manifest_path($packCode)
{
    $dir = template_pack_dir($packCode);
    return $dir !== '' ? $dir . DIRECTORY_SEPARATOR . 'manifest.json' : '';
}

function load_template_pack_manifest($packCode)
{
    static $cache = array();
    $cacheKey = (string)$packCode;
    if (array_key_exists($cacheKey, $cache)) {
        return $cache[$cacheKey];
    }
    $path = template_pack_manifest_path($packCode);
    if ($path === '' || !is_file($path)) {
        $cache[$cacheKey] = null;
        return null;
    }
    $json = @file_get_contents($path);
    if ($json === false) {
        $cache[$cacheKey] = null;
        return null;
    }
    $data = json_decode($json, true);
    if (!is_array($data) || empty($data['code'])) {
        $cache[$cacheKey] = null;
        return null;
    }
    $cache[$cacheKey] = $data;
    return $data;
}

function list_template_packs()
{
    $baseDir = template_packs_base_dir();
    if (!is_dir($baseDir)) {
        return array();
    }
    $packs = array();
    $seenCodes = array();
    // 扫描 variation/ 和 standard/ 子目录
    foreach (array('variation', 'standard', 'page') as $_type) {
        $typeDir = $baseDir . DIRECTORY_SEPARATOR . $_type;
        if (!is_dir($typeDir)) { continue; }
        $dirs = @scandir($typeDir);
        if (!is_array($dirs)) { continue; }
        foreach ($dirs as $entry) {
            if ($entry === '.' || $entry === '..') { continue; }
            $entryPath = $typeDir . DIRECTORY_SEPARATOR . $entry;
            if (!is_dir($entryPath)) { continue; }
            $manifestFile = $entryPath . DIRECTORY_SEPARATOR . 'manifest.json';
            if (!is_file($manifestFile)) { continue; }
            $json = @file_get_contents($manifestFile);
            if ($json === false) { continue; }
            $manifest = json_decode($json, true);
            if (!is_array($manifest) || empty($manifest['code'])) { continue; }
            $manifest['_dir'] = $entry;
            $manifest['_path'] = $entryPath;
            $packs[] = $manifest;
            $seenCodes[$_type . '/' . $entry] = true;
        }
    }
    // 向后兼容：扫描平铺目录中尚未迁移的套件
    $flatDirs = @scandir($baseDir);
    if (is_array($flatDirs)) {
        $skip = array('.', '..', 'variation', 'standard', 'page');
        foreach ($flatDirs as $entry) {
            if (in_array($entry, $skip)) { continue; }
            $entryPath = $baseDir . DIRECTORY_SEPARATOR . $entry;
            if (!is_dir($entryPath)) { continue; }
            if (isset($seenCodes[$entry])) { continue; }
            $manifest = load_template_pack_manifest($entry);
            if (!$manifest) { continue; }
            $manifest['_dir'] = $entry;
            $manifest['_path'] = $entryPath;
            $packs[] = $manifest;
        }
    }
    // 为缺少 serial_no 或 pack_type 的模板自动补全并保存
    $maxSerial = 0;
    foreach ($packs as $p) {
        if (isset($p['serial_no']) && (int)$p['serial_no'] > $maxSerial) {
            $maxSerial = (int)$p['serial_no'];
        }
    }
    foreach ($packs as &$p) {
        $needSave = false;
        if (!isset($p['serial_no']) || (int)$p['serial_no'] <= 0) {
            $maxSerial++;
            $p['serial_no'] = $maxSerial;
            $needSave = true;
        }
        if (!isset($p['pack_type']) || $p['pack_type'] === '') {
            $p['pack_type'] = 'variation';
            $needSave = true;
        }
        if ($needSave) {
            $code = isset($p['code']) ? $p['code'] : (isset($p['_dir']) ? $p['_dir'] : '');
            if ($code !== '') {
                $saveData = $p;
                unset($saveData['_dir'], $saveData['_path']);
                save_template_pack_manifest($code, $saveData);
            }
        }
    }
    unset($p);
    // 按编号降序排列（最新在前）
    usort($packs, function ($a, $b) {
        $aNo = isset($a['serial_no']) ? (int)$a['serial_no'] : 0;
        $bNo = isset($b['serial_no']) ? (int)$b['serial_no'] : 0;
        return $bNo - $aNo;
    });
    return $packs;
}

function template_pack_serial_label($serialNo, $packType = 'variation')
{
    $prefixMap = array('standard' => 'beeai-c', 'page' => 'beeai-p', 'variation' => 'beeai-b');
    $prefix = isset($prefixMap[$packType]) ? $prefixMap[$packType] : 'beeai-b';
    return $prefix . str_pad((string)(int)$serialNo, 3, '0', STR_PAD_LEFT);
}

/**
 * 按类型筛选模板套件
 * @param string $type  'variation' | 'standard' | '' (all)
 */
function list_template_packs_by_type($type = '')
{
    $packs = list_template_packs();
    if ($type === '') {
        return $packs;
    }
    $filtered = array();
    foreach ($packs as $p) {
        $pt = isset($p['pack_type']) ? (string)$p['pack_type'] : 'variation';
        if ($pt === $type) {
            $filtered[] = $p;
        }
    }
    return $filtered;
}

function list_template_pack_options($excludeTypes = array())
{
    $packs = list_template_packs();
    $options = array();
    foreach ($packs as $pack) {
        $code = isset($pack['code']) ? (string)$pack['code'] : '';
        if ($code === '') {
            continue;
        }
        $serialNo = isset($pack['serial_no']) ? (int)$pack['serial_no'] : 0;
        $packType = isset($pack['pack_type']) ? (string)$pack['pack_type'] : 'variation';
        if (!empty($excludeTypes) && in_array($packType, $excludeTypes, true)) {
            continue;
        }
        $options[] = array(
            'code' => $code,
            'value' => $packType . ':' . $code,
            'name' => isset($pack['name']) ? (string)$pack['name'] : $code,
            'serial_no' => $serialNo,
            'pack_type' => $packType,
            'serial_label' => template_pack_serial_label($serialNo, $packType),
            'description' => isset($pack['description']) ? (string)$pack['description'] : '',
            'version' => isset($pack['version']) ? (string)$pack['version'] : '',
            'screenshot' => isset($pack['screenshot']) ? (string)$pack['screenshot'] : ''
        );
    }
    return $options;
}

function template_pack_file_path($packCode, $fileKey)
{
    $manifest = load_template_pack_manifest($packCode);
    if (!$manifest) {
        return '';
    }
    $files = isset($manifest['files']) ? (array)$manifest['files'] : array();
    $filename = isset($files[$fileKey]) ? trim((string)$files[$fileKey]) : '';
    if ($filename === '') {
        return '';
    }
    $dir = template_pack_dir($packCode);
    $fullPath = $dir . DIRECTORY_SEPARATOR . $filename;
    return is_file($fullPath) ? $fullPath : '';
}

function load_template_pack_file($packCode, $fileKey)
{
    static $cache = array();
    $cacheKey = (string)$packCode . '|' . (string)$fileKey;
    if (array_key_exists($cacheKey, $cache)) {
        return $cache[$cacheKey];
    }
    $path = template_pack_file_path($packCode, $fileKey);
    if ($path === '') {
        $cache[$cacheKey] = '';
        return '';
    }
    $content = @file_get_contents($path);
    $result = $content !== false ? $content : '';
    $cache[$cacheKey] = $result;
    return $result;
}

function save_template_pack_file($packCode, $fileKey, $content)
{
    require_license_guard('保存模板套件文件', false, 'template_manage');
    $manifest = load_template_pack_manifest($packCode);
    if (!$manifest) {
        throw new Exception('未找到模板套件: ' . $packCode);
    }
    $files = isset($manifest['files']) ? (array)$manifest['files'] : array();
    $filename = isset($files[$fileKey]) ? trim((string)$files[$fileKey]) : '';
    if ($filename === '') {
        throw new Exception('模板套件 ' . $packCode . ' 中未定义文件: ' . $fileKey);
    }
    $dir = template_pack_dir($packCode);
    $fullPath = $dir . DIRECTORY_SEPARATOR . $filename;
    if (file_put_contents($fullPath, $content) === false) {
        throw new Exception('写入文件失败: ' . $fullPath);
    }
}

function save_template_pack_manifest($packCode, $manifest)
{
    require_license_guard('保存模板套件信息', false, 'template_manage');
    $dir = template_pack_dir($packCode);
    if ($dir === '' || !is_dir($dir)) {
        throw new Exception('模板套件目录不存在: ' . $packCode);
    }
    $path = $dir . DIRECTORY_SEPARATOR . 'manifest.json';
    $json = json_encode($manifest, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
    if (file_put_contents($path, $json) === false) {
        throw new Exception('写入 manifest.json 失败');
    }
}

function template_pack_file_keys()
{
    return array(
        'homepage' => '首页模板',
        'article'  => '文章页模板',
        'archive'  => '栏目页模板',
        'header'   => '页眉模板',
        'footer'   => '页脚模板',
        'css'      => '共享样式 (CSS)',
        'js'       => '共享脚本 (JS)'
    );
}

function render_template_pack_page($packCode, $pageType, $variables)
{
    $htmlContent = load_template_pack_file($packCode, $pageType);
    if ($htmlContent === '') {
        return '';
    }

    $headerHtml = load_template_pack_file($packCode, 'header');
    $footerHtml = load_template_pack_file($packCode, 'footer');
    $cssContent = load_template_pack_file($packCode, 'css');
    $jsContent = load_template_pack_file($packCode, 'js');

    if (trim($headerHtml) !== '') {
        $htmlContent = str_replace('{{header}}', $headerHtml, $htmlContent);
    }
    if (trim($footerHtml) !== '') {
        $htmlContent = str_replace('{{footer}}', $footerHtml, $htmlContent);
    }

    return render_template_html($htmlContent, $cssContent, $variables, $jsContent);
}

function create_template_pack($code, $name, $description, $packType = 'variation')
{
    require_license_guard('创建模板套件', false, 'template_manage');
    $code = trim((string)$code);
    if ($code === '' || !preg_match('/^[a-z0-9][a-z0-9\-]*$/', $code)) {
        throw new Exception('模板套件编码只能包含小写字母、数字和短横线，且不能为空。');
    }
    // 检查编码在所有子目录中是否已被占用
    $existingDir = template_pack_dir($code);
    if (is_dir($existingDir)) {
        throw new Exception('编码「' . $code . '」已被占用，请更换一个编码。');
    }
    if (!in_array($packType, array('variation', 'standard', 'page'))) {
        $packType = 'variation';
    }
    $dir = template_pack_dir($code, $packType);
    $inputName = trim((string)$name);
    if ($inputName !== '') {
        $existingPacks = list_template_packs();
        foreach ($existingPacks as $_ep) {
            if (isset($_ep['name']) && $_ep['name'] === $inputName) {
                throw new Exception('套件名称「' . $inputName . '」已存在，请更换一个名称。');
            }
        }
    }
    if (!@mkdir($dir, 0777, true)) {
        throw new Exception('创建模板套件目录失败: ' . $dir);
    }

    // 计算下一个序号（同类型内独立编号）
    $existingPacksForSerial = list_template_packs();
    $nextSerial = 1;
    foreach ($existingPacksForSerial as $_sp) {
        $spType = isset($_sp['pack_type']) ? (string)$_sp['pack_type'] : 'variation';
        if ($spType === $packType && isset($_sp['serial_no']) && (int)$_sp['serial_no'] >= $nextSerial) {
            $nextSerial = (int)$_sp['serial_no'] + 1;
        }
    }

    // 页面模板与套件的文件结构不同
    if ($packType === 'page') {
        $manifest = array(
            'name' => trim((string)$name) !== '' ? trim((string)$name) : $code,
            'code' => $code,
            'version' => '1.0.0',
            'pack_type' => 'page',
            'serial_no' => $nextSerial,
            'description' => trim((string)$description),
            'author' => '',
            'screenshot' => 'screenshot.png',
            'files' => array(
                'page' => 'page.html',
                'header' => 'header.html',
                'footer' => 'footer.html',
                'css' => 'style.css',
                'js' => 'script.js'
            )
        );
        save_template_pack_manifest($code, $manifest);
        $defaultFiles = array(
            'page.html' => "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>{{seo_title}}</title>\n    <meta name=\"keywords\" content=\"{{seo_keywords}}\">\n    <meta name=\"description\" content=\"{{seo_description}}\">\n</head>\n<body>\n{{header}}\n<main>\n    <h1>{{title}}</h1>\n    <div class=\"page-content\">{{content}}</div>\n</main>\n{{footer}}\n</body>\n</html>",
            'header.html' => "<header>\n    <a href=\"/\">{{site_name}}</a>\n    <nav>{{menu_nav}}</nav>\n</header>",
            'footer.html' => "<footer>\n    <p>&copy; {{year}} {{site_name}} · {{footer_text}}</p>\n</footer>",
            'style.css' => "/* " . $code . " 页面模板样式 */\n.page-content { max-width: 960px; margin: 0 auto; padding: 40px 20px; }\n.page-content h1 { text-align: center; margin-bottom: 30px; }",
            'script.js' => "/* " . $code . " 页面模板脚本 */\n"
        );
        foreach ($defaultFiles as $filename => $fileContent) {
            file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $fileContent);
        }
        return $manifest;
    }

    $manifest = array(
        'name' => trim((string)$name) !== '' ? trim((string)$name) : $code,
        'code' => $code,
        'version' => '1.0.0',
        'pack_type' => $packType,
        'serial_no' => $nextSerial,
        'description' => trim((string)$description),
        'author' => '',
        'screenshot' => 'screenshot.png',
        'files' => array(
            'homepage' => 'homepage.html',
            'article' => 'article.html',
            'archive' => 'archive.html',
            'header' => 'header.html',
            'footer' => 'footer.html',
            'css' => 'style.css',
            'js' => 'script.js'
        )
    );

    save_template_pack_manifest($code, $manifest);

    $defaultFiles = array(
        'homepage.html' => "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>{{seo_title}}</title>\n    <meta name=\"keywords\" content=\"{{seo_keywords}}\">\n    <meta name=\"description\" content=\"{{seo_description}}\">\n</head>\n<body>\n{{header}}\n<main>\n    <h1>{{hero_title}}</h1>\n    <p>{{hero_subtitle}}</p>\n</main>\n{{footer}}\n</body>\n</html>",
        'article.html' => "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>{{seo_title}}</title>\n    <meta name=\"keywords\" content=\"{{seo_keywords}}\">\n    <meta name=\"description\" content=\"{{seo_description}}\">\n</head>\n<body>\n{{header}}\n<main>\n    {{breadcrumb}}\n    <article>\n        <h1>{{title}}</h1>\n        <div>{{content}}</div>\n    </article>\n</main>\n{{footer}}\n</body>\n</html>",
        'archive.html' => "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>{{seo_title}}</title>\n    <meta name=\"keywords\" content=\"{{seo_keywords}}\">\n    <meta name=\"description\" content=\"{{seo_description}}\">\n</head>\n<body>\n{{header}}\n<main>\n    {{breadcrumb}}\n    {{archive_content}}\n    {{archive_sidebar}}\n</main>\n{{footer}}\n</body>\n</html>",
        'header.html' => "<header>\n    <a href=\"/\">{{site_name}}</a>\n    <nav>{{menu_nav}}</nav>\n</header>",
        'footer.html' => "<footer>\n    <p>&copy; {{year}} {{site_name}} · {{footer_text}}</p>\n</footer>",
        'style.css' => "/* " . $code . " 共享样式 */\nbody { margin: 0; font-family: \"PingFang SC\", \"Microsoft YaHei\", sans-serif; }",
        'script.js' => "/* " . $code . " 共享脚本 */\n"
    );

    foreach ($defaultFiles as $filename => $fileContent) {
        $filePath = $dir . DIRECTORY_SEPARATOR . $filename;
        file_put_contents($filePath, $fileContent);
    }

    return $manifest;
}

function delete_template_pack($code)
{
    require_license_guard('删除模板套件', false, 'template_manage');
    $code = trim((string)$code);
    $dir = template_pack_dir($code);
    if ($dir === '' || !is_dir($dir)) {
        throw new Exception('模板套件目录不存在: ' . $code);
    }
    $files = @scandir($dir);
    if (is_array($files)) {
        foreach ($files as $file) {
            if ($file === '.' || $file === '..') {
                continue;
            }
            $filePath = $dir . DIRECTORY_SEPARATOR . $file;
            if (is_file($filePath)) {
                @unlink($filePath);
            }
        }
    }
    if (!@rmdir($dir)) {
        throw new Exception('删除模板套件目录失败: ' . $dir);
    }
    if (function_exists('db_is_ready') && db_is_ready() && function_exists('table_exists') && table_exists('templates')) {
        $pdo = db_connection();
        $pdo->prepare('DELETE FROM templates WHERE code = :code')->execute(array(':code' => $code));
    }
}

function upload_template_pack_zip($uploadedFile, $packType = 'variation')
{
    require_license_guard('上传模板套件', false, 'template_manage');

    if (!isset($uploadedFile['tmp_name']) || !is_uploaded_file($uploadedFile['tmp_name'])) {
        throw new Exception('请选择要上传的 ZIP 文件。');
    }
    if ($uploadedFile['error'] !== UPLOAD_ERR_OK) {
        $errMap = array(
            UPLOAD_ERR_INI_SIZE => '文件超过服务器允许的最大上传大小',
            UPLOAD_ERR_FORM_SIZE => '文件超过表单允许的最大上传大小',
            UPLOAD_ERR_PARTIAL => '文件仅部分上传',
            UPLOAD_ERR_NO_FILE => '未选择文件',
        );
        throw new Exception(isset($errMap[$uploadedFile['error']]) ? $errMap[$uploadedFile['error']] : '上传失败（错误码 ' . $uploadedFile['error'] . '）');
    }

    $maxSize = 50 * 1024 * 1024;
    if ($uploadedFile['size'] > $maxSize) {
        throw new Exception('文件大小超过限制（最大 50MB）。');
    }

    $ext = strtolower(pathinfo($uploadedFile['name'], PATHINFO_EXTENSION));
    if ($ext !== 'zip') {
        throw new Exception('仅支持 .zip 格式的压缩包。');
    }

    if (!class_exists('ZipArchive')) {
        throw new Exception('服务器未安装 PHP zip 扩展，无法解压。');
    }

    $zip = new ZipArchive();
    $openResult = $zip->open($uploadedFile['tmp_name']);
    if ($openResult !== true) {
        throw new Exception('无法打开 ZIP 文件（错误码 ' . $openResult . '），请确认文件未损坏。');
    }

    $manifestContent = null;
    $manifestPrefix = '';

    for ($i = 0; $i < $zip->numFiles; $i++) {
        $entryName = $zip->getNameIndex($i);
        $basename = basename($entryName);
        if ($basename === 'manifest.json') {
            $content = $zip->getFromIndex($i);
            if ($content !== false) {
                $decoded = json_decode($content, true);
                if (is_array($decoded) && !empty($decoded['code'])) {
                    $manifestContent = $decoded;
                    $dir = dirname($entryName);
                    $manifestPrefix = ($dir === '.' || $dir === '') ? '' : $dir . '/';
                    break;
                }
            }
        }
    }

    if ($manifestContent === null) {
        $zip->close();
        throw new Exception('ZIP 中未找到有效的 manifest.json 文件（需包含 code 字段）。');
    }

    $code = trim((string)$manifestContent['code']);
    if (!preg_match('/^[a-z0-9][a-z0-9\-]*$/', $code)) {
        $zip->close();
        throw new Exception('manifest.json 中的 code 字段不合法（仅允许小写字母、数字和短横线）：' . $code);
    }

    if (empty($manifestContent['files']) || !is_array($manifestContent['files'])) {
        $zip->close();
        throw new Exception('manifest.json 中缺少 files 字段或格式不正确。');
    }

    $requiredKeys = array('homepage', 'article', 'archive', 'header', 'footer');
    $files = $manifestContent['files'];
    $missingFiles = array();
    $fileKeyLabels = template_pack_file_keys();

    foreach ($requiredKeys as $rk) {
        if (empty($files[$rk])) {
            $label = isset($fileKeyLabels[$rk]) ? $fileKeyLabels[$rk] : $rk;
            $missingFiles[] = $label . ' (' . $rk . ')';
            continue;
        }
        $filename = $files[$rk];
        $zipPath = $manifestPrefix . $filename;
        if ($zip->locateName($zipPath) === false) {
            $label = isset($fileKeyLabels[$rk]) ? $fileKeyLabels[$rk] : $rk;
            $missingFiles[] = $label . ' → ' . $filename;
        }
    }

    if (!empty($missingFiles)) {
        $zip->close();
        throw new Exception('ZIP 中缺少必需的模板文件：' . implode('、', $missingFiles));
    }

    // 检查编码在所有子目录中是否已被占用
    $existCheck = template_pack_dir($code);
    $isOverwrite = is_dir($existCheck);

    if ($isOverwrite) {
        $zip->close();
        throw new Exception('编码「' . $code . '」已被占用，无法上传。如需更新请先删除已有套件再重新上传。');
    }

    // 按用户选择的 pack_type 创建子目录路径
    if (!in_array($packType, array('variation', 'standard'))) { $packType = 'variation'; }
    $targetDir = template_pack_dir($code, $packType);

    $uploadName = isset($manifestContent['name']) ? trim((string)$manifestContent['name']) : '';
    if ($uploadName !== '') {
        $existingPacks = list_template_packs();
        foreach ($existingPacks as $_ep) {
            if (isset($_ep['name']) && $_ep['name'] === $uploadName) {
                $zip->close();
                throw new Exception('套件名称「' . $uploadName . '」已存在，请修改 manifest.json 中的 name 后重新打包上传。');
            }
        }
    }

    if (!@mkdir($targetDir, 0777, true)) {
        $zip->close();
        throw new Exception('创建模板套件目录失败: ' . $code);
    }

    $extractedCount = 0;
    $allowedExtensions = array('html', 'htm', 'css', 'js', 'json', 'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'txt', 'md', 'woff', 'woff2', 'ttf', 'eot', 'otf');

    for ($i = 0; $i < $zip->numFiles; $i++) {
        $entryName = $zip->getNameIndex($i);

        if ($manifestPrefix !== '' && strpos($entryName, $manifestPrefix) !== 0) {
            continue;
        }

        $relativePath = $manifestPrefix !== '' ? substr($entryName, strlen($manifestPrefix)) : $entryName;

        if ($relativePath === '' || $relativePath === false) {
            continue;
        }

        if (substr($entryName, -1) === '/') {
            $subDir = $targetDir . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, rtrim($relativePath, '/'));
            if (!is_dir($subDir)) {
                @mkdir($subDir, 0777, true);
            }
            continue;
        }

        if (strpos($relativePath, '..') !== false) {
            continue;
        }

        $fileExt = strtolower(pathinfo($relativePath, PATHINFO_EXTENSION));
        if (!in_array($fileExt, $allowedExtensions)) {
            continue;
        }

        $content = $zip->getFromIndex($i);
        if ($content === false) {
            continue;
        }

        $destPath = $targetDir . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relativePath);
        $destDir = dirname($destPath);
        if (!is_dir($destDir)) {
            @mkdir($destDir, 0777, true);
        }

        file_put_contents($destPath, $content);
        $extractedCount++;
    }

    $zip->close();

    if ($isOverwrite && isset($backupDir) && is_dir($backupDir)) {
        _remove_directory_recursive($backupDir);
    }

    // 将用户选择的 pack_type 写入 manifest（覆盖 ZIP 中原有的值）
    $manifestContent['pack_type'] = $packType;
    // 自动分配 serial_no（如果 manifest 中没有）
    if (!isset($manifestContent['serial_no']) || (int)$manifestContent['serial_no'] <= 0) {
        $existingPacks = list_template_packs();
        $maxSerial = 0;
        foreach ($existingPacks as $_ep) {
            if (isset($_ep['serial_no']) && (int)$_ep['serial_no'] > $maxSerial) { $maxSerial = (int)$_ep['serial_no']; }
        }
        $manifestContent['serial_no'] = $maxSerial + 1;
    }
    $saveManifest = $manifestContent;
    unset($saveManifest['_dir'], $saveManifest['_path']);
    $manifestJson = json_encode($saveManifest, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
    file_put_contents($targetDir . DIRECTORY_SEPARATOR . 'manifest.json', $manifestJson);

    $name = isset($manifestContent['name']) ? (string)$manifestContent['name'] : $code;
    return array(
        'code' => $code,
        'name' => $name,
        'files_count' => $extractedCount,
        'is_overwrite' => $isOverwrite
    );
}

function _remove_directory_recursive($dir)
{
    if (!is_dir($dir)) {
        return;
    }
    $entries = @scandir($dir);
    if (!is_array($entries)) {
        return;
    }
    foreach ($entries as $entry) {
        if ($entry === '.' || $entry === '..') {
            continue;
        }
        $path = $dir . DIRECTORY_SEPARATOR . $entry;
        if (is_dir($path)) {
            _remove_directory_recursive($path);
        } else {
            @unlink($path);
        }
    }
    @rmdir($dir);
}

function ensure_template_packs_dir()
{
    $dir = template_packs_base_dir();
    if (!is_dir($dir)) {
        @mkdir($dir, 0777, true);
    }
    // 确保 variation/ 和 standard/ 子目录存在
    foreach (array('variation', 'standard', 'page') as $_t) {
        $subDir = $dir . DIRECTORY_SEPARATOR . $_t;
        if (!is_dir($subDir)) {
            @mkdir($subDir, 0777, true);
        }
    }
    // 自动迁移：将平铺在 templates/ 下的旧套件移入对应子目录
    $entries = @scandir($dir);
    if (!is_array($entries)) { return; }
    $skip = array('.', '..', 'variation', 'standard', 'page');
    foreach ($entries as $entry) {
        if (in_array($entry, $skip)) { continue; }
        $entryPath = $dir . DIRECTORY_SEPARATOR . $entry;
        if (!is_dir($entryPath)) { continue; }
        $manifestFile = $entryPath . DIRECTORY_SEPARATOR . 'manifest.json';
        if (!is_file($manifestFile)) { continue; }
        $json = @file_get_contents($manifestFile);
        $data = $json !== false ? json_decode($json, true) : null;
        $type = (is_array($data) && isset($data['pack_type']) && $data['pack_type'] === 'standard') ? 'standard' : 'variation';
        $dest = $dir . DIRECTORY_SEPARATOR . $type . DIRECTORY_SEPARATOR . $entry;
        if (is_dir($dest)) { continue; }
        @rename($entryPath, $dest);
    }
}

function domain_template_pack($domain)
{
    return isset($domain['template_pack']) ? trim((string)$domain['template_pack']) : '';
}


function initialize_builtin_template_packs()
{
    ensure_template_packs_dir();
}

// ────────────────────────────────────────────────────────────────
// 云端模板市场 (Template Cloud Market)
// ────────────────────────────────────────────────────────────────

define('TEMPLATE_CLOUD_BASE', 'https://cloud.beeai.chat');

function cloud_http_get($url, $timeout = 10)
{
    $parsed = parse_url($url);
    $host   = isset($parsed['host']) ? $parsed['host'] : '';
    $scheme = isset($parsed['scheme']) ? $parsed['scheme'] : 'https';
    $port   = isset($parsed['port']) ? (int)$parsed['port'] : ($scheme === 'https' ? 443 : 80);

    $localIps = array('127.0.0.1', '::1');
    $serverIp = isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : '';
    if ($serverIp !== '') {
        $localIps[] = $serverIp;
    }
    $resolved = ($host !== '') ? @gethostbyname($host) : '';
    $isLocal  = ($resolved !== '' && $resolved !== $host && in_array($resolved, array_unique($localIps)));

    $ua = 'Mozilla/5.0 (compatible; BeeAI/1.0; +https://beeai.com)';

    if (function_exists('curl_init')) {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
        curl_setopt($ch, CURLOPT_USERAGENT, $ua);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_MAXREDIRS, 3);
        if ($isLocal) {
            curl_setopt($ch, CURLOPT_RESOLVE, array($host . ':' . $port . ':127.0.0.1'));
        }
        $result = curl_exec($ch);
        curl_close($ch);
        return ($result === false || $result === '') ? false : $result;
    }

    $ctx = stream_context_create(array(
        'http' => array(
            'timeout' => $timeout,
            'ignore_errors' => true,
            'header' => 'User-Agent: ' . $ua,
        ),
        'ssl' => array('verify_peer' => false, 'verify_peer_name' => false),
    ));
    return @file_get_contents($url, false, $ctx);
}

function fetch_cloud_template_list()
{
    $url = TEMPLATE_CLOUD_BASE . '/market.json';
    $json = cloud_http_get($url, 10);
    if ($json === false) {
        throw new Exception('无法连接模板云端服务，请检查网络。');
    }
    $data = json_decode($json, true);
    if (!is_array($data) || !isset($data['templates'])) {
        throw new Exception('云端返回数据格式异常。');
    }
    return $data['templates'];
}

function install_template_from_cloud($code, $requestType = '')
{
    require_license_guard('下载云端模板', false, 'template_manage');

    $code = trim((string)$code);
    if ($code === '' || !preg_match('/^[a-z0-9][a-z0-9\-]*$/', $code)) {
        throw new Exception('模板编码不合法。');
    }

    // 从云端获取模板信息
    $templates = fetch_cloud_template_list();
    $target = null;
    foreach ($templates as $t) {
        if (isset($t['code']) && $t['code'] === $code) {
            if ($requestType !== '' && isset($t['pack_type']) && $t['pack_type'] !== $requestType) {
                continue;
            }
            $target = $t;
            break;
        }
    }
    if (!$target) {
        throw new Exception('云端未找到编码为「' . $code . '」的模板。');
    }

    $packType = isset($target['pack_type']) && $target['pack_type'] === 'standard' ? 'standard' : 'variation';

    // 如果本地已存在同名模板，自动删除旧版以支持覆盖更新
    $existDir = template_pack_dir($code, $packType);
    if (is_dir($existDir)) {
        _remove_directory_recursive($existDir);
    }

    $downloadUrl = isset($target['download_url']) ? $target['download_url'] : '';
    if ($downloadUrl === '') {
        $downloadUrl = TEMPLATE_CLOUD_BASE . '/packs/' . $packType . '/' . urlencode($code) . '.zip';
    }

    // 下载 ZIP 到临时文件
    $zipContent = cloud_http_get($downloadUrl, 60);
    if ($zipContent === false || strlen($zipContent) < 100) {
        throw new Exception('下载模板包失败，请稍后重试。');
    }

    $tmpFile = tempnam(sys_get_temp_dir(), 'tpl_') . '.zip';
    file_put_contents($tmpFile, $zipContent);

    // 构造与 upload_template_pack_zip 兼容的文件结构
    $fakeUpload = array(
        'tmp_name' => $tmpFile,
        'name' => $code . '.zip',
        'error' => UPLOAD_ERR_OK,
        'size' => strlen($zipContent)
    );

    try {
        // 临时替换 is_uploaded_file 检查 —— 用专门的安装函数
        $result = _install_zip_from_path($tmpFile, $packType);
    } finally {
        @unlink($tmpFile);
    }

    // 用云端 market.json 中的版本号覆盖本地 manifest 版本，确保版本一致
    $cloudVersion = isset($target['version']) ? trim((string)$target['version']) : '';
    if ($cloudVersion !== '' && isset($result['code'])) {
        $installedDir = template_pack_dir($result['code'], $packType);
        $mfPath = $installedDir . DIRECTORY_SEPARATOR . 'manifest.json';
        if (is_file($mfPath)) {
            $mfJson = @file_get_contents($mfPath);
            $mfData = $mfJson !== false ? json_decode($mfJson, true) : null;
            if (is_array($mfData)) {
                $mfData['version'] = $cloudVersion;
                file_put_contents($mfPath, json_encode($mfData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
            }
        }
    }

    return $result;
}

function _install_zip_from_path($zipPath, $packType = 'variation')
{
    if (!class_exists('ZipArchive')) {
        throw new Exception('服务器未安装 PHP zip 扩展，无法解压。');
    }

    $zip = new ZipArchive();
    $openResult = $zip->open($zipPath);
    if ($openResult !== true) {
        throw new Exception('无法打开 ZIP 文件（错误码 ' . $openResult . '）。');
    }

    // 查找 manifest.json
    $manifestContent = null;
    $manifestPrefix = '';
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $entryName = $zip->getNameIndex($i);
        $basename = basename($entryName);
        if ($basename === 'manifest.json') {
            $content = $zip->getFromIndex($i);
            if ($content !== false) {
                $decoded = json_decode($content, true);
                if (is_array($decoded) && !empty($decoded['code'])) {
                    $manifestContent = $decoded;
                    $dir = dirname($entryName);
                    $manifestPrefix = ($dir === '.' || $dir === '') ? '' : $dir . '/';
                    break;
                }
            }
        }
    }

    if ($manifestContent === null) {
        $zip->close();
        throw new Exception('ZIP 中未找到有效的 manifest.json。');
    }

    $code = trim((string)$manifestContent['code']);
    if (!preg_match('/^[a-z0-9][a-z0-9\-]*$/', $code)) {
        $zip->close();
        throw new Exception('manifest.json 中的 code 字段不合法：' . $code);
    }

    if (!in_array($packType, array('variation', 'standard'))) { $packType = 'variation'; }
    $targetDir = template_pack_dir($code, $packType);

    // 如果目标目录已存在，先清除旧版以支持覆盖更新
    if (is_dir($targetDir)) {
        $zip->close();
        _remove_directory_recursive($targetDir);
        $zip = new ZipArchive();
        $zip->open($zipPath);
    }

    if (!@mkdir($targetDir, 0777, true)) {
        $zip->close();
        throw new Exception('创建模板目录失败: ' . $code);
    }

    $extractedCount = 0;
    $allowedExtensions = array('html', 'htm', 'css', 'js', 'json', 'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'txt', 'md', 'woff', 'woff2', 'ttf', 'eot', 'otf');

    for ($i = 0; $i < $zip->numFiles; $i++) {
        $entryName = $zip->getNameIndex($i);
        if ($manifestPrefix !== '' && strpos($entryName, $manifestPrefix) !== 0) { continue; }
        $relativePath = $manifestPrefix !== '' ? substr($entryName, strlen($manifestPrefix)) : $entryName;
        if ($relativePath === '' || $relativePath === false) { continue; }
        if (substr($entryName, -1) === '/') {
            $subDir = $targetDir . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, rtrim($relativePath, '/'));
            if (!is_dir($subDir)) { @mkdir($subDir, 0777, true); }
            continue;
        }
        if (strpos($relativePath, '..') !== false) { continue; }
        $fileExt = strtolower(pathinfo($relativePath, PATHINFO_EXTENSION));
        if (!in_array($fileExt, $allowedExtensions)) { continue; }
        $content = $zip->getFromIndex($i);
        if ($content === false) { continue; }
        $destPath = $targetDir . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relativePath);
        $destDir = dirname($destPath);
        if (!is_dir($destDir)) { @mkdir($destDir, 0777, true); }
        file_put_contents($destPath, $content);
        $extractedCount++;
    }

    $zip->close();

    // 写入 pack_type 到 manifest
    $manifestContent['pack_type'] = $packType;
    if (!isset($manifestContent['serial_no']) || (int)$manifestContent['serial_no'] <= 0) {
        $existingPacks = list_template_packs();
        $maxSerial = 0;
        foreach ($existingPacks as $_ep) {
            if (isset($_ep['serial_no']) && (int)$_ep['serial_no'] > $maxSerial) { $maxSerial = (int)$_ep['serial_no']; }
        }
        $manifestContent['serial_no'] = $maxSerial + 1;
    }
    $saveManifest = $manifestContent;
    unset($saveManifest['_dir'], $saveManifest['_path']);
    file_put_contents($targetDir . DIRECTORY_SEPARATOR . 'manifest.json', json_encode($saveManifest, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));

    return array(
        'code' => $code,
        'name' => isset($manifestContent['name']) ? (string)$manifestContent['name'] : $code,
        'files_count' => $extractedCount,
        'pack_type' => $packType
    );
}

function ensure_domains_table_has_template_pack()
{
    static $checked = false;
    if ($checked) {
        return;
    }
    $checked = true;
    $pdo = db_connection();
    try {
        $cols = $pdo->query("SHOW COLUMNS FROM domains LIKE 'template_pack'")->fetchAll();
        if (empty($cols)) {
            $pdo->exec("ALTER TABLE `domains` ADD COLUMN `template_pack` VARCHAR(80) NOT NULL DEFAULT '' AFTER `template_name`");
        }
    } catch (Exception $e) {
        // table may not exist yet
    }
}
