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

/**
 * 将 staging 中的 vendor.bundle.zip 解压并原子替换项目 vendor 目录。
 */
final class HostSystemUpdateVendorBundleReplacer
{
    public function __construct(
        private readonly HostSystemUpdateExecutionContextResolver $contextResolver,
        private readonly HostSystemUpdateProtectedPathGuard $protectedPathGuard,
        private readonly HostSystemUpdateFileChecksumVerifier $checksumVerifier,
        private readonly HostSystemUpdateExecutionLogger $executionLogger,
        private readonly HostSystemUpdateVendorSnapshotService $vendorSnapshotService,
    ) {}

    /**
     * @param  array<string, mixed>  $manifest
     * @return array{strategy: string, replaced_count: int, skipped_count: int, deferred: bool, bundle_path: string}
     */
    public function applyBundle(HostSystemUpdateExecution $execution, array $manifest): array
    {
        $bundlePath = $this->resolveBundlePath($manifest);
        $stagingDirectory = $this->contextResolver->stagingDirectory($execution);
        $stagedBundlePath = $this->contextResolver->stagedFilePath($stagingDirectory, $bundlePath);

        if (! is_file($stagedBundlePath)) {
            throw HostSystemUpdateFileExecutionException::stagedSourceMissing($bundlePath);
        }

        $this->protectedPathGuard->assertPathWithinDirectory($stagedBundlePath, $stagingDirectory);
        $this->checksumVerifier->verifyStagedFile($bundlePath, $stagedBundlePath, $manifest);

        $vendorDirectory = $this->protectedPathGuard->projectAbsolutePath('vendor');
        $extractDirectory = dirname($vendorDirectory).DIRECTORY_SEPARATOR.'vendor.__dberp_extract_'.$execution->execution_no;

        $this->removeDirectoryIfExists($extractDirectory);

        HostSystemUpdateWorkspaceStorage::ensureDirectoryExists($extractDirectory);

        try {
            $this->extractBundle($stagedBundlePath, $extractDirectory);

            $this->vendorSnapshotService->switchToExtractedVendor($execution, $extractDirectory);
            $this->removeProjectBundleArtifact($bundlePath);

            $this->executionLogger->logTransition(
                $execution,
                HostSystemUpdateExecutionStep::ReplaceVendor->value,
                (string) __('dberp.system_update.file_execution.log.vendor_bundle_applied', ['path' => $bundlePath]),
                ['bundle_path' => $bundlePath, 'status' => 'replaced'],
            );

            return [
                'strategy' => HostSystemUpdateVendorStrategyResolver::STRATEGY_INCLUDED_VENDOR_BUNDLE,
                'replaced_count' => 1,
                'skipped_count' => 0,
                'deferred' => false,
                'bundle_path' => $bundlePath,
            ];
        } catch (\Throwable $exception) {
            $this->removeDirectoryIfExists($extractDirectory);

            if ($exception instanceof HostSystemUpdateFileExecutionException) {
                throw $exception;
            }

            throw HostSystemUpdateFileExecutionException::vendorCopyFailed('vendor', true);
        }
    }

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

        if (is_array($vendor) && is_string($vendor['bundle_path'] ?? null) && trim($vendor['bundle_path']) !== '') {
            return str_replace('\\', '/', trim($vendor['bundle_path']));
        }

        return HostSystemUpdateVendorBundlePaths::BUNDLE_RELATIVE_PATH;
    }

    private function extractBundle(string $bundlePath, string $targetDirectory): void
    {
        $zip = new ZipArchive;
        $opened = $zip->open($bundlePath);

        if ($opened !== true) {
            throw HostSystemUpdateFileExecutionException::vendorCopyFailed(HostSystemUpdateVendorBundlePaths::BUNDLE_RELATIVE_PATH);
        }

        try {
            for ($index = 0; $index < $zip->numFiles; $index++) {
                $entryName = $zip->getNameIndex($index);

                if (! is_string($entryName) || $entryName === '') {
                    continue;
                }

                $normalizedEntry = str_replace('\\', '/', $entryName);

                if (str_contains($normalizedEntry, '../') || str_starts_with($normalizedEntry, '/')) {
                    throw HostSystemUpdateFileExecutionException::vendorCopyFailed($normalizedEntry);
                }

                $destination = $targetDirectory.DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $normalizedEntry);

                if (str_ends_with($normalizedEntry, '/')) {
                    HostSystemUpdateWorkspaceStorage::ensureDirectoryExists(rtrim($destination, DIRECTORY_SEPARATOR));

                    continue;
                }

                HostSystemUpdateWorkspaceStorage::ensureDirectoryExists(dirname($destination));

                $contents = $zip->getFromIndex($index);

                if ($contents === false) {
                    throw HostSystemUpdateFileExecutionException::vendorCopyFailed($normalizedEntry);
                }

                if (file_put_contents($destination, $contents) === false) {
                    throw HostSystemUpdateFileExecutionException::vendorCopyFailed($normalizedEntry);
                }
            }
        } finally {
            $zip->close();
        }

        if (! is_file($targetDirectory.DIRECTORY_SEPARATOR.'autoload.php')) {
            throw HostSystemUpdateFileExecutionException::vendorCopyFailed('vendor/autoload.php');
        }
    }

    private function removeProjectBundleArtifact(string $bundlePath): void
    {
        $this->protectedPathGuard->assertSafeUpdateTargetPath($bundlePath, 'vendor_bundle_artifact');
        $projectBundlePath = $this->protectedPathGuard->projectAbsolutePath($bundlePath);

        if (! is_file($projectBundlePath)) {
            return;
        }

        if (! @unlink($projectBundlePath)) {
            throw HostSystemUpdateFileExecutionException::vendorCopyFailed($bundlePath, true);
        }
    }

    private function removeDirectoryIfExists(string $directory): void
    {
        if (! is_dir($directory)) {
            return;
        }

        $items = scandir($directory);

        if ($items === false) {
            return;
        }

        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }

            $path = $directory.DIRECTORY_SEPARATOR.$item;

            if (is_dir($path)) {
                $this->removeDirectoryIfExists($path);
            } elseif (is_file($path)) {
                unlink($path);
            }
        }

        @rmdir($directory);
    }
}
