<?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\Enums\SupportCenter\HostSystemUpdateStepPhase;
use App\Exceptions\SupportCenter\HostSystemUpdateExecutionException;
use App\Exceptions\SupportCenter\HostSystemUpdateFileExecutionException;
use App\Exceptions\SupportCenter\HostSystemUpdatePostApplyException;
use App\Exceptions\SupportCenter\HostSystemUpdateRecoveryException;
use App\Exceptions\SupportCenter\HostSystemUpdateStagingException;
use App\Exceptions\SupportCenter\SupportCenterException;
use App\Models\HostSystemUpdateCheck;
use App\Models\HostSystemUpdateExecution;
use App\Models\HostSystemUpdatePackage;
use DomainException;
use Illuminate\Support\Facades\DB;
use RuntimeException;
use Throwable;

/**
 * Web 分步系统更新编排（每步一次 HTTP 请求，参考 WebInstallStepRunner）。
 */
final class HostSystemUpdateStepRunner
{
    public function __construct(
        private readonly HostSystemUpdateStepProgressStore $progressStore,
        private readonly HostSystemUpdateExecutionLock $executionLock,
        private readonly HostSystemUpdateEntitlementGate $entitlementGate,
        private readonly HostSystemUpdateCheckService $checkService,
        private readonly HostSystemUpdatePackageDownloadService $downloadService,
        private readonly HostSystemUpdateManifestParseService $manifestParseService,
        private readonly HostSystemUpdateExecutionCreateService $executionCreateService,
        private readonly HostSystemUpdateMaintenanceModeService $maintenanceModeService,
        private readonly HostSystemUpdatePrepareService $prepareService,
        private readonly HostSystemUpdateApplyService $applyService,
        private readonly HostSystemUpdateFinalizeService $finalizeService,
        private readonly HostSystemUpdateMigrationRunner $migrationRunner,
        private readonly HostSystemUpdateMigrationBatchRunner $migrationBatchRunner,
        private readonly HostSystemUpdateBatchPhaseRunner $batchPhaseRunner,
        private readonly HostSystemUpdateExecutionStateMachine $stateMachine,
        private readonly HostSystemUpdateRuntimeResetService $runtimeResetService,
        private readonly HostSystemUpdateBootstrapCacheCleaner $bootstrapCacheCleaner,
        private readonly HostSystemUpdateRuntimeReadinessProcessService $runtimeReadinessService,
        private readonly HostSystemUpdatePostUpdateHealthCheckService $postUpdateHealthCheckService,
        private readonly HostSystemUpdateVendorSnapshotService $vendorSnapshotService,
        private readonly HostSystemUpdateFailureHandler $failureHandler,
        private readonly HostSystemUpdateResultReporter $resultReporter,
        private readonly SupportCenterAuditLogger $auditLogger,
    ) {}

    public function beginSession(): string
    {
        $this->assertCanBegin();

        return $this->progressStore->createSession();
    }

    public function assertCanBegin(): void
    {
        $this->executionLock->assertNoActiveExecution();

        if (! $this->entitlementGate->canCheckUpdate()) {
            throw SupportCenterException::entitlementBlocked(
                $this->entitlementGate->checkReason(),
                'host_system_update_step',
            );
        }

        if (! $this->entitlementGate->canDownloadPackage()) {
            throw SupportCenterException::entitlementBlocked(
                $this->entitlementGate->downloadReason(),
                'host_system_update_step',
            );
        }

        if (! $this->entitlementGate->canExecuteUpdate()) {
            throw SupportCenterException::entitlementBlocked(
                $this->entitlementGate->executeReason(),
                'host_system_update_step',
            );
        }
    }

