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

if (!defined('RELEASE_VERSION')) define('RELEASE_VERSION', '1.1.5');
if (!defined('RELEASE_VERSION_CODE')) define('RELEASE_VERSION_CODE', 115);
if (!defined('RELEASE_MINIMUM_VERSION')) define('RELEASE_MINIMUM_VERSION', '1.1.4');
if (!defined('RELEASE_DESCRIPTION')) define('RELEASE_DESCRIPTION', '增加产品最多5张多图功能，并安装完整k620前台模板。');
if (!defined('RELEASE_INCLUDE_TEMPLATE_UPGRADE')) define('RELEASE_INCLUDE_TEMPLATE_UPGRADE', true);
if (!defined('RELEASE_UPGRADE_SQL')) define('RELEASE_UPGRADE_SQL', "ALTER TABLE `{prefix}product` ADD COLUMN `pictures` TEXT NULL COMMENT '产品多图JSON，最多5张' AFTER `litpic`;\n");

$root = str_replace('\\', '/', dirname(__DIR__));
$releaseDir = $root . '/release/' . RELEASE_VERSION;
$fullZip = $releaseDir . '/EnterpriseCMS-' . RELEASE_VERSION . '-full.zip';
$upgradeZip = $releaseDir . '/EnterpriseCMS-' . RELEASE_VERSION . '-upgrade-from-' . RELEASE_MINIMUM_VERSION . '.zip';
$incrementalZip = $releaseDir . '/yutou_' . RELEASE_VERSION . '.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 release115RelativePath($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 release115ShouldExclude($relative)
{
    $relative = str_replace('\\', '/', $relative);
    if (in_array($relative, ['core/db/config.php', 'data/config.php', 'data/install_report_cache.json'], true)) {
        return true;
    }
    if (preg_match('#(?:^|/)(?:backup|backups|cache|logs?|sessions?|tmp|temp|tests?|release)(?:/|$)#i', $relative)) {
        return true;
    }
    return (bool) preg_match('/(?:\.bak(?:-|$)|\.old$|\.orig$|\.rej$|\.log$|~$|Thumbs\.db$|\.DS_Store$)/i', basename($relative));
}

function release115CollectDirectory($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 = release115RelativePath($root, $item->getPathname());
        if (!release115ShouldExclude($relative)) {
            $files[$relative] = $item->getPathname();
        }
    }
    return $files;
}

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

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

function release115VirtualHashes(array $files)
{
    $hashes = [];
    foreach ($files as $relative => $content) {
        $hashes[$relative] = hash('sha256', $content);
    }
    return $hashes;
}

