<?php
/**
 * 芋头CMS - 本地模板标准包导出 / 导入
 */

namespace Core\Lib;

require_once __DIR__ . '/template_loader.php';
require_once __DIR__ . '/safe_zip.php';

class TemplatePackageManager
{
    const PACKAGE_MANIFEST_FILE = '.yutou-template-package.json';
    const PACKAGE_VERSION = 1;

    const CONFIG_SECTION_TEMPLATE = 'template';
    const CONFIG_SECTION_BASIC = 'basic';
    const CONFIG_SECTION_SEO = 'seo';
    const CONFIG_SECTION_APPEARANCE = 'appearance';
    const CONFIG_SECTION_CONTACT = 'contact';

    private $db;
    private $license;
    private $templateLoader;
    private $rootPath;
    private $workPath;
    private $configSectionLabels = [
        self::CONFIG_SECTION_TEMPLATE => '模板绑定',
        self::CONFIG_SECTION_BASIC => '基础信息',
        self::CONFIG_SECTION_SEO => 'SEO 与链接',
        self::CONFIG_SECTION_APPEARANCE => '视觉外观',
        self::CONFIG_SECTION_CONTACT => '联系信息',
    ];
    private $configExportSections = [
        self::CONFIG_SECTION_TEMPLATE => [
            'site_template',
            'last_template_key',
        ],
        self::CONFIG_SECTION_BASIC => [
            'site_name',
            'site_title',
            'site_status',
            'site_close_msg',
        ],
        self::CONFIG_SECTION_SEO => [
            'site_keywords',
            'site_description',
            'url_mode',
            'rewrite_auto_enabled',
            'seo_internal_link_enabled',
            'seo_internal_link_max',
            'indexnow_enabled',
            'baidu_push_enabled',
            'robots_extra_rules',
        ],
        self::CONFIG_SECTION_APPEARANCE => [
            'site_logo',
            'watermark_image',
            'watermark_text',
            'watermark_position',
            'watermark_alpha',
        ],
        self::CONFIG_SECTION_CONTACT => [
            'company_intro',
            'contact_person',
            'site_phone',
            'site_email',
            'site_address',
            'contact_wechat',
            'contact_qq',
        ],
    ];

    public function __construct($db, $license = null)
    {
        $this->db = $db;
        $this->license = LicenseGuard::getLicense($license);
        $this->templateLoader = new TemplateLoader($this->license);
        $this->rootPath = rtrim(str_replace('\\', '/', dirname(__DIR__, 2)), '/');
        $this->workPath = $this->rootPath . '/work/template_package';
    }

    public function resolveActiveTemplateKey()
    {
        $last = $this->db->fetch(
            "SELECT config_value FROM {$this->db->table('config')} WHERE config_key = ? LIMIT 1",
            ['last_template_key']
        );
        $site = $this->db->fetch(
            "SELECT config_value FROM {$this->db->table('config')} WHERE config_key = ? LIMIT 1",
            ['site_template']
        );

        $lastKey = trim((string) ($last['config_value'] ?? ''));
        $siteKey = trim((string) ($site['config_value'] ?? ''));
        if ($lastKey !== '' && is_dir($this->getTemplatePath($lastKey))) {
            return $lastKey;
        }
        if ($siteKey !== '' && is_dir($this->getTemplatePath($siteKey))) {
            return $siteKey;
        }

        return TemplateLoader::DEFAULT_TEMPLATE;
    }

