<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;

class BuildUpgrade extends Command
{
    protected $signature = 'upgrade:build
                            {to_version : 目标版本号，如 5.1.0}
                            {--from= : 源版本号，不指定则自动检测}
                            {--type=minor : 版本类型：patch|minor|major}
                            {--desc= : 版本描述}
                            {--from-commit= : 从指定 git commit 开始收集变更}
                            {--dry-run : 仅输出变更清单，不生成文件}
                            {--force : 跳过交互确认，已存在时自动覆盖}
                            {--no-baseline : 不更新 baseline.json（测试构建时使用）}';

    protected $description = '生成代码升级包（cmspro格式），与VersionManager应用对齐';

    private string $codeRoot;
    private string $upgradeRoot;

    private array $excludePatterns = [
        '.env', '.env.*',
        'vendor/', 'node_modules/',
        'storage/',
        'public/Uploads/',
        'public/apps/',
        'public/uploads/',
        'tests/',
        '*.log', '.git/',
        'composer.lock',
        'app/Apps/',
    ];

    private array $protectedFiles = [
        '.env', '.env.production', '.env.backup',
        'config/auth.php',
        'config/sanctum.php',
        'database/database.sqlite',
    ];

    public function handle(): int
    {
        $this->codeRoot = base_path();
        $this->upgradeRoot = storage_path('upgrades');

        $toVersion = $this->argument('to_version');
        $fromVersion = $this->option('from') ?? $this->detectFromVersion();
        $type = $this->option('type');
        $description = $this->option('desc') ?? '';
        $fromCommit = $this->option('from-commit');
        $dryRun = $this->option('dry-run');
        $force = $this->option('force');

        if (!$this->validateVersion($toVersion)) {
            $this->error("目标版本号格式无效：{$toVersion}，应为语义化版本号如 5.1.0");
            return self::FAILURE;
        }

        if (!$this->validateVersion($fromVersion)) {
            $this->error("源版本号格式无效：{$fromVersion}");
            return self::FAILURE;
        }

        if (!in_array($type, ['patch', 'minor', 'major'])) {
            $this->error("版本类型无效：{$type}，应为 patch|minor|major");
            return self::FAILURE;
        }

        $build = date('Ymd');
        $packageName = "cmspro-{$fromVersion}-to-{$toVersion}";

        $this->info("正在构建升级包 {$packageName}...");
        $this->info("源版本：{$fromVersion} → 目标版本：{$toVersion}");
        $this->info("构建号：{$build}");

        $changes = $this->collectChanges($fromCommit);

        if (empty($changes['added']) && empty($changes['modified']) && empty($changes['deleted'])) {
            $this->warn('未检测到任何代码变更，升级包为空。');
            return self::FAILURE;
        }

        $this->info(sprintf(
            '变更统计：新增 %d | 修改 %d | 删除 %d',
            count($changes['added']),
            count($changes['modified']),
            count($changes['deleted'])
        ));

        if (!$this->validateFilePaths(array_merge($changes['added'], $changes['modified'], $changes['deleted']))) {
            $this->error('文件路径安全校验失败，终止构建。');
            return self::FAILURE;
        }

        if ($dryRun) {
            $this->displayChanges($changes, $fromVersion, $toVersion);
            return self::SUCCESS;
        }

        $packageDir = $this->upgradeRoot . "/{$packageName}";

        if (File::isDirectory($packageDir)) {
            if ($force || $this->confirm("升级包目录 {$packageDir} 已存在，是否覆盖？")) {
                File::deleteDirectory($packageDir);
            } else {
                $this->info('已取消。');
                return self::SUCCESS;
            }
        }

        $this->createPackageStructure($packageDir);
        $this->copyCategorizedFiles($packageDir, $changes);
        $this->collectMigrations($packageDir, $changes);
        $this->generateScripts($packageDir);
        $this->generateUpgradeJson($packageDir, $fromVersion, $toVersion, $build, $type, $description, $changes);
        $this->generatePackageChangelog($packageDir, $fromVersion, $toVersion, $build, $description, $changes);
        $this->generateDevChangelog($fromVersion, $toVersion, $build, $changes);
        $zipPath = $this->createZipPackage($packageDir, $packageName);
        $this->writeChecksum($packageDir, $zipPath);
        $this->autoBackup($packageDir, $toVersion, $changes);

        $this->finalValidation($packageDir, $changes);

        // 发布完成后更新 baseline.json，作为下次发布的基线
        // dry-run 模式已在前面 return，不会走到这里
        // --no-baseline 选项跳过更新（测试构建时使用，避免污染基线）
        if (!$this->option('no-baseline')) {
            $this->updateBaseline($toVersion);
        } else {
            $this->info('已跳过 baseline.json 更新（--no-baseline）');
        }

        $this->info("升级包构建完成：{$packageName}.zip");

        return self::SUCCESS;
    }

    private function detectFromVersion(): string
    {
        if (!File::isDirectory($this->upgradeRoot)) {
            return config('app.version', '0.0.0');
        }

        $packages = collect(File::directories($this->upgradeRoot))
            ->map(fn($dir) => basename($dir))
            ->filter(fn($name) => preg_match('/^cmspro-[\d.]+-to-([\d.]+)$/', $name, $m))
            ->map(fn($name) => [
                'name' => $name,
                'to' => preg_replace('/^cmspro-[\d.]+-to-([\d.]+)$/', '$1', $name),
            ])
            ->sortByDesc(fn($item) => version_compare($item['to'], '0.0.0'))
            ->values();

        $latest = $packages->first();
        return $latest ? $latest['to'] : config('app.version', '0.0.0');
    }

    private function validateVersion(string $version): bool
    {
        return (bool) preg_match('/^\d+\.\d+\.\d+$/', $version);
    }

    private function collectChanges(?string $fromCommit): array
    {
        // 显式指定 --from-commit 时，强制走 git 模式
        if ($fromCommit) {
            $this->info("使用 git 模式收集变更（基线 commit: {$fromCommit}）");
            $changes = $this->collectChangesByGit($fromCommit);
        } elseif (File::exists($this->upgradeRoot . '/baseline.json')) {
            // 默认走 baseline 模式（基于上次发布时的文件快照对比）
            $this->info('使用 baseline 模式收集变更（基于上次发布的文件快照）');
            $changes = $this->collectChangesByBaseline();
        } else {
            // 无 baseline.json，fallback 到 git 模式（合并 HEAD~1 diff 与工作区状态）
            $this->warn('未检测到 baseline.json，本次使用 git 模式（HEAD~1 diff + 工作区 status）。');
            $this->warn('发布完成后将自动建立 baseline.json，后续发布会基于此基线对比。');
            $changes = $this->collectChangesByGit(null);
        }

        // 自动检测依赖文件：扫描变更文件中的类引用，将缺失的依赖加入 modify 列表
        $changes = $this->resolveDependencies($changes);

        return $changes;
    }

    /**
     * baseline 模式：扫描当前所有非排除文件，与 baseline.json 中记录的文件清单+sha256 对比
     */
    private function collectChangesByBaseline(): array
    {
        $baselinePath = $this->upgradeRoot . '/baseline.json';
        $baseline = json_decode(File::get($baselinePath), true);
        $baselineFiles = $baseline['files'] ?? [];

        $currentFiles = $this->scanCurrentFiles();

        $changes = ['added' => [], 'modified' => [], 'deleted' => []];

        foreach ($currentFiles as $path => $hash) {
            if (!isset($baselineFiles[$path])) {
                $changes['added'][] = $path;
            } elseif ($baselineFiles[$path] !== $hash) {
                $changes['modified'][] = $path;
            }
        }

        foreach ($baselineFiles as $path => $hash) {
            // 跳过被排除的路径（如 storage/、vendor/、app/Apps/ 等）
            // 旧 baseline.json 可能包含这些路径的历史快照，不应将其误判为删除
            if ($this->shouldExclude($path)) continue;
            if (!isset($currentFiles[$path])) {
                $changes['deleted'][] = $path;
            }
        }

        sort($changes['added']);
        sort($changes['modified']);
        sort($changes['deleted']);

        return $changes;
    }

    /**
     * 扫描 code/ 目录下所有非排除文件，返回 path => sha256 映射
     */
    private function scanCurrentFiles(): array
    {
        $result = [];
        $codeRootLength = strlen($this->codeRoot);

        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($this->codeRoot, \FilesystemIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::LEAVES_ONLY
        );

        foreach ($iterator as $file) {
            if (!$file->isFile()) continue;

            $relativePath = str_replace('\\', '/', substr($file->getPathname(), $codeRootLength + 1));

            if ($this->shouldExclude($relativePath)) continue;

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

        ksort($result);
        return $result;
    }

    /**
     * git 模式：合并 git diff（已提交变更）+ git status --short（工作区未提交变更），去重
     */
    private function collectChangesByGit(?string $fromCommit): array
    {
        // path => 'added' | 'modified' | 'deleted'
        $classified = [];

        // 第一来源：git diff（已提交变更）
        if ($fromCommit) {
            $diffOutput = $this->execGitCommand("git diff --name-status {$fromCommit} HEAD");
        } else {
            $diffOutput = $this->execGitCommand('git diff --name-status HEAD~1 HEAD');
        }
        $this->parseGitNameStatus($diffOutput, $classified);

        // 第二来源：git status --short（工作区未提交变更，优先级更高，会覆盖 git diff 的分类）
        $statusOutput = $this->execGitCommand('git status --short');
        $this->parseGitStatusShort($statusOutput, $classified);

        $changes = ['added' => [], 'modified' => [], 'deleted' => []];
        foreach ($classified as $path => $type) {
            $changes[$type][] = $path;
        }

        sort($changes['added']);
        sort($changes['modified']);
        sort($changes['deleted']);

        return $changes;
    }

    /**
     * 解析 git diff --name-status 输出
     * 格式：STATUS<tab>path 或 STATUS<tab>old_path<tab>new_path（R/C 重命名/复制）
     */
    private function parseGitNameStatus(string $output, array &$classified): void
    {
        if (empty(trim($output))) return;

        foreach (explode("\n", trim($output)) as $line) {
            $line = trim($line);
            if (empty($line)) continue;

            $parts = preg_split('/\s+/', $line, 3);
            if (count($parts) < 2) continue;

            $status = strtoupper($parts[0]);
            $path = null;
            $type = null;

            if (in_array($status, ['R', 'C']) && count($parts) >= 3) {
                // 重命名/复制：取新路径，视为新增
                $path = $parts[2];
                $type = 'added';
            } elseif ($status === 'A') {
                $path = $parts[1];
                $type = 'added';
            } elseif ($status === 'M' || $status === 'T') {
                $path = $parts[1];
                $type = 'modified';
            } elseif ($status === 'D') {
                $path = $parts[1];
                $type = 'deleted';
            } else {
                $path = $parts[1];
                $type = 'modified';
            }

            $relativePath = $this->normalizePath($path);
            if ($this->shouldExclude($relativePath)) continue;

            $classified[$relativePath] = $type;
        }
    }

    /**
     * 解析 git status --short 输出
     * 格式：XY<space>path 或 XY<space>old_path -> new_path（R/C 重命名/复制）
     * X = staged 状态，Y = 工作区状态
     */
    private function parseGitStatusShort(string $output, array &$classified): void
    {
        if (empty(trim($output))) return;

        foreach (explode("\n", trim($output)) as $line) {
            $line = rtrim($line);
            if (empty($line) || strlen($line) < 3) continue;

            $xy = substr($line, 0, 2);
            $rest = ltrim(substr($line, 2));

            $path = null;
            $type = null;

            if (str_contains($rest, ' -> ')) {
                // 重命名/复制：取新路径，视为新增
                $parts = explode(' -> ', $rest, 2);
                $path = $parts[1] ?? '';
                $type = 'added';
            } else {
                $path = $rest;
                $x = strtoupper(substr($xy, 0, 1));
                $y = strtoupper(substr($xy, 1, 1));

                // 优先级：删除 > 未跟踪 > 新增/重命名 > 修改
                if ($x === 'D' || $y === 'D') {
                    $type = 'deleted';
                } elseif ($x === '?' || $y === '?') {
                    $type = 'added';
                } elseif (in_array($x, ['A', 'R', 'C']) || in_array($y, ['A', 'R', 'C'])) {
                    $type = 'added';
                } else {
                    $type = 'modified';
                }
            }

            if (empty($path)) continue;

            $relativePath = $this->normalizePath($path);
            if ($this->shouldExclude($relativePath)) continue;

            $classified[$relativePath] = $type;
        }
    }

    /**
     * 发布完成后，将当前文件清单写入 baseline.json，作为下次发布的基线
     */
    private function updateBaseline(string $toVersion): void
    {
        $baselinePath = $this->upgradeRoot . '/baseline.json';

        // 备份旧 baseline.json
        if (File::exists($baselinePath)) {
            $backupDir = $this->upgradeRoot . '/baseline_backups';
            File::ensureDirectoryExists($backupDir);
            $oldBaseline = json_decode(File::get($baselinePath), true);
            $oldVersion = $oldBaseline['version'] ?? 'unknown';
            $backupPath = $backupDir . "/baseline_v{$oldVersion}_" . date('Ymd_His') . '.json';
            File::copy($baselinePath, $backupPath);
            $this->info("旧 baseline.json 已备份：baseline_backups/baseline_v{$oldVersion}_" . date('Ymd_His') . '.json');
        }

        $currentFiles = $this->scanCurrentFiles();

        $baseline = [
            'version' => $toVersion,
            'generated_at' => date('Y-m-d H:i:s'),
            'files' => $currentFiles,
        ];

        File::put($baselinePath, json_encode($baseline, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));

        $this->info("baseline.json 已更新（版本: {$toVersion}，文件数: " . count($currentFiles) . "）");
    }

    /**
     * 自动检测依赖文件，防止目标服务器因历史升级遗漏而缺失依赖。
     *
     * 三层检测机制：
     * 1. PSR-4 类依赖：扫描变更文件和关键入口文件的 use 语句，将被引用但未纳入变更的类文件加入 modify 列表
     * 2. 全局函数依赖：扫描变更文件中调用的全局函数，在 composer.json autoload.files 声明的辅助函数文件中搜索定义，若发现则纳入
     * 3. 关键入口文件：始终扫描系统核心入口文件（bootstrap/app.php、helpers.php 等）的依赖
     */
    private function resolveDependencies(array $changes): array
    {
        // 系统关键入口文件：始终扫描其依赖，即使未在当前变更列表中
        $criticalFiles = [
            'bootstrap/app.php',
            'bootstrap/providers.php',
            'app/Http/Kernel.php',
            'app/Providers/AppServiceProvider.php',
            'app/Providers/RouteServiceProvider.php',
            'app/Providers/AuthServiceProvider.php',
            'app/Providers/EventServiceProvider.php',
            'app/Providers/BroadcastServiceProvider.php',
            'routes/web.php',
            'routes/api.php',
            'routes/console.php',
        ];

        // 解析 composer.json autoload.files，纳入全局辅助函数文件
        $autoloadFiles = $this->parseComposerAutoloadFiles();
        $criticalFiles = array_merge($criticalFiles, $autoloadFiles);

        $allChanged = array_merge($changes['added'], $changes['modified']);
        $scanFiles = array_unique(array_merge($allChanged, $criticalFiles));
        $changedSet = array_flip($allChanged);
        $missingDeps = [];

        // 预构建全局函数索引：autoload.files 中定义的函数名 → 文件路径
        $globalFuncIndex = $this->buildGlobalFunctionIndex($autoloadFiles);

        foreach ($scanFiles as $relativePath) {
            if (!str_ends_with($relativePath, '.php')) continue;

            $fullPath = $this->codeRoot . '/' . $relativePath;
            if (!File::exists($fullPath)) continue;

            $content = File::get($fullPath);

            // 第一层：提取 use 语句中的类名（如 use App\Http\Middleware\AllowSubdomainCors;）
            if (preg_match_all('/^use\s+([A-Za-z_][\w\\\\]*);/m', $content, $matches)) {
                foreach ($matches[1] as $fullClassName) {
                    $depPath = $this->classToPath($fullClassName);
                    if ($depPath === null) continue;
                    if (isset($changedSet[$depPath])) continue;
                    if ($this->shouldExclude($depPath)) continue;
                    if (!File::exists($this->codeRoot . '/' . $depPath)) continue;

                    $missingDeps[$depPath] = true;
                }
            }

            // 第二层：提取全局函数调用，匹配 autoload.files 中定义的函数
            if (!empty($globalFuncIndex) && preg_match_all('/\b([a-z_][a-z0-9_]*)\s*\(/i', $content, $funcMatches)) {
                foreach ($funcMatches[1] as $funcName) {
                    $funcLower = strtolower($funcName);
                    if (!isset($globalFuncIndex[$funcLower])) continue;

                    $depPath = $globalFuncIndex[$funcLower];
                    if (isset($changedSet[$depPath])) continue;
                    if (isset($missingDeps[$depPath])) continue;
                    if ($this->shouldExclude($depPath)) continue;
                    if (!File::exists($this->codeRoot . '/' . $depPath)) continue;

                    $missingDeps[$depPath] = true;
                }
            }
        }

        if (empty($missingDeps)) {
            return $changes;
        }

        $deps = array_keys($missingDeps);
        sort($deps);

        $this->warn('检测到 ' . count($deps) . ' 个依赖文件未被纳入变更，已自动加入 modify 列表：');
        foreach ($deps as $dep) {
            $this->line("  ↳ {$dep}");
        }

        $changes['modified'] = array_values(array_unique(array_merge($changes['modified'], $deps)));
        sort($changes['modified']);

        return $changes;
    }

    /**
     * 解析 composer.json 的 autoload.files 配置，返回辅助函数文件相对路径列表
     */
    private function parseComposerAutoloadFiles(): array
    {
        $composerPath = $this->codeRoot . '/composer.json';
        if (!File::exists($composerPath)) {
            return [];
        }

        $composer = json_decode(File::get($composerPath), true);
        if (!is_array($composer)) {
            return [];
        }

        $files = $composer['autoload']['files'] ?? [];
        $result = [];
        foreach ($files as $file) {
            $file = $this->normalizePath($file);
            if (File::exists($this->codeRoot . '/' . $file)) {
                $result[] = $file;
            }
        }

        return $result;
    }

    /**
     * 构建"全局函数名 → 定义文件路径"的索引
     *
     * 扫描 autoload.files 声明的文件，提取其中通过 function 定义的函数名，
     * 用于后续匹配变更文件中的全局函数调用。
     */
    private function buildGlobalFunctionIndex(array $autoloadFiles): array
    {
        $index = [];
        foreach ($autoloadFiles as $relativePath) {
            $fullPath = $this->codeRoot . '/' . $relativePath;
            if (!File::exists($fullPath)) continue;

            $content = File::get($fullPath);

            // 匹配 function xxx( 形式的函数定义（不含方法定义中的 $this->、:: 前缀）
            // 同时匹配 if (!function_exists('xxx')) 形式的守卫
            if (preg_match_all('/\bfunction\s+([a-z_][a-z0-9_]*)\s*\(/i', $content, $matches)) {
                foreach ($matches[1] as $funcName) {
                    $funcLower = strtolower($funcName);
                    // 仅索引第一个定义，避免重复覆盖
                    if (!isset($index[$funcLower])) {
                        $index[$funcLower] = $relativePath;
                    }
                }
            }
        }

        return $index;
    }

    /**
     * 将完整类名转换为相对文件路径（PSR-4 映射）
     * App\Http\Middleware\AllowSubdomainCors → app/Http/Middleware/AllowSubdomainCors.php
     */
    private function classToPath(string $fullClassName): ?string
    {
        // 仅处理 App\ 命名空间（项目自有类）
        if (!str_starts_with($fullClassName, 'App\\')) {
            return null;
        }

        $relative = substr($fullClassName, 4); // 去掉 App\
        $path = 'app/' . str_replace('\\', '/', $relative) . '.php';

        return $path;
    }

    private function execGitCommand(string $command): string
    {
        $cwd = $this->codeRoot;
        if (PHP_OS_FAMILY === 'Windows') {
            $fullCmd = "cd /d \"{$cwd}\" && {$command} 2>nul";
        } else {
            $fullCmd = "cd \"{$cwd}\" && {$command} 2>/dev/null";
        }

        exec($fullCmd, $lines, $result);
        return implode("\n", $lines);
    }

    private function shouldExclude(string $file): bool
    {
        // 排除根目录下的 PHP 文件（测试遗留文件）
        if (!str_contains($file, '/') && str_ends_with($file, '.php')) {
            return true;
        }

        foreach ($this->excludePatterns as $pattern) {
            if (str_starts_with($pattern, '*.')) {
                if (str_ends_with($file, substr($pattern, 1))) return true;
            } elseif (str_ends_with($pattern, '/')) {
                if (str_starts_with($file, $pattern) || str_contains($file, '/' . $pattern)) return true;
            } else {
                if ($file === $pattern || str_starts_with($file . '/', $pattern)) return true;
            }
        }
        return false;
    }

    private function normalizePath(string $path): string
    {
        $path = str_replace('\\', '/', $path);
        $prefix = 'code/';
        if (str_starts_with($path, $prefix)) {
            $path = substr($path, strlen($prefix));
        }
        return ltrim($path, '/');
    }

    private function validateFilePaths(array $paths): bool
    {
        $valid = true;

        foreach ($paths as $relativePath) {
            if (str_contains($relativePath, '..')) {
                $this->error("路径穿越检测失败：{$relativePath}");
                $valid = false;
            }

            if (str_starts_with($relativePath, '/')) {
                $this->error("绝对路径检测失败：{$relativePath}");
                $valid = false;
            }

            if (str_contains($relativePath, "\0")) {
                $this->error("空字节检测失败：{$relativePath}");
                $valid = false;
            }

            if (in_array($relativePath, $this->protectedFiles)) {
                $this->error("敏感文件保护：{$relativePath} 禁止纳入升级包");
                $valid = false;
            }
        }

        return $valid;
    }

    private function displayChanges(array $changes, string $from, string $to): void
    {
        $this->newLine();
        $this->info("=== 变更清单（dry-run）===");
        $this->info("升级包名称：cmspro-{$from}-to-{$to}.zip");
        $this->newLine();

        if (!empty($changes['added'])) {
            $this->warn('新增文件（files/add/）：');
            foreach ($changes['added'] as $file) $this->line("  + {$file}");
        }

        if (!empty($changes['modified'])) {
            $this->newLine();
            $this->warn('修改文件（files/modify/）：');
            foreach ($changes['modified'] as $file) $this->line("  M {$file}");
        }

        if (!empty($changes['deleted'])) {
            $this->newLine();
            $this->warn('删除文件（files/delete/ 声明）：');
            foreach ($changes['deleted'] as $file) $this->line("  - {$file}");
        }
    }

    private function createPackageStructure(string $packageDir): void
    {
        $dirs = [
            $packageDir . '/files/add',
            $packageDir . '/files/modify',
            $packageDir . '/files/delete',
            $packageDir . '/migrations',
            $packageDir . '/scripts',
        ];

        foreach ($dirs as $dir) File::ensureDirectoryExists($dir);

        $this->info('目录结构已创建（add/modify/delete/migrations/scripts）');
    }

    private function copyCategorizedFiles(string $packageDir, array $changes): void
    {
        $copied = 0;

        foreach ($changes['added'] as $relativePath) {
            $sourcePath = $this->codeRoot . '/' . $relativePath;
            $destPath = $packageDir . '/files/add/' . $relativePath;

            if (!File::exists($sourcePath)) {
                $this->warn("新增文件不存在，跳过：{$relativePath}");
                continue;
            }

            File::ensureDirectoryExists(dirname($destPath));
            File::copy($sourcePath, $destPath);
            $copied++;
        }

        foreach ($changes['modified'] as $relativePath) {
            $sourcePath = $this->codeRoot . '/' . $relativePath;
            $destPath = $packageDir . '/files/modify/' . $relativePath;

            if (!File::exists($sourcePath)) {
                $this->warn("修改文件不存在，跳过：{$relativePath}");
                continue;
            }

            File::ensureDirectoryExists(dirname($destPath));
            File::copy($sourcePath, $destPath);
            $copied++;
        }

        $this->info("已复制 {$copied} 个变更文件到 files/add/ 和 files/modify/");
    }

    private function collectMigrations(string $packageDir, array $changes): void
    {
        // 仅收集系统级迁移文件（database/migrations/）
        // 应用级迁移（app/Apps/*/Migrations/）通过 Appstore 独立处理，不纳入系统升级包
        $migrationFiles = [];

        foreach (array_merge($changes['added'], $changes['modified']) as $relativePath) {
            if (preg_match('#^database/migrations/(.+\.php)$#', $relativePath, $m)) {
                $migrationFiles[] = [
                    'source' => $this->codeRoot . '/database/migrations/' . $m[1],
                    'name' => $m[1],
                    'dest' => $packageDir . '/migrations/' . $m[1],
                ];
            }
        }

        if (empty($migrationFiles)) {
            $this->info('无数据库迁移文件');
            return;
        }

        $copied = 0;
        foreach ($migrationFiles as $migration) {
            if (File::exists($migration['source'])) {
                File::ensureDirectoryExists(dirname($migration['dest']));
                File::copy($migration['source'], $migration['dest']);
                $copied++;
            }
        }

        $this->info("已收集 {$copied} 个系统级迁移文件到 migrations/");
    }

    private function generateScripts(string $packageDir): void
    {
        $beforeScript = <<<'PHP'
<?php
echo "升级前检查开始...\n";
$requiredPhp = '8.3';
if (version_compare(PHP_VERSION, $requiredPhp, '<')) {
    echo "错误：PHP 版本要求 >= {$requiredPhp}，当前 " . PHP_VERSION . "\n";
    exit(1);
}
echo "升级前检查完成\n";
PHP;

        $afterScript = <<<'PHP'
<?php
echo "升级后处理开始...\n";
echo "缓存清理和重建将由 VersionManager 统一执行\n";
echo "升级后处理完成\n";
PHP;

        File::put($packageDir . '/scripts/before_upgrade.php', $beforeScript);
        File::put($packageDir . '/scripts/after_upgrade.php', $afterScript);

        $this->info('升级脚本模板已生成（before_upgrade.php / after_upgrade.php）');
    }

    private function generateUpgradeJson(
        string $packageDir,
        string $fromVersion,
        string $toVersion,
        string $build,
        string $type,
        string $description,
        array $changes
    ): void {
        $hasBefore = File::exists($packageDir . '/scripts/before_upgrade.php');
        $hasAfter = File::exists($packageDir . '/scripts/after_upgrade.php');

        $migrationFiles = [];
        $migrationsDir = $packageDir . '/migrations';
        if (File::isDirectory($migrationsDir)) {
            $migrationFiles = collect(File::allFiles($migrationsDir))
                ->map(fn($f) => $f->getFilename())
                ->sort()
                ->values()
                ->toArray();
        }

        $upgradeJson = [
            'from_version' => $fromVersion,
            'to_version' => $toVersion,
            'build' => $build,
            'release_date' => date('Y-m-d'),
            'changelog' => $description,
            'require' => ['php' => '>=8.3'],
            'files' => [
                'add' => array_values($changes['added']),
                'modify' => array_values($changes['modified']),
                'delete' => array_values($changes['deleted']),
            ],
            'migrations' => $migrationFiles,
            'has_before_script' => $hasBefore,
            'has_after_script' => $hasAfter,
            'estimated_size' => '',
            'checksum' => '',
        ];

        File::put(
            $packageDir . '/upgrade.json',
            json_encode($upgradeJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
        );

        $this->info('upgrade.json 已生成');
    }

    private function generatePackageChangelog(
        string $packageDir,
        string $fromVersion,
        string $toVersion,
        string $build,
        string $description,
        array $changes
    ): void {
        $content = "# CmsPro {$toVersion} 更新日志\n\n";
        $content .= "> 发布日期：" . date('Y-m-d') . "\n";
        $content .= "> 构建版本：{$build}\n";
        $content .= "> 升级要求：CmsPro = {$fromVersion} | PHP >= 8.3\n\n";

        if ($description) {
            $content .= "## 概述\n\n{$description}\n\n";
        }

        if (!empty($changes['added'])) {
            $content .= "## 新增文件\n\n";
            foreach ($changes['added'] as $f) $content .= "- `{$f}`\n";
            $content .= "\n";
        }

        if (!empty($changes['modified'])) {
            $content .= "## 修改文件\n\n";
            foreach ($changes['modified'] as $f) $content .= "- `{$f}`\n";
            $content .= "\n";
        }

        if (!empty($changes['deleted'])) {
            $content .= "## 删除文件\n\n";
            foreach ($changes['deleted'] as $f) $content .= "- `{$f}`\n";
            $content .= "\n";
        }

        $content .= "## 升级说明\n\n";
        $content .= "- 通过 VersionManager 应用或 artisan upgrade:deploy 执行升级\n";
        $content .= "- 升级过程会自动备份数据库和受影响的代码文件\n";
        $content .= "- 如遇问题可使用 rollback 命令回滚到 {$fromVersion}\n";

        File::put($packageDir . '/changelog.md', $content);
        $this->info('包内 changelog.md 已生成');
    }

    private function generateDevChangelog(string $from, string $to, string $build, array $changes): void
    {
        $changelogDir = $this->upgradeRoot . '/changelog';
        File::ensureDirectoryExists($changelogDir);

        $content = "# cmspro-{$from}-to-{$to} 开发变更记录\n\n";
        $content .= "> 存档时间：" . date('Y-m-d H:i:s') . "\n";
        $content .= "> 构建号：{$build}\n\n";

        $content .= "## 文件变更统计\n\n";
        $content .= "| 类型 | 数量 |\n|------|------|\n";
        $content .= "| 新增 (add) | " . count($changes['added']) . " |\n";
        $content .= "| 修改 (modify) | " . count($changes['modified']) . " |\n";
        $content .= "| 删除 (delete) | " . count($changes['deleted']) . " |\n\n";

        if (!empty($changes['added'])) {
            $content .= "## 新增文件完整列表\n\n";
            foreach ($changes['added'] as $f) $content .= "- {$f}\n";
            $content .= "\n";
        }

        if (!empty($changes['modified'])) {
            $content .= "## 修改文件列表\n\n";
            foreach ($changes['modified'] as $f) $content .= "- {$f}\n";
            $content .= "\n";
        }

        if (!empty($changes['deleted'])) {
            $content .= "## 删除文件列表\n\n";
            foreach ($changes['deleted'] as $f) $content .= "- {$f}\n";
            $content .= "\n";
        }

        File::put($changelogDir . "/cmspro-{$from}-to-{$to}.md", $content);
        $this->info("开发侧 changelog 已生成：changelog/cmspro-{$from}-to-{$to}.md");
    }

    private function createZipPackage(string $packageDir, string $packageName): string
    {
        $zipPath = $this->upgradeRoot . "/{$packageName}.zip";

        if (File::exists($zipPath)) File::delete($zipPath);

        $zip = new \ZipArchive();
        $result = $zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);

        if ($result !== true) {
            $this->error("无法创建ZIP文件：{$zipPath}（错误码：{$result}）");
            return '';
        }

        $files = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($packageDir, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::LEAVES_ONLY
        );

        foreach ($files as $file) {
            $filePath = $file->getRealPath();
            $relativePath = substr($filePath, strlen($packageDir) + 1);
            $relativePath = str_replace('\\', '/', $relativePath);
            $zip->addFile($filePath, $relativePath);
        }

        $zip->close();

        return $zipPath;
    }

    private function writeChecksum(string $packageDir, string $zipPath): void
    {
        if (empty($zipPath) || !File::exists($zipPath)) return;

        $hash = hash_file('sha256', $zipPath);
        $checksum = "sha256:{$hash}";

        $jsonPath = $packageDir . '/upgrade.json';
        $data = json_decode(File::get($jsonPath), true);
        $data['checksum'] = $checksum;
        $data['estimated_size'] = $this->formatBytes(filesize($zipPath));

        File::put($jsonPath, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));

        $size = $this->formatBytes(filesize($zipPath));
        $this->info("ZIP 打包完成：{$size}");
        $this->info("SHA256 校验值：{$checksum}");
    }

    private function autoBackup(string $packageDir, string $toVersion, array $changes): void
    {
        $backupDir = $this->upgradeRoot . '/backups';
        File::ensureDirectoryExists($backupDir);

        $timestamp = date('Ymd_His');
        $backupFile = $backupDir . "/pre_{$toVersion}_{$timestamp}.json";

        $backupData = [
            'version' => $toVersion,
            'from_package' => basename($packageDir),
            'backup_time' => date('Y-m-d H:i:s'),
            'files' => [],
        ];

        $targets = array_merge($changes['modified'], $changes['deleted']);
        foreach ($targets as $relativePath) {
            $fullPath = $this->codeRoot . '/' . $relativePath;
            if (File::exists($fullPath)) {
                $backupData['files'][$relativePath] = base64_encode(File::get($fullPath));
            }
        }

        File::put($backupFile, json_encode($backupData, JSON_UNESCAPED_SLASHES));
        $this->info("开发侧备份已保存：backups/pre_{$toVersion}_{$timestamp}.json");
    }

    private function finalValidation(string $packageDir, array $changes): void
    {
        $this->info('--- 最终校验 ---');
        $errors = [];

        $upgradeJsonPath = $packageDir . '/upgrade.json';
        if (!File::exists($upgradeJsonPath)) {
            $errors[] = 'upgrade.json 不存在';
        } else {
            $json = json_decode(File::get($upgradeJsonPath), true);
            $requiredFields = ['from_version', 'to_version', 'build', 'release_date', 'changelog', 'files', 'checksum'];
            foreach ($requiredFields as $field) {
                if (!isset($json[$field])) $errors[] = "upgrade.json 缺少必填字段: {$field}";
            }
        }

        foreach ($changes['added'] as $f) {
            if (!File::exists($packageDir . '/files/add/' . $f)) {
                $errors[] = "files/add/ 缺少: {$f}";
            }
        }

        foreach ($changes['modified'] as $f) {
            if (!File::exists($packageDir . '/files/modify/' . $f)) {
                $errors[] = "files/modify/ 缺少: {$f}";
            }
        }

        if (!empty($errors)) {
            $this->warn('校验发现以下问题：');
            foreach ($errors as $err) $this->warn("  - {$err}");
        } else {
            $this->info('所有校验项通过 ✓');
        }
    }

    private function formatBytes(int $bytes): string
    {
        $units = ['B', 'KB', 'MB', 'GB'];
        $i = 0;
        while ($bytes >= 1024 && $i < count($units) - 1) {
            $bytes /= 1024;
            $i++;
        }
        return round($bytes, 2) . ' ' . $units[$i];
    }
}
