<?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\HostSystemUpdateExecutionStep;
use App\Enums\SupportCenter\HostSystemUpdateStepPhase;
use App\Exceptions\SupportCenter\HostSystemUpdateStagingException;
use App\Models\HostSystemUpdateBackup;
use App\Models\HostSystemUpdateExecution;
use App\Models\HostSystemUpdatePackage;
use Illuminate\Support\Carbon;
use RuntimeException;

/**
 * 分批创建更新前文件与 .env 快照备份。
 */
final class HostSystemUpdateBackupBatchRunner
{
    private const LIST_KEY_PATHS = 'backup_paths';

    private const META_KEY = 'backup_meta';

    public function __construct(
        private readonly HostSystemUpdateExecutionStateMachine $stateMachine,
        private readonly HostSystemUpdateBackupService $backupService,
        private readonly HostSystemUpdateExecutionBatchProgressStore $progressStore,
        private readonly HostSystemUpdateExecutionLogger $executionLogger,
    ) {}

    public function runNextBatch(HostSystemUpdateExecution $execution): HostSystemUpdateBatchPhaseResult
    {
        $phase = HostSystemUpdateStepPhase::Backup->value;
        $existing = $this->progressStore->read((string) $execution->execution_no, $phase);

        if ($existing?->isCompleted() === true) {
            return new HostSystemUpdateBatchPhaseResult(true, $existing, $execution);
        }

        $package = $this->resolvePackage($execution);
        $manifest = $this->manifestArray($package);
        $batchSize = max(1, (int) config('dberp.system_update.file_batch_size', 100));
        $backupDirectory = HostSystemUpdateWorkspaceStorage::backupsDirectory($execution->execution_no);
        $zipPath = $backupDirectory.DIRECTORY_SEPARATOR.'files.zip';

        if ($existing === null) {
            $execution = $this->stateMachine->advanceStep(
                $execution->fresh() ?? $execution,
                HostSystemUpdateExecutionStep::Backup,
                (string) __('dberp.system_update.backup.log.prepare'),
                progressPercent: 40,
            );

            $paths = $this->backupService->collectBackupTargetPaths($manifest);
            sort($paths, SORT_STRING);
            $this->progressStore->writeStableList((string) $execution->execution_no, self::LIST_KEY_PATHS, $paths);

            $this->backupService->beginFileSnapshotBackup($execution, $paths, $backupDirectory);

            $existing = new HostSystemUpdateBatchProgress(
                phase: $phase,
                status: HostSystemUpdateBatchProgress::STATUS_IN_PROGRESS,
                total: count($paths) + 1,
                processed: 0,
                cursor: 0,
                current: null,
                batchSize: $batchSize,
                subStep: HostSystemUpdateBatchProgress::SUBSTEP_FILES,
            );
        }

        $subStep = $existing->subStep ?? HostSystemUpdateBatchProgress::SUBSTEP_FILES;

        if ($subStep === HostSystemUpdateBatchProgress::SUBSTEP_FILES) {
            return $this->runFilesSubStep($execution, $existing, $zipPath);
        }

        return $this->runEnvSubStep($execution, $backupDirectory, $existing);
    }

    public function snapshotProgress(HostSystemUpdateExecution $execution): ?HostSystemUpdateBatchProgress
    {
        return $this->progressStore->read(
            (string) $execution->execution_no,
            HostSystemUpdateStepPhase::Backup->value,
        );
    }

    private function runFilesSubStep(
        HostSystemUpdateExecution $execution,
        HostSystemUpdateBatchProgress $progress,
        string $zipPath,
    ): HostSystemUpdateBatchPhaseResult {
        $paths = $this->progressStore->readStableList((string) $execution->execution_no, self::LIST_KEY_PATHS, required: true);
        $slice = array_slice($paths, $progress->cursor, $progress->batchSize);

        if ($slice !== []) {
            $batchResult = $this->backupService->appendFileSnapshotBatch($zipPath, $slice);
            $this->mergeBackupMeta(
                (string) $execution->execution_no,
                $batchResult,
                requireExisting: $progress->cursor > 0,
            );

            $this->executionLogger->logTransition(
                $execution,
                HostSystemUpdateExecutionStep::Backup->value,
                (string) __('dberp.system_update.backup.log.batch_completed', [
                    'count' => (string) count($slice),
                    'processed' => (string) min(count($paths), $progress->cursor + count($slice)),
                    'total' => (string) count($paths),
                ]),
                ['batch_count' => count($slice)],
            );
        }

        $processed = min(count($paths), $progress->cursor + count($slice));
        $current = $slice !== [] ? (string) end($slice) : $progress->current;

        if ($processed < count($paths)) {
            $updated = $progress->withCursor($progress->cursor + count($slice), $processed, $current);
            $this->progressStore->save((string) $execution->execution_no, $updated);

            return new HostSystemUpdateBatchPhaseResult(false, $updated, $execution);
        }

        $meta = $this->readBackupMeta(
            (string) $execution->execution_no,
            required: $paths !== [] || $progress->cursor > 0,
        );
        $backup = $this->resolveRunningFileBackup($execution);
        $startedAt = $backup->started_at instanceof Carbon ? $backup->started_at : now()->utc();

        $this->backupService->finalizeFileSnapshotBackup(
            $execution,
            $backup,
            $zipPath,
            $meta['backed_up'],
            $meta['missing'],
            $meta['file_meta'],
            $startedAt,
        );

        $envProgress = $progress->withSubStep(HostSystemUpdateBatchProgress::SUBSTEP_ENV, 1, 0, count($paths));
        $this->progressStore->save((string) $execution->execution_no, $envProgress);

        return new HostSystemUpdateBatchPhaseResult(false, $envProgress, $execution->fresh(['package', 'backups']));
    }

