<?php

declare(strict_types=1);

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

namespace App\Services\Install;

use App\Services\Install\Contracts\WebInstallMigrationCompletionGuardContract;
use App\Services\Install\Contracts\WebInstallStagedPhaseExecutor;
use App\Services\Install\Contracts\WebInstallStepRunnerContract;
use DomainException;
use InvalidArgumentException;
use JsonException;
use RuntimeException;
use Throwable;

/**
 * 分阶段 Web 安装单步执行器（每步一次短 HTTP 请求）。
 */
final class WebInstallStepRunner implements WebInstallStepRunnerContract
{
    public function __construct(
        private readonly WebInstallProgressStore $progressStore,
        private readonly WebInstallPendingPayloadStore $pendingPayloadStore,
        private readonly WebInstallStagedPhaseExecutor $coordinator,
        private readonly WebInstallMigrationRunner $migrationRunner,
        private readonly WebInstallMigrationCompletionGuardContract $migrationCompletionGuard,
        private readonly DBErpInstallLogger $installLogger,
        private readonly WebInstallStepLock $stepLock,
        private readonly DBErpInstallStatus $installStatus,
    ) {}

    public function runNextStep(string $installId): WebInstallProgressState
    {
        $state = $this->requireProgress($installId);

        // 已安装且进度已到终止态：直接返回（幂等保护）
        if ($this->installStatus->isInstalled() && $this->isTerminal($state)) {
            $this->cleanupSensitivePayload($installId);

            return $state;
        }

        // 已安装但本次向导尚未开始执行任何步骤：视为重复访问，不再跑阶段。
        if ($this->installStatus->isInstalled() && $this->shouldShortCircuitAsAlreadyInstalled($state)) {
            return $this->resolveAlreadyInstalledState($installId);
        }

        if ($this->isTerminal($state)) {
            $this->cleanupSensitivePayload($installId);

            return $state;
        }

        if ($state->status === WebInstallProgressStatus::Failed) {
            return $state;
        }

        if (! $this->stepLock->tryAcquire($installId)) {
            return $this->requireProgress($installId);
        }

        try {
            $state = $this->requireProgress($installId);

            if ($this->isTerminal($state) || $state->status === WebInstallProgressStatus::Failed) {
                return $state;
            }

            $payload = $this->pendingPayloadStore->readForExecution($installId);
            if ($payload === null) {
                return $this->progressStore->markFailed($installId, WebInstallProgressDisplay::MISSING_PAYLOAD_MESSAGE);
            }

            $nextPhase = $this->resolveNextPhase($state);
            if ($nextPhase === null) {
                return $state;
            }

            if ($nextPhase === WebInstallPhase::CheckingDatabase) {
                $this->installLogger->installSessionStarted($installId);
            }

            if ($nextPhase === WebInstallPhase::Migrating) {
                return $this->executeMigratingBatchStep($installId);
            }

            $stageStartedAt = microtime(true);
            $this->progressStore->markStageRunning($installId, $nextPhase);

            try {
                $warnings = $this->executePhase($nextPhase, $payload);
            } catch (DomainException|DatabaseNotInstallableException|WebInstallFinalizationIncompleteException $e) {
                return $this->finalizeFailedStage($installId, $nextPhase, $stageStartedAt, $e->getMessage());
            } catch (JsonException) {
                return $this->finalizeFailedStage($installId, $nextPhase, $stageStartedAt, WebInstallUserMessages::WRITING_ENV_FAILED);
            } catch (RuntimeException $e) {
                return $this->finalizeFailedStage($installId, $nextPhase, $stageStartedAt, WebInstallUserMessages::failureMessageForPhase($nextPhase, $e));
            } catch (Throwable $e) {
                return $this->finalizeFailedStage($installId, $nextPhase, $stageStartedAt, WebInstallUserMessages::failureMessageForPhase($nextPhase, $e));
            }

            $state = $this->progressStore->markStageCompleted($installId, $nextPhase);

            foreach ($warnings as $warning) {
                $state = $this->progressStore->appendWarning($installId, $warning);
            }

            if ($nextPhase === WebInstallPhase::Completed) {
                $state = $this->progressStore->markCompleted($installId);
                $this->cleanupSensitivePayload($installId);
            }

            $this->logStageOutcome($installId, $nextPhase, $state, $stageStartedAt);

            return $state;
        } finally {
            $this->stepLock->release($installId);
        }
    }

