<?php

declare(strict_types=1);

namespace App\Services\Install;

use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\File;
use InvalidArgumentException;
use JsonException;

/**
 * 基于文件的 Web 安装进度存储（storage/framework/dberp-install/progress/{install_id}.json）。
 */
final class WebInstallProgressStore
{
    public function __construct(
        private readonly ?string $progressDirectory = null,
    ) {}

    public function createInitialState(string $installId): WebInstallProgressState
    {
        $this->assertValidInstallId($installId);

        $now = Carbon::now()->toIso8601String();
        $state = WebInstallProgressState::initial($installId, $now);
        $this->writeState($state);

        return $state;
    }

    public function read(string $installId): ?WebInstallProgressState
    {
        $this->assertValidInstallId($installId);

        $path = $this->pathFor($installId);
        if (! is_file($path)) {
            return null;
        }

        $raw = file_get_contents($path);
        if ($raw === false || trim($raw) === '') {
            return null;
        }

        try {
            /** @var array<string, mixed> $decoded */
            $decoded = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
        } catch (JsonException) {
            return null;
        }

        return WebInstallProgressState::fromArray($decoded);
    }

    public function markStageRunning(string $installId, WebInstallPhase $phase): WebInstallProgressState
    {
        return $this->mutate($installId, function (WebInstallProgressState $state) use ($phase): WebInstallProgressState {
            return new WebInstallProgressState(
                installId: $state->installId,
                status: WebInstallProgressStatus::Running,
                currentStage: $phase->value,
                currentLabel: $phase->labelZh(),
                percent: $state->percentForPhase($phase, completed: false),
                completedStages: $state->completedStages,
                warnings: $state->warnings,
                errors: $state->errors,
                startedAt: $state->startedAt,
                updatedAt: Carbon::now()->toIso8601String(),
                migrationProgress: $state->migrationProgress,
            );
        });
    }

    public function applyMigrationBatchResult(string $installId, WebInstallMigrationBatchResult $result): WebInstallProgressState
    {
        return $this->mutate($installId, function (WebInstallProgressState $state) use ($result): WebInstallProgressState {
            return new WebInstallProgressState(
                installId: $state->installId,
                status: WebInstallProgressStatus::Running,
                currentStage: WebInstallPhase::Migrating->value,
                currentLabel: WebInstallMigrationProgress::businessLabel($result->progress),
                percent: $state->percentDuringMigration($result->progress),
                completedStages: $state->completedStages,
                warnings: $state->warnings,
                errors: $state->errors,
                startedAt: $state->startedAt,
                updatedAt: Carbon::now()->toIso8601String(),
                migrationProgress: $result->progress,
            );
        });
    }

    public function markMigrationFailed(
        string $installId,
        string $error,
        ?WebInstallMigrationProgress $progress = null,
    ): WebInstallProgressState {
        $error = trim($error);
        if ($error === '') {
            $error = WebInstallUserMessages::MIGRATION_FAILED;
        }

        return $this->mutate($installId, function (WebInstallProgressState $state) use ($error, $progress): WebInstallProgressState {
            $errors = $state->errors;
            $errors[] = $error;

            return new WebInstallProgressState(
                installId: $state->installId,
                status: WebInstallProgressStatus::Failed,
                currentStage: WebInstallPhase::Migrating->value,
                currentLabel: '安装失败',
                percent: $state->percent,
                completedStages: $state->completedStages,
                warnings: $state->warnings,
                errors: $errors,
                startedAt: $state->startedAt,
                updatedAt: Carbon::now()->toIso8601String(),
                migrationProgress: $progress ?? $state->migrationProgress,
            );
        });
    }

