<?php
/**
 * 芋头CMS
 * ============================================================================
 * 版权所有 2016-2028 沅陵县乌托邦网络服务有限公司，并保留所有权利。
 * 网站地址: http://www.yutoucms.cn
 * ----------------------------------------------------------------------------
 * 如果商业用途务必到官方购买正版授权, 以免引起不必要的法律纠纷.
 * ============================================================================
 * Author: 芋头 <mxgsf@qq.com>
 * Date: 2026-6-10
 */

namespace Core\Lib;

require_once __DIR__ . '/license_guard.php';

class TemplateLoader
{
    const DEFAULT_TEMPLATE = 'default';
    const MANIFEST_FILE = '.yutou-template.json';
    const RUNTIME_GUARD_FILE = '_runtime_guard.php';

    const FALLBACK_REASON_LEVEL = 'license_level_denied';
    const FALLBACK_REASON_MANIFEST = 'manifest_invalid';
    const FALLBACK_REASON_SYNC = 'sync_failed';
    const FALLBACK_REASON_HASH = 'integrity_invalid';
    const FALLBACK_REASON_MISSING = 'template_missing';
    const FALLBACK_REASON_RUNTIME = 'runtime_guard_denied';

    private $license;

    public function __construct($license = null)
    {
        $this->license = LicenseGuard::getLicense($license);
    }

    public function fetchRemoteTemplateList($level = 999)
    {
        $level = intval($level);
        $cacheDir = dirname(__DIR__, 2) . '/data/cache';
        $cacheFile = $cacheDir . '/remote_templates_' . $level . '.json';
        $cacheTtl = 900;
        $cached = null;
        if (is_file($cacheFile)) {
            $cached = json_decode((string) @file_get_contents($cacheFile), true);
            if (is_array($cached) && intval($cached['cached_at'] ?? 0) + $cacheTtl >= time()) {
                return [
                    'code' => 200,
                    'msg' => 'ok',
                    'templates' => is_array($cached['templates'] ?? null) ? $cached['templates'] : [],
                ];
            }
        }

        $apiUrl = $this->getLicenseCenterUrl() . '/api.php?action=template_list';
        $payload = json_encode(['level' => $level]);

        $ch = curl_init($apiUrl);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $payload,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 12,
            CURLOPT_CONNECTTIMEOUT => 6,
            CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
        ]);
        $body = curl_exec($ch);
        $httpCode = intval(curl_getinfo($ch, CURLINFO_HTTP_CODE));
        $curlErr = curl_error($ch);
        $curlErrno = curl_errno($ch);
        curl_close($ch);

        if ($body === false || $curlErrno) {
            if (is_array($cached) && is_array($cached['templates'] ?? null)) {
                return ['code' => 200, 'msg' => 'stale-cache', 'templates' => $cached['templates']];
            }
            return [
                'code' => 500,
                'msg' => $curlErr !== '' ? $curlErr : '无法连接授权平台',
                'templates' => [],
            ];
        }

        $json = json_decode($body, true);
        if ($httpCode !== 200 || !is_array($json) || intval($json['code'] ?? 500) !== 200) {
            if (is_array($cached) && is_array($cached['templates'] ?? null)) {
                return ['code' => 200, 'msg' => 'stale-cache', 'templates' => $cached['templates']];
            }
            return [
                'code' => intval($json['code'] ?? 500),
                'msg' => (string) ($json['msg'] ?? '授权平台返回异常'),
                'templates' => [],
            ];
        }

        $templates = isset($json['templates']) && is_array($json['templates']) ? $json['templates'] : [];
        if ((is_dir($cacheDir) || @mkdir($cacheDir, 0755, true)) && is_writable($cacheDir)) {
            @file_put_contents($cacheFile, json_encode([
                'cached_at' => time(),
                'templates' => $templates,
            ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX);
        }

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

    public function getRemoteTemplate($templateKey)
    {
        $templateKey = trim((string) $templateKey);
        if ($templateKey === '') {
            return null;
        }

        $result = $this->fetchRemoteTemplateList(999);
        if (intval($result['code'] ?? 500) !== 200) {
            return null;
        }

        foreach ($result['templates'] as $template) {
            if (trim((string) ($template['template_key'] ?? '')) === $templateKey) {
                return $template;
            }
        }

        return null;
    }

    public function isInstalled($templateKey)
    {
        $templateKey = trim((string) $templateKey);
        if ($templateKey === '') {
            return false;
        }

        if (!is_dir($this->getTemplatePath($templateKey))) {
            return false;
        }

        if ($templateKey === self::DEFAULT_TEMPLATE) {
            return true;
        }

        $manifest = $this->readManifest($templateKey);
        if (!is_array($manifest) || trim((string) ($manifest['template_key'] ?? '')) !== $templateKey) {
            return false;
        }

        return $this->validateInstalledTemplate($templateKey, $manifest);
    }

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

        if ($templateKey === self::DEFAULT_TEMPLATE && is_dir($this->getTemplatePath($templateKey))) {
            return ['code' => 200, 'msg' => '默认模板已可用'];
        }

        if ($templateInfo === null) {
            $templateInfo = $this->getRemoteTemplate($templateKey);
        }
        if (!is_array($templateInfo)) {
            return ['code' => 404, 'msg' => '授权平台中不存在该模板'];
        }

        $requiredLevel = intval($templateInfo['required_level'] ?? License::LEVEL_FREE);
        $isFree = intval($templateInfo['is_free'] ?? 0) === 1 || $requiredLevel <= License::LEVEL_FREE;
        $currentLevel = intval($this->license->getLevel());
        if (!$isFree && $currentLevel < $requiredLevel) {
            return ['code' => 403, 'msg' => '当前授权版本无权使用该模板'];
        }

        if ($this->isInstalled($templateKey)) {
            $this->ensureRuntimeGuard($templateKey, [
                'template_key' => $templateKey,
                'required_level' => $requiredLevel,
                'is_free' => $isFree ? 1 : 0,
            ]);
            return ['code' => 200, 'msg' => '模板已安装'];
        }

        if (!class_exists('ZipArchive')) {
            return ['code' => 500, 'msg' => '服务器未启用 ZipArchive，无法安装模板'];
        }

        $licenseStatus = $this->license->getStatus();
        $payload = ['template_key' => $templateKey];
        $licenseKey = trim((string) ($licenseStatus['license_key'] ?? ''));
        $domain = trim((string) ($licenseStatus['domain'] ?? ($_SERVER['HTTP_HOST'] ?? '')));
        if ($licenseKey !== '') {
            $payload['license_key'] = $licenseKey;
        }
        if ($domain !== '') {
            $payload['domain'] = $domain;
        }

        $downloadResult = $this->downloadTemplateArchive($templateKey, $payload, $isFree);
        if (intval($downloadResult['code'] ?? 500) !== 200) {
            return $downloadResult;
        }

        $body = (string) ($downloadResult['body'] ?? '');
        goto yt_template_loader_zip_ready;

        $apiUrl = $this->getLicenseCenterUrl() . '/api.php?action=template_package';
        $ch = curl_init($apiUrl);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => json_encode($payload),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 20,
            CURLOPT_CONNECTTIMEOUT => 8,
            CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
        ]);
        $body = curl_exec($ch);
        $httpCode = intval(curl_getinfo($ch, CURLINFO_HTTP_CODE));
        $curlErr = curl_error($ch);
        $curlErrno = curl_errno($ch);
        curl_close($ch);

        if ($body === false || $curlErrno) {
            return ['code' => 500, 'msg' => $curlErr !== '' ? $curlErr : '无法下载模板压缩包'];
        }

        if ($httpCode !== 200 || substr($body, 0, 2) !== 'PK') {
            $json = json_decode($body, true);
            if (is_array($json)) {
                return [
                    'code' => intval($json['code'] ?? 500),
                    'msg' => (string) ($json['msg'] ?? '模板下载失败'),
                ];
            }

            return ['code' => 500, 'msg' => '授权平台未返回有效模板压缩包'];
        }