    public function listInstalledTemplates($currentTemplateKey = '')
    {
        $currentTemplateKey = trim((string) $currentTemplateKey);
        $licenseStatus = $this->license->getStatus();
        $currentLevel = intval($licenseStatus['level'] ?? 1);
        $baseDir = $this->rootPath . '/template';
        $items = [];

        if (!is_dir($baseDir)) {
            return [];
        }

        foreach (glob($baseDir . '/*', GLOB_ONLYDIR) ?: [] as $dir) {
            $templateKey = basename($dir);
            if ($templateKey === '') {
                continue;
            }

            $manifest = $this->templateLoader->readManifest($templateKey);
            $isDefault = $templateKey === TemplateLoader::DEFAULT_TEMPLATE;
            $requiredLevel = intval($manifest['required_level'] ?? 1);
            $isFree = intval($manifest['is_free'] ?? ($requiredLevel <= 1 ? 1 : 0)) === 1;
            $isLocked = !$isDefault && !$isFree && $requiredLevel > $currentLevel;
            $isValid = $isDefault || $this->templateLoader->isInstalled($templateKey);

            $items[] = [
                'template_key' => $templateKey,
                'name' => trim((string) ($manifest['name'] ?? $templateKey)),
                'version' => trim((string) ($manifest['version'] ?? '')),
                'required_level' => $requiredLevel,
                'is_free' => $isFree ? 1 : 0,
                'is_default' => $isDefault ? 1 : 0,
                'is_valid' => $isValid ? 1 : 0,
                'is_locked' => $isLocked ? 1 : 0,
                'is_active' => $templateKey === $currentTemplateKey ? 1 : 0,
                'updated_at' => intval($manifest['synced_at'] ?? @filemtime($dir) ?: 0),
                'path' => $dir,
            ];
        }

        usort($items, function ($a, $b) {
            if ($a['is_active'] !== $b['is_active']) {
                return $a['is_active'] ? -1 : 1;
            }
            if ($a['is_default'] !== $b['is_default']) {
                return $a['is_default'] ? -1 : 1;
            }
            return strcmp($a['template_key'], $b['template_key']);
        });

        return $items;
    }

    public function getPackagePreview($templateKey = '', array $options = [])
    {
        $templateKey = trim((string) $templateKey);
        if ($templateKey === '') {
            $templateKey = $this->resolveActiveTemplateKey();
        }

        $templateDir = $this->getTemplatePath($templateKey);
        if (!is_dir($templateDir)) {
            return ['code' => 404, 'msg' => '模板目录不存在'];
        }

        $sourceManifest = $this->templateLoader->readManifest($templateKey) ?: [];
        $configSections = $this->buildSelectedConfigSections($options, 'export_');
        $includeTemplateConfig = !empty($options['include_template_config']);
        $includeBuilderPages = !empty($options['include_builder_pages']);
        $includeCustomVars = !empty($options['include_custom_vars']);

        $siteConfigs = $includeTemplateConfig ? $this->collectSiteConfigs($configSections) : [];
        $builderPages = $includeBuilderPages ? $this->collectBuilderPages() : [];
        $customVars = $includeCustomVars ? $this->collectCustomVars() : [];

        $sectionSummary = [];
        foreach ($this->configSectionLabels as $sectionKey => $sectionLabel) {
            $count = 0;
            foreach ($siteConfigs as $row) {
                if (($row['config_section'] ?? '') === $sectionKey) {
                    $count++;
                }
            }
            $sectionSummary[] = [
                'key' => $sectionKey,
                'label' => $sectionLabel,
                'selected' => in_array($sectionKey, $configSections, true) ? 1 : 0,
                'count' => $count,
            ];
        }

        return [
            'code' => 200,
            'msg' => 'ok',
            'preview' => [
                'template' => [
                    'template_key' => $templateKey,
                    'name' => trim((string) ($sourceManifest['name'] ?? $templateKey)),
                    'version' => trim((string) ($sourceManifest['version'] ?? '1.0.0')),
                    'required_level' => intval($sourceManifest['required_level'] ?? 1),
                    'is_free' => intval($sourceManifest['is_free'] ?? 1),
                ],
                'summary' => [
                    'site_configs' => count($siteConfigs),
                    'builder_pages' => count($builderPages),
                    'custom_vars' => count($customVars),
                ],
                'site_config_sections' => $sectionSummary,
                'site_configs' => $siteConfigs,
                'builder_pages' => $builderPages,
                'custom_vars' => $customVars,
            ],
        ];
    }