    public function retryFailedStep(string $installId): WebInstallProgressState
    {
        if ($this->installStatus->isInstalled()) {
            return $this->resolveAlreadyInstalledState($installId);
        }

        $state = $this->requireProgress($installId);

        if ($state->status !== WebInstallProgressStatus::Failed) {
            return $state;
        }

        if ($state->currentStage === '') {
            return $this->progressStore->markFailed($installId, '无法确定需要重试的安装步骤，请返回上一步重新提交。');
        }

        if (! $this->stepLock->tryAcquire($installId)) {
            return $this->requireProgress($installId);
        }

        try {
            $state = $this->progressStore->resetForRetry($installId);

            $payload = $this->pendingPayloadStore->readForExecution($installId);
            if ($payload === null) {
                return $this->progressStore->markFailed($installId, WebInstallProgressDisplay::MISSING_PAYLOAD_MESSAGE);
            }

            $phase = WebInstallPhase::tryFrom($state->currentStage);
            if ($phase === null || $phase === WebInstallPhase::Completed) {
                return $this->progressStore->markFailed($installId, '无法确定需要重试的安装步骤，请返回上一步重新提交。');
            }

            if ($phase === WebInstallPhase::Migrating) {
                return $this->executeMigratingBatchStep($installId);
            }

            $stageStartedAt = microtime(true);
            $this->progressStore->markStageRunning($installId, $phase);

            try {
                $warnings = $this->executePhase($phase, $payload);
            } catch (DomainException|DatabaseNotInstallableException|WebInstallFinalizationIncompleteException $e) {
                return $this->finalizeFailedStage($installId, $phase, $stageStartedAt, $e->getMessage());
            } catch (JsonException) {
                return $this->finalizeFailedStage($installId, $phase, $stageStartedAt, WebInstallUserMessages::WRITING_ENV_FAILED);
            } catch (RuntimeException $e) {
                return $this->finalizeFailedStage($installId, $phase, $stageStartedAt, WebInstallUserMessages::failureMessageForPhase($phase, $e));
            } catch (Throwable $e) {
                return $this->finalizeFailedStage($installId, $phase, $stageStartedAt, WebInstallUserMessages::failureMessageForPhase($phase, $e));
            }

            $state = $this->progressStore->markStageCompleted($installId, $phase);

            foreach ($warnings as $warning) {
                $state = $this->progressStore->appendWarning($installId, $warning);
            }

            if ($phase === WebInstallPhase::Completed) {
                $state = $this->progressStore->markCompleted($installId);
                $this->cleanupSensitivePayload($installId);
            }

            $this->logStageOutcome($installId, $phase, $state, $stageStartedAt);

            return $state;
        } finally {
            $this->stepLock->release($installId);
        }
    }

    private function resolveAlreadyInstalledState(string $installId): WebInstallProgressState
    {
        $this->cleanupSensitivePayload($installId);

        try {
            $existing = $this->requireProgress($installId);
        } catch (WebInstallProgressNotFoundException) {
            return $this->syntheticAlreadyInstalledState($installId);
        }

        if ($this->isTerminal($existing)) {
            return $existing;
        }

        return $this->progressStore->markApplicationAlreadyInstalled($installId);
    }

