<?php

declare(strict_types=1);

/**
 * 本文件属于 DBErp 进销存系统。
 *
 * 版权所有 © 2026 北京珑大钜商科技有限公司。保留所有权利。
 *
 * 未经官方授权，不得删除、篡改或隐藏本文件中的版权声明、产品标识与来源信息。
 * 商业授权、官方更新与技术支持服务，请以珑大官方支持中心记录为准。
 *
 * @link https://support.loongdom.cn
 * @see  https://www.dberp.com.cn
 */

namespace App\Services\SupportCenter;

use App\Enums\SupportCenter\HostSystemUpdateExecutionStatus;
use App\Enums\SupportCenter\HostSystemUpdateExecutionStep;
use App\Models\HostSystemUpdateExecution;
use App\Models\HostSystemUpdatePackage;
use App\Support\SystemUpdate\HostSystemUpdateVendorBundlePaths;
use Throwable;

/**
 * 系统更新工作区磁盘回收，降低整站备份体积。
 *
 * WARNING: 清理失败不得影响已 Completed / Failed 等终态；一律 best-effort。
 * WARNING: RollbackFailed 的 backups/package 必须跨后续执行保留，供人工恢复。
 */
final class HostSystemUpdateWorkspaceCleanupService
{
    public const COMPLETED_BACKUPS_KEEP_LATEST = 1;

    public function __construct(
        private readonly HostSystemUpdateExecutionLogger $executionLogger,
        private readonly HostSystemUpdateExecutionLogPruningService $executionLogPruning,
    ) {}

    /**
     * 更新成功后：仅保留最近一次 Completed backups，并保留全部 RollbackFailed 恢复资料。
     */
    public function cleanupAfterCompleted(HostSystemUpdateExecution $execution): void
    {
        $this->runSafely($execution, 'completed', function () use ($execution): void {
            $keepBackupNos = $this->backupRetentionExecutionNos(extraExecutionNos: [
                trim((string) ($execution->execution_no ?? '')),
            ]);

            $this->deleteStagingAndExecutionWorkspace($execution);
            $this->deletePackageFileIfUnused($execution->package);
            $this->pruneBackupsExcept($keepBackupNos);
            $this->pruneUnusedPackageFiles();
            $this->pruneOrphanStagingAndExecutionDirectories(true);
            $this->pruneSessionFilesIfNoActiveExecutions();
            $this->pruneProjectUpdateTempArtifactsIfNoActiveExecutions();
            $this->pruneOsTempTarArtifactsIfNoActiveExecutions();
        });
    }

    /**
     * 失败终态（Failed / Cancelled / RolledBack）：删 staging/无用 package，保留本次 backups。
     * RollbackFailed：仅清 staging/executions，保留 backups 与 package。
     */
    public function cleanupAfterTerminalFailure(HostSystemUpdateExecution $execution): void
    {
        $status = HostSystemUpdateExecutionStatus::tryFrom((string) ($execution->status ?? ''));

        if ($status === null) {
            return;
        }

        if ($status === HostSystemUpdateExecutionStatus::RollbackFailed) {
            $this->runSafely($execution, 'rollback_failed', function () use ($execution): void {
                $this->deleteStagingAndExecutionWorkspace($execution);
                $this->pruneOrphanStagingAndExecutionDirectories(true);
                $this->pruneSessionFilesIfNoActiveExecutions();
            });

            return;
        }

        if (! in_array($status, [
            HostSystemUpdateExecutionStatus::Failed,
            HostSystemUpdateExecutionStatus::Cancelled,
            HostSystemUpdateExecutionStatus::RolledBack,
        ], true)) {
            return;
        }

        $this->runSafely($execution, $status->value, function () use ($execution): void {
            $keepBackupNos = $this->backupRetentionExecutionNos(extraExecutionNos: [
                trim((string) ($execution->execution_no ?? '')),
            ]);

            $this->deleteStagingAndExecutionWorkspace($execution);
            $this->deletePackageFileIfUnused($execution->package);
            $this->pruneBackupsExcept($keepBackupNos);
            $this->pruneUnusedPackageFiles();
            $this->pruneOrphanStagingAndExecutionDirectories(true);
            $this->pruneSessionFilesIfNoActiveExecutions();
            $this->pruneProjectUpdateTempArtifactsIfNoActiveExecutions();
            $this->pruneOsTempTarArtifactsIfNoActiveExecutions();
        });
    }