    public function validateLocalActivation($templateKey)
    {
        $templateKey = trim((string) $templateKey);
        if ($templateKey === '') {
            return ['code' => 400, 'msg' => '模板标识不能为空'];
        }

        $templateDir = $this->getTemplatePath($templateKey);
        if (!is_dir($templateDir)) {
            return ['code' => 404, 'msg' => '模板目录不存在'];
        }

        if ($templateKey === TemplateLoader::DEFAULT_TEMPLATE) {
            return ['code' => 200, 'msg' => 'ok'];
        }

        $manifest = $this->templateLoader->readManifest($templateKey);
        if (!is_array($manifest) || !$this->templateLoader->validateInstalledTemplate($templateKey, $manifest)) {
            return ['code' => 400, 'msg' => '模板文件校验失败，请重新导入模板包'];
        }

        $requiredLevel = intval($manifest['required_level'] ?? 1);
        $isFree = intval($manifest['is_free'] ?? ($requiredLevel <= 1 ? 1 : 0)) === 1;
        $currentLevel = intval(($this->license->getStatus()['level'] ?? 1));
        if (!$isFree && $requiredLevel > $currentLevel) {
            return ['code' => 403, 'msg' => '当前授权等级不足，无法启用该模板'];
        }

        return ['code' => 200, 'msg' => 'ok', 'manifest' => $manifest];
    }

    public function deleteLocalTemplate($templateKey)
    {
        $templateKey = trim((string) $templateKey);
        if ($templateKey === '') {
            return ['code' => 400, 'msg' => '模板标识不能为空'];
        }

        if ($templateKey === TemplateLoader::DEFAULT_TEMPLATE) {
            return ['code' => 403, 'msg' => '默认模板不允许删除'];
        }

        $activeTemplateKey = $this->resolveActiveTemplateKey();
        if ($templateKey === $activeTemplateKey) {
            return ['code' => 403, 'msg' => '当前启用中的模板不允许删除'];
        }

        $templateDir = $this->getTemplatePath($templateKey);
        if (!is_dir($templateDir)) {
            return ['code' => 404, 'msg' => '模板目录不存在'];
        }

        $templateRoot = realpath($this->rootPath . '/template');
        $realTemplateDir = realpath($templateDir);
        if ($templateRoot === false || $realTemplateDir === false || strpos($realTemplateDir, $templateRoot . DIRECTORY_SEPARATOR) !== 0) {
            return ['code' => 400, 'msg' => '模板目录路径异常，禁止删除'];
        }

        try {
            $this->removeDirectory($realTemplateDir);
        } catch (\Throwable $e) {
            return ['code' => 500, 'msg' => '删除模板失败：' . $e->getMessage()];
        }

        return ['code' => 200, 'msg' => '模板已删除', 'template_key' => $templateKey];
    }

    public function deleteInvalidLocalTemplates()
    {
        $templates = $this->listInstalledTemplates($this->resolveActiveTemplateKey());
        $deleted = [];
        $skipped = [];

        foreach ($templates as $template) {
            $templateKey = trim((string) ($template['template_key'] ?? ''));
            $isActive = !empty($template['is_active']);
            $isDefault = !empty($template['is_default']);
            $isValid = !empty($template['is_valid']);

            if ($templateKey === '' || $isActive || $isDefault || $isValid) {
                if ($templateKey !== '' && !$isValid) {
                    $skipped[] = $templateKey;
                }
                continue;
            }

            $result = $this->deleteLocalTemplate($templateKey);
            if (intval($result['code'] ?? 500) === 200) {
                $deleted[] = $templateKey;
            } else {
                $skipped[] = $templateKey;
            }
        }

        return [
            'code' => 200,
            'msg' => '批量清理完成',
            'deleted' => $deleted,
            'skipped' => $skipped,
            'deleted_count' => count($deleted),
            'skipped_count' => count($skipped),
        ];
    }

