<?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\HostSystemUpdateExecutionStatus;
use App\Enums\SupportCenter\HostSystemUpdateExecutionStep;
use App\Enums\SupportCenter\HostSystemUpdateStepPhase;
use App\Exceptions\SupportCenter\HostSystemUpdateStagingException;
use App\Models\HostSystemUpdateExecution;
use App\Models\HostSystemUpdatePackage;

/**
 * 分批解压更新包至 staging 并校验 manifest 文件。
 */
final class HostSystemUpdateExtractBatchRunner
{
    private const LIST_KEY_ENTRIES = 'extract_entries';

    private const LIST_KEY_VERIFY = 'verify_manifest_files';

    private const LIST_KEY_ZIP_ENTRY_INDEX = 'zip_entry_index';

    public function __construct(
        private readonly HostSystemUpdateExecutionStateMachine $stateMachine,
        private readonly HostSystemUpdateArchiveExtractor $archiveExtractor,
        private readonly HostSystemUpdateProtectedPathGuard $protectedPathGuard,
        private readonly HostSystemUpdateExecutionBatchProgressStore $progressStore,
        private readonly HostSystemUpdateExecutionLogger $executionLogger,
        private readonly SupportCenterAuditLogger $auditLogger,
    ) {}

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

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

        $execution = $this->ensureRunningForExtract($execution);
        $package = $this->resolveReadyPackage($execution);
        $stagingDirectory = HostSystemUpdateWorkspaceStorage::stagingDirectory($execution->execution_no);
        $batchSize = $this->extractBatchSize();

        if ($existing === null) {
            HostSystemUpdateWorkspaceStorage::ensureBaseStructureExists();
            HostSystemUpdateWorkspaceStorage::recreateCleanDirectory($stagingDirectory);

            $this->auditLogger->logSystemUpdateStagingExtractionStarted($execution, $package);
            $this->executionLogger->logStepStarted(
                $execution,
                HostSystemUpdateExecutionStep::ExtractStaging->value,
                (string) __('dberp.system_update.staging.log.extract_started'),
                ['staging_relative_path' => HostSystemUpdateWorkspaceStorage::relativeFromStorage($stagingDirectory)],
            );

            $archivePath = (string) $package->local_path;
            $archiveFormat = HostSystemUpdatePackageArchiveFormat::detectFromFile($archivePath);

            if ($archiveFormat === HostSystemUpdatePackageArchiveFormat::ZIP) {
                $zipEntryIndexByPath = $this->archiveExtractor->buildZipEntryIndexMap($archivePath);
                $entries = array_keys($zipEntryIndexByPath);
                sort($entries, SORT_STRING);
                $entries = array_values(array_unique($entries));
                $this->progressStore->writeStableIndexMap(
                    (string) $execution->execution_no,
                    self::LIST_KEY_ZIP_ENTRY_INDEX,
                    $zipEntryIndexByPath,
                );
            } else {
                $entries = $this->archiveExtractor->listExtractableEntries($archivePath);
            }

            $this->progressStore->writeStableList((string) $execution->execution_no, self::LIST_KEY_ENTRIES, $entries);

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

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

        if ($subStep === HostSystemUpdateBatchProgress::SUBSTEP_EXTRACT) {
            return $this->runExtractSubStep($execution, $package, $stagingDirectory, $existing);
        }

        return $this->runVerifySubStep($execution, $package, $stagingDirectory, $existing);
    }

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

