<?php
if (PHP_SAPI !== 'cli') {
    http_response_code(404);
    exit;
}

const RELEASE_VERSION = '1.1.4';
const RELEASE_VERSION_CODE = 114;

$root = str_replace('\\', '/', dirname(__DIR__));
$releaseDir = $root . '/release/' . RELEASE_VERSION;
$fullZip = $releaseDir . '/EnterpriseCMS-' . RELEASE_VERSION . '-full.zip';
$upgradeZip = $releaseDir . '/yutou_' . RELEASE_VERSION . '.zip';
$hotfixZip = $releaseDir . '/EnterpriseCMS-' . RELEASE_VERSION . '-upgrade-service-hotfix.zip';
$licensePackage = $root . '/license-center/packages/yutou_' . RELEASE_VERSION . '.zip';

if (!class_exists('ZipArchive')) {
    fwrite(STDERR, "ZipArchive extension is required.\n");
    exit(2);
}
if (!is_dir($releaseDir) && !mkdir($releaseDir, 0775, true) && !is_dir($releaseDir)) {
    fwrite(STDERR, "Cannot create release directory.\n");
    exit(2);
}

function releaseRelativePath($root, $path)
{
    $root = rtrim(str_replace('\\', '/', $root), '/') . '/';
    $path = str_replace('\\', '/', $path);
    if (strpos($path, $root) !== 0) {
        throw new RuntimeException('Path escaped project root: ' . $path);
    }
    return ltrim(substr($path, strlen($root)), '/');
}

function releaseShouldExclude($relative)
{
    $relative = str_replace('\\', '/', $relative);
    if (in_array($relative, ['core/db/config.php', 'data/config.php'], true)) {
        return true;
    }
    if (preg_match('#(?:^|/)(?:backup|backups|cache|logs?|sessions?|tmp|temp)(?:/|$)#i', $relative)) {
        return true;
    }
    return (bool) preg_match('/(?:\.bak(?:-|$)|\.old$|\.orig$|\.rej$|\.log$|~$|Thumbs\.db$|\.DS_Store$)/i', basename($relative));
}

function releaseCollectDirectory($root, $directory)
{
    $base = $root . '/' . trim($directory, '/');
    $files = [];
    if (!is_dir($base)) {
        return $files;
    }
    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($base, FilesystemIterator::SKIP_DOTS),
        RecursiveIteratorIterator::LEAVES_ONLY
    );
    foreach ($iterator as $item) {
        if (!$item->isFile() || $item->isLink()) {
            continue;
        }
        $relative = releaseRelativePath($root, $item->getPathname());
        if (!releaseShouldExclude($relative)) {
            $files[$relative] = $item->getPathname();
        }
    }
    return $files;
}

function releaseCollect($root, array $directories, array $rootFiles)
{
    $files = [];
    foreach ($directories as $directory) {
        $files += releaseCollectDirectory($root, $directory);
    }
    foreach ($rootFiles as $relative) {
        $path = $root . '/' . $relative;
        if (is_file($path) && !releaseShouldExclude($relative)) {
            $files[$relative] = $path;
        }
    }
    ksort($files, SORT_STRING);
    return $files;
}

function releaseChecksums(array $files)
{
    $checksums = [];
    foreach ($files as $relative => $source) {
        $checksums[$relative] = hash_file('sha256', $source);
    }
    return $checksums;
}

function releaseCreateZip($path, array $files, array $virtualFiles)
{
    if (is_file($path)) {
        unlink($path);
    }
    $zip = new ZipArchive();
    if ($zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
        throw new RuntimeException('Cannot create ZIP: ' . $path);
    }
    foreach ($files as $relative => $source) {
        if (!$zip->addFile($source, str_replace('\\', '/', $relative))) {
            throw new RuntimeException('Cannot add file: ' . $relative);
        }
    }
    foreach ($virtualFiles as $relative => $content) {
        if (!$zip->addFromString($relative, $content)) {
            throw new RuntimeException('Cannot add virtual file: ' . $relative);
        }
    }
    $zip->close();
}