    public function exportPackage($templateKey = '', array $options = [])
    {
        if (!class_exists('ZipArchive')) {
            return ['code' => 500, 'msg' => '当前环境未启用 ZipArchive，无法导出模板包'];
        }

        $templateKey = trim((string) $templateKey);
        if ($templateKey === '') {
            $templateKey = $this->resolveActiveTemplateKey();
        }

        $templateDir = $this->getTemplatePath($templateKey);
        if (!is_dir($templateDir)) {
            return ['code' => 404, 'msg' => '模板目录不存在，无法导出'];
        }

        $sourceManifest = $this->templateLoader->readManifest($templateKey) ?: [];
        $includeTemplateConfig = !empty($options['include_template_config']);
        $includeBuilderPages = !empty($options['include_builder_pages']);
        $includeCustomVars = !empty($options['include_custom_vars']);
        $configSections = $this->buildSelectedConfigSections($options, 'export_');

        $packageId = 'tplpkg_' . date('YmdHis') . '_' . substr(md5(uniqid('', true)), 0, 8);
        $tempRoot = $this->workPath . '/' . $packageId;
        $packageRoot = $tempRoot . '/package';
        $downloadName = $templateKey . '-' . date('Ymd-His') . '.yttpl.zip';
        $zipPath = $tempRoot . '/' . $downloadName;

        try {
            $this->ensureDirectory($packageRoot . '/template/' . $templateKey);
            $this->copyDirectory($templateDir, $packageRoot . '/template/' . $templateKey);

            $builderPages = $includeBuilderPages ? $this->collectBuilderPages() : [];
            $customVars = $includeCustomVars ? $this->collectCustomVars() : [];
            $siteConfigs = $includeTemplateConfig ? $this->collectSiteConfigs($configSections) : [];

            if ($includeBuilderPages) {
                $this->writeJsonFile($packageRoot . '/data/builder_pages.json', $builderPages);
            }
            if ($includeCustomVars) {
                $this->writeJsonFile($packageRoot . '/data/custom_vars.json', $customVars);
            }
            if ($includeTemplateConfig) {
                $this->writeJsonFile($packageRoot . '/data/site_configs.json', $siteConfigs);
            }

            $packageManifest = [
                'format' => 'yutou_template_package',
                'package_version' => self::PACKAGE_VERSION,
                'exported_at' => date('c'),
                'options' => $this->normalizePackageOptions($options),
                'template' => [
                    'template_key' => $templateKey,
                    'name' => trim((string) ($sourceManifest['name'] ?? $templateKey)),
                    'version' => trim((string) ($sourceManifest['version'] ?? '1.0.0')),
                    'required_level' => intval($sourceManifest['required_level'] ?? 1),
                    'is_free' => intval($sourceManifest['is_free'] ?? 1),
                ],
                'includes' => [
                    'template_files' => true,
                    'site_configs' => count($siteConfigs),
                    'builder_pages' => count($builderPages),
                    'custom_vars' => count($customVars),
                ],
                'paths' => [
                    'template_root' => 'template/' . $templateKey,
                    'site_configs' => 'data/site_configs.json',
                    'builder_pages' => 'data/builder_pages.json',
                    'custom_vars' => 'data/custom_vars.json',
                ],
            ];
            $this->writeJsonFile($packageRoot . '/' . self::PACKAGE_MANIFEST_FILE, $packageManifest);

            $this->zipDirectory($packageRoot, $zipPath);

            return [
                'code' => 200,
                'msg' => '模板包导出成功',
                'path' => $zipPath,
                'cleanup_root' => $tempRoot,
                'download_name' => $downloadName,
                'template_key' => $templateKey,
            ];
        } catch (\Throwable $e) {
            $this->removeDirectory($tempRoot);
            return ['code' => 500, 'msg' => '模板包导出失败：' . $e->getMessage()];
        }
    }

    public function importPackage($uploadedFile, array $options = [])
    {
        if (!class_exists('ZipArchive')) {
            return ['code' => 500, 'msg' => '当前环境未启用 ZipArchive，无法导入模板包'];
        }

        $uploadedFile = trim((string) $uploadedFile);
        if ($uploadedFile === '' || !is_file($uploadedFile)) {
            return ['code' => 400, 'msg' => '未找到上传的模板包文件'];
        }

        $activateAfterImport = !empty($options['activate_after_import']);
        $applySiteConfig = !empty($options['apply_site_config']);
        $importBuilderPages = !empty($options['import_builder_pages']);
        $importCustomVars = !empty($options['import_custom_vars']);
        $importConfigSections = $this->buildSelectedConfigSections($options, 'import_');

        $jobId = 'tplimp_' . date('YmdHis') . '_' . substr(md5(uniqid('', true)), 0, 8);
        $tempRoot = $this->workPath . '/' . $jobId;
        $extractDir = $tempRoot . '/extract';
        $backupDir = $tempRoot . '/backup';
        $stagingDir = $tempRoot . '/staging';

        try {
            $this->ensureDirectory($extractDir);
            $this->ensureDirectory($backupDir);
            $this->ensureDirectory($stagingDir);

            $zip = new \ZipArchive();
            if ($zip->open($uploadedFile) !== true) {
                throw new \RuntimeException('模板包解压失败');
            }
            SafeZip::extract($zip, $extractDir);
            $zip->close();

            $packageManifest = $this->readJsonFile($extractDir . '/' . self::PACKAGE_MANIFEST_FILE);
            if (!is_array($packageManifest) || ($packageManifest['format'] ?? '') !== 'yutou_template_package') {
                throw new \RuntimeException('模板包格式无效');
            }

            $templateKey = trim((string) ($packageManifest['template']['template_key'] ?? ''));
            if (!$this->isValidTemplateKey($templateKey)) {
                throw new \RuntimeException('模板包缺少模板标识');
            }

            $templateRoot = trim((string) ($packageManifest['paths']['template_root'] ?? ('template/' . $templateKey)));
            $sourceTemplateDir = $extractDir . '/' . str_replace(['\\', '..'], ['/', ''], $templateRoot);
            if (!is_dir($sourceTemplateDir)) {
                throw new \RuntimeException('模板文件目录不存在');
            }

            $targetDir = $this->getTemplatePath($templateKey);
            $backupTargetDir = $backupDir . '/' . $templateKey;
            $stagingTargetDir = $stagingDir . '/' . $templateKey;
            $this->copyDirectory($sourceTemplateDir, $stagingTargetDir);

            if (is_dir($targetDir)) {
                $this->copyDirectory($targetDir, $backupTargetDir);
            }

            $this->removeDirectory($targetDir);
            $this->copyDirectory($stagingTargetDir, $targetDir);
            $this->writeInstalledTemplateManifest($templateKey, $targetDir, $packageManifest);

            $this->db->beginTransaction();
            try {
                if ($importBuilderPages) {
                    $builderFile = $extractDir . '/' . trim((string) ($packageManifest['paths']['builder_pages'] ?? 'data/builder_pages.json'));
                    $builderPages = $this->readJsonFile($builderFile);
                    if (is_array($builderPages)) {
                        $this->importBuilderPages($builderPages);
                    }
                }

                if ($importCustomVars) {
                    $customFile = $extractDir . '/' . trim((string) ($packageManifest['paths']['custom_vars'] ?? 'data/custom_vars.json'));
                    $customVars = $this->readJsonFile($customFile);
                    if (is_array($customVars)) {
                        $this->importCustomVars($customVars);
                    }
                }

                if ($applySiteConfig) {
                    $siteConfigFile = $extractDir . '/' . trim((string) ($packageManifest['paths']['site_configs'] ?? 'data/site_configs.json'));
                    $siteConfigs = $this->readJsonFile($siteConfigFile);
                    if (is_array($siteConfigs)) {
                        $this->importSiteConfigs($siteConfigs, $importConfigSections);
                    }
                }

                if ($activateAfterImport) {
                    $activationCheck = $this->validateLocalActivation($templateKey);
                    if (intval($activationCheck['code'] ?? 500) !== 200) {
                        throw new \RuntimeException((string) ($activationCheck['msg'] ?? '模板启用失败'));
                    }

                    $activateResult = $this->templateLoader->activateTemplate($templateKey, $this->db);
                    if (intval($activateResult['code'] ?? 500) !== 200) {
                        throw new \RuntimeException((string) ($activateResult['msg'] ?? '模板启用失败'));
                    }
                }

                $this->db->commit();
            } catch (\Throwable $e) {
                $this->db->rollBack();
                $this->removeDirectory($targetDir);
                if (is_dir($backupTargetDir)) {
                    $this->copyDirectory($backupTargetDir, $targetDir);
                }
                throw $e;
            }

            $this->removeDirectory($tempRoot);

            return [
                'code' => 200,
                'msg' => '模板包导入成功',
                'template_key' => $templateKey,
                'activated' => $activateAfterImport ? 1 : 0,
            ];
        } catch (\Throwable $e) {
            $this->removeDirectory($tempRoot);
            return ['code' => 500, 'msg' => '模板包导入失败：' . $e->getMessage()];
        }
    }

    private function collectBuilderPages()
    {
        if (!$this->hasTable('builder_page')) {
            return [];
        }

        $columns = $this->getTableColumns('builder_page');
        $wanted = ['page_key', 'page_type', 'ref_id', 'title', 'layout_json', 'status', 'is_enabled', 'version_no', 'create_time', 'update_time'];
        $selected = array_values(array_intersect($wanted, $columns));
        if (empty($selected)) {
            return [];
        }

        return $this->db->fetchAll(
            "SELECT " . implode(', ', $selected) . " FROM {$this->db->table('builder_page')} ORDER BY page_key ASC"
        );
    }

    private function collectCustomVars()
    {
        if (!$this->hasTable('custom_var')) {
            return [];
        }

        $columns = $this->getTableColumns('custom_var');
        $wanted = ['var_key', 'var_name', 'var_type', 'var_value', 'create_time'];
        $selected = array_values(array_intersect($wanted, $columns));
        if (empty($selected)) {
            return [];
        }

        return $this->db->fetchAll(
            "SELECT " . implode(', ', $selected) . " FROM {$this->db->table('custom_var')} ORDER BY var_key ASC"
        );
    }

    private function collectSiteConfigs(array $sections = [])
    {
        if (!$this->hasTable('config')) {
            return [];
        }

        $rows = $this->db->fetchAll("SELECT config_key, config_value, config_type, config_group FROM {$this->db->table('config')}");
        $keys = [];
        foreach ($this->configExportSections as $section => $sectionKeys) {
            foreach ($sectionKeys as $key) {
                $keys[$key] = $section;
            }
        }

        $result = [];
        foreach ($rows as $row) {
            $key = trim((string) ($row['config_key'] ?? ''));
            if ($key === '' || !isset($keys[$key])) {
                continue;
            }

            $section = $keys[$key];
            if (!empty($sections) && !in_array($section, $sections, true)) {
                continue;
            }
            if ($key === 'site_url' || $key === 'license_code') {
                continue;
            }

            $result[] = [
                'config_key' => $key,
                'config_value' => $row['config_value'] ?? '',
                'config_type' => $row['config_type'] ?? 'text',
                'config_group' => $row['config_group'] ?? 'basic',
                'config_section' => $section,
            ];
        }

        return $result;
    }

    private function normalizePackageOptions(array $options)
    {
        $normalized = [];
        foreach ($this->configExportSections as $section => $keys) {
            $normalized['export_' . $section] = !empty($options['export_' . $section]) ? 1 : 0;
            $normalized['import_' . $section] = !empty($options['import_' . $section]) ? 1 : 0;
        }
        $normalized['include_builder_pages'] = !empty($options['include_builder_pages']) ? 1 : 0;
        $normalized['include_custom_vars'] = !empty($options['include_custom_vars']) ? 1 : 0;
        $normalized['include_template_config'] = !empty($options['include_template_config']) ? 1 : 0;
        return $normalized;
    }

    private function buildSelectedConfigSections(array $options, $prefix = 'export_')
    {
        $sections = [];
        foreach (array_keys($this->configExportSections) as $section) {
            if (!empty($options[$prefix . $section])) {
                $sections[] = $section;
            }
        }
        return $sections;
    }

    private function importBuilderPages(array $rows)
    {
        if (!$this->hasTable('builder_page')) {
            return;
        }

        $columns = $this->getTableColumns('builder_page');
        foreach ($rows as $row) {
            if (!is_array($row)) {
                continue;
            }
            $pageKey = trim((string) ($row['page_key'] ?? ''));
            if ($pageKey === '') {
                continue;
            }

            $data = $this->filterDataByColumns($row, $columns, ['id']);
            if (empty($data)) {
                continue;
            }

            $exists = $this->db->fetch(
                "SELECT id FROM {$this->db->table('builder_page')} WHERE page_key = ? LIMIT 1",
                [$pageKey]
            );
            if ($exists) {
                $this->db->update('builder_page', $data, 'id = ?', [$exists['id']]);
            } else {
                $this->db->insert('builder_page', $data);
            }
        }
    }

    private function importCustomVars(array $rows)
    {
        if (!$this->hasTable('custom_var')) {
            return;
        }

        $columns = $this->getTableColumns('custom_var');
        foreach ($rows as $row) {
            if (!is_array($row)) {
                continue;
            }
            $varKey = trim((string) ($row['var_key'] ?? ''));
            if ($varKey === '') {
                continue;
            }

            $data = $this->filterDataByColumns($row, $columns, ['id']);
            if (empty($data)) {
                continue;
            }

            $exists = $this->db->fetch(
                "SELECT id FROM {$this->db->table('custom_var')} WHERE var_key = ? LIMIT 1",
                [$varKey]
            );
            if ($exists) {
                $this->db->update('custom_var', $data, 'id = ?', [$exists['id']]);
            } else {
                $this->db->insert('custom_var', $data);
            }
        }
    }

    private function importSiteConfigs(array $rows, array $sections = [])
    {
        if (!$this->hasTable('config')) {
            return;
        }

        $allowed = [];
        foreach ($this->configExportSections as $section => $sectionKeys) {
            if (!empty($sections) && !in_array($section, $sections, true)) {
                continue;
            }
            foreach ($sectionKeys as $key) {
                if ($key === 'site_url' || $key === 'license_code') {
                    continue;
                }
                $allowed[$key] = true;
            }
        }

        foreach ($rows as $row) {
            if (!is_array($row)) {
                continue;
            }

            $key = trim((string) ($row['config_key'] ?? ''));
            if ($key === '' || !isset($allowed[$key])) {
                continue;
            }

            $value = (string) ($row['config_value'] ?? '');
            $type = trim((string) ($row['config_type'] ?? 'text'));
            $group = trim((string) ($row['config_group'] ?? 'basic'));

            $exists = $this->db->fetch(
                "SELECT id FROM {$this->db->table('config')} WHERE config_key = ? LIMIT 1",
                [$key]
            );
            $data = [
                'config_key' => $key,
                'config_value' => $value,
                'config_type' => $type !== '' ? $type : 'text',
                'config_group' => $group !== '' ? $group : 'basic',
            ];
            if ($exists) {
                $this->db->update('config', $data, 'id = ?', [$exists['id']]);
            } else {
                $this->db->insert('config', $data);
            }
        }
    }

    private function writeInstalledTemplateManifest($templateKey, $targetDir, array $packageManifest)
    {
        $templateMeta = is_array($packageManifest['template'] ?? null) ? $packageManifest['template'] : [];
        $manifest = [
            'template_key' => $templateKey,
            'name' => trim((string) ($templateMeta['name'] ?? $templateKey)),
            'version' => trim((string) ($templateMeta['version'] ?? '1.0.0')),
            'required_level' => intval($templateMeta['required_level'] ?? 1),
            'is_free' => intval($templateMeta['is_free'] ?? 1),
            'package_hash' => $this->calculateTemplateHash($targetDir),
            'synced_at' => time(),
            'source' => 'local_package',
        ];

        $this->writeJsonFile(
            rtrim($targetDir, '/\\') . '/' . TemplateLoader::MANIFEST_FILE,
            $manifest
        );

        $runtimeGuard = rtrim($targetDir, '/\\') . '/' . TemplateLoader::RUNTIME_GUARD_FILE;
        if (is_file($runtimeGuard)) {
            @unlink($runtimeGuard);
        }
    }

    private function zipDirectory($sourceDir, $zipPath)
    {
        $sourceDir = rtrim($sourceDir, '/\\');
        $this->ensureDirectory(dirname($zipPath));

        $zip = new \ZipArchive();
        if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
            throw new \RuntimeException('模板包压缩失败');
        }

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

        foreach ($iterator as $item) {
            $pathname = $item->getPathname();
            $relative = str_replace('\\', '/', substr($pathname, strlen($sourceDir) + 1));
            if ($relative === '') {
                continue;
            }

            if ($item->isDir()) {
                $zip->addEmptyDir($relative);
                continue;
            }

            if (!$zip->addFile($pathname, $relative)) {
                $zip->close();
                throw new \RuntimeException('模板包压缩写入失败');
            }
        }

