<?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\Models\HostSystemUpdateExecution;
use App\Support\SystemUpdate\HostSystemUpdateReplaceFileOrderSorter;

/**
 * 分批替换 manifest 文件并执行 delete_files。
 */
final class HostSystemUpdateFileReplaceBatchRunner
{
    private const LIST_KEY_REPLACE = 'replace_files';

    private const LIST_KEY_DELETE = 'delete_files';

    public function __construct(
        private readonly HostSystemUpdateExecutionStateMachine $stateMachine,
        private readonly HostSystemUpdateExecutionContextResolver $contextResolver,
        private readonly HostSystemUpdateFileReplacer $fileReplacer,
        private readonly HostSystemUpdateFileDeletionService $fileDeletionService,
        private readonly HostSystemUpdateExecutionBatchProgressStore $progressStore,
        private readonly HostSystemUpdateExecutionLogger $executionLogger,
        private readonly SupportCenterAuditLogger $auditLogger,
    ) {}

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

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

        // 低于 100 会把更新器自身的调用方与新依赖拆进不同 HTTP 请求，形成不可启动的混合版本窗口。
        $batchSize = max(100, (int) config('dberp.system_update.file_batch_size', 100));

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

            $manifest = $this->contextResolver->manifest($execution);
            $programFiles = array_values(array_filter(
                $this->collectPathList($manifest[HostSystemUpdateManifestContract::FILES] ?? []),
                fn (string $path): bool => ! $this->isVendorPayloadPath($manifest, $path),
            ));
            $replacePaths = HostSystemUpdateReplaceFileOrderSorter::sortForReplace(
                $programFiles,
            );
            $deletePaths = $this->collectPathList($manifest[HostSystemUpdateManifestContract::DELETE_FILES] ?? []);
            sort($deletePaths, SORT_STRING);
            $deletePaths = array_values(array_unique($deletePaths));

            $this->progressStore->writeStableList((string) $execution->execution_no, self::LIST_KEY_REPLACE, $replacePaths);
            $this->progressStore->writeStableList((string) $execution->execution_no, self::LIST_KEY_DELETE, $deletePaths);

            $this->contextResolver->resolveCompletedFileSnapshotBackup($execution);
            $package = $this->contextResolver->resolveReadyPackage($execution);

            $this->auditLogger->logSystemUpdateFileReplacementStarted($execution, $package, count($replacePaths));
            $this->executionLogger->logStepStarted(
                $execution,
                HostSystemUpdateExecutionStep::ReplaceFiles->value,
                (string) __('dberp.system_update.file_execution.log.replace_started'),
                ['file_count' => count($replacePaths)],
            );

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

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

        if ($subStep === HostSystemUpdateBatchProgress::SUBSTEP_REPLACE) {
            return $this->runReplaceSubStep($execution, $existing);
        }

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

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

    private function runReplaceSubStep(
        HostSystemUpdateExecution $execution,
        HostSystemUpdateBatchProgress $progress,
    ): HostSystemUpdateBatchPhaseResult {
        $paths = $this->progressStore->readStableList((string) $execution->execution_no, self::LIST_KEY_REPLACE, required: true);
        $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->fileReplacer->replaceManifestFile($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);
        }

        $deletePaths = $this->progressStore->readStableList((string) $execution->execution_no, self::LIST_KEY_DELETE, required: true);

        if ($deletePaths === []) {
            return $this->completeReplacePhase($execution, $progress, $processed);
        }

        $deleteProgress = new HostSystemUpdateBatchProgress(
            phase: $progress->phase,
            status: HostSystemUpdateBatchProgress::STATUS_IN_PROGRESS,
            total: count($paths) + count($deletePaths),
            processed: count($paths),
            cursor: 0,
            current: null,
            batchSize: $progress->batchSize,
            subStep: HostSystemUpdateBatchProgress::SUBSTEP_DELETE,
        );
        $this->progressStore->save((string) $execution->execution_no, $deleteProgress);

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

