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

/**
 * 解压更新包到 staging 并校验路径安全（不写入项目根目录）。
 */
final class HostSystemUpdateStagingService
{
    public function __construct(
        private readonly HostSystemUpdateArchiveExtractor $archiveExtractor,
        private readonly HostSystemUpdateProtectedPathGuard $protectedPathGuard,
        private readonly HostSystemUpdateExecutionLogger $executionLogger,
        private readonly SupportCenterAuditLogger $auditLogger,
    ) {}

    /**
     * @return list<string>
     */
    public function extractAndVerify(HostSystemUpdateExecution $execution): array
    {
        $startedAt = now()->utc();
        $package = $this->resolveReadyPackage($execution);
        $stagingDirectory = HostSystemUpdateWorkspaceStorage::stagingDirectory($execution->execution_no);

        $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)],
        );

        try {
            HostSystemUpdateWorkspaceStorage::ensureBaseStructureExists();
            HostSystemUpdateWorkspaceStorage::recreateCleanDirectory($stagingDirectory);

            $archivePath = (string) $package->local_path;
            $extracted = $this->archiveExtractor->extractToStaging($archivePath, $stagingDirectory);

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

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

            $manifest = $this->manifestArray($package);
            $this->validateManifestUpdateTargets($manifest);
            $this->verifyStagedManifestFiles($manifest, $stagingDirectory);

            $this->executionLogger->logStepCompleted(
                $execution,
                HostSystemUpdateExecutionStep::VerifyStaging->value,
                (string) __('dberp.system_update.staging.log.verify_completed'),
                ['verified_file_count' => count($manifest[HostSystemUpdateManifestContract::FILES] ?? [])],
                $verifyStartedAt,
            );

            $this->auditLogger->logSystemUpdateStagingExtractionCompleted(
                $execution,
                $package,
                count($extracted),
            );

            return $extracted;
        } catch (HostSystemUpdateStagingException $exception) {
            $this->executionLogger->logStepFailed(
                $execution,
                HostSystemUpdateExecutionStep::ExtractStaging->value,
                $exception->userFacingMessage(),
                $exception->errorCode(),
                $exception->userFacingMessage(),
                null,
                $startedAt,
            );
            $this->auditLogger->logSystemUpdateStagingExtractionFailed($execution, $package, $exception);

            throw $exception;
        }
    }

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

        if (! $package instanceof HostSystemUpdatePackage) {
            throw HostSystemUpdateStagingException::packageNotReady();
        }

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

        if (! is_string($package->local_path) || ! is_file($package->local_path)) {
            throw HostSystemUpdateStagingException::packageFileMissing();
        }

        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
     */
    private function verifyStagedManifestFiles(array $manifest, string $stagingDirectory): void
    {
        $files = $manifest[HostSystemUpdateManifestContract::FILES] ?? [];

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

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

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