<?php

namespace App\Apps\Versionmgr\Services;

use App\Apps\Versionmgr\Models\SystemVersion;
use App\Http\Responses\ApiResponse;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use ZipArchive;

class RepairService
{
    /**
     * 受保护路径黑名单：这些路径下的文件不参与对比和修复
     */
    protected const PROTECTED_PATHS = [
        '.env',
        '.env.example',
        'config/database.php',
        'config/auth.php',
        'config/app.php',
        'storage/',
        'vendor/',
        'bootstrap/cache/',
        'bootstrap/providers.php',
    ];

    /**
     * 修复工作根目录：storage/app_versionmgr/repair/
     */
    protected string $workDir;

    /**
     * 修复任务锁名
     */
    protected string $lockName = 'versionmgr_repair';

    public function __construct()
    {
        $this->workDir = storage_path('app_versionmgr/repair');
    }

    /**
     * 获取修复任务锁
     */
    protected function acquireLock(int $timeout = 3600): ?\Illuminate\Contracts\Cache\Lock
    {
        $lock = Cache::lock($this->lockName, $timeout);
        if ($lock->get()) {
            return $lock;
        }
        return null;
    }

    /**
     * 检查远程最新版本
     */
    public function checkLatest(): array
    {
        try {
            $current = SystemVersion::getCurrentVersion();
        } catch (\Throwable $e) {
            Log::warning('获取当前版本失败', ['error' => $e->getMessage()]);
            $current = null;
        }

        $currentVersion = $current ? $current->version : config('app.version', '5.0.1');
        $currentBuild = $current ? $current->build : '';

        if (!config('apps.market.enabled')) {
            return ApiResponse::error(60011, '未启用远程版本服务');
        }

        try {
            $apiUrl = rtrim(config('apps.market.api_url'), '/');
            $response = Http::withOptions(['verify' => false])
                ->withHeaders(['Accept' => 'application/json'])
                ->get($apiUrl . '/api/versionsvr/latest');

            if (!$response->successful()) {
                return ApiResponse::error(60011, '远程版本服务不可用');
            }

            $json = $response->json();
            $latest = $json['data'] ?? null;

            if (!$latest) {
                return ApiResponse::error(60011, '远程服务未返回版本信息');
            }

            $hasUpdate = version_compare($latest['version'], $currentVersion, '>');

            return ApiResponse::success([
                'current_version' => $currentVersion,
                'current_build' => $currentBuild,
                'latest_version' => $latest['version'],
                'latest_build' => $latest['build'] ?? '',
                'changelog' => $latest['changelog'] ?? '',
                'release_date' => $latest['release_date'] ?? null,
                'has_update' => $hasUpdate,
                'program_download_url' => $apiUrl . '/api/versionsvr/program/' . $latest['version'],
            ]);
        } catch (\Throwable $e) {
            Log::error('检查远程版本失败', ['error' => $e->getMessage()]);
            return ApiResponse::error(60011, '远程版本服务不可用，请查看日志');
        }
    }

    /**
     * 下载完整程序包并解压、对比，返回差异列表
     */
    public function prepare(string $toVersion): array
    {
        @set_time_limit(0);
        @ini_set('memory_limit', '512M');

        $lock = $this->acquireLock();
        if ($lock === null) {
            return ApiResponse::error(60013, '当前有修复任务正在进行，请稍后重试');
        }

        try {
            if (session_status() === PHP_SESSION_ACTIVE) {
                session_write_close();
            }

            // 检查是否有未完成的修复任务
            $state = $this->getState();
            if (in_array($state['status'] ?? 'idle', ['downloading', 'extracting', 'comparing', 'repairing'])) {
                return ApiResponse::error(60013, '当前有修复任务正在进行，请先完成或清理');
            }

            // 清理旧的临时文件
            $this->cleanupWorkDir();

            $this->saveState([
                'status' => 'downloading',
                'to_version' => $toVersion,
                'started_at' => date('Y-m-d H:i:s'),
                'diff_files' => [],
                'extract_dir' => null,
                'zip_path' => null,
                'error_message' => null,
            ]);

            try {
                // 1. 下载完整程序包
                $apiUrl = rtrim(config('apps.market.api_url'), '/');
                $downloadUrl = $apiUrl . '/api/versionsvr/program/' . $toVersion;

                $this->updateState(['status' => 'downloading']);

                $downloadsDir = $this->workDir . '/downloads';
                if (!File::isDirectory($downloadsDir)) {
                    File::makeDirectory($downloadsDir, 0755, true);
                }
                $zipPath = $downloadsDir . "/cmspro-{$toVersion}-program.zip";

                $response = Http::withOptions([
                    'verify' => false,
                    'sink' => $zipPath,
                    'timeout' => 300,
                    'connect_timeout' => 10,
                ])->get($downloadUrl);

                if (!$response->successful()) {
                    throw new \RuntimeException('下载失败：HTTP ' . $response->status());
                }

                if (!File::exists($zipPath) || File::size($zipPath) === 0) {
                    throw new \RuntimeException('下载文件为空');
                }

                $this->updateState(['zip_path' => $zipPath]);

                // 2. 解压
                $this->updateState(['status' => 'extracting']);

                $extractDir = $this->workDir . '/extracted_' . time();
                File::makeDirectory($extractDir, 0755, true);

                $zip = new ZipArchive();
                if ($zip->open($zipPath) !== true) {
                    throw new \RuntimeException('解压失败：无法打开 ZIP 文件');
                }
                $zip->extractTo($extractDir);
                $zip->close();

                // 检测并剥离 ZIP 的公共顶层目录（如打包时带了 code/ 目录层）
                $extractDir = $this->resolveExtractDir($extractDir);

                $this->updateState(['extract_dir' => $extractDir]);

                // 3. 对比文件
                $this->updateState(['status' => 'comparing']);

                $diffFiles = $this->compareFiles($extractDir);

                $this->updateState([
                    'status' => 'awaiting_selection',
                    'diff_files' => $diffFiles,
                ]);

                return ApiResponse::success([
                    'to_version' => $toVersion,
                    'diff_files' => $diffFiles,
                    'diff_count' => count($diffFiles),
                    'extract_dir' => $extractDir,
                ], '对比完成');
            } catch (\Throwable $e) {
                Log::error('版本修复准备阶段失败', [
                    'to_version' => $toVersion,
                    'error' => $e->getMessage(),
                    'file' => $e->getFile(),
                    'line' => $e->getLine(),
                ]);
                $this->updateState([
                    'status' => 'failed',
                    'error_message' => '准备阶段失败，详情见日志',
                ]);
                // 失败时不清理现场，保留 extract_dir 和 zip_path 供诊断
                // 用户可手动调用 cleanup 接口清理
                return ApiResponse::error(60001, '准备阶段失败，请查看日志或手动清理临时文件');
            }
        } finally {
            $lock->release();
        }
    }

    /**
     * 对比解压目录与本地文件，返回差异列表
     */
    protected function compareFiles(string $extractDir): array
    {
        $diffFiles = [];
        $allFiles = $this->scanDirectory($extractDir);

        foreach ($allFiles as $relativePath) {
            // 跳过黑名单路径
            if ($this->isProtectedPath($relativePath)) {
                continue;
            }

            $sourcePath = $extractDir . '/' . $relativePath;
            $targetPath = base_path($relativePath);

            if (!File::exists($targetPath)) {
                // 本地不存在 → 新增
                $diffFiles[] = [
                    'path' => $relativePath,
                    'status' => 'added',
                    'local_size' => 0,
                    'remote_size' => File::size($sourcePath),
                    'local_hash' => '',
                    'remote_hash' => hash_file('sha256', $sourcePath),
                ];
                continue;
            }

            $localSize = File::size($targetPath);
            $remoteSize = File::size($sourcePath);

            // 大小不同 → 修改
            if ($localSize !== $remoteSize) {
                $diffFiles[] = [
                    'path' => $relativePath,
                    'status' => 'modified',
                    'local_size' => $localSize,
                    'remote_size' => $remoteSize,
                    'local_hash' => hash_file('sha256', $targetPath),
                    'remote_hash' => hash_file('sha256', $sourcePath),
                ];
                continue;
            }

            // 大小相同，比 hash
            $localHash = hash_file('sha256', $targetPath);
            $remoteHash = hash_file('sha256', $sourcePath);
            if ($localHash !== $remoteHash) {
                $diffFiles[] = [
                    'path' => $relativePath,
                    'status' => 'modified',
                    'local_size' => $localSize,
                    'remote_size' => $remoteSize,
                    'local_hash' => $localHash,
                    'remote_hash' => $remoteHash,
                ];
            }
        }

        return $diffFiles;
    }

    /**
     * 递归扫描目录，返回相对路径列表
     */
    protected function scanDirectory(string $dir): array
    {
        $results = [];
        if (!is_dir($dir)) {
            return $results;
        }

        $items = scandir($dir);
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }

            $fullPath = $dir . '/' . $item;
            if (is_dir($fullPath)) {
                $subResults = $this->scanDirectory($fullPath);
                foreach ($subResults as $sub) {
                    $results[] = $item . '/' . $sub;
                }
            } else {
                $results[] = $item;
            }
        }

        return $results;
    }

    /**
     * 判断路径是否命中黑名单
     */
    protected function isProtectedPath(string $relativePath): bool
    {
        $relativePath = str_replace('\\', '/', $relativePath);

        // 防路径穿越
        if (str_contains($relativePath, '..')) {
            return true;
        }

        foreach (self::PROTECTED_PATHS as $protected) {
            // 目录前缀匹配（如 "storage/"）
            if (str_ends_with($protected, '/')) {
                if (str_starts_with($relativePath, $protected)) {
                    return true;
                }
            } else {
                // 文件精确匹配或前缀匹配（如 ".env"）
                if ($relativePath === $protected || str_starts_with($relativePath, $protected . '/')) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * 检测并剥离 ZIP 包的公共顶层目录
     *
     * 如果解压后的根目录下仅有一个目录（且无根级文件），说明 ZIP 打包时带了顶层目录，
     * 则返回该子目录的路径，使 scanDirectory 产生的相对路径直接映射到项目根。
     */
    protected function resolveExtractDir(string $extractDir): string
    {
        if (!is_dir($extractDir)) {
            return $extractDir;
        }

        $items = scandir($extractDir);
        $subDirs = [];
        $hasFile = false;

        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }
            $fullPath = $extractDir . '/' . $item;
            if (is_dir($fullPath)) {
                $subDirs[] = $item;
            } else {
                $hasFile = true;
            }
        }

        // 仅当没有根级文件且仅有一个子目录时，剥离顶层目录
        if (!$hasFile && count($subDirs) === 1) {
            $stripped = $extractDir . '/' . $subDirs[0];
            Log::info('版本修复检测到 ZIP 顶层目录，已剥离', [
                'top_dir' => $subDirs[0],
                'original' => $extractDir,
                'resolved' => $stripped,
            ]);
            return $stripped;
        }

        return $extractDir;
    }

    /**
     * 执行修复（接收用户选中的文件列表）
     */
    public function execute(array $selectedFiles): array
    {
        @set_time_limit(0);
        @ini_set('memory_limit', '512M');

        $lock = $this->acquireLock();
        if ($lock === null) {
            return ApiResponse::error(60013, '当前有修复任务正在进行，请稍后重试');
        }

        try {
            if (session_status() === PHP_SESSION_ACTIVE) {
                session_write_close();
            }

            $state = $this->getState();
            $status = $state['status'] ?? 'idle';

            if ($status !== 'awaiting_selection') {
                return ApiResponse::error(60013, '当前状态不允许执行修复：' . $status);
            }

            $diffFiles = $state['diff_files'] ?? [];
            $diffPaths = array_column($diffFiles, 'path');

            // 校验选中文件全部在差异列表中
            $invalidFiles = array_diff($selectedFiles, $diffPaths);
            if (!empty($invalidFiles)) {
                return ApiResponse::error(60002, '存在不在差异列表中的文件：' . implode(', ', array_slice($invalidFiles, 0, 5)));
            }

            // 双重校验黑名单
            $protectedFiles = array_filter($selectedFiles, fn($f) => $this->isProtectedPath($f));
            if (!empty($protectedFiles)) {
                return ApiResponse::error(60002, '选中文件命中黑名单：' . implode(', ', array_slice($protectedFiles, 0, 5)));
            }

            $this->updateState(['status' => 'repairing']);

            $extractDir = $state['extract_dir'] ?? '';
            if (!$extractDir || !File::isDirectory($extractDir)) {
                $this->updateState(['status' => 'failed', 'error_message' => '解压目录不存在']);
                return ApiResponse::error(60001, '解压目录不存在，请重新准备');
            }

            try {
                // 创建备份
                $backupsDir = $this->workDir . '/backups';
                if (!File::isDirectory($backupsDir)) {
                    File::makeDirectory($backupsDir, 0755, true);
                }
                $backupZipPath = $backupsDir . '/repair_' . date('Ymd_His') . '.zip';

                $backupZip = new ZipArchive();
                if ($backupZip->open($backupZipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
                    throw new \RuntimeException('创建备份文件失败');
                }

                $backupManifest = [];
                $successCount = 0;
                $failedFiles = [];
                $skippedCount = 0;
                $migrationExecuted = false;

                try {
                    foreach ($selectedFiles as $relativePath) {
                        $sourcePath = $extractDir . '/' . $relativePath;
                        $targetPath = base_path($relativePath);

                        if (!File::exists($sourcePath)) {
                            $skippedCount++;
                            Log::warning('源文件不存在，跳过', ['file' => $relativePath]);
                            continue;
                        }

                        // 校验目标路径未逃逸出 base_path（必须在目录创建和备份之前执行）
                        $destDir = dirname($targetPath);
                        $baseReal = realpath(base_path());

                        // 路径逃逸检查辅助函数
                        $isWithinBase = function ($realPath) use ($baseReal) {
                            return $realPath !== false && ($realPath === $baseReal || str_starts_with($realPath, $baseReal . DIRECTORY_SEPARATOR));
                        };

                        if (File::exists($targetPath)) {
                            // 已存在场景：直接校验目标文件的真实路径
                            $realTarget = realpath($targetPath);
                            if (!$isWithinBase($realTarget)) {
                                $failedFiles[] = ['path' => $relativePath, 'error' => '目标路径逃逸'];
                                continue;
                            }
                        } else {
                            // 新增场景：目标文件不存在，校验其父目录
                            $realTargetDir = is_dir($destDir) ? realpath($destDir) : false;
                            if ($realTargetDir === false) {
                                // 父目录尚不存在，向上查找最近存在的祖先目录，确认祖先未逃逸
                                $ancestorDir = $destDir;
                                while (!is_dir($ancestorDir) && $ancestorDir !== dirname($ancestorDir)) {
                                    $ancestorDir = dirname($ancestorDir);
                                }
                                $realAncestor = realpath($ancestorDir);
                                if (!$isWithinBase($realAncestor)) {
                                    $failedFiles[] = ['path' => $relativePath, 'error' => '目标路径逃逸'];
                                    continue;
                                }
                            } else {
                                if (!$isWithinBase($realTargetDir)) {
                                    $failedFiles[] = ['path' => $relativePath, 'error' => '目标路径逃逸'];
                                    continue;
                                }
                            }
                        }

                        // 备份被覆盖的本地文件（校验通过后才备份）
                        if (File::exists($targetPath)) {
                            $localHash = hash_file('sha256', $targetPath);
                            $backupZip->addFile($targetPath, $relativePath);
                            $backupManifest[] = [
                                'path' => $relativePath,
                                'original_hash' => $localHash,
                                'original_size' => File::size($targetPath),
                            ];
                        }

                        // 确保目标目录存在（校验通过后才创建）
                        if (!File::isDirectory($destDir)) {
                            File::makeDirectory($destDir, 0755, true);
                        }

                        // 校验目标目录可写
                        if (!is_writable($destDir)) {
                            $failedFiles[] = ['path' => $relativePath, 'error' => '目录不可写'];
                            continue;
                        }

                        if (File::exists($targetPath) && !is_writable($targetPath)) {
                            $failedFiles[] = ['path' => $relativePath, 'error' => '文件不可写'];
                            continue;
                        }

                        if (!File::copy($sourcePath, $targetPath)) {
                            $failedFiles[] = ['path' => $relativePath, 'error' => '复制失败'];
                            continue;
                        }

                        $successCount++;
                    }

                    // 写入备份 manifest
                    $backupZip->addFromString('_repair_manifest.json', json_encode([
                        'repair_time' => date('Y-m-d H:i:s'),
                        'to_version' => $state['to_version'] ?? '',
                        'files' => $backupManifest,
                    ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
                } finally {
                    $backupZip->close();
                }

                // 执行数据库迁移（仅限选中的迁移文件）
                $migrationFiles = array_filter($selectedFiles, fn($f) => str_starts_with($f, 'database/migrations/') && str_ends_with($f, '.php'));
                if (!empty($migrationFiles)) {
                    // 将选中的迁移文件复制到独立临时目录
                    $stagedMigrationsDir = $this->workDir . '/migrations_staged_' . time();
                    if (!File::isDirectory($stagedMigrationsDir)) {
                        File::makeDirectory($stagedMigrationsDir, 0755, true);
                    }

                    try {
                        foreach ($migrationFiles as $migrationFile) {
                            $sourcePath = $extractDir . '/' . $migrationFile;
                            $destFile = $stagedMigrationsDir . '/' . basename($migrationFile);
                            if (File::exists($sourcePath)) {
                                File::copy($sourcePath, $destFile);
                            }
                        }
                        // 使用相对路径（相对于 base_path()）执行迁移
                        $relativeStagedPath = str_replace(base_path() . DIRECTORY_SEPARATOR, '', $stagedMigrationsDir);
                        $relativeStagedPath = str_replace('\\', '/', $relativeStagedPath);
                        Artisan::call('migrate', [
                            '--path' => $relativeStagedPath,
                            '--force' => true,
                        ]);
                        $migrationOutput = Artisan::output();
                        $migrationExecuted = true;
                        Log::info('版本修复执行数据库迁移', ['output' => $migrationOutput, 'files' => array_map('basename', $migrationFiles)]);

                        // 检测迁移输出中的错误
                        if (str_contains($migrationOutput, 'ErrorException') || str_contains($migrationOutput, 'SQLSTATE')) {
                            Log::warning('版本修复迁移可能存在错误', ['output' => $migrationOutput]);
                        }
                    } catch (\Throwable $e) {
                        Log::warning('版本修复迁移执行失败（不影响文件修复）', ['error' => $e->getMessage()]);
                    } finally {
                        // 始终清理临时迁移目录，避免异常路径下残留
                        if (File::isDirectory($stagedMigrationsDir)) {
                            File::deleteDirectory($stagedMigrationsDir);
                        }
                    }
                }

                // 清理临时文件
                $this->cleanupWorkDir();

                $this->updateState([
                    'status' => 'done',
                    'completed_at' => date('Y-m-d H:i:s'),
                    'backup_path' => $backupZipPath,
                    'result' => [
                        'success_count' => $successCount,
                        'failed_count' => count($failedFiles),
                        'skipped_count' => $skippedCount,
                        'migration_executed' => $migrationExecuted,
                        'failed_files' => $failedFiles,
                    ],
                ]);

                return ApiResponse::success([
                    'success_count' => $successCount,
                    'failed_count' => count($failedFiles),
                    'skipped_count' => $skippedCount,
                    'migration_executed' => $migrationExecuted,
                    'failed_files' => $failedFiles,
                    'backup_path' => $backupZipPath,
                ], '修复完成');
            } catch (\Throwable $e) {
                Log::error('版本修复执行失败', [
                    'error' => $e->getMessage(),
                    'file' => $e->getFile(),
                    'line' => $e->getLine(),
                ]);
                $this->updateState([
                    'status' => 'failed',
                    'error_message' => '修复失败，详情见日志',
                ]);
                return ApiResponse::error(60001, '修复失败，请查看日志');
            }
        } finally {
            $lock->release();
        }
    }

    /**
     * 查询修复状态
     */
    public function getStatus(): array
    {
        $state = $this->getState();
        return ApiResponse::success($state);
    }

    /**
     * 手动清理临时文件
     */
    public function cleanup(): array
    {
        $this->cleanupWorkDir();
        $this->saveState([
            'status' => 'idle',
            'to_version' => null,
            'started_at' => null,
            'diff_files' => [],
            'extract_dir' => null,
            'zip_path' => null,
            'error_message' => null,
        ]);
        return ApiResponse::success(null, '清理完成');
    }

    /**
     * 清理工作目录（保留 backups/ 子目录）
     */
    protected function cleanupWorkDir(): void
    {
        if (!File::isDirectory($this->workDir)) {
            return;
        }

        $state = $this->getState();
        $extractDir = $state['extract_dir'] ?? null;
        $zipPath = $state['zip_path'] ?? null;

        // 清理解压目录
        if ($extractDir && File::isDirectory($extractDir)) {
            File::deleteDirectory($extractDir);
        }

        // 清理下载的 zip
        if ($zipPath && File::exists($zipPath)) {
            File::delete($zipPath);
        }

        // 清理可能残留的 extracted_* 目录
        $entries = scandir($this->workDir);
        foreach ($entries as $entry) {
            if ($entry === '.' || $entry === '..' || $entry === 'backups') {
                continue;
            }
            $fullPath = $this->workDir . '/' . $entry;
            if (is_dir($fullPath) && str_starts_with($entry, 'extracted_')) {
                File::deleteDirectory($fullPath);
            }
        }
    }

    /**
     * 读取状态
     */
    protected function getState(): ?array
    {
        $stateFile = $this->workDir . '/state.json';
        if (!File::exists($stateFile)) {
            return ['status' => 'idle'];
        }
        $state = json_decode(File::get($stateFile), true);
        return $state ?? ['status' => 'idle'];
    }

    /**
     * 保存状态
     */
    protected function saveState(array $state): void
    {
        if (!File::isDirectory($this->workDir)) {
            File::makeDirectory($this->workDir, 0755, true);
        }
        File::put(
            $this->workDir . '/state.json',
            json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
        );
    }

    /**
     * 更新状态（合并）
     */
    protected function updateState(array $updates): void
    {
        $state = $this->getState();
        $state = array_merge($state, $updates);
        $this->saveState($state);
    }
}
