<?php
declare(strict_types=1);

/**
 * 仅管理「客户区 default 主题」：public/themes/clientarea/default
 * 独立入口，与系统后台无关。
 *
 * 界面在 console_view.php；本文件末尾必须 require console_view.php，
 * 切勿再拼接旧版 HTML（文件编辑器 UI），否则注册/登录/购买授权会失效。
 */

session_name('THEME_CLIENT_PORTAL');
session_start();
require_once __DIR__ . '/portal_hub_client.php';

const EDITABLE_EXTENSIONS = ['tpl', 'css', 'js', 'html', 'htm', 'json', 'svg', 'txt', 'md', 'map'];

$base = realpath(__DIR__ . '/../themes/clientarea/default');
if ($base === false) {
    http_response_code(500);
    exit('主题目录不存在，请检查路径。');
}
define('THEME_ROOT', $base);
require_once __DIR__ . '/theme_editor_lib.php';

portal_ensure_config_file();

/** @var array<string, mixed> $cfg */
$cfg = portal_hub_config();

function h(string $s): string
{
    return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function backup_dir(): string
{
    return dirname(__DIR__, 2) . '/data/clientarea_default_theme_backups';
}

function is_logged_in(): bool
{
    return !empty($_SESSION['portal_token']);
}

function portal_auth_token(): string
{
    return (string) ($_SESSION['portal_token'] ?? '');
}

function require_login(): void
{
    if (!is_logged_in()) {
        http_response_code(401);
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode(['error' => '未登录'], JSON_UNESCAPED_UNICODE);
        exit;
    }
}

function csrf_token(): string
{
    if (empty($_SESSION['ca_theme_csrf'])) {
        $_SESSION['ca_theme_csrf'] = bin2hex(random_bytes(16));
    }
    return $_SESSION['ca_theme_csrf'];
}

function verify_csrf(?string $t): bool
{
    return is_string($t) && isset($_SESSION['ca_theme_csrf']) && hash_equals($_SESSION['ca_theme_csrf'], $t);
}

/** 校验相对路径，返回绝对路径或 null */
function theme_path(string $rel, bool $mustExist): ?string
{
    $rel = str_replace('\\', '/', $rel);
    $rel = trim($rel, '/');
    if ($rel !== '' && (str_contains($rel, '..') || str_contains($rel, "\0"))) {
        return null;
    }
    $full = $rel === '' ? THEME_ROOT : THEME_ROOT . '/' . $rel;
    $rp = realpath($full);
    if ($rp !== false) {
        return str_starts_with($rp, THEME_ROOT . DIRECTORY_SEPARATOR) || $rp === THEME_ROOT ? $rp : null;
    }
    if ($mustExist) {
        return null;
    }
    $dir = dirname($full);
    $base = realpath($dir);
    if ($base === false || !str_starts_with($base, THEME_ROOT)) {
        return null;
    }
    $bn = basename($full);
    if ($bn === '' || $bn === '.' || $bn === '..') {
        return null;
    }
    return $full;
}

function ext_ok(string $path): bool
{
    $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
    return in_array($ext, EDITABLE_EXTENSIONS, true);
}

function backup_suffix_for_file(string $fileRel): string
{
    $safe = preg_replace('/[^a-zA-Z0-9._\/-]+/', '_', $fileRel);
    return str_replace('/', '__', $safe);
}

// —— 登出 ——
if (isset($_GET['logout'])) {
    $_SESSION = [];
    if (ini_get('session.use_cookies')) {
        $p = session_get_cookie_params();
        setcookie(session_name(), '', time() - 42000, $p['path'], $p['domain'], $p['secure'], $p['httponly']);
    }
    session_destroy();
    header('Location: ' . strtok($_SERVER['REQUEST_URI'] ?? '/', '?'));
    exit;
}

$action = $_GET['action'] ?? ($_POST['action'] ?? '');

$portalPublicActions = ['portal_send_sms_code', 'portal_register', 'portal_login'];

if ($action !== '' && $action !== 'page' && !in_array($action, $portalPublicActions, true)) {
    require_login();
}

/**
 * @param array<string, mixed> $res
 */
function portal_emit_auth_result(array $res, int $failStatus = 400): void
{
    header('Content-Type: application/json; charset=utf-8');
    if (empty($res['ok'])) {
        http_response_code($failStatus);
        echo json_encode($res, JSON_UNESCAPED_UNICODE);
        exit;
    }
    $_SESSION['portal_token'] = (string) ($res['token'] ?? '');
    unset($_SESSION['portal_user_id']);
    session_regenerate_id(true);
    echo json_encode(['ok' => true, 'user' => $res['user'] ?? null], JSON_UNESCAPED_UNICODE);
    exit;
}

if ($action === 'portal_send_sms_code' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-Type: application/json; charset=utf-8');
    $data = json_decode(file_get_contents('php://input') ?: '', true);
    if (!is_array($data)) {
        http_response_code(400);
        echo json_encode(['ok' => false, 'error' => '无效数据'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    echo json_encode(portal_hub_request('send_sms_code', [
        'phone' => (string) ($data['phone'] ?? ''),
        'purpose' => (string) ($data['purpose'] ?? ''),
        'geetest' => $data['geetest'] ?? [],
    ]), JSON_UNESCAPED_UNICODE);
    exit;
}

if ($action === 'portal_register' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-Type: application/json; charset=utf-8');
    $data = json_decode(file_get_contents('php://input') ?: '', true);
    if (!is_array($data)) {
        http_response_code(400);
        echo json_encode(['error' => '无效数据'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    portal_emit_auth_result(portal_hub_request('register', [
        'phone' => (string) ($data['phone'] ?? ''),
        'code' => (string) ($data['code'] ?? ''),
        'company' => (string) ($data['company'] ?? ''),
        'username' => (string) ($data['username'] ?? ''),
        'password' => (string) ($data['password'] ?? ''),
        'geetest' => $data['geetest'] ?? [],
    ]));
}

if ($action === 'portal_login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-Type: application/json; charset=utf-8');
    $data = json_decode(file_get_contents('php://input') ?: '', true);
    if (!is_array($data)) {
        http_response_code(400);
        echo json_encode(['error' => '无效数据'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $mode = (string) ($data['mode'] ?? 'sms');
    $payload = ['mode' => $mode, 'geetest' => $data['geetest'] ?? []];
    if ($mode === 'password') {
        $payload['account'] = (string) ($data['account'] ?? '');
        $payload['password'] = (string) ($data['password'] ?? '');
    } else {
        $payload['phone'] = (string) ($data['phone'] ?? '');
        $payload['code'] = (string) ($data['code'] ?? '');
    }
    portal_emit_auth_result(portal_hub_request('login', $payload), 401);
}

if ($action === 'portal_me') {
    header('Content-Type: application/json; charset=utf-8');
    require_login();
    echo json_encode(portal_hub_request('me', ['token' => portal_auth_token()], 'GET'), JSON_UNESCAPED_UNICODE);
    exit;
}

if ($action === 'portal_payment_config') {
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode(portal_hub_request('payment_config', [], 'GET'), JSON_UNESCAPED_UNICODE);
    exit;
}

if ($action === 'portal_create_order' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-Type: application/json; charset=utf-8');
    require_login();
    $data = json_decode(file_get_contents('php://input') ?: '', true);
    if (!is_array($data) || !verify_csrf($data['csrf'] ?? null)) {
        http_response_code(403);
        echo json_encode(['error' => '校验失败'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    echo json_encode(portal_hub_request('create_order', [
        'token' => portal_auth_token(),
        'domain_id' => (string) ($data['domain_id'] ?? ''),
        'pay_type' => (string) ($data['pay_type'] ?? 'alipay'),
        'return_url' => (string) ($data['return_url'] ?? ''),
    ]), JSON_UNESCAPED_UNICODE);
    exit;
}

if ($action === 'portal_order_status') {
    header('Content-Type: application/json; charset=utf-8');
    require_login();
    echo json_encode(portal_hub_request('order_status', [
        'token' => portal_auth_token(),
        'order_no' => (string) ($_GET['order_no'] ?? ''),
    ], 'GET'), JSON_UNESCAPED_UNICODE);
    exit;
}

if ($action === 'portal_bind_domain' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-Type: application/json; charset=utf-8');
    require_login();
    $data = json_decode(file_get_contents('php://input') ?: '', true);
    if (!is_array($data) || !verify_csrf($data['csrf'] ?? null)) {
        http_response_code(403);
        echo json_encode(['error' => '校验失败'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    echo json_encode(portal_hub_request('bind_domain', [
        'token' => portal_auth_token(),
        'domain' => (string) ($data['domain'] ?? ''),
    ]), JSON_UNESCAPED_UNICODE);
    exit;
}

if ($action === 'portal_unbind_domain' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-Type: application/json; charset=utf-8');
    require_login();
    $data = json_decode(file_get_contents('php://input') ?: '', true);
    if (!is_array($data) || !verify_csrf($data['csrf'] ?? null)) {
        http_response_code(403);
        echo json_encode(['error' => '校验失败'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    echo json_encode(portal_hub_request('unbind_domain', [
        'token' => portal_auth_token(),
        'domain_id' => (string) ($data['domain_id'] ?? ''),
    ]), JSON_UNESCAPED_UNICODE);
    exit;
}

if ($action === 'list') {
    header('Content-Type: application/json; charset=utf-8');
    $dirRel = (string) ($_GET['dir'] ?? '');
    $abs = theme_path($dirRel, true);
    if ($abs === null || !is_dir($abs)) {
        http_response_code(400);
        echo json_encode(['error' => '无效目录'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $dirs = [];
    $files = [];
    foreach (scandir($abs) ?: [] as $name) {
        if ($name === '.' || $name === '..') {
            continue;
        }
        $child = $abs . DIRECTORY_SEPARATOR . $name;
        $rel = ($dirRel === '' ? '' : $dirRel . '/') . $name;
        if (is_dir($child)) {
            $dirs[] = ['name' => $name, 'path' => $rel];
        } else {
            $files[] = ['name' => $name, 'path' => $rel, 'editable' => ext_ok($name)];
        }
    }
    usort($dirs, fn ($a, $b) => strcmp($a['name'], $b['name']));
    usort($files, fn ($a, $b) => strcmp($a['name'], $b['name']));
    echo json_encode(['dirs' => $dirs, 'files' => $files], JSON_UNESCAPED_UNICODE);
    exit;
}

if ($action === 'search') {
    header('Content-Type: application/json; charset=utf-8');
    $q = trim((string) ($_GET['q'] ?? ''));
    if (strlen($q) < 1 || strlen($q) > 80) {
        echo json_encode(['results' => []], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $results = [];
    try {
        $it = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator(THEME_ROOT, FilesystemIterator::SKIP_DOTS),
            RecursiveIteratorIterator::LEAVES_ONLY
        );
        foreach ($it as $file) {
            if (!$file->isFile()) {
                continue;
            }
            $name = $file->getFilename();
            if (stripos($name, $q) === false && stripos($file->getPathname(), $q) === false) {
                continue;
            }
            $rel = substr($file->getPathname(), strlen(THEME_ROOT) + 1);
            $rel = str_replace('\\', '/', $rel);
            $results[] = ['path' => $rel, 'editable' => ext_ok($name)];
            if (count($results) >= 80) {
                break;
            }
        }
    } catch (Throwable $e) {
        http_response_code(500);
        echo json_encode(['error' => '搜索失败'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    echo json_encode(['results' => $results], JSON_UNESCAPED_UNICODE);
    exit;
}

if ($action === 'read') {
    header('Content-Type: application/json; charset=utf-8');
    $fileRel = (string) ($_GET['file'] ?? '');
    $abs = theme_path($fileRel, true);
    if ($abs === null || !is_file($abs) || !ext_ok($abs)) {
        http_response_code(400);
        echo json_encode(['error' => '无效或不可编辑文件'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $sz = filesize($abs);
    if ($sz > 1_500_000) {
        http_response_code(400);
        echo json_encode(['error' => '文件过大，请用 SFTP 编辑'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $content = file_get_contents($abs);
    $mtime = filemtime($abs);
    echo json_encode([
        'path' => $fileRel,
        'content' => $content === false ? '' : $content,
        'size' => $sz,
        'mtime' => $mtime !== false ? $mtime : null,
        'mtime_text' => $mtime ? date('Y-m-d H:i:s', $mtime) : '',
    ], JSON_UNESCAPED_UNICODE);
    exit;
}

if ($action === 'list_backups') {
    header('Content-Type: application/json; charset=utf-8');
    $fileRel = (string) ($_GET['file'] ?? '');
    $abs = theme_path($fileRel, true);
    if ($abs === null || !is_file($abs)) {
        http_response_code(400);
        echo json_encode(['error' => '无效文件'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $bd = backup_dir();
    if (!is_dir($bd)) {
        echo json_encode(['backups' => []], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $suffix = backup_suffix_for_file($fileRel);
    $glob = $bd . DIRECTORY_SEPARATOR . '*__' . $suffix;
    $list = glob($glob) ?: [];
    rsort($list);
    $out = [];
    foreach (array_slice($list, 0, 30) as $p) {
        $bn = basename($p);
        $out[] = [
            'name' => $bn,
            'size' => @filesize($p) ?: 0,
            'mtime' => @filemtime($p) ?: 0,
            'mtime_text' => @filemtime($p) ? date('Y-m-d H:i:s', (int) filemtime($p)) : '',
        ];
    }
    echo json_encode(['backups' => $out], JSON_UNESCAPED_UNICODE);
    exit;
}

if ($action === 'restore' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-Type: application/json; charset=utf-8');
    $raw = file_get_contents('php://input') ?: '';
    $data = json_decode($raw, true);
    if (!is_array($data) || !verify_csrf($data['csrf'] ?? null)) {
        http_response_code(403);
        echo json_encode(['error' => '校验失败'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $fileRel = (string) ($data['file'] ?? '');
    $bakName = basename((string) ($data['backup'] ?? ''));
    $abs = theme_path($fileRel, true);
    if ($abs === null || !is_file($abs) || !ext_ok($abs)) {
        http_response_code(400);
        echo json_encode(['error' => '无效文件'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $suffix = backup_suffix_for_file($fileRel);
    if (!preg_match('/^\d{8}_\d{6}__' . preg_quote($suffix, '/') . '$/', $bakName)) {
        http_response_code(400);
        echo json_encode(['error' => '无效备份名'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $src = backup_dir() . '/' . $bakName;
    $rsrc = realpath($src);
    $rbd = realpath(backup_dir());
    if ($rsrc === false || $rbd === false || !str_starts_with($rsrc, $rbd . DIRECTORY_SEPARATOR) || !is_file($rsrc)) {
        http_response_code(400);
        echo json_encode(['error' => '备份不存在'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $content = file_get_contents($rsrc);
    if ($content === false) {
        http_response_code(500);
        echo json_encode(['error' => '读取备份失败'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    if (file_put_contents($abs, $content) === false) {
        http_response_code(500);
        echo json_encode(['error' => '写入主题失败'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    echo json_encode(['ok' => true, 'content' => $content], JSON_UNESCAPED_UNICODE);
    exit;
}

if ($action === 'download') {
    $fileRel = (string) ($_GET['file'] ?? '');
    $abs = theme_path($fileRel, true);
    if ($abs === null || !is_file($abs) || !ext_ok($abs)) {
        http_response_code(404);
        exit('Not found');
    }
    if (filesize($abs) > 5_000_000) {
        http_response_code(400);
        exit('Too large');
    }
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="' . basename($fileRel) . '"');
    readfile($abs);
    exit;
}

if ($action === 'save' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-Type: application/json; charset=utf-8');
    $raw = file_get_contents('php://input') ?: '';
    $data = json_decode($raw, true);
    if (!is_array($data) || !verify_csrf($data['csrf'] ?? null)) {
        http_response_code(403);
        echo json_encode(['error' => '校验失败，请刷新页面'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $fileRel = (string) ($data['file'] ?? '');
    $abs = theme_path($fileRel, true);
    if ($abs === null || !is_file($abs) || !ext_ok($abs)) {
        http_response_code(400);
        echo json_encode(['error' => '无效或不可编辑文件'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $content = (string) ($data['content'] ?? '');
    if (strlen($content) > 2_000_000) {
        http_response_code(400);
        echo json_encode(['error' => '内容过长'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $backupDir = backup_dir();
    if (!is_dir($backupDir)) {
        @mkdir($backupDir, 0750, true);
    }
    if (is_dir($backupDir) && is_writable($backupDir)) {
        $stamp = date('Ymd_His');
        $bak = $backupDir . '/' . $stamp . '__' . backup_suffix_for_file($fileRel);
        @copy($abs, $bak);
    }
    if (file_put_contents($abs, $content) === false) {
        http_response_code(500);
        echo json_encode(['error' => '写入失败，检查目录权限'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    echo json_encode(['ok' => true], JSON_UNESCAPED_UNICODE);
    exit;
}

if ($action === 'tokens_get') {
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode([
        'tokens' => theme_tokens_read(),
        'pack' => theme_pack_read(),
        'override_css_exists' => is_file(theme_override_css_path()),
    ], JSON_UNESCAPED_UNICODE);
    exit;
}

if ($action === 'tokens_save' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-Type: application/json; charset=utf-8');
    $raw = file_get_contents('php://input') ?: '';
    $data = json_decode($raw, true);
    if (!is_array($data) || !verify_csrf($data['csrf'] ?? null)) {
        http_response_code(403);
        echo json_encode(['error' => '校验失败'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $tokens = $data['tokens'] ?? null;
    if (!is_array($tokens)) {
        http_response_code(400);
        echo json_encode(['error' => '无效数据'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $flat = [];
    foreach ($tokens as $k => $v) {
        if (is_string($k) && is_string($v)) {
            $flat[$k] = $v;
        }
    }
    if (!theme_tokens_write($flat)) {
        http_response_code(400);
        echo json_encode(['error' => '保存失败：请检查颜色格式（#RRGGBB）与目录权限'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    echo json_encode(['ok' => true], JSON_UNESCAPED_UNICODE);
    exit;
}

if ($action === 'tokens_reset' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-Type: application/json; charset=utf-8');
    $raw = file_get_contents('php://input') ?: '';
    $data = json_decode($raw, true);
    if (!is_array($data) || !verify_csrf($data['csrf'] ?? null)) {
        http_response_code(403);
        echo json_encode(['error' => '校验失败'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    if (!theme_tokens_write(theme_token_defaults())) {
        http_response_code(500);
        echo json_encode(['error' => '写入失败'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    echo json_encode(['ok' => true, 'tokens' => theme_token_defaults()], JSON_UNESCAPED_UNICODE);
    exit;
}

if ($action === 'update_check') {
    header('Content-Type: application/json; charset=utf-8');
    $manifestUrl = trim((string) ($cfg['update_manifest_url'] ?? ''));
    if ($manifestUrl === '') {
        echo json_encode([
            'configured' => false,
            'error' => '未配置 update_manifest_url：请在 config.php 填写你们发布站上的 manifest.json 完整地址。',
            'pack' => theme_pack_read(),
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $key = trim((string) ($cfg['update_download_key'] ?? ''));
    $append = $key !== '' ? ('key=' . rawurlencode($key)) : null;
    $man = theme_editor_fetch_manifest($manifestUrl, $append);
    if ($man === null) {
        echo json_encode([
            'configured' => true,
            'error' => '无法拉取 manifest（网络、HTTPS 或 JSON 格式）',
            'pack' => theme_pack_read(),
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $pack = theme_pack_read();
    $cur = $pack['version'];
    $latest = isset($man['latest']) && is_string($man['latest']) ? $man['latest'] : '';
    $releases = isset($man['releases']) && is_array($man['releases']) ? $man['releases'] : [];
    $newer = [];
    foreach ($releases as $r) {
        if (!is_array($r)) {
            continue;
        }
        $v = isset($r['version']) && is_string($r['version']) ? $r['version'] : '';
        if ($v === '' || version_compare($v, $cur, '<=')) {
            continue;
        }
        $newer[] = [
            'version' => $v,
            'package_url' => isset($r['package_url']) && is_string($r['package_url']) ? $r['package_url'] : '',
            'sha256' => isset($r['sha256']) && is_string($r['sha256']) ? strtolower($r['sha256']) : '',
            'changelog' => isset($r['changelog']) && is_string($r['changelog']) ? $r['changelog'] : '',
        ];
    }
    usort($newer, fn ($a, $b) => version_compare($a['version'], $b['version']));
    echo json_encode([
        'configured' => true,
        'pack' => $pack,
        'remote_latest' => $latest,
        'releases' => $newer,
        'channel' => isset($man['channel']) && is_string($man['channel']) ? $man['channel'] : '',
    ], JSON_UNESCAPED_UNICODE);
    exit;
}

if ($action === 'update_apply' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-Type: application/json; charset=utf-8');
    $raw = file_get_contents('php://input') ?: '';
    $data = json_decode($raw, true);
    if (!is_array($data) || !verify_csrf($data['csrf'] ?? null)) {
        http_response_code(403);
        echo json_encode(['error' => '校验失败'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $manifestUrl = trim((string) ($cfg['update_manifest_url'] ?? ''));
    if ($manifestUrl === '') {
        http_response_code(400);
        echo json_encode(['error' => '未配置 manifest 地址'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $version = isset($data['version']) && is_string($data['version']) ? preg_replace('/[^0-9a-zA-Z._-]/', '', $data['version']) : '';
    $packageUrl = isset($data['package_url']) && is_string($data['package_url']) ? trim($data['package_url']) : '';
    $sha256 = isset($data['sha256']) && is_string($data['sha256']) ? strtolower(preg_replace('/[^a-f0-9]/', '', $data['sha256'])) : '';
    if ($version === '' || $packageUrl === '' || strlen($sha256) !== 64) {
        http_response_code(400);
        echo json_encode(['error' => '参数不完整'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    if (!preg_match('#^https://#i', $packageUrl)) {
        http_response_code(400);
        echo json_encode(['error' => '仅允许 https 下载包'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $mh = parse_url($manifestUrl, PHP_URL_HOST);
    $ph = parse_url($packageUrl, PHP_URL_HOST);
    if (!$mh || !$ph || strtolower((string) $mh) !== strtolower((string) $ph)) {
        http_response_code(400);
        echo json_encode(['error' => '安装包域名必须与 manifest 域名一致（防 SSRF）'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $key = trim((string) ($cfg['update_download_key'] ?? ''));
    $append = $key !== '' ? ('key=' . rawurlencode($key)) : null;
    $man = theme_editor_fetch_manifest($manifestUrl, $append);
    if ($man === null || !isset($man['releases']) || !is_array($man['releases'])) {
        http_response_code(400);
        echo json_encode(['error' => '无法校验 manifest'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $matched = null;
    foreach ($man['releases'] as $r) {
        if (!is_array($r)) {
            continue;
        }
        $v = isset($r['version']) && is_string($r['version']) ? $r['version'] : '';
        if ($v !== $version) {
            continue;
        }
        $pu = isset($r['package_url']) && is_string($r['package_url']) ? trim($r['package_url']) : '';
        $sh = isset($r['sha256']) && is_string($r['sha256']) ? strtolower(preg_replace('/[^a-f0-9]/', '', $r['sha256'])) : '';
        if ($pu === $packageUrl && $sh === $sha256) {
            $matched = true;
            break;
        }
    }
    if ($matched !== true) {
        http_response_code(400);
        echo json_encode(['error' => '该版本与 manifest 记录不一致，拒绝安装'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $tmp = tempnam(sys_get_temp_dir(), 'ca_theme_zip_');
    if ($tmp === false) {
        http_response_code(500);
        echo json_encode(['error' => '无法创建临时文件'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $ctx = stream_context_create([
        'http' => ['timeout' => 120, 'header' => "User-Agent: YuserThemeEditor/1\r\n"],
        'ssl' => ['verify_peer' => true, 'verify_peer_name' => true],
    ]);
    $bin = @file_get_contents($packageUrl, false, $ctx, 0, 52_000_000);
    if ($bin === false || strlen($bin) > 50 * 1024 * 1024) {
        @unlink($tmp);
        http_response_code(400);
        echo json_encode(['error' => '下载更新包失败或体积过大'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    file_put_contents($tmp, $bin);
    if (hash_file('sha256', $tmp) !== $sha256) {
        @unlink($tmp);
        http_response_code(400);
        echo json_encode(['error' => 'SHA256 校验失败，文件可能被篡改'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $ex = theme_extract_update_zip($tmp, $version);
    @unlink($tmp);
    if (!$ex['ok']) {
        http_response_code(400);
        echo json_encode(['error' => $ex['error'] ?? '解压失败'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    echo json_encode(['ok' => true, 'files' => $ex['files'] ?? 0, 'version' => $version], JSON_UNESCAPED_UNICODE);
    exit;
}

if ($action === 'overview') {
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode([
        'pack' => theme_pack_read(),
        'brand' => theme_site_brand_read(),
        'manifest_configured' => trim((string) ($cfg['update_manifest_url'] ?? '')) !== '',
    ], JSON_UNESCAPED_UNICODE);
    exit;
}

if ($action === 'brand_get') {
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode(['brand' => theme_site_brand_read()], JSON_UNESCAPED_UNICODE);
    exit;
}

if ($action === 'brand_save' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-Type: application/json; charset=utf-8');
    $raw = file_get_contents('php://input') ?: '';
    $data = json_decode($raw, true);
    if (!is_array($data) || !verify_csrf($data['csrf'] ?? null)) {
        http_response_code(403);
        echo json_encode(['error' => '校验失败'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $brand = $data['brand'] ?? null;
    if (!is_array($brand)) {
        http_response_code(400);
        echo json_encode(['error' => '无效数据'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $flat = [];
    foreach ($brand as $k => $v) {
        if (is_string($k) && is_string($v)) {
            $flat[$k] = $v;
        }
    }
    if (!theme_site_brand_write($flat)) {
        http_response_code(400);
        echo json_encode(['error' => '保存失败，请检查 Logo 链接与邮箱格式'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    echo json_encode(['ok' => true], JSON_UNESCAPED_UNICODE);
    exit;
}