    private function runExtractSubStep(
        HostSystemUpdateExecution $execution,
        HostSystemUpdatePackage $package,
        string $stagingDirectory,
        HostSystemUpdateBatchProgress $progress,
    ): HostSystemUpdateBatchPhaseResult {
        $entries = $this->progressStore->readStableList((string) $execution->execution_no, self::LIST_KEY_ENTRIES, required: true);
        $archivePath = (string) $package->local_path;
        $archiveFormat = HostSystemUpdatePackageArchiveFormat::detectFromFile($archivePath);

        $zipEntryIndexByPath = $this->progressStore->readStableIndexMap(
            (string) $execution->execution_no,
            self::LIST_KEY_ZIP_ENTRY_INDEX,
            required: $archiveFormat === HostSystemUpdatePackageArchiveFormat::ZIP,
        );

        $batch = $this->archiveExtractor->extractEntryBatch(
            $archivePath,
            $stagingDirectory,
            $entries,
            $progress->cursor,
            $progress->batchSize,
            $zipEntryIndexByPath !== [] ? $zipEntryIndexByPath : null,
        );

        $processed = min(count($entries), $batch['next_cursor']);
        $current = $batch['extracted'] !== [] ? $batch['extracted'][array_key_last($batch['extracted'])] : $progress->current;

        if ($batch['extracted'] !== []) {
            $this->executionLogger->logTransition(
                $execution,
                HostSystemUpdateExecutionStep::ExtractStaging->value,
                (string) __('dberp.system_update.staging.log.extract_batch_completed', [
                    'count' => count($batch['extracted']),
                    'processed' => (string) $processed,
                    'total' => (string) count($entries),
                ]),
                ['extracted_in_batch' => count($batch['extracted']), 'processed' => $processed],
            );
        }

        if ($processed < count($entries)) {
            $updated = $progress->withCursor($batch['next_cursor'], $processed, $current);
            $this->progressStore->save((string) $execution->execution_no, $updated);

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

        $this->executionLogger->logStepCompleted(
            $execution,
            HostSystemUpdateExecutionStep::ExtractStaging->value,
            (string) __('dberp.system_update.staging.log.extract_completed', ['count' => count($entries)]),
            ['extracted_count' => count($entries)],
            now()->utc(),
        );

        $manifest = $this->manifestArray($package);
        $this->validateManifestUpdateTargets($manifest);
        $verifyFiles = $this->normalizedManifestFilePaths($manifest);
        $this->progressStore->writeStableList((string) $execution->execution_no, self::LIST_KEY_VERIFY, $verifyFiles);

        $this->executionLogger->logStepStarted(
            $execution,
            HostSystemUpdateExecutionStep::VerifyStaging->value,
            (string) __('dberp.system_update.staging.log.verify_started'),
        );

        $verifyProgress = $progress->withSubStep(
            HostSystemUpdateBatchProgress::SUBSTEP_VERIFY,
            count($verifyFiles),
        );
        $this->progressStore->save((string) $execution->execution_no, $verifyProgress);

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

    private function runVerifySubStep(
        HostSystemUpdateExecution $execution,
        HostSystemUpdatePackage $package,
        string $stagingDirectory,
        HostSystemUpdateBatchProgress $progress,
    ): HostSystemUpdateBatchPhaseResult {
        $verifyFiles = $this->progressStore->readStableList((string) $execution->execution_no, self::LIST_KEY_VERIFY, required: true);
        $slice = array_slice($verifyFiles, $progress->cursor, $progress->batchSize);
        $manifest = $this->manifestArray($package);

        foreach ($slice as $path) {
            $normalized = $this->protectedPathGuard->normalizeRelativePath($path);

            if ($normalized === null) {
                throw HostSystemUpdateStagingException::stagingVerificationFailed($path);
            }

            $stagedPath = $stagingDirectory.DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $normalized);

            if (! is_file($stagedPath)) {
                throw HostSystemUpdateStagingException::stagingVerificationFailed($normalized);
            }
        }

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

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

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

        $this->executionLogger->logStepCompleted(
            $execution,
            HostSystemUpdateExecutionStep::VerifyStaging->value,
            (string) __('dberp.system_update.staging.log.verify_completed'),
            ['verified_file_count' => count($verifyFiles)],
            now()->utc(),
        );

        $this->auditLogger->logSystemUpdateStagingExtractionCompleted(
            $execution,
            $package,
            count($this->progressStore->readStableList(
                (string) $execution->execution_no,
                self::LIST_KEY_ENTRIES,
                required: true,
            )),
        );

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

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

    private function ensureRunningForExtract(HostSystemUpdateExecution $execution): HostSystemUpdateExecution
    {
        $status = (string) ($execution->status ?? '');

        if ($status === HostSystemUpdateExecutionStatus::Running->value) {
            return $execution;
        }

        return $this->stateMachine->transition(
            $execution->fresh() ?? $execution,
            HostSystemUpdateExecutionStatus::Running,
            HostSystemUpdateExecutionStep::ExtractStaging,
            (string) __('dberp.system_update.staging.log.running_started'),
        );
    }

    private function extractBatchSize(): int
    {
        return max(1, (int) config('dberp.system_update.extract_batch_size', 100));
    }

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

        if (! $package instanceof HostSystemUpdatePackage
            || ! $package->verified
            || $package->download_status !== HostSystemUpdatePackage::STATUS_VERIFIED
            || $package->manifest_status !== HostSystemUpdatePackage::MANIFEST_STATUS_VALID
            || ! is_string($package->local_path)
            || ! is_file($package->local_path)) {
            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;
    }

    /**
     * @param  array<string, mixed>  $manifest
     */
    private function validateManifestUpdateTargets(array $manifest): void
    {
        foreach ([
            HostSystemUpdateManifestContract::FILES => 'files',
            HostSystemUpdateManifestContract::DELETE_FILES => 'delete_files',
        ] as $field => $context) {
            $paths = $manifest[$field] ?? [];

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

            foreach ($paths as $path) {
                if (is_string($path)) {
                    $this->protectedPathGuard->assertSafeUpdateTargetPath($path, $context);
                }
            }
        }
    }

    /**
     * @param  array<string, mixed>  $manifest
     * @return list<string>
     */
    private function normalizedManifestFilePaths(array $manifest): array
    {
        $files = $manifest[HostSystemUpdateManifestContract::FILES] ?? [];

        if (! is_array($files)) {
            throw HostSystemUpdateStagingException::stagingVerificationFailed('manifest.files invalid');
        }

        $normalized = [];

        foreach ($files as $path) {
            if (! is_string($path)) {
                throw HostSystemUpdateStagingException::stagingVerificationFailed('manifest.files invalid');
            }

            $relative = $this->protectedPathGuard->normalizeRelativePath($path);

            if ($relative !== null) {
                $normalized[] = $relative;
            }
        }

        sort($normalized, SORT_STRING);

        return array_values(array_unique($normalized));
    }
}