    private function syntheticAlreadyInstalledState(string $installId): WebInstallProgressState
    {
        $now = now()->toIso8601String();

        return new WebInstallProgressState(
            installId: $installId,
            status: WebInstallProgressStatus::Completed,
            currentStage: WebInstallPhase::Completed->value,
            currentLabel: WebInstallUserMessages::APPLICATION_ALREADY_INSTALLED,
            percent: 100,
            completedStages: [WebInstallPhase::Completed->value],
            warnings: [],
            errors: [],
            startedAt: $now,
            updatedAt: $now,
        );
    }

    private function cleanupSensitivePayload(string $installId): void
    {
        $this->pendingPayloadStore->delete($installId);
    }

    private function executeMigratingBatchStep(string $installId): WebInstallProgressState
    {
        $stageStartedAt = microtime(true);
        $state = $this->requireProgress($installId);
        if ($state->currentStage !== WebInstallPhase::Migrating->value) {
            $this->progressStore->markStageRunning($installId, WebInstallPhase::Migrating);
        }

        try {
            $result = $this->migrationRunner->runNextBatch($installId);
        } catch (Throwable) {
            $failed = $this->progressStore->markMigrationFailed(
                $installId,
                WebInstallUserMessages::MIGRATION_FAILED,
                $this->migrationRunner->snapshotProgress(),
            );

            $this->logStageOutcome($installId, WebInstallPhase::Migrating, $failed, $stageStartedAt);

            return $failed;
        }

        $state = $this->progressStore->applyMigrationBatchResult($installId, $result);

        if ($result->stageCompleted) {
            try {
                $this->migrationCompletionGuard->ensureRequiredTablesExist();
            } catch (DomainException $e) {
                $failed = $this->progressStore->markFailed($installId, $e->getMessage());
                $this->logStageOutcome($installId, WebInstallPhase::Migrating, $failed, $stageStartedAt);

                return $failed;
            }

            $this->installLogger->stepProgress(
                'post_migrate',
                '数据库迁移已成功完成（含基线字典与编号规则），继续创建超级管理员与系统档案',
                ['install_id' => $installId],
            );

            $state = $this->progressStore->markStageCompleted($installId, WebInstallPhase::Migrating);
            $this->logStageOutcome($installId, WebInstallPhase::Migrating, $state, $stageStartedAt);

            return $state;
        }

        $this->logStageOutcome($installId, WebInstallPhase::Migrating, $state, $stageStartedAt);

        return $state;
    }

    private function finalizeFailedStage(
        string $installId,
        WebInstallPhase $phase,
        float $stageStartedAt,
        string $message,
    ): WebInstallProgressState {
        $message = WebInstallUserMessages::sanitizeFailureDetail($message, $phase);
        $state = $this->progressStore->markFailed($installId, $message);
        $this->logStageOutcome($installId, $phase, $state, $stageStartedAt);

        return $state;
    }

    private function logStageOutcome(
        string $installId,
        WebInstallPhase $phase,
        WebInstallProgressState $state,
        float $stageStartedAt,
    ): void {
        $this->installLogger->stageProgress(
            installId: $installId,
            stage: $phase->value,
            status: $state->status->value,
            startedAt: date('c', (int) $stageStartedAt),
            finishedAt: $state->updatedAt,
            durationMs: max(0, (int) round((microtime(true) - $stageStartedAt) * 1000)),
            userFacingMessage: WebInstallProgressApiResponse::userMessageForState($state),
            warningCount: count($state->warnings),
            errorCount: count($state->errors),
        );
    }

    /**
     * @param  array{
     *     system_url: string,
     *     timezone: string,
     *     admin_name: string,
     *     login_account: string,
     *     admin_email?: string|null,
     *     password: string,
     *     reinstall_reset?: bool,
     * }  $payload
     * @return list<string>
     */
    private function executePhase(WebInstallPhase $phase, array $payload): array
    {
        return match ($phase) {
            WebInstallPhase::CheckingDatabase => $this->runCheckingDatabase($payload),
            WebInstallPhase::WritingEnv => $this->runWritingEnv($payload),
            WebInstallPhase::Migrating => [],
            WebInstallPhase::CreatingAdmin => $this->runCreatingAdmin($payload),
            WebInstallPhase::UpdatingSystemProfile => $this->runUpdatingSystemProfile($payload),
            WebInstallPhase::WritingInstallMarkers => $this->runWritingInstallMarkers($payload),
            WebInstallPhase::RunningOptionalFinalizers => $this->runOptionalFinalizers(),
            WebInstallPhase::Completed => $this->runCompleted(),
            WebInstallPhase::Seeding => [],
        };
    }