    public function markStageCompleted(string $installId, WebInstallPhase $phase): WebInstallProgressState
    {
        return $this->mutate($installId, function (WebInstallProgressState $state) use ($phase): WebInstallProgressState {
            $completedStages = $state->completedStages;
            if (! in_array($phase->value, $completedStages, true)) {
                $completedStages[] = $phase->value;
            }

            return new WebInstallProgressState(
                installId: $state->installId,
                status: $state->status === WebInstallProgressStatus::Warning
                    ? WebInstallProgressStatus::Warning
                    : WebInstallProgressStatus::Running,
                currentStage: $phase->value,
                currentLabel: $phase->labelZh().'（已完成）',
                percent: $state->percentForPhase($phase, completed: true),
                completedStages: $completedStages,
                warnings: $state->warnings,
                errors: $state->errors,
                startedAt: $state->startedAt,
                updatedAt: Carbon::now()->toIso8601String(),
                migrationProgress: $state->migrationProgress,
            );
        });
    }

    public function addWarning(string $installId, string $warning): WebInstallProgressState
    {
        $warning = trim($warning);
        if ($warning === '') {
            return $this->requireState($installId);
        }

        return $this->mutate($installId, function (WebInstallProgressState $state) use ($warning): WebInstallProgressState {
            $warnings = $state->warnings;
            $warnings[] = $warning;

            return new WebInstallProgressState(
                installId: $state->installId,
                status: WebInstallProgressStatus::Warning,
                currentStage: $state->currentStage,
                currentLabel: $state->currentLabel,
                percent: $state->percent,
                completedStages: $state->completedStages,
                warnings: $warnings,
                errors: $state->errors,
                startedAt: $state->startedAt,
                updatedAt: Carbon::now()->toIso8601String(),
                migrationProgress: $state->migrationProgress,
            );
        });
    }

    public function appendWarning(string $installId, string $warning): WebInstallProgressState
    {
        $warning = trim($warning);
        if ($warning === '') {
            return $this->requireState($installId);
        }

        return $this->mutate($installId, function (WebInstallProgressState $state) use ($warning): WebInstallProgressState {
            $warnings = $state->warnings;
            $warnings[] = $warning;

            return new WebInstallProgressState(
                installId: $state->installId,
                status: $state->status,
                currentStage: $state->currentStage,
                currentLabel: $state->currentLabel,
                percent: $state->percent,
                completedStages: $state->completedStages,
                warnings: $warnings,
                errors: $state->errors,
                startedAt: $state->startedAt,
                updatedAt: Carbon::now()->toIso8601String(),
                migrationProgress: $state->migrationProgress,
            );
        });
    }

    public function markFailed(string $installId, string $error): WebInstallProgressState
    {
        return $this->mutate($installId, function (WebInstallProgressState $state) use ($error): WebInstallProgressState {
            $phase = WebInstallPhase::tryFrom($state->currentStage);
            $error = WebInstallUserMessages::sanitizeFailureDetail($error, $phase);
            if ($error === '') {
                $error = '安装过程中发生错误，请稍后重试。';
            }

            $errors = $state->errors;
            $errors[] = $error;

            return new WebInstallProgressState(
                installId: $state->installId,
                status: WebInstallProgressStatus::Failed,
                currentStage: $state->currentStage,
                currentLabel: '安装失败',
                percent: $state->percent,
                completedStages: $state->completedStages,
                warnings: $state->warnings,
                errors: $errors,
                startedAt: $state->startedAt,
                updatedAt: Carbon::now()->toIso8601String(),
                migrationProgress: $state->migrationProgress,
            );
        });
    }

    public function markCompleted(string $installId): WebInstallProgressState
    {
        return $this->mutate($installId, function (WebInstallProgressState $state): WebInstallProgressState {
            $completedStages = $state->completedStages;
            if (! in_array(WebInstallPhase::Completed->value, $completedStages, true)) {
                $completedStages[] = WebInstallPhase::Completed->value;
            }

            $status = $state->warnings !== []
                ? WebInstallProgressStatus::Warning
                : WebInstallProgressStatus::Completed;

            return new WebInstallProgressState(
                installId: $state->installId,
                status: $status,
                currentStage: WebInstallPhase::Completed->value,
                currentLabel: $status === WebInstallProgressStatus::Warning
                    ? '安装已完成，但存在需关注的收尾提示'
                    : WebInstallPhase::Completed->labelZh(),
                percent: 100,
                completedStages: $completedStages,
                warnings: $state->warnings,
                errors: $state->errors,
                startedAt: $state->startedAt,
                updatedAt: Carbon::now()->toIso8601String(),
                migrationProgress: $state->migrationProgress,
            );
        });
    }

    public function markApplicationAlreadyInstalled(string $installId): WebInstallProgressState
    {
        return $this->mutate($installId, function (WebInstallProgressState $state): WebInstallProgressState {
            $completedStages = $state->completedStages;
            if (! in_array(WebInstallPhase::Completed->value, $completedStages, true)) {
                $completedStages[] = WebInstallPhase::Completed->value;
            }

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

    public function resetForRetry(string $installId): WebInstallProgressState
    {
        return $this->mutate($installId, function (WebInstallProgressState $state): WebInstallProgressState {
            if ($state->status !== WebInstallProgressStatus::Failed) {
                return $state;
            }

            $failedStage = $state->currentStage;
            $label = $failedStage !== ''
                ? WebInstallProgressDisplay::businessLabelForStage($failedStage)
                : WebInstallProgressDisplay::WAITING_FOR_EXECUTION_LABEL;

            return new WebInstallProgressState(
                installId: $state->installId,
                status: WebInstallProgressStatus::Running,
                currentStage: $failedStage,
                currentLabel: $label !== '' ? $label : '准备重试安装步骤',
                percent: $state->percent,
                completedStages: $state->completedStages,
                warnings: $state->warnings,
                errors: $state->errors,
                startedAt: $state->startedAt,
                updatedAt: Carbon::now()->toIso8601String(),
                migrationProgress: $state->migrationProgress,
            );
        });
    }

    public function delete(string $installId): bool
    {
        $this->assertValidInstallId($installId);

        $path = $this->pathFor($installId);
        if (! is_file($path)) {
            return false;
        }

        return unlink($path);
    }

    /**
     * @param  callable(WebInstallProgressState): WebInstallProgressState  $mutator
     */
    private function mutate(string $installId, callable $mutator): WebInstallProgressState
    {
        $state = $this->requireState($installId);
        $next = $mutator($state);
        $this->writeState($next);

        return $next;
    }

    private function requireState(string $installId): WebInstallProgressState
    {
        $state = $this->read($installId);

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

        return $state;
    }

    private function writeState(WebInstallProgressState $state): void
    {
        $path = $this->pathFor($state->installId);
        File::ensureDirectoryExists(dirname($path));

        try {
            $payload = json_encode(
                $state->toArray(),
                JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
            );
        } catch (JsonException $e) {
            throw new InvalidArgumentException('无法序列化安装进度状态。', 0, $e);
        }

        $payload .= "\n";
        $temporaryPath = $path.'.tmp.'.uniqid('', true);

        if (file_put_contents($temporaryPath, $payload, LOCK_EX) === false) {
            throw new InvalidArgumentException('无法写入安装进度临时文件。');
        }

        if (is_file($path) && ! @unlink($path)) {
            @unlink($temporaryPath);

            throw new InvalidArgumentException('无法替换已有安装进度文件。');
        }

        if (! @rename($temporaryPath, $path)) {
            @unlink($temporaryPath);

            throw new InvalidArgumentException('无法落盘安装进度文件。');
        }
    }

    private function pathFor(string $installId): string
    {
        return $this->directory().DIRECTORY_SEPARATOR.$installId.'.json';
    }

    private function directory(): string
    {
        return $this->progressDirectory ?? storage_path('framework/dberp-install/progress');
    }

    private function assertValidInstallId(string $installId): void
    {
        if (! preg_match(
            '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i',
            $installId,
        )) {
            throw new InvalidArgumentException('install_id 格式无效。');
        }
    }
}