function release115CreateZip($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 release115VerifyZip($path, array $hashes, 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 ($hashes 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', 'db_upgrade.php'];
$fullFiles = release115Collect(
    $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);
}

$changedFiles = defined('RELEASE_CHANGED_FILES') ? RELEASE_CHANGED_FILES : ['admin/index.php', 'admin/modules/product.php', 'core/db/schema.php', 'core/lib/license.php', 'db_upgrade.php', 'RELEASE-NOTES.md'];
$upgradeFiles = RELEASE_INCLUDE_TEMPLATE_UPGRADE ? release115CollectDirectory($root, 'template/default') : [];
foreach ($changedFiles as $relative) {
    $upgradeFiles[$relative] = $root . '/' . $relative;
}
ksort($upgradeFiles, SORT_STRING);
$incrementalFiles = [];
foreach ($changedFiles as $relative) {
    $incrementalFiles[$relative] = $root . '/' . $relative;
}
ksort($incrementalFiles, SORT_STRING);

$buildTime = date('Y-m-d H:i:s');
$jsonFlags = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT;
$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/'],
];
$upgradeManifest = [
    'name' => 'EnterpriseCMS ' . RELEASE_VERSION . ' 手工升级包',
    'version' => RELEASE_VERSION,
    'version_code' => RELEASE_VERSION_CODE,
    'build_time' => $buildTime,
    'build_php_version' => PHP_VERSION,
    'package_type' => 'manual-root-overwrite',
    'minimum_version' => RELEASE_MINIMUM_VERSION,
    'database_changes' => true,
    'description' => RELEASE_DESCRIPTION,
    'file_count' => count($upgradeFiles),
    'files' => array_keys($upgradeFiles),
    'excluded_paths' => ['data/', 'uploads/', 'install/', 'license-center/'],
];
$incrementalManifest = [
    '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' => RELEASE_MINIMUM_VERSION,
    'database_changes' => true,
    'description' => RELEASE_DESCRIPTION,
    'file_count' => count($incrementalFiles),
    'files' => array_keys($incrementalFiles),
    'excluded_paths' => ['data/', 'uploads/', 'template/', 'install/', 'license-center/'],
];
$upgradeSql = '-- EnterpriseCMS ' . RELEASE_VERSION . "\n" . RELEASE_UPGRADE_SQL;
$upgradeReadme = <<<TXT
EnterpriseCMS {RELEASE_VERSION} 手工升级说明

适用版本：{RELEASE_MINIMUM_VERSION}

1. 备份网站文件和数据库。
2. 将本升级包解压到网站根目录并覆盖同名文件。
3. 在网站根目录执行：php db_upgrade.php
4. 登录后台检查版本管理、模板管理、产品多图和留言功能。

注意：手工升级前请先在测试环境验证，不适用于后台在线升级入口。
TXT;
$upgradeReadme = str_replace(['{RELEASE_VERSION}', '{RELEASE_MINIMUM_VERSION}'], [RELEASE_VERSION, RELEASE_MINIMUM_VERSION], $upgradeReadme);

$fullVirtual = ['release.json' => json_encode($fullManifest, $jsonFlags) . "\n"];
$fullHashes = array_merge(release115FileHashes($fullFiles), release115VirtualHashes($fullVirtual));
$fullVirtual['checksums.json'] = json_encode($fullHashes, $jsonFlags) . "\n";

$upgradeVirtual = [
    'update.json' => json_encode($upgradeManifest, $jsonFlags) . "\n",
    'upgrade.sql' => $upgradeSql,
    'UPGRADE-README.txt' => $upgradeReadme,
];
$upgradeHashes = array_merge(release115FileHashes($upgradeFiles), release115VirtualHashes($upgradeVirtual));
$upgradeVirtual['checksums.json'] = json_encode($upgradeHashes, $jsonFlags) . "\n";
$incrementalVirtual = [
    'update.json' => json_encode($incrementalManifest, $jsonFlags) . "\n",
    'upgrade.sql' => $upgradeSql,
];
$incrementalHashes = array_merge(release115FileHashes($incrementalFiles), release115VirtualHashes($incrementalVirtual));
$incrementalVirtual['checksums.json'] = json_encode($incrementalHashes, $jsonFlags) . "\n";

release115CreateZip($fullZip, $fullFiles, $fullVirtual);
release115CreateZip($upgradeZip, $upgradeFiles, $upgradeVirtual);
release115CreateZip($incrementalZip, $incrementalFiles, $incrementalVirtual);
release115VerifyZip($fullZip, $fullHashes, ['release.json', 'checksums.json', 'install/index.php', 'template/default/index.html'], ['license-center/', 'core/db/config.php', 'data/config.php', 'release/']);
$upgradeRequired = ['update.json', 'upgrade.sql', 'checksums.json', 'UPGRADE-README.txt', 'db_upgrade.php'];
if (RELEASE_INCLUDE_TEMPLATE_UPGRADE) $upgradeRequired[] = 'template/default/index.html';
release115VerifyZip($upgradeZip, $upgradeHashes, $upgradeRequired, ['data/', 'uploads/', 'install/', 'license-center/']);
release115VerifyZip($incrementalZip, $incrementalHashes, ['update.json', 'upgrade.sql', 'checksums.json', 'db_upgrade.php'], ['data/', 'uploads/', 'template/', 'install/', 'license-center/']);
if (!copy($incrementalZip, $licensePackage)) {
    throw new RuntimeException('Cannot copy incremental package to license center.');
}

$sums = hash_file('sha256', $fullZip) . '  ' . basename($fullZip) . "\n"
    . hash_file('sha256', $upgradeZip) . '  ' . basename($upgradeZip) . "\n"
    . hash_file('sha256', $incrementalZip) . '  ' . basename($incrementalZip) . "\n";
file_put_contents($releaseDir . '/SHA256SUMS.txt', $sums);
file_put_contents($releaseDir . '/release.json', json_encode([
    'version' => RELEASE_VERSION,
    'build_time' => $buildTime,
    'packages' => [basename($fullZip), basename($upgradeZip), basename($incrementalZip)],
], $jsonFlags) . "\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)],
    'incremental' => ['path' => $incrementalZip, 'files' => count($incrementalFiles), 'bytes' => filesize($incrementalZip), 'sha256' => hash_file('sha256', $incrementalZip)],
    'license_package' => $licensePackage,
], $jsonFlags) . PHP_EOL;