    /**
     * @param  callable(): void  $callback
     */
    private function runSafely(HostSystemUpdateExecution $execution, string $reason, callable $callback): void
    {
        $logStep = $this->resolveCleanupLogStep($execution);

        try {
            $callback();

            $this->executionLogger->logStepCompleted(
                $execution,
                $logStep,
                (string) __('dberp.system_update.workspace_cleanup.log.completed', ['reason' => $reason]),
                ['cleanup_reason' => $reason],
            );
        } catch (Throwable $throwable) {
            // WARNING: 磁盘清理失败不得回滚更新终态；仅记日志便于排障。
            try {
                $this->executionLogger->logStepFailed(
                    $execution,
                    $logStep,
                    (string) __('dberp.system_update.workspace_cleanup.log.failed'),
                    'host_system_update_workspace_cleanup_failed',
                    $throwable->getMessage(),
                    [
                        'cleanup_reason' => $reason,
                        'exception_class' => $throwable::class,
                    ],
                );
            } catch (Throwable) {
                // 日志写入失败同样忽略，避免掩盖主流程终态。
            }
        }

        $this->executionLogPruning->pruneDueSafely(force: true);
    }

    private function resolveCleanupLogStep(HostSystemUpdateExecution $execution): string
    {
        $status = HostSystemUpdateExecutionStatus::tryFrom((string) ($execution->status ?? ''));

        return match ($status) {
            HostSystemUpdateExecutionStatus::Completed => HostSystemUpdateExecutionStep::Completed->value,
            HostSystemUpdateExecutionStatus::RolledBack,
            HostSystemUpdateExecutionStatus::RollbackFailed => HostSystemUpdateExecutionStep::Rollback->value,
            default => HostSystemUpdateExecutionStep::Failed->value,
        };
    }

    private function deleteStagingAndExecutionWorkspace(HostSystemUpdateExecution $execution): void
    {
        $executionNo = trim((string) ($execution->execution_no ?? ''));

        if ($executionNo === '') {
            return;
        }

        $this->deleteDirectoryIfSafe(HostSystemUpdateWorkspaceStorage::stagingDirectory($executionNo));
        $this->deleteDirectoryIfSafe(HostSystemUpdateWorkspaceStorage::executionWorkspaceDirectory($executionNo));
    }

    private function deletePackageFileIfUnused(?HostSystemUpdatePackage $package): void
    {
        if ($package === null) {
            return;
        }

        if ($this->packageMustRetainLocalFile($package)) {
            return;
        }

        $this->unlinkPackageLocalFile($package);
    }

    /**
     * 删除所有无保留需求的本地更新包文件（含 verified / downloaded 孤儿包）。
     */
    private function pruneUnusedPackageFiles(): void
    {
        $packages = HostSystemUpdatePackage::query()
            ->whereNotNull('local_path')
            ->get();

        foreach ($packages as $package) {
            if ($this->packageMustRetainLocalFile($package)) {
                continue;
            }

            $this->unlinkPackageLocalFile($package);
        }

        $this->pruneUntrackedPackageFilesOnDisk();
    }