    /**
     * @param  array{reinstall_reset?: bool}  $payload
     * @return list<string>
     */
    private function runCheckingDatabase(array $payload): array
    {
        $this->coordinator->executeCheckingDatabasePhase($payload);

        return [];
    }

    /**
     * @param  array{system_url: string, timezone: string}  $payload
     * @return list<string>
     */
    private function runWritingEnv(array $payload): array
    {
        $this->coordinator->executeWritingEnvPhase($payload);

        return [];
    }

    /**
     * @param  array{
     *     admin_name: string,
     *     login_account: string,
     *     admin_email?: string|null,
     *     password: string,
     * }  $payload
     * @return list<string>
     */
    private function runCreatingAdmin(array $payload): array
    {
        $this->coordinator->executeCreatingAdminPhase($payload);

        return [];
    }

    /**
     * @param  array{timezone: string}  $payload
     * @return list<string>
     */
    private function runUpdatingSystemProfile(array $payload): array
    {
        $this->coordinator->executeUpdatingSystemProfilePhase($payload);

        return [];
    }

    /**
     * @param  array{system_url: string, timezone: string}  $payload
     * @return list<string>
     */
    private function runWritingInstallMarkers(array $payload): array
    {
        $this->coordinator->executeWritingInstallMarkersPhase($payload);

        return [];
    }

    /**
     * @return list<string>
     */
    private function runOptionalFinalizers(): array
    {
        return $this->coordinator->executeOptionalFinalizersPhase();
    }

    /**
     * @return list<string>
     */
    private function runCompleted(): array
    {
        $this->coordinator->executeCompletedPhase();

        return [];
    }

    private function resolveNextPhase(WebInstallProgressState $state): ?WebInstallPhase
    {
        foreach ($this->executablePhases() as $phase) {
            if (! in_array($phase->value, $state->completedStages, true)) {
                return $phase;
            }
        }

        return null;
    }

    /**
     * @return list<WebInstallPhase>
     */
    private function executablePhases(): array
    {
        return [
            WebInstallPhase::CheckingDatabase,
            WebInstallPhase::WritingEnv,
            WebInstallPhase::Migrating,
            WebInstallPhase::CreatingAdmin,
            WebInstallPhase::UpdatingSystemProfile,
            WebInstallPhase::WritingInstallMarkers,
            WebInstallPhase::RunningOptionalFinalizers,
            WebInstallPhase::Completed,
        ];
    }

    private function isTerminal(WebInstallProgressState $state): bool
    {
        return in_array($state->status, [
            WebInstallProgressStatus::Completed,
            WebInstallProgressStatus::Warning,
        ], true);
    }

    /**
     * installed.lock 已存在，但当前 install_id 的进度尚未执行过任何阶段（重复打开进度页等场景）。
     */
    private function shouldShortCircuitAsAlreadyInstalled(WebInstallProgressState $state): bool
    {
        if ($state->completedStages !== []) {
            return false;
        }

        return $state->status === WebInstallProgressStatus::Pending
            && $state->currentStage === '';
    }

    private function requireProgress(string $installId): WebInstallProgressState
    {
        try {
            $state = $this->progressStore->read($installId);
        } catch (InvalidArgumentException) {
            throw WebInstallProgressNotFoundException::forInstallId($installId);
        }

        if ($state === null) {
            throw WebInstallProgressNotFoundException::forInstallId($installId);
        }

        return $state;
    }
}