        $zip->close();
    }

    private function copyDirectory($source, $target)
    {
        $source = rtrim((string) $source, '/\\');
        $target = rtrim((string) $target, '/\\');
        if (!is_dir($source)) {
            throw new \RuntimeException('目录不存在：' . $source);
        }

        $this->ensureDirectory($target);
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ($iterator as $item) {
            $relative = substr($item->getPathname(), strlen($source) + 1);
            $relative = str_replace('\\', '/', $relative);
            $destination = $target . '/' . $relative;
            if ($item->isDir()) {
                $this->ensureDirectory($destination);
                continue;
            }

            $this->ensureDirectory(dirname($destination));
            if (!copy($item->getPathname(), $destination)) {
                throw new \RuntimeException('文件复制失败：' . $relative);
            }
        }
    }

    private function removeDirectory($dir)
    {
        $dir = rtrim((string) $dir, '/\\');
        if ($dir === '' || !is_dir($dir)) {
            return;
        }

        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::CHILD_FIRST
        );
        foreach ($iterator as $item) {
            if ($item->isDir()) {
                @rmdir($item->getPathname());
            } else {
                @unlink($item->getPathname());
            }
        }
        @rmdir($dir);
    }

    public function cleanupWorkPath($dir)
    {
        $dir = rtrim((string) $dir, '/\\');
        if ($dir === '') {
            return;
        }

        $base = rtrim($this->workPath, '/\\');
        $normalizedDir = str_replace('\\', '/', $dir);
        $normalizedBase = str_replace('\\', '/', $base);
        if (strpos($normalizedDir, $normalizedBase) !== 0) {
            return;
        }

        $this->removeDirectory($dir);
    }

    private function ensureDirectory($dir)
    {
        $dir = rtrim((string) $dir, '/\\');
        if ($dir === '') {
            return;
        }
        if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) {
            throw new \RuntimeException('目录创建失败：' . $dir);
        }
    }

    private function writeJsonFile($path, $data)
    {
        $this->ensureDirectory(dirname($path));
        $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
        if ($json === false || file_put_contents($path, $json) === false) {
            throw new \RuntimeException('JSON 文件写入失败：' . basename($path));
        }
    }

    private function readJsonFile($path)
    {
        if (!is_file($path)) {
            return null;
        }
        $json = json_decode((string) file_get_contents($path), true);
        return is_array($json) ? $json : null;
    }

    private function hasTable($table)
    {
        try {
            $tableName = $this->db->table($table);
            $config = $this->db->getConfig();
            $row = $this->db->fetch(
                'SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? LIMIT 1',
                [$config['database'] ?? '', $tableName]
            );
            return !empty($row);
        } catch (\Throwable $e) {
            return false;
        }
    }

    private function getTableColumns($table)
    {
        try {
            $columns = $this->db->fetchAll('SHOW COLUMNS FROM ' . $this->db->table($table));
            return array_values(array_map(function ($column) {
                return (string) ($column['Field'] ?? '');
            }, $columns));
        } catch (\Throwable $e) {
            return [];
        }
    }

    private function filterDataByColumns(array $row, array $columns, array $exclude = [])
    {
        $allowed = array_flip($columns);
        $blocked = array_flip($exclude);
        $data = [];
        foreach ($row as $key => $value) {
            if (!isset($allowed[$key]) || isset($blocked[$key])) {
                continue;
            }
            $data[$key] = $value;
        }
        return $data;
    }

    private function calculateTemplateHash($dir)
    {
        $dir = rtrim((string) $dir, '/\\');
        if (!is_dir($dir)) {
            return '';
        }

        $records = [];
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ($iterator as $item) {
            if ($item->isDir()) {
                continue;
            }

            $relativePath = str_replace('\\', '/', substr($item->getPathname(), strlen($dir) + 1));
            if ($relativePath === TemplateLoader::MANIFEST_FILE || $relativePath === TemplateLoader::RUNTIME_GUARD_FILE) {
                continue;
            }

            $records[] = $relativePath . ':' . hash_file('sha256', $item->getPathname());
        }

        sort($records, SORT_STRING);
        return hash('sha256', implode("\n", $records));
    }

    private function getTemplatePath($templateKey)
    {
        $templateKey = trim((string) $templateKey);
        if (!$this->isValidTemplateKey($templateKey)) {
            return $this->rootPath . '/template/__invalid_template_key__';
        }
        return $this->rootPath . '/template/' . $templateKey;
    }

    private function isValidTemplateKey($templateKey)
    {
        return preg_match('/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/', (string) $templateKey) === 1;
    }
}