yt_template_loader_zip_ready:
        $tempZip = tempnam(sys_get_temp_dir(), 'tplpkg_');
        if ($tempZip === false) {
            return ['code' => 500, 'msg' => '无法创建模板临时文件'];
        }

        file_put_contents($tempZip, $body);
        $targetDir = $this->getTemplatePath($templateKey);
        if (!is_dir($targetDir) && !mkdir($targetDir, 0755, true) && !is_dir($targetDir)) {
            @unlink($tempZip);
            return ['code' => 500, 'msg' => '无法创建本地模板目录'];
        }

        $this->clearDirectory($targetDir);
        $zip = new \ZipArchive();
        if ($zip->open($tempZip) !== true) {
            @unlink($tempZip);
            return ['code' => 500, 'msg' => '模板压缩包无法解压'];
        }

        $ok = $this->extractTemplateZip($zip, $templateKey, $targetDir);
        $zip->close();
        @unlink($tempZip);
        if (!$ok) {
            return ['code' => 500, 'msg' => '模板文件写入失败'];
        }

        $manifest = [
            'template_key' => $templateKey,
            'name' => (string) ($templateInfo['name'] ?? $templateKey),
            'required_level' => $requiredLevel,
            'is_free' => $isFree ? 1 : 0,
            'package_hash' => $this->calculateTemplateHash($targetDir),
            'synced_at' => time(),
        ];
        file_put_contents(
            $targetDir . DIRECTORY_SEPARATOR . self::MANIFEST_FILE,
            json_encode($manifest, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
        );
        $this->ensureRuntimeGuard($templateKey, $manifest);

        return ['code' => 200, 'msg' => '模板同步成功', 'template' => $templateKey];
    }

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

        $this->upsertConfig($db, 'site_template', $templateKey);
        $this->upsertConfig($db, 'last_template_key', $templateKey);

        return ['code' => 200, 'msg' => '模板启用成功', 'template' => $templateKey];
    }

    public function resolveUsableTemplate($templateKey, $db = null)
    {
        $templateKey = trim((string) $templateKey);
        if ($templateKey === '') {
            $templateKey = self::DEFAULT_TEMPLATE;
        }

        if ($templateKey === self::DEFAULT_TEMPLATE) {
            return self::DEFAULT_TEMPLATE;
        }

        $manifest = $this->readManifest($templateKey);
        if (!is_array($manifest) || trim((string) ($manifest['template_key'] ?? '')) !== $templateKey) {
            $sync = $this->ensureTemplateAvailable($templateKey);
            if (intval($sync['code'] ?? 500) !== 200) {
                return $this->fallbackToDefault($db, $templateKey, self::FALLBACK_REASON_MANIFEST);
            }
            $manifest = $this->readManifest($templateKey);
        }

        if (!is_array($manifest) || !is_dir($this->getTemplatePath($templateKey))) {
            return $this->fallbackToDefault($db, $templateKey, self::FALLBACK_REASON_MISSING);
        }

        $remoteTemplate = $this->getRemoteTemplate($templateKey);
        if (is_array($remoteTemplate) && !$this->matchesRemoteMetadata($manifest, $remoteTemplate)) {
            $sync = $this->ensureTemplateAvailable($templateKey, $remoteTemplate);
            if (intval($sync['code'] ?? 500) !== 200) {
                return $this->fallbackToDefault($db, $templateKey, self::FALLBACK_REASON_MANIFEST);
            }
            $manifest = $this->readManifest($templateKey);
            if (!is_array($manifest) || !$this->matchesRemoteMetadata($manifest, $remoteTemplate)) {
                return $this->fallbackToDefault($db, $templateKey, self::FALLBACK_REASON_MANIFEST);
            }
        }

        $requiredLevel = intval($manifest['required_level'] ?? License::LEVEL_FREE);
        $isFree = intval($manifest['is_free'] ?? 0) === 1 || $requiredLevel <= License::LEVEL_FREE;
        if (!$isFree && intval($this->license->getLevel()) < $requiredLevel) {
            return $this->fallbackToDefault($db, $templateKey, self::FALLBACK_REASON_LEVEL);
        }

        if (!$this->validateInstalledTemplate($templateKey, $manifest)) {
            $sync = $this->ensureTemplateAvailable($templateKey, is_array($remoteTemplate) ? $remoteTemplate : null);
            if (intval($sync['code'] ?? 500) !== 200) {
                return $this->fallbackToDefault($db, $templateKey, self::FALLBACK_REASON_HASH);
            }
            $manifest = $this->readManifest($templateKey);
            if (!is_array($manifest) || !$this->validateInstalledTemplate($templateKey, $manifest)) {
                return $this->fallbackToDefault($db, $templateKey, self::FALLBACK_REASON_SYNC);
            }
        }

        if (!$this->assertRuntimeGuard($templateKey, $db, $manifest)) {
            return $this->fallbackToDefault($db, $templateKey, self::FALLBACK_REASON_RUNTIME);
        }

        return $templateKey;
    }

    public function readManifest($templateKey)
    {
        $manifestFile = $this->getTemplatePath($templateKey) . DIRECTORY_SEPARATOR . self::MANIFEST_FILE;
        if (!is_file($manifestFile)) {
            return null;
        }

        $json = json_decode((string) file_get_contents($manifestFile), true);
        return is_array($json) ? $json : null;
    }

    public function resetTemplateConfigToDefault($db, $reason = '')
    {
        if (!$db) {
            return;
        }

        $this->upsertConfig($db, 'site_template', self::DEFAULT_TEMPLATE);
        $this->upsertConfig($db, 'last_template_key', self::DEFAULT_TEMPLATE);

        if (session_status() === PHP_SESSION_ACTIVE) {
            $_SESSION['template_auto_reset_notice'] = [
                'reason' => $reason,
                'time' => time(),
            ];
        }
    }

    public function validateInstalledTemplate($templateKey, array $manifest = null)
    {
        $templateKey = trim((string) $templateKey);
        if ($templateKey === '' || $templateKey === self::DEFAULT_TEMPLATE) {
            return true;
        }

        if ($manifest === null) {
            $manifest = $this->readManifest($templateKey);
        }
        if (!is_array($manifest)) {
            return false;
        }

        $storedHash = trim((string) ($manifest['package_hash'] ?? ''));
        if ($storedHash === '') {
            return false;
        }

        $templateDir = $this->getTemplatePath($templateKey);
        if (!is_dir($templateDir)) {
            return false;
        }

        return hash_equals($storedHash, $this->calculateTemplateHash($templateDir));
    }

    public function assertRuntimeGuard($templateKey, $db = null, array $manifest = null)
    {
        $templateKey = trim((string) $templateKey);
        if ($templateKey === '' || $templateKey === self::DEFAULT_TEMPLATE) {
            return true;
        }

        if ($manifest === null) {
            $manifest = $this->readManifest($templateKey);
        }
        if (!is_array($manifest)) {
            return false;
        }

        $guardFile = $this->getTemplatePath($templateKey) . DIRECTORY_SEPARATOR . self::RUNTIME_GUARD_FILE;
        if (!is_file($guardFile) && !$this->ensureRuntimeGuard($templateKey, $manifest)) {
            return false;
        }

        $result = include $guardFile;
        if ($result === false) {
            if ($db) {
                $this->resetTemplateConfigToDefault($db, self::FALLBACK_REASON_RUNTIME);
            }
            return false;
        }

        return true;
    }

    private function extractTemplateZip(\ZipArchive $zip, $templateKey, $targetDir)
    {
        $templateKey = trim((string) $templateKey);
        $targetDir = rtrim($targetDir, '/\\');

        for ($i = 0; $i < $zip->numFiles; $i++) {
            $entry = $zip->statIndex($i);
            $entryName = str_replace('\\', '/', (string) ($entry['name'] ?? ''));
            if ($entryName === '') {
                continue;
            }

            $relativePath = $entryName;
            if (strpos($entryName, $templateKey . '/') === 0) {
                $relativePath = substr($entryName, strlen($templateKey) + 1);
            }

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

            $relativePath = str_replace('/', DIRECTORY_SEPARATOR, $relativePath);
            $fullPath = $targetDir . DIRECTORY_SEPARATOR . $relativePath;

            if (substr($entryName, -1) === '/') {
                if (!is_dir($fullPath) && !mkdir($fullPath, 0755, true) && !is_dir($fullPath)) {
                    return false;
                }
                continue;
            }

            $dir = dirname($fullPath);
            if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) {
                return false;
            }

            $content = $zip->getFromIndex($i);
            if ($content === false || file_put_contents($fullPath, $content) === false) {
                return false;
            }
        }

        return true;
    }

    private function clearDirectory($dir)
    {
        if (!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());
            }
        }
    }

    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 === self::MANIFEST_FILE || $relativePath === self::RUNTIME_GUARD_FILE) {
                continue;
            }

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

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

    private function ensureRuntimeGuard($templateKey, array $manifest = null)
    {
        $templateKey = trim((string) $templateKey);
        if ($templateKey === '' || $templateKey === self::DEFAULT_TEMPLATE) {
            return true;
        }

        if ($manifest === null) {
            $manifest = $this->readManifest($templateKey);
        }
        if (!is_array($manifest)) {
            return false;
        }

        $licenseStatus = $this->license->getStatus();
        $siteId = trim((string) ($licenseStatus['site_id'] ?? ''));
        $domainValue = trim((string) ($licenseStatus['domain'] ?? ($_SERVER['HTTP_HOST'] ?? '')));
        $domains = array_values(array_filter(array_map('trim', explode(',', $domainValue))));
        if (empty($domains) && !empty($_SERVER['HTTP_HOST'])) {
            $domains[] = trim((string) $_SERVER['HTTP_HOST']);
        }

        $requiredLevel = intval($manifest['required_level'] ?? License::LEVEL_FREE);
        $guardFile = $this->getTemplatePath($templateKey) . DIRECTORY_SEPARATOR . self::RUNTIME_GUARD_FILE;
        $source = $this->buildRuntimeGuardSource($templateKey, $requiredLevel, $siteId, $domains);

        return file_put_contents($guardFile, $source) !== false;
    }

    private function buildRuntimeGuardSource($templateKey, $requiredLevel, $siteId, array $allowedDomains)
    {
        $exportTemplateKey = var_export((string) $templateKey, true);
        $exportSiteId = var_export((string) $siteId, true);
        $exportDomains = var_export(array_values($allowedDomains), true);
        $exportLevel = intval($requiredLevel);

        $source = <<<'PHP'
<?php
$__yt_template_key = __YT_TEMPLATE_KEY__;
$__yt_required_level = __YT_REQUIRED_LEVEL__;
$__yt_site_id = __YT_SITE_ID__;
$__yt_allowed_domains = __YT_ALLOWED_DOMAINS__;

if (!defined('ROOT_PATH')) {
    return false;
}

if (!class_exists('\Core\Lib\License')) {
    require_once ROOT_PATH . 'core/lib/license.php';
}
if (!class_exists('\Core\Lib\TemplateLoader')) {
    require_once ROOT_PATH . 'core/lib/template_loader.php';
}

$__yt_guard_license = new \Core\Lib\License();
$__yt_guard_loader = new \Core\Lib\TemplateLoader($__yt_guard_license);
$__yt_guard_status = $__yt_guard_license->getStatus();
$__yt_guard_site_id = trim((string) ($__yt_guard_status['site_id'] ?? ''));
$__yt_guard_level = intval($__yt_guard_status['level'] ?? 1);
$__yt_guard_domain = trim((string) ($_SERVER['HTTP_HOST'] ?? ''));

if ($__yt_site_id !== '' && $__yt_guard_site_id !== $__yt_site_id) {
    return false;
}
if ($__yt_guard_level < $__yt_required_level) {
    return false;
}
if (!empty($__yt_allowed_domains) && $__yt_guard_domain !== '' && !in_array($__yt_guard_domain, $__yt_allowed_domains, true)) {
    return false;
}
if (!$__yt_guard_loader->validateInstalledTemplate($__yt_template_key)) {
    return false;
}

return true;
PHP;

        return str_replace(
            ['__YT_TEMPLATE_KEY__', '__YT_REQUIRED_LEVEL__', '__YT_SITE_ID__', '__YT_ALLOWED_DOMAINS__'],
            [$exportTemplateKey, (string) $exportLevel, $exportSiteId, $exportDomains],
            $source
        );
    }

    private function matchesRemoteMetadata(array $manifest, array $remoteTemplate)
    {
        $manifestKey = trim((string) ($manifest['template_key'] ?? ''));
        $remoteKey = trim((string) ($remoteTemplate['template_key'] ?? ''));
        if ($manifestKey === '' || $remoteKey === '' || $manifestKey !== $remoteKey) {
            return false;
        }

        $manifestLevel = intval($manifest['required_level'] ?? License::LEVEL_FREE);
        $remoteLevel = intval($remoteTemplate['required_level'] ?? License::LEVEL_FREE);
        $manifestFree = intval($manifest['is_free'] ?? 0);
        $remoteFree = intval($remoteTemplate['is_free'] ?? 0);

        return $manifestLevel === $remoteLevel && $manifestFree === $remoteFree;
    }

    private function fallbackToDefault($db, $templateKey, $reason)
    {
        if ($templateKey !== self::DEFAULT_TEMPLATE) {
            $this->resetTemplateConfigToDefault($db, $reason);
        }

        return self::DEFAULT_TEMPLATE;
    }

    private function upsertConfig($db, $key, $value)
    {
        $exists = $db->fetch("SELECT id FROM {$db->table('config')} WHERE config_key = ?", [$key]);
        if ($exists) {
            $db->update('config', ['config_value' => $value], 'config_key = ?', [$key]);
            return;
        }

        $db->insert('config', [
            'config_key' => $key,
            'config_value' => $value,
            'config_type' => 'text',
            'config_group' => 'basic',
        ]);
    }

    private function getTemplatePath($templateKey)
    {
        $templateKey = trim((string) $templateKey);
        if (!preg_match('/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/', $templateKey)) {
            return rtrim($this->rootPath('template/__invalid_template_key__'), '/\\');
        }
        return rtrim($this->rootPath('template/' . $templateKey), '/\\');
    }

    private function downloadTemplateArchive($templateKey, array $payload, $allowLegacyFreeDownload = false)
    {
        $primaryResult = $this->requestRemoteBinary(
            $this->getLicenseCenterUrl() . '/api.php?action=template_package',
            json_encode($payload),
            ['Content-Type: application/json']
        );

        if (intval($primaryResult['http_code'] ?? 500) === 200 && substr((string) ($primaryResult['body'] ?? ''), 0, 2) === 'PK') {
            return [
                'code' => 200,
                'msg' => 'ok',
                'body' => (string) $primaryResult['body'],
            ];
        }

        // The current license center still serves template ZIP files through
        // template_download. Keep it as a compatibility fallback for all
        // locally-authorized templates, otherwise users see "未知操作" from
        // the unsupported template_package action.
        if ($allowLegacyFreeDownload) {
            $legacyUrl = $this->getLicenseCenterUrl() . '/api.php?action=template_download&key=' . urlencode($templateKey);
            $legacyResult = $this->requestRemoteBinary($legacyUrl);
            if (intval($legacyResult['http_code'] ?? 500) === 200 && substr((string) ($legacyResult['body'] ?? ''), 0, 2) === 'PK') {
                return [
                    'code' => 200,
                    'msg' => 'ok',
                    'body' => (string) $legacyResult['body'],
                ];
            }

            if (is_array($legacyResult['json'] ?? null)) {
                return [
                    'code' => intval($legacyResult['json']['code'] ?? 500),
                    'msg' => (string) ($legacyResult['json']['msg'] ?? 'Template download failed'),
                ];
            }
        }

        if (is_array($primaryResult['json'] ?? null)) {
            return [
                'code' => intval($primaryResult['json']['code'] ?? 500),
                'msg' => (string) ($primaryResult['json']['msg'] ?? 'Template download failed'),
            ];
        }

        return [
            'code' => intval($primaryResult['http_code'] ?? 500) ?: 500,
            'msg' => (string) ($primaryResult['msg'] ?? 'License center did not return a valid template archive'),
        ];
    }

    private function getLicenseCenterUrl()
    {
        return rtrim(defined('LICENSE_CENTER_URL') ? LICENSE_CENTER_URL : 'http://yutou.wtbidc.cn/license-center', '/');
    }

    private function requestRemoteBinary($url, $postBody = null, array $headers = [])
    {
        $ch = curl_init($url);
        $options = [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 20,
            CURLOPT_CONNECTTIMEOUT => 8,
        ];

        if ($postBody !== null) {
            $options[CURLOPT_POST] = true;
            $options[CURLOPT_POSTFIELDS] = $postBody;
        }
        if (!empty($headers)) {
            $options[CURLOPT_HTTPHEADER] = $headers;
        }

        curl_setopt_array($ch, $options);
        $body = curl_exec($ch);
        $httpCode = intval(curl_getinfo($ch, CURLINFO_HTTP_CODE));
        $curlErr = curl_error($ch);
        $curlErrno = curl_errno($ch);
        curl_close($ch);

        if ($body === false || $curlErrno) {
            return [
                'http_code' => 500,
                'body' => '',
                'json' => null,
                'msg' => $curlErr !== '' ? $curlErr : 'Unable to connect to license center',
            ];
        }

        $json = null;
        if ($body !== '' && substr((string) $body, 0, 2) !== 'PK') {
            $decoded = json_decode($body, true);
            if (is_array($decoded)) {
                $json = $decoded;
            }
        }

        return [
            'http_code' => $httpCode,
            'body' => $body,
            'json' => $json,
            'msg' => null,
        ];
    }

    private function rootPath($relativePath)
    {
        $root = defined('ROOT_PATH') ? ROOT_PATH : dirname(__DIR__, 2) . DIRECTORY_SEPARATOR;
        return rtrim($root, '/\\') . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $relativePath);
    }
}