function releaseVerifyZip($path, array $checksums, array $requiredFiles, array $forbiddenPrefixes)
{
    $zip = new ZipArchive();
    if ($zip->open($path) !== true) {
        throw new RuntimeException('Cannot verify ZIP: ' . $path);
    }
    $seen = [];
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $entry = (string) $zip->getNameIndex($i);
        if ($entry === '' || strpos($entry, '\\') !== false || $entry[0] === '/'
            || preg_match('#^[A-Za-z]:/#', $entry) || preg_match('#(?:^|/)\.\.(?:/|$)#', $entry)) {
            throw new RuntimeException('Unsafe ZIP path: ' . $entry);
        }
        if (isset($seen[$entry])) {
            throw new RuntimeException('Duplicate ZIP path: ' . $entry);
        }
        foreach ($forbiddenPrefixes as $prefix) {
            if (strpos($entry, $prefix) === 0) {
                throw new RuntimeException('Forbidden ZIP path: ' . $entry);
            }
        }
        $seen[$entry] = true;
    }
    foreach ($requiredFiles as $required) {
        if (!isset($seen[$required])) {
            throw new RuntimeException('ZIP is missing required file: ' . $required);
        }
    }
    foreach ($checksums as $relative => $expected) {
        $stream = $zip->getStream($relative);
        if (!$stream) {
            throw new RuntimeException('Cannot read ZIP entry: ' . $relative);
        }
        $hash = hash_init('sha256');
        hash_update_stream($hash, $stream);
        fclose($stream);
        if (!hash_equals($expected, hash_final($hash))) {
            throw new RuntimeException('ZIP checksum mismatch: ' . $relative);
        }
    }
    $zip->close();
}

$runtimeDirs = ['admin', 'api', 'cli', 'core', 'member', 'modules-premium', 'public'];
$runtimeRootFiles = ['.htaccess', 'index.php', 'nginx.htaccess', 'RELEASE-NOTES.md'];
$upgradeCandidates = releaseCollect($root, $runtimeDirs, $runtimeRootFiles);
$upgradeCutoff = strtotime('2026-07-19 11:00:00');
$upgradeAlwaysInclude = ['admin/index.php', 'admin/modules/dashboard.php', 'core/lib/common.php', 'core/lib/license.php', 'core/lib/upgrade_service.php', 'RELEASE-NOTES.md'];
$upgradeFiles = [];
foreach ($upgradeCandidates as $relative => $source) {
    if (filemtime($source) >= $upgradeCutoff || in_array($relative, $upgradeAlwaysInclude, true)) {
        $upgradeFiles[$relative] = $source;
    }
}
ksort($upgradeFiles, SORT_STRING);
$fullFiles = releaseCollect(
    $root,
    array_merge($runtimeDirs, ['install', 'template']),
    array_merge($runtimeRootFiles, ['robots.txt', 'sitemap.xml', '安装说明.txt', 'data/.htaccess', 'data/.keep', 'uploads/.gitignore'])
);

$snapshot = @file_get_contents($root . '/install/data_snapshot.sql');
if ($snapshot !== false && preg_match_all('#/uploads/([A-Za-z0-9_./-]+)#', $snapshot, $matches)) {
    foreach (array_unique($matches[1]) as $uploadRelative) {
        if (strpos($uploadRelative, '..') !== false) {
            continue;
        }
        $source = $root . '/uploads/' . $uploadRelative;
        if (is_file($source)) {
            $fullFiles['uploads/' . $uploadRelative] = $source;
        }
    }
    ksort($fullFiles, SORT_STRING);
}

$buildTime = date('Y-m-d H:i:s');
$jsonFlags = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT;
$upgradeChecksums = releaseChecksums($upgradeFiles);
$fullChecksums = releaseChecksums($fullFiles);
$upgradeManifest = [
    'name' => 'EnterpriseCMS ' . RELEASE_VERSION . ' 升级包',
    'version' => RELEASE_VERSION,
    'version_code' => RELEASE_VERSION_CODE,
    'build_time' => $buildTime,
    'build_php_version' => PHP_VERSION,
    'package_type' => 'direct-root-overwrite',
    'minimum_version' => '1.1.3',
    'database_changes' => false,
    'description' => '微信充值、后台公告链接、仪表盘 3D 数据效果及安全与会员功能增强。',
    'file_count' => count($upgradeFiles),
    'files' => array_keys($upgradeFiles),
    'excluded_paths' => ['data/', 'uploads/', 'template/', 'install/', 'license-center/'],
];
$fullManifest = [
    'name' => 'EnterpriseCMS ' . RELEASE_VERSION . ' 全新安装包',
    'version' => RELEASE_VERSION,
    'version_code' => RELEASE_VERSION_CODE,
    'build_time' => $buildTime,
    'build_php_version' => PHP_VERSION,
    'package_type' => 'clean-install',
    'file_count' => count($fullFiles),
    'contains_demo_data' => is_file($root . '/install/data_snapshot.sql'),
    'contains_private_config' => false,
    'excluded_paths' => ['license-center/', 'core/db/config.php', 'data/config.php', 'data/cache/', 'data/backups/', 'tests/', 'release/'],
];
$upgradeSql = "-- EnterpriseCMS " . RELEASE_VERSION . ": no database schema changes.\n";

releaseCreateZip($upgradeZip, $upgradeFiles, [
    'update.json' => json_encode($upgradeManifest, $jsonFlags) . "\n",
    'upgrade.sql' => $upgradeSql,
    'checksums.json' => json_encode($upgradeChecksums, $jsonFlags) . "\n",
]);
releaseCreateZip($fullZip, $fullFiles, [
    'release.json' => json_encode($fullManifest, $jsonFlags) . "\n",
    'checksums.json' => json_encode($fullChecksums, $jsonFlags) . "\n",
]);
releaseCreateZip($hotfixZip, [
    'core/lib/upgrade_service.php' => $root . '/core/lib/upgrade_service.php',
], [
    'HOTFIX-README.txt' => "先覆盖网站根目录 core/lib/upgrade_service.php，再从后台重新执行 1.1.4 升级。\n",
]);

releaseVerifyZip($upgradeZip, $upgradeChecksums, ['update.json', 'upgrade.sql', 'checksums.json'], ['data/', 'uploads/', 'template/', 'install/', 'license-center/']);
releaseVerifyZip($fullZip, $fullChecksums, ['release.json', 'checksums.json', 'install/index.php'], ['license-center/', 'core/db/config.php', 'data/config.php']);
releaseVerifyZip($hotfixZip, ['core/lib/upgrade_service.php' => hash_file('sha256', $root . '/core/lib/upgrade_service.php')], ['core/lib/upgrade_service.php', 'HOTFIX-README.txt'], []);

if (!copy($upgradeZip, $licensePackage)) {
    throw new RuntimeException('Cannot copy upgrade package to license center.');
}
file_put_contents($releaseDir . '/update.json', json_encode($upgradeManifest, $jsonFlags) . "\n");
file_put_contents($releaseDir . '/SHA256SUMS.txt',
    hash_file('sha256', $fullZip) . '  ' . basename($fullZip) . "\n"
    . hash_file('sha256', $upgradeZip) . '  ' . basename($upgradeZip) . "\n"
    . hash_file('sha256', $hotfixZip) . '  ' . basename($hotfixZip) . "\n"
);

echo json_encode([
    'success' => true,
    'version' => RELEASE_VERSION,
    'full' => ['path' => $fullZip, 'files' => count($fullFiles), 'bytes' => filesize($fullZip), 'sha256' => hash_file('sha256', $fullZip)],
    'upgrade' => ['path' => $upgradeZip, 'files' => count($upgradeFiles), 'bytes' => filesize($upgradeZip), 'sha256' => hash_file('sha256', $upgradeZip)],
    'hotfix' => ['path' => $hotfixZip, 'files' => 1, 'bytes' => filesize($hotfixZip), 'sha256' => hash_file('sha256', $hotfixZip)],
    'license_package' => $licensePackage,
], $jsonFlags) . PHP_EOL;