    /**
     * packages/ 下无 DB 引用或路径已清空的落盘文件一并删除。
     */
    private function pruneUntrackedPackageFilesOnDisk(): void
    {
        $directory = HostSystemUpdateWorkspaceStorage::packagesDirectory();

        if (! is_dir($directory) || ! $this->isPathWithinUpdatesWorkspace($directory)) {
            return;
        }

        $trackedPaths = [];

        foreach (HostSystemUpdatePackage::query()->whereNotNull('local_path')->pluck('local_path') as $localPath) {
            if (! is_string($localPath) || trim($localPath) === '') {
                continue;
            }

            $resolved = $this->resolveAbsolutePathString($localPath);

            if ($resolved !== null) {
                $trackedPaths[$this->normalizePathKey($resolved)] = true;
            }
        }

        $items = scandir($directory);

        if ($items === false) {
            return;
        }

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

            $path = $directory.DIRECTORY_SEPARATOR.$item;

            if (! is_file($path) || ! $this->isPathWithinUpdatesWorkspace($path)) {
                continue;
            }

            if (isset($trackedPaths[$this->normalizePathKey($path)])) {
                continue;
            }

            @unlink($path);
        }
    }

    /**
     * 活跃执行或 RollbackFailed 仍依赖的包文件不得删除。
     */
    private function packageMustRetainLocalFile(HostSystemUpdatePackage $package): bool
    {
        $packageKey = $package->getKey();

        if ($packageKey === null) {
            return false;
        }

        return HostSystemUpdateExecution::query()
            ->where('host_system_update_package_id', $packageKey)
            ->whereIn('status', $this->packageRetainingStatusValues())
            ->exists();
    }

    /**
     * @return list<string>
     */
    private function packageRetainingStatusValues(): array
    {
        return [
            ...HostSystemUpdateExecutionStatus::activeValues(),
            HostSystemUpdateExecutionStatus::RollbackFailed->value,
        ];
    }

    private function unlinkPackageLocalFile(HostSystemUpdatePackage $package): void
    {
        $path = $this->resolvePackageLocalPath($package);

        if ($path !== null && is_file($path) && $this->isPathWithinUpdatesWorkspace($path)) {
            @unlink($path);
        }

        if ($package->local_path !== null || $package->local_filename !== null || $package->local_size !== null) {
            $package->fill([
                'local_path' => null,
                'local_filename' => null,
                'local_size' => null,
            ]);
            $package->save();
        }
    }

    private function resolvePackageLocalPath(HostSystemUpdatePackage $package): ?string
    {
        $raw = $package->local_path;

        if (! is_string($raw) || trim($raw) === '') {
            return null;
        }

        return $this->resolveAbsolutePathString($raw);
    }

    private function resolveAbsolutePathString(string $raw): ?string
    {
        $normalized = str_replace('\\', '/', trim($raw));

        if ($normalized === '') {
            return null;
        }

        if (str_starts_with($normalized, 'storage/')) {
            return HostSystemUpdateWorkspaceStorage::absoluteFromRelativeStorage($normalized);
        }

        return $raw;
    }

    private function normalizePathKey(string $path): string
    {
        return strtolower(str_replace('\\', '/', $path));
    }

    /**
     * 保留：最近 N 次 Completed + 全部 RollbackFailed + 活跃执行 + 额外指定执行。
     *
     * @param  list<string>  $extraExecutionNos
     * @return list<string>
     */
    private function backupRetentionExecutionNos(array $extraExecutionNos = []): array
    {
        $nos = [
            ...$this->latestCompletedExecutionNos(self::COMPLETED_BACKUPS_KEEP_LATEST),
            ...$this->executionNosWithStatuses([
                HostSystemUpdateExecutionStatus::RollbackFailed->value,
                ...HostSystemUpdateExecutionStatus::activeValues(),
            ]),
            ...$extraExecutionNos,
        ];

        $unique = [];

        foreach ($nos as $executionNo) {
            $normalized = trim((string) $executionNo);

            if ($normalized === '') {
                continue;
            }

            $unique[$normalized] = $normalized;
        }

        return array_values($unique);
    }

    /**
     * @return list<string>
     */
    private function latestCompletedExecutionNos(int $keepLatest): array
    {
        $keepLatest = max(0, $keepLatest);

        if ($keepLatest === 0) {
            return [];
        }

        return HostSystemUpdateExecution::query()
            ->where('status', HostSystemUpdateExecutionStatus::Completed->value)
            ->orderByDesc('finished_at')
            ->orderByDesc('id')
            ->limit($keepLatest)
            ->pluck('execution_no')
            ->map(static fn (mixed $value): string => trim((string) $value))
            ->filter(static fn (string $value): bool => $value !== '')
            ->values()
            ->all();
    }

    /**
     * @param  list<string>  $statuses
     * @return list<string>
     */
    private function executionNosWithStatuses(array $statuses): array
    {
        if ($statuses === []) {
            return [];
        }

        return HostSystemUpdateExecution::query()
            ->whereIn('status', $statuses)
            ->pluck('execution_no')
            ->map(static fn (mixed $value): string => trim((string) $value))
            ->filter(static fn (string $value): bool => $value !== '')
            ->values()
            ->all();
    }

    /**
     * 删除 backups 下除保留列表以外的全部执行备份目录。
     *
     * @param  list<string>  $keepExecutionNos
     */
    private function pruneBackupsExcept(array $keepExecutionNos): void
    {
        $keep = $this->sanitizedExecutionNoSet($keepExecutionNos);
        $backupsRoot = HostSystemUpdateWorkspaceStorage::baseDirectory().'/backups';

        if (! is_dir($backupsRoot) || ! $this->isPathWithinUpdatesWorkspace($backupsRoot)) {
            return;
        }

        $items = scandir($backupsRoot);

        if ($items === false) {
            return;
        }

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

            if (isset($keep[$item])) {
                continue;
            }

            $this->deleteDirectoryIfSafe($backupsRoot.DIRECTORY_SEPARATOR.$item);
        }
    }

    /**
     * 清理 staging/executions 中不属于活跃执行的目录（含历史上未清的终态残留）。
     */
    private function pruneOrphanStagingAndExecutionDirectories(bool $keepActiveOnly): void
    {
        $keep = [];

        if ($keepActiveOnly) {
            $keep = $this->sanitizedExecutionNoSet(
                $this->executionNosWithStatuses(HostSystemUpdateExecutionStatus::activeValues()),
            );
        }

        foreach (['staging', 'executions'] as $subdir) {
            $root = HostSystemUpdateWorkspaceStorage::baseDirectory().'/'.$subdir;

            if (! is_dir($root) || ! $this->isPathWithinUpdatesWorkspace($root)) {
                continue;
            }

            $items = scandir($root);

            if ($items === false) {
                continue;
            }

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

                if (isset($keep[$item])) {
                    continue;
                }

                $this->deleteDirectoryIfSafe($root.DIRECTORY_SEPARATOR.$item);
            }
        }
    }

    /**
     * @param  list<string>  $executionNos
     * @return array<string, true>
     */
    private function sanitizedExecutionNoSet(array $executionNos): array
    {
        $keep = [];

        foreach ($executionNos as $executionNo) {
            $normalized = trim((string) $executionNo);

            if ($normalized === '') {
                continue;
            }

            $keep[HostSystemUpdateWorkspaceStorage::sanitizeExecutionNo($normalized)] = true;
        }

        return $keep;
    }

    private function pruneSessionFilesIfNoActiveExecutions(): void
    {
        if ($this->hasActiveExecutions()) {
            return;
        }

        $directory = HostSystemUpdateWorkspaceStorage::sessionsDirectory();

        if (! is_dir($directory) || ! $this->isPathWithinUpdatesWorkspace($directory)) {
            return;
        }

        $items = scandir($directory);

        if ($items === false) {
            return;
        }

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

            $path = $directory.DIRECTORY_SEPARATOR.$item;

            if (is_file($path) && str_ends_with(strtolower($item), '.json') && $this->isPathWithinUpdatesWorkspace($path)) {
                @unlink($path);
            }
        }
    }

    /**
     * 清理项目根旁 vendor bundle 硬杀残留目录（vendor.__dberp_*）。
     */
    private function pruneProjectUpdateTempArtifactsIfNoActiveExecutions(): void
    {
        if ($this->hasActiveExecutions()) {
            return;
        }

        $projectRoot = base_path();
        $items = @scandir($projectRoot);

        if ($items === false) {
            return;
        }

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

            if ($item === HostSystemUpdateVendorBundlePaths::BUNDLE_RELATIVE_PATH) {
                $path = $projectRoot.DIRECTORY_SEPARATOR.$item;

                if (is_file($path) && $this->isPathWithinProjectRoot($path)) {
                    @unlink($path);
                }

                continue;
            }

            if (! str_starts_with($item, 'vendor.__dberp_')) {
                continue;
            }

            $path = $projectRoot.DIRECTORY_SEPARATOR.$item;

            if (! is_dir($path) || ! $this->isPathWithinProjectRoot($path)) {
                continue;
            }

            HostSystemUpdateWorkspaceStorage::deleteDirectory($path);
        }
    }

    /**
     * 清理 OS 临时目录中 tar.gz 解压留下的 dberp-update-*.tar。
     */
    private function pruneOsTempTarArtifactsIfNoActiveExecutions(): void
    {
        if ($this->hasActiveExecutions()) {
            return;
        }

        $tempRoot = sys_get_temp_dir();

        if (! is_dir($tempRoot)) {
            return;
        }

        $items = @scandir($tempRoot);

        if ($items === false) {
            return;
        }

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

            if (! str_starts_with($item, 'dberp-update-') || ! str_ends_with(strtolower($item), '.tar')) {
                continue;
            }

            $path = $tempRoot.DIRECTORY_SEPARATOR.$item;

            if (is_file($path)) {
                @unlink($path);
            }
        }
    }

    private function hasActiveExecutions(): bool
    {
        return HostSystemUpdateExecution::query()
            ->whereIn('status', HostSystemUpdateExecutionStatus::activeValues())
            ->exists();
    }

    private function deleteDirectoryIfSafe(string $directory): void
    {
        if (! is_dir($directory)) {
            return;
        }

        if (! $this->isPathWithinUpdatesWorkspace($directory)) {
            return;
        }

        HostSystemUpdateWorkspaceStorage::deleteDirectory($directory);
    }

    /**
     * 权限：仅允许删除 updates 工作区下的路径，防止误删项目其它目录。
     */
    private function isPathWithinUpdatesWorkspace(string $path): bool
    {
        $realPath = realpath($path);
        $workspaceRoot = realpath(HostSystemUpdateWorkspaceStorage::baseDirectory());

        if ($realPath === false || $workspaceRoot === false) {
            // 目录可能刚创建尚未稳定；用规范化前缀兜底，仍要求落在 baseDirectory 下。
            $normalizedPath = str_replace('\\', '/', $path);
            $normalizedRoot = rtrim(str_replace('\\', '/', HostSystemUpdateWorkspaceStorage::baseDirectory()), '/');

            return str_starts_with($normalizedPath, $normalizedRoot.'/');
        }

        $normalizedPath = str_replace('\\', '/', $realPath);
        $normalizedRoot = str_replace('\\', '/', $workspaceRoot);

        return $normalizedPath === $normalizedRoot
            || str_starts_with($normalizedPath, $normalizedRoot.'/');
    }

    private function isPathWithinProjectRoot(string $path): bool
    {
        $realPath = realpath($path);
        $projectRoot = realpath(base_path());

        if ($realPath === false || $projectRoot === false) {
            $normalizedPath = str_replace('\\', '/', $path);
            $normalizedRoot = rtrim(str_replace('\\', '/', base_path()), '/');

            return str_starts_with($normalizedPath, $normalizedRoot.'/');
        }

        $normalizedPath = str_replace('\\', '/', $realPath);
        $normalizedRoot = str_replace('\\', '/', $projectRoot);

        return $normalizedPath === $normalizedRoot
            || str_starts_with($normalizedPath, $normalizedRoot.'/');
    }
}