    private function runDeleteSubStep(
        HostSystemUpdateExecution $execution,
        HostSystemUpdateBatchProgress $progress,
    ): HostSystemUpdateBatchPhaseResult {
        $paths = $this->progressStore->readStableList((string) $execution->execution_no, self::LIST_KEY_DELETE, required: true);
        $replaceCount = count($this->progressStore->readStableList(
            (string) $execution->execution_no,
            self::LIST_KEY_REPLACE,
            required: true,
        ));
        $slice = array_slice($paths, $progress->cursor, $progress->batchSize);

        if ($progress->cursor === 0 && $paths !== []) {
            $package = $this->contextResolver->resolveReadyPackage($execution);
            $this->auditLogger->logSystemUpdateFileDeletionStarted($execution, $package, count($paths));
            $this->executionLogger->logStepStarted(
                $execution,
                HostSystemUpdateExecutionStep::ReplaceFiles->value,
                (string) __('dberp.system_update.file_execution.log.delete_started'),
                ['file_count' => count($paths)],
            );
        }

        $backedUpPaths = $this->contextResolver->backedUpPaths($execution);
        $partial = $progress->processed > 0;

        foreach ($slice as $path) {
            $this->fileDeletionService->deleteManifestFile($path, $backedUpPaths, $partial);
            $partial = true;
        }

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

        if ($processed < $replaceCount + 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->completeReplacePhase($execution, $progress, $processed);
    }

    private function completeReplacePhase(
        HostSystemUpdateExecution $execution,
        HostSystemUpdateBatchProgress $progress,
        int $processed,
    ): HostSystemUpdateBatchPhaseResult {
        $package = $this->contextResolver->resolveReadyPackage($execution);

        $this->executionLogger->logStepCompleted(
            $execution,
            HostSystemUpdateExecutionStep::ReplaceFiles->value,
            (string) __('dberp.system_update.file_execution.log.replace_completed', [
                'count' => $processed,
            ]),
            ['replaced_count' => $processed],
            now()->utc(),
        );

        $this->auditLogger->logSystemUpdateFileReplacementCompleted($execution, $package, [
            'replaced_count' => $processed,
            'skipped_count' => 0,
            'file_results' => [],
        ]);

        $deletePaths = $this->progressStore->readStableList((string) $execution->execution_no, self::LIST_KEY_DELETE, required: true);

        if ($deletePaths !== []) {
            $this->executionLogger->logStepCompleted(
                $execution,
                HostSystemUpdateExecutionStep::ReplaceFiles->value,
                (string) __('dberp.system_update.file_execution.log.delete_completed', [
                    'count' => count($deletePaths),
                ]),
                ['deleted_count' => count($deletePaths)],
                now()->utc(),
            );
            $this->auditLogger->logSystemUpdateFileDeletionCompleted($execution, $package, [
                'deleted_count' => count($deletePaths),
                'skipped_count' => 0,
                'missing_count' => 0,
                'file_results' => [],
            ]);
        }

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

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

    /**
     * @return list<string>
     */
    private function collectPathList(mixed $items): array
    {
        if (! is_array($items)) {
            return [];
        }

        $paths = [];

        foreach ($items as $path) {
            if (is_string($path) && $path !== '') {
                $paths[] = $path;
            }
        }

        return $paths;
    }

    /** @param array<string, mixed> $manifest */
    private function isVendorPayloadPath(array $manifest, string $path): bool
    {
        $vendor = $manifest[HostSystemUpdateManifestContract::VENDOR] ?? null;

        if (! is_array($vendor)) {
            return false;
        }

        $normalized = str_replace('\\', '/', trim($path));
        $strategy = strtolower(trim((string) ($vendor['strategy'] ?? '')));

        if ($strategy === HostSystemUpdateVendorStrategyResolver::STRATEGY_INCLUDED_VENDOR_BUNDLE) {
            $bundlePath = str_replace('\\', '/', trim((string) ($vendor['bundle_path'] ?? 'vendor.bundle.zip')));

            return $normalized === $bundlePath;
        }

        if ($strategy !== HostSystemUpdateVendorStrategyResolver::STRATEGY_INCLUDED_VENDOR) {
            return false;
        }

        $vendorFiles = [];

        foreach ([$vendor['files'] ?? null, $vendor['vendor_path'] ?? null, $manifest['vendor_files'] ?? null, $manifest['vendor_path'] ?? null] as $items) {
            $items = is_string($items) ? [$items] : $items;

            if (! is_array($items)) {
                continue;
            }

            foreach ($items as $vendorPath) {
                if (is_string($vendorPath) && trim($vendorPath) !== '') {
                    $vendorFiles[] = str_replace('\\', '/', trim($vendorPath));
                }
            }
        }

        return in_array($normalized, $vendorFiles, true);
    }
}