    public function runNextStep(string $sessionId): HostSystemUpdateStepResult
    {
        $progress = $this->progressStore->read($sessionId);

        if (! $progress instanceof HostSystemUpdateStepProgress) {
            throw new DomainException((string) __('dberp.system_update_center.errors.step_session_not_found'));
        }

        if ($progress->terminal) {
            return $this->resultFromProgress($progress);
        }

        $lockHandle = $this->tryAcquireSessionStepLock($sessionId);

        if ($lockHandle === null) {
            return $this->resultFromProgress($progress);
        }

        try {
            $progress = $this->progressStore->read($sessionId);

            if (! $progress instanceof HostSystemUpdateStepProgress) {
                throw new DomainException((string) __('dberp.system_update_center.errors.step_session_not_found'));
            }

            if ($progress->terminal) {
                return $this->resultFromProgress($progress);
            }

            return $this->runNextStepWithLock($sessionId, $progress);
        } finally {
            @flock($lockHandle, LOCK_UN);
            @fclose($lockHandle);
        }
    }

    private function runNextStepWithLock(
        string $sessionId,
        HostSystemUpdateStepProgress $progress,
    ): HostSystemUpdateStepResult {
        try {
            if ($progress->phase->isBootstrap()) {
                $progress = $this->runBootstrapPhase($progress);
            } else {
                $progress = $this->runExecutionPhase($progress);

                // WriteVersion 仅推进到 Complete；须在同一次请求内执行 maintenance_up，避免依赖额外 Livewire 轮询导致长期 503。
                if (! $progress->terminal && $progress->phase === HostSystemUpdateStepPhase::Complete) {
                    $progress = $this->runExecutionPhase($progress);
                }
            }

            if ($progress->terminal) {
                $this->progressStore->delete($sessionId);

                return $this->resultFromProgress($progress);
            }

            return $this->resultFromProgress($progress);
        } catch (Throwable $throwable) {
            return $this->handleStepFailure($sessionId, $progress, $throwable);
        }
    }

    /** @return resource|null */
    private function tryAcquireSessionStepLock(string $sessionId): mixed
    {
        HostSystemUpdateWorkspaceStorage::ensureSessionsDirectoryExists();
        $path = HostSystemUpdateWorkspaceStorage::sessionsDirectory()
            .DIRECTORY_SEPARATOR.$sessionId.'.step.lock';
        $handle = @fopen($path, 'c');

        if ($handle === false) {
            throw new RuntimeException('无法创建系统更新分步执行锁。');
        }

        if (! @flock($handle, LOCK_EX | LOCK_NB)) {
            @fclose($handle);

            return null;
        }

        return $handle;
    }

    private function runBootstrapPhase(HostSystemUpdateStepProgress $progress): HostSystemUpdateStepProgress
    {
        return match ($progress->phase) {
            HostSystemUpdateStepPhase::BootstrapCheck => $this->runBootstrapCheck($progress),
            HostSystemUpdateStepPhase::BootstrapDownload => $this->runBootstrapDownload($progress),
            HostSystemUpdateStepPhase::BootstrapParse => $this->runBootstrapParse($progress),
            HostSystemUpdateStepPhase::BootstrapCreateExecution => $this->runBootstrapCreateExecution($progress),
            default => $progress,
        };
    }

    private function runBootstrapCheck(HostSystemUpdateStepProgress $progress): HostSystemUpdateStepProgress
    {
        $check = $this->checkService->check();

        if (! $check->update_available) {
            return $this->progressStore->markTerminal(
                $progress,
                failed: true,
                errorMessage: (string) __('dberp.system_update_center.errors.no_update_available'),
            );
        }

        return $this->progressStore->advancePhase($progress);
    }

    private function runBootstrapDownload(HostSystemUpdateStepProgress $progress): HostSystemUpdateStepProgress
    {
        $check = HostSystemUpdateCheck::singleton();
        $package = $this->downloadService->downloadAndVerify($check);

        if (! $package->verified) {
            return $this->progressStore->markTerminal(
                $progress,
                failed: true,
                errorMessage: (string) ($package->download_message
                    ?? __('dberp.system_update.package.verification_checksum_mismatch')),
            );
        }

        return $this->progressStore->advancePhase($progress);
    }

    private function runBootstrapParse(HostSystemUpdateStepProgress $progress): HostSystemUpdateStepProgress
    {
        $package = $this->resolveLatestPackage();

        $this->manifestParseService->parse($package->fresh() ?? $package);
        $package = $package->fresh() ?? $package;

        if ($package->manifest_status !== HostSystemUpdatePackage::MANIFEST_STATUS_VALID) {
            return $this->progressStore->markTerminal(
                $progress,
                failed: true,
                errorMessage: (string) __('dberp.system_update_center.errors.package_not_ready'),
            );
        }

        return $this->progressStore->advancePhase($progress);
    }

    private function runBootstrapCreateExecution(HostSystemUpdateStepProgress $progress): HostSystemUpdateStepProgress
    {
        $package = $this->resolveLatestPackage();
        $execution = $this->executionCreateService->create($package);

        $this->auditLogger->logSystemUpdateExecutionStarted($execution);

        return $this->progressStore->advancePhase(
            $progress,
            executionNo: (string) $execution->execution_no,
        );
    }

    private function runExecutionPhase(HostSystemUpdateStepProgress $progress): HostSystemUpdateStepProgress
    {
        $execution = $this->resolveExecution($progress);

        if ((string) $execution->status === HostSystemUpdateExecutionStatus::ManualRecoveryRequired->value) {
            throw new DomainException((string) __('dberp.system_update_center.values.result_manual_recovery'));
        }

        return match ($progress->phase) {
            HostSystemUpdateStepPhase::MaintenanceDown => $this->afterExecutionStep(
                $progress,
                $this->runMaintenanceDown($execution),
            ),
            HostSystemUpdateStepPhase::Precheck => $this->afterExecutionStep(
                $progress,
                $this->prepareService->runPrecheckPhase($execution),
            ),
            HostSystemUpdateStepPhase::ExtractStaging => $this->runBatchablePhase(
                $progress,
                $execution,
                HostSystemUpdateStepPhase::ExtractStaging,
            ),
            HostSystemUpdateStepPhase::Backup => $this->runBatchablePhase(
                $progress,
                $execution,
                HostSystemUpdateStepPhase::Backup,
            ),
            HostSystemUpdateStepPhase::ReplaceFiles => $this->runBatchablePhase(
                $progress,
                $execution,
                HostSystemUpdateStepPhase::ReplaceFiles,
            ),
            HostSystemUpdateStepPhase::ReplaceVendor => $this->runBatchablePhase(
                $progress,
                $execution,
                HostSystemUpdateStepPhase::ReplaceVendor,
            ),
            HostSystemUpdateStepPhase::ResetRuntime => $this->runResetRuntimePhase($progress, $execution),
            HostSystemUpdateStepPhase::ClearBootstrapCache => $this->runClearBootstrapCachePhase($progress, $execution),
            HostSystemUpdateStepPhase::RuntimeReadiness => $this->runRuntimeReadinessPhase($progress, $execution),
            HostSystemUpdateStepPhase::MigrateBatch => $this->runMigrateBatchPhase($progress, $execution),
            HostSystemUpdateStepPhase::ClearCache => $this->afterExecutionStep(
                $progress,
                $this->finalizeService->runClearCachePhase($execution),
            ),
            HostSystemUpdateStepPhase::PostUpdateHealthCheck => $this->runPostUpdateHealthCheckPhase($progress, $execution),
            HostSystemUpdateStepPhase::WriteVersion => $this->runWriteVersionPhase($progress, $execution),
            HostSystemUpdateStepPhase::CommitVendorBackup => $this->runCommitVendorBackupPhase($progress, $execution),
            HostSystemUpdateStepPhase::Complete => $this->runCompletePhase($progress, $execution),
            default => $progress,
        };
    }

    private function runResetRuntimePhase(
        HostSystemUpdateStepProgress $progress,
        HostSystemUpdateExecution $execution,
    ): HostSystemUpdateStepProgress {
        $execution = $this->stateMachine->advanceStep(
            $execution,
            HostSystemUpdateExecutionStep::ResetRuntime,
            progressPercent: 63,
        );
        $this->runtimeResetService->reset($execution);

        return $this->progressStore->advancePhase($progress, executionNo: (string) $execution->execution_no);
    }

    private function runClearBootstrapCachePhase(
        HostSystemUpdateStepProgress $progress,
        HostSystemUpdateExecution $execution,
    ): HostSystemUpdateStepProgress {
        $execution = $this->stateMachine->advanceStep(
            $execution,
            HostSystemUpdateExecutionStep::ClearBootstrapCache,
            progressPercent: 65,
        );
        $this->bootstrapCacheCleaner->clear($execution);

        return $this->progressStore->advancePhase($progress, executionNo: (string) $execution->execution_no);
    }

    private function runRuntimeReadinessPhase(
        HostSystemUpdateStepProgress $progress,
        HostSystemUpdateExecution $execution,
    ): HostSystemUpdateStepProgress {
        $execution = $this->stateMachine->advanceStep(
            $execution,
            HostSystemUpdateExecutionStep::RuntimeReadiness,
            progressPercent: 68,
        );
        $this->runtimeReadinessService->check($execution);

        return $this->progressStore->advancePhase($progress, executionNo: (string) $execution->execution_no);
    }

    private function runPostUpdateHealthCheckPhase(
        HostSystemUpdateStepProgress $progress,
        HostSystemUpdateExecution $execution,
    ): HostSystemUpdateStepProgress {
        $execution = $this->stateMachine->advanceStep(
            $execution,
            HostSystemUpdateExecutionStep::PostUpdateHealthCheck,
            progressPercent: 85,
        );
        $this->postUpdateHealthCheckService->check($execution);

        return $this->progressStore->advancePhase($progress, executionNo: (string) $execution->execution_no);
    }

    private function runCommitVendorBackupPhase(
        HostSystemUpdateStepProgress $progress,
        HostSystemUpdateExecution $execution,
    ): HostSystemUpdateStepProgress {
        $execution = $this->stateMachine->advanceStep(
            $execution,
            HostSystemUpdateExecutionStep::CommitVendorBackup,
            progressPercent: 93,
        );
        $this->vendorSnapshotService->commit($execution);

        return $this->progressStore->advancePhase($progress, executionNo: (string) $execution->execution_no);
    }

    private function runBatchablePhase(
        HostSystemUpdateStepProgress $progress,
        HostSystemUpdateExecution $execution,
        HostSystemUpdateStepPhase $phase,
    ): HostSystemUpdateStepProgress {
        if ($this->batchPhaseRunner->isPhaseComplete($execution, $phase)) {
            $this->batchPhaseRunner->clearPhaseProgress($execution, $phase);

            return $this->progressStore->advancePhase(
                $progress,
                executionNo: (string) $execution->execution_no,
                batchProgress: null,
            );
        }

        $batchResult = $this->batchPhaseRunner->runNextBatch($execution, $phase);
        $execution = $batchResult->execution ?? $execution;

        if (! $batchResult->stageCompleted) {
            return $this->progressStore->save(new HostSystemUpdateStepProgress(
                sessionId: $progress->sessionId,
                phase: $phase,
                executionNo: (string) $execution->execution_no,
                terminal: false,
                failed: false,
                errorMessage: null,
                migrationProgress: null,
                migratePhaseComplete: false,
                batchProgress: $batchResult->progress,
                startedAt: $progress->startedAt,
                updatedAt: $progress->updatedAt,
            ));
        }

        $this->batchPhaseRunner->clearPhaseProgress($execution, $phase);

        return $this->progressStore->advancePhase(
            $progress,
            executionNo: (string) $execution->execution_no,
            batchProgress: null,
        );
    }

    private function runMaintenanceDown(HostSystemUpdateExecution $execution): HostSystemUpdateExecution
    {
        $this->maintenanceModeService->enterMaintenance($execution);

        return $execution->fresh(['package', 'backups']) ?? $execution;
    }

    private function runMigrateBatchPhase(
        HostSystemUpdateStepProgress $progress,
        HostSystemUpdateExecution $execution,
    ): HostSystemUpdateStepProgress {
        if (! $progress->migratePhaseComplete) {
            if ($progress->migrationProgress === null) {
                $execution = $this->finalizeService->advanceToRunMigrationsStep($execution);
                $this->migrationRunner->beginMigrationStage($execution);
            }

            $batchResult = $this->migrationRunner->runNextMigrationBatch($execution);

            if (! $batchResult->stageCompleted) {
                return $this->progressStore->save(new HostSystemUpdateStepProgress(
                    sessionId: $progress->sessionId,
                    phase: HostSystemUpdateStepPhase::MigrateBatch,
                    executionNo: $progress->executionNo,
                    terminal: false,
                    failed: false,
                    errorMessage: null,
                    migrationProgress: $batchResult->progress,
                    migratePhaseComplete: false,
                    batchProgress: null,
                    startedAt: $progress->startedAt,
                    updatedAt: $progress->updatedAt,
                ));
            }

            $execution = $execution->fresh(['package']) ?? $execution;
            $this->migrationBatchRunner->logMigrationStageCompleted($execution, $batchResult->progress);

            $progress = $this->progressStore->save(new HostSystemUpdateStepProgress(
                sessionId: $progress->sessionId,
                phase: HostSystemUpdateStepPhase::MigrateBatch,
                executionNo: $progress->executionNo,
                terminal: false,
                failed: false,
                errorMessage: null,
                migrationProgress: $batchResult->progress,
                migratePhaseComplete: true,
                batchProgress: null,
                startedAt: $progress->startedAt,
                updatedAt: $progress->updatedAt,
            ));
        }

        return $this->progressStore->advancePhase(
            $progress,
            HostSystemUpdateStepPhase::ClearCache,
            migratePhaseComplete: false,
            migrationProgress: null,
        );
    }

    private function runWriteVersionPhase(
        HostSystemUpdateStepProgress $progress,
        HostSystemUpdateExecution $execution,
    ): HostSystemUpdateStepProgress {
        $execution = $this->finalizeService->runWriteVersionPhase($execution);

        return $this->progressStore->advancePhase(
            $progress,
            HostSystemUpdateStepPhase::CommitVendorBackup,
            executionNo: (string) $execution->execution_no,
        );
    }

    private function runCompletePhase(
        HostSystemUpdateStepProgress $progress,
        HostSystemUpdateExecution $execution,
    ): HostSystemUpdateStepProgress {
        $execution = $execution->fresh(['package', 'backups', 'logs']) ?? $execution;

        $this->maintenanceModeService->attemptLeaveMaintenance($execution);
        $execution = $this->finalizeService->markExecutionCompleted($execution->fresh(['package', 'backups', 'logs']) ?? $execution);
        $this->resultReporter->reportSuccess($execution);

        return $this->progressStore->markTerminal($progress, failed: false);
    }

    private function afterExecutionStep(
        HostSystemUpdateStepProgress $progress,
        HostSystemUpdateExecution $execution,
    ): HostSystemUpdateStepProgress {
        return $this->progressStore->advancePhase(
            $progress,
            executionNo: (string) $execution->execution_no,
        );
    }

    private function handleStepFailure(
        string $sessionId,
        HostSystemUpdateStepProgress $progress,
        Throwable $throwable,
    ): HostSystemUpdateStepResult {
        report($throwable);

        $userMessage = $this->resolveUserFacingMessage($throwable);
        $execution = $progress->executionNo !== null
            ? HostSystemUpdateExecution::query()->where('execution_no', $progress->executionNo)->first()
            : null;

        if ($execution instanceof HostSystemUpdateExecution) {
            try {
                $execution = DB::transaction(function () use ($execution, $throwable, $progress): HostSystemUpdateExecution {
                    $fresh = $execution->fresh(['package', 'backups', 'logs']) ?? $execution;

                    $handled = $this->failureHandler->handle(
                        $fresh,
                        new HostSystemUpdateFailureContext(
                            failedStep: (string) ($fresh->current_step ?? $progress->phase->value),
                            previousStatus: (string) ($fresh->status ?? ''),
                            errorCode: method_exists($throwable, 'errorCode') ? (string) $throwable->errorCode() : 'host_system_update_step_failed',
                            userFacingMessage: $this->resolveUserFacingMessage($throwable),
                            requiresRollback: ($throwable instanceof HostSystemUpdateFileExecutionException
                                    && $throwable->partialChangesApplied())
                                || ($throwable instanceof HostSystemUpdatePostApplyException
                                    && $throwable->requiresRollback()),
                            exceptionClass: $throwable::class,
                        ),
                    );

                    $this->resultReporter->reportFailure(
                        $handled,
                        method_exists($throwable, 'errorCode') ? (string) $throwable->errorCode() : null,
                        $this->resolveUserFacingMessage($throwable),
                        $handled->current_step,
                    );

                    return $handled;
                });
            } catch (Throwable $nested) {
                report($nested);
                $execution = $execution->fresh(['package', 'backups', 'logs']) ?? $execution;

                if (! $this->shouldRemainInMaintenanceForRecovery($execution)) {
                    $this->maintenanceModeService->attemptLeaveMaintenanceOnFailure($execution);
                }
            }
        }

        $failedProgress = $this->progressStore->markTerminal(
            $progress,
            failed: true,
            errorMessage: $userMessage,
        );
        $this->progressStore->delete($sessionId);

        return new HostSystemUpdateStepResult(
            sessionId: $sessionId,
            phase: $failedProgress->phase,
            terminal: true,
            failed: true,
            execution: $execution instanceof HostSystemUpdateExecution ? $execution->fresh(['package', 'backups', 'logs']) : null,
            userMessage: $userMessage,
            migrationProgress: null,
            batchProgress: null,
        );
    }

    private function resolveUserFacingMessage(Throwable $throwable): string
    {
        if ($throwable instanceof SupportCenterException
            || $throwable instanceof HostSystemUpdateExecutionException
            || $throwable instanceof HostSystemUpdateRecoveryException
            || $throwable instanceof HostSystemUpdateFileExecutionException
            || $throwable instanceof HostSystemUpdatePostApplyException
            || $throwable instanceof HostSystemUpdateStagingException) {
            return $throwable->userFacingMessage();
        }

        if ($throwable instanceof DomainException) {
            return $throwable->getMessage();
        }

        return (string) __('dberp.system_update_center.notifications.update_now_failed_body');
    }

    private function shouldRemainInMaintenanceForRecovery(HostSystemUpdateExecution $execution): bool
    {
        if (method_exists($this->maintenanceModeService, 'shouldRemainInMaintenanceForRecovery')) {
            return $this->maintenanceModeService->shouldRemainInMaintenanceForRecovery($execution);
        }

        return in_array((string) ($execution->status ?? ''), [
            HostSystemUpdateExecutionStatus::RollbackRequired->value,
            HostSystemUpdateExecutionStatus::RollingBack->value,
            HostSystemUpdateExecutionStatus::RollbackFailed->value,
            HostSystemUpdateExecutionStatus::ManualRecoveryRequired->value,
        ], true);
    }

    private function resultFromProgress(HostSystemUpdateStepProgress $progress): HostSystemUpdateStepResult
    {
        $execution = $progress->executionNo !== null
            ? HostSystemUpdateExecution::query()
                ->with(['package', 'backups', 'logs'])
                ->where('execution_no', $progress->executionNo)
                ->first()
            : null;

        return new HostSystemUpdateStepResult(
            sessionId: $progress->sessionId,
            phase: $progress->phase,
            terminal: $progress->terminal,
            failed: $progress->failed,
            execution: $execution,
            userMessage: $progress->errorMessage,
            migrationProgress: $progress->migrationProgress,
            batchProgress: $progress->batchProgress,
        );
    }

    private function resolveExecution(HostSystemUpdateStepProgress $progress): HostSystemUpdateExecution
    {
        $executionNo = $progress->executionNo;

        if (! is_string($executionNo) || $executionNo === '') {
            throw new DomainException((string) __('dberp.system_update_center.errors.step_session_not_found'));
        }

        $execution = HostSystemUpdateExecution::query()
            ->with(['package', 'backups'])
            ->where('execution_no', $executionNo)
            ->first();

        if (! $execution instanceof HostSystemUpdateExecution) {
            throw new DomainException((string) __('dberp.system_update_center.errors.step_session_not_found'));
        }

        return $execution;
    }

    private function resolveLatestPackage(): HostSystemUpdatePackage
    {
        $package = HostSystemUpdatePackage::query()->latest('id')->first();

        if (! $package instanceof HostSystemUpdatePackage) {
            throw new DomainException((string) __('dberp.system_update_center.errors.package_not_ready'));
        }

        return $package;
    }
}