    private function runEnvSubStep(
        HostSystemUpdateExecution $execution,
        string $backupDirectory,
        HostSystemUpdateBatchProgress $progress,
    ): HostSystemUpdateBatchPhaseResult {
        $this->backupService->createEnvSnapshotBackup($execution, $backupDirectory);

        $completed = $progress->markCompleted();
        $this->progressStore->save((string) $execution->execution_no, $completed);
        $this->progressStore->clearList((string) $execution->execution_no, self::LIST_KEY_PATHS);
        $this->clearBackupMeta((string) $execution->execution_no);

        return new HostSystemUpdateBatchPhaseResult(
            true,
            $completed,
            $execution->fresh(['package', 'backups']),
        );
    }

    private function resolveRunningFileBackup(HostSystemUpdateExecution $execution): HostSystemUpdateBackup
    {
        $backup = $execution->backups()
            ->where('backup_type', HostSystemUpdateBackup::TYPE_FILE_SNAPSHOT)
            ->where('status', HostSystemUpdateBackup::STATUS_RUNNING)
            ->latest('id')
            ->first();

        if (! $backup instanceof HostSystemUpdateBackup) {
            throw HostSystemUpdateStagingException::backupFailed('backup_record_missing');
        }

        return $backup;
    }

    /**
     * @param  array{backed_up: list<string>, missing: list<string>, file_meta: array<string, array{size: int, checksum: string}>}  $batchResult
     */
    private function mergeBackupMeta(string $executionNo, array $batchResult, bool $requireExisting): void
    {
        $meta = $this->readBackupMeta($executionNo, $requireExisting);
        $meta['backed_up'] = array_values(array_unique(array_merge($meta['backed_up'], $batchResult['backed_up'])));
        $meta['missing'] = array_values(array_unique(array_merge($meta['missing'], $batchResult['missing'])));
        $meta['file_meta'] = array_merge($meta['file_meta'], $batchResult['file_meta']);

        $this->writeBackupMeta($executionNo, $meta);
    }

    /**
     * @return array{backed_up: list<string>, missing: list<string>, file_meta: array<string, array{size: int, checksum: string}>}
     */
    private function readBackupMeta(string $executionNo, bool $required = false): array
    {
        $items = $this->progressStore->readStableList($executionNo, self::META_KEY, $required);

        if ($items === [] || ! isset($items[0])) {
            return ['backed_up' => [], 'missing' => [], 'file_meta' => []];
        }

        try {
            /** @var array<string, mixed> $decoded */
            $decoded = json_decode($items[0], true, 512, JSON_THROW_ON_ERROR);
        } catch (\JsonException $exception) {
            throw new RuntimeException('系统更新备份元数据已损坏。', previous: $exception);
        }

        return [
            'backed_up' => is_array($decoded['backed_up'] ?? null) ? array_values($decoded['backed_up']) : [],
            'missing' => is_array($decoded['missing'] ?? null) ? array_values($decoded['missing']) : [],
            'file_meta' => is_array($decoded['file_meta'] ?? null) ? $decoded['file_meta'] : [],
        ];
    }

    /**
     * @param  array{backed_up: list<string>, missing: list<string>, file_meta: array<string, array{size: int, checksum: string}>}  $meta
     */
    private function writeBackupMeta(string $executionNo, array $meta): void
    {
        $encoded = json_encode($meta, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
        $this->progressStore->writeStableList($executionNo, self::META_KEY, [$encoded]);
    }

    private function clearBackupMeta(string $executionNo): void
    {
        $this->progressStore->clearList($executionNo, self::META_KEY);
    }

    private function resolvePackage(HostSystemUpdateExecution $execution): HostSystemUpdatePackage
    {
        $package = $execution->package;

        if (! $package instanceof HostSystemUpdatePackage || $package->manifest_status !== HostSystemUpdatePackage::MANIFEST_STATUS_VALID) {
            throw HostSystemUpdateStagingException::packageNotReady();
        }

        return $package;
    }

    /**
     * @return array<string, mixed>
     */
    private function manifestArray(HostSystemUpdatePackage $package): array
    {
        $manifest = $package->manifest;

        if (! is_array($manifest) || $manifest === []) {
            throw HostSystemUpdateStagingException::packageNotReady();
        }

        return $manifest;
    }
}
