<?php

declare(strict_types=1);

namespace App\Services\SupportCenter;

use App\Enums\SupportCenter\HostSystemUpdateExecutionStep;
use App\Enums\SupportCenter\HostSystemUpdateStepPhase;
use App\Models\HostSystemUpdateExecution;

/**
 * 分批应用 vendor 目录更新策略。
 */
final class HostSystemUpdateVendorReplaceBatchRunner
{
    private const LIST_KEY_VENDOR = 'vendor_files';

    public function __construct(
        private readonly HostSystemUpdateExecutionStateMachine $stateMachine,
        private readonly HostSystemUpdateExecutionContextResolver $contextResolver,
        private readonly HostSystemUpdateVendorStrategyResolver $vendorStrategyResolver,
        private readonly HostSystemUpdateVendorReplacer $vendorReplacer,
        private readonly HostSystemUpdateExecutionBatchProgressStore $progressStore,
        private readonly HostSystemUpdateExecutionLogger $executionLogger,
        private readonly SupportCenterAuditLogger $auditLogger,
    ) {}

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

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

        if ($existing === null) {
            $execution = $this->stateMachine->advanceStep(
                $execution->fresh() ?? $execution,
                HostSystemUpdateExecutionStep::ReplaceVendor,
                (string) __('dberp.system_update.file_execution.log.apply_vendor'),
                progressPercent: 60,
            );

            $manifest = $this->contextResolver->manifest($execution);
            $strategy = $this->vendorStrategyResolver->resolveStrategy($manifest);
            $package = $this->contextResolver->resolveReadyPackage($execution);

            $this->auditLogger->logSystemUpdateVendorReplacementStarted($execution, $package, $strategy);
            $this->executionLogger->logStepStarted(
                $execution,
                HostSystemUpdateExecutionStep::ReplaceVendor->value,
                (string) __('dberp.system_update.file_execution.log.vendor_started', ['strategy' => $strategy]),
                ['vendor_strategy' => $strategy],
            );

            if ($strategy !== HostSystemUpdateVendorStrategyResolver::STRATEGY_INCLUDED_VENDOR) {
                return $this->completeDeferredOrSkipped($execution, $strategy);
            }

            $vendorFiles = $this->vendorStrategyResolver->resolveVendorFiles($manifest);
            sort($vendorFiles, SORT_STRING);
            $this->progressStore->writeStableList((string) $execution->execution_no, self::LIST_KEY_VENDOR, $vendorFiles);

            $batchSize = max(1, (int) config('dberp.system_update.vendor_batch_size', 100));

            $existing = new HostSystemUpdateBatchProgress(
                phase: $phase,
                status: HostSystemUpdateBatchProgress::STATUS_IN_PROGRESS,
                total: count($vendorFiles),
                processed: 0,
                cursor: 0,
                current: null,
                batchSize: $batchSize,
            );
        }

        if ($existing->total === 0) {
            return $this->completeVendorPhase($execution, $existing, [
                'strategy' => HostSystemUpdateVendorStrategyResolver::STRATEGY_INCLUDED_VENDOR,
                'replaced_count' => 0,
                'skipped_count' => 0,
                'deferred' => false,
            ]);
        }

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

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

    private function runVendorFilesBatch(
        HostSystemUpdateExecution $execution,
        HostSystemUpdateBatchProgress $progress,
    ): HostSystemUpdateBatchPhaseResult {
        $paths = $this->progressStore->readStableList((string) $execution->execution_no, self::LIST_KEY_VENDOR);
        $slice = array_slice($paths, $progress->cursor, $progress->batchSize);
        $manifest = $this->contextResolver->manifest($execution);
        $stagingDirectory = $this->contextResolver->stagingDirectory($execution);
        $partial = $progress->processed > 0;

        foreach ($slice as $path) {
            $this->vendorReplacer->replaceVendorManifestFile(
                $execution,
                $manifest,
                $stagingDirectory,
                $path,
                $partial,
            );
            $partial = true;
        }

        $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);
        }

        return $this->completeVendorPhase($execution, $progress, [
            'strategy' => HostSystemUpdateVendorStrategyResolver::STRATEGY_INCLUDED_VENDOR,
            'replaced_count' => $processed,
            'skipped_count' => 0,
            'deferred' => false,
        ]);
    }

    private function completeDeferredOrSkipped(
        HostSystemUpdateExecution $execution,
        string $strategy,
    ): HostSystemUpdateBatchPhaseResult {
        $summary = match ($strategy) {
            HostSystemUpdateVendorStrategyResolver::STRATEGY_COMPOSER_REQUIRED => [
                'strategy' => $strategy,
                'replaced_count' => 0,
                'skipped_count' => 0,
                'deferred' => true,
            ],
            default => [
                'strategy' => HostSystemUpdateVendorStrategyResolver::STRATEGY_NONE,
                'replaced_count' => 0,
                'skipped_count' => 0,
                'deferred' => false,
            ],
        };

        $progress = new HostSystemUpdateBatchProgress(
            phase: HostSystemUpdateStepPhase::ReplaceVendor->value,
            status: HostSystemUpdateBatchProgress::STATUS_COMPLETED,
            total: 0,
            processed: 0,
            cursor: 0,
            current: null,
            batchSize: 1,
        );

        return $this->completeVendorPhase($execution, $progress, $summary);
    }

    /**
     * @param  array{strategy: string, replaced_count: int, skipped_count: int, deferred: bool}  $summary
     */
    private function completeVendorPhase(
        HostSystemUpdateExecution $execution,
        HostSystemUpdateBatchProgress $progress,
        array $summary,
    ): HostSystemUpdateBatchPhaseResult {
        $package = $this->contextResolver->resolveReadyPackage($execution);
        $strategy = $summary['strategy'];

        $this->executionLogger->logStepCompleted(
            $execution,
            HostSystemUpdateExecutionStep::ReplaceVendor->value,
            (string) __('dberp.system_update.file_execution.log.vendor_completed', [
                'strategy' => $strategy,
            ]),
            $summary,
            now()->utc(),
        );

        if (($summary['deferred'] ?? false) === true) {
            $this->auditLogger->logSystemUpdateVendorReplacementDeferred($execution, $package, $strategy, $summary);
        } else {
            $this->auditLogger->logSystemUpdateVendorReplacementCompleted($execution, $package, $strategy, $summary);
        }

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

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