<?php

declare(strict_types=1);

/**
 * 本文件属于 DBErp 进销存系统。
 *
 * 版权所有 © 2026 北京珑大钜商科技有限公司。保留所有权利。
 *
 * 未经官方授权，不得删除、篡改或隐藏本文件中的版权声明、产品标识与来源信息。
 * 商业授权、官方更新与技术支持服务，请以珑大官方支持中心记录为准。
 *
 * @link https://support.loongdom.cn
 * @see  https://www.dberp.com.cn
 */

namespace App\Services\SupportCenter;

use App\Exceptions\SupportCenter\HostSystemUpdateManifestException;
use App\Exceptions\SupportCenter\HostSystemUpdateStagingException;
use App\Support\SystemUpdate\HostPluginPackagePathExclusion;

/**
 * 更新包 manifest 与 staging 路径安全校验（相对路径、禁止覆盖受保护目录）。
 */
final class HostSystemUpdateProtectedPathGuard
{
    public function __construct(
        private readonly HostPluginPackagePathExclusion $pluginPathExclusion = new HostPluginPackagePathExclusion,
    ) {}

    /**
     * @var list<string>
     */
    private const EXACT_BLOCKED = [
        '.env',
        'installed.lock',
        'storage/app/dberp/installed.lock',
    ];

    /**
     * @var list<string>
     */
    private const PREFIX_BLOCKED = [
        '.env.',
        'storage/',
        'storage\\',
        'public/storage',
        'public\\storage',
        'storage/app/dberp/host-installation-site-identity',
        'bootstrap/cache/',
        'bootstrap\\cache\\',
        'storage/logs/',
        'storage\\logs\\',
        'storage/framework/sessions/',
        'storage\\framework\\sessions\\',
        'storage/framework/cache/',
        'storage\\framework\\cache\\',
        'storage/framework/views/',
        'storage\\framework\\views\\',
        'storage/app/public/',
        'storage\\app\\public\\',
    ];

    public function assertSafeRelativePath(string $path, string $context = 'path'): void
    {
        $normalized = $this->normalizeRelativePath($path);

        if ($normalized === null) {
            throw HostSystemUpdateManifestException::validationFailed(
                (string) __('dberp.system_update.manifest.unsafe_path', [
                    'path' => $path,
                    'context' => $context,
                ]),
                'manifest_unsafe_path',
            );
        }

        if ($this->isBlocked($normalized)) {
            throw HostSystemUpdateManifestException::validationFailed(
                (string) __('dberp.system_update.manifest.protected_path', [
                    'path' => $normalized,
                    'context' => $context,
                ]),
                'manifest_protected_path',
            );
        }
    }

    /**
     * manifest.protected_paths 条目：声明更新时不得覆盖的路径（可含 .env 等 blocked 路径）。
     */
    public function assertProtectedPathDeclaration(string $path, string $context = 'protected_paths'): void
    {
        if ($this->normalizeRelativePath($path) === null) {
            throw HostSystemUpdateManifestException::validationFailed(
                (string) __('dberp.system_update.manifest.unsafe_path', [
                    'path' => $path,
                    'context' => $context,
                ]),
                'manifest_unsafe_path',
            );
        }
    }

    public function assertSafeUpdateTargetPath(string $path, string $context = 'path'): void
    {
        $normalized = $this->normalizeRelativePath($path);

        if ($normalized === null) {
            throw HostSystemUpdateStagingException::protectedUpdateTarget($path, $context);
        }

        if ($this->isBlocked($normalized)) {
            throw HostSystemUpdateStagingException::protectedUpdateTarget($normalized, $context);
        }

        $this->assertWithinProjectRoot($this->projectAbsolutePath($normalized), $context);
    }

    public function assertSafeArchiveEntryName(string $entryName): string
    {
        if (str_contains($entryName, "\0")) {
            throw HostSystemUpdateStagingException::unsafeArchiveEntry($entryName);
        }

        $normalized = $this->normalizeRelativePath(str_replace('\\', '/', $entryName));

        if ($normalized === null) {
            throw HostSystemUpdateStagingException::unsafeArchiveEntry($entryName);
        }

        if ($this->isBlocked($normalized)) {
            throw HostSystemUpdateStagingException::protectedUpdateTarget($normalized, 'archive');
        }

        return $normalized;
    }

    public function assertPathWithinDirectory(string $absolutePath, string $directory): void
    {
        $resolvedTarget = $this->resolveRealPath($absolutePath);
        $resolvedDirectory = $this->resolveRealPath($directory);

        if ($resolvedTarget === null || $resolvedDirectory === null) {
            throw HostSystemUpdateStagingException::unsafeArchiveEntry(basename($absolutePath));
        }

        $directoryPrefix = rtrim($resolvedDirectory, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;

        if ($resolvedTarget !== $resolvedDirectory
            && ! str_starts_with($resolvedTarget, $directoryPrefix)) {
            throw HostSystemUpdateStagingException::unsafeArchiveEntry(basename($absolutePath));
        }
    }

    public function normalizeRelativePath(string $path): ?string
    {
        $trimmed = trim(str_replace('\\', '/', $path));

        if ($trimmed === '') {
            return null;
        }

        if (str_starts_with($trimmed, '/')) {
            return null;
        }

        if (preg_match('/^[A-Za-z]:[\/\\\\]/', $path) === 1) {
            return null;
        }

        if (str_contains($trimmed, '../') || str_contains($trimmed, '/..')
            || str_starts_with($trimmed, '..') || str_contains($trimmed, '..\\')) {
            return null;
        }

        $segments = explode('/', $trimmed);
        $safe = [];

        foreach ($segments as $segment) {
            if ($segment === '' || $segment === '.') {
                continue;
            }

            if ($segment === '..') {
                return null;
            }

            $safe[] = $segment;
        }

        return $safe === [] ? null : implode('/', $safe);
    }

    public function projectAbsolutePath(string $relativePath): string
    {
        $normalized = $this->normalizeRelativePath($relativePath);

        if ($normalized === null) {
            throw HostSystemUpdateStagingException::protectedUpdateTarget($relativePath, 'path');
        }

        return base_path($normalized);
    }

    public function isBlockedPath(string $path): bool
    {
        $normalized = $this->normalizeRelativePath($path);

        return $normalized === null || $this->isBlocked($normalized);
    }

    private function assertWithinProjectRoot(string $absolutePath, string $context): void
    {
        $projectRoot = $this->resolveRealPath(base_path());
        $resolved = $this->resolveRealPath($absolutePath);

        if ($projectRoot === null || $resolved === null) {
            return;
        }

        $prefix = rtrim($projectRoot, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;

        if ($resolved !== $projectRoot && ! str_starts_with($resolved, $prefix)) {
            throw HostSystemUpdateStagingException::protectedUpdateTarget(basename($absolutePath), $context);
        }
    }

    private function isBlocked(string $normalized): bool
    {
        $lower = strtolower($normalized);

        foreach (self::EXACT_BLOCKED as $blocked) {
            if ($lower === strtolower($blocked)) {
                return true;
            }
        }

        foreach (self::PREFIX_BLOCKED as $prefix) {
            $normalizedPrefix = strtolower(str_replace('\\', '/', $prefix));

            if ($lower === rtrim($normalizedPrefix, '/')
                || str_starts_with($lower, $normalizedPrefix)) {
                return true;
            }
        }

        if (str_contains($lower, 'support_center_host_token')
            || str_contains($lower, 'support_center_host_key')) {
            return true;
        }

        if ($this->pluginPathExclusion->isExcludedPluginRelativePath($normalized)) {
            return true;
        }

        return false;
    }

    private function resolveRealPath(string $path): ?string
    {
        $realpath = realpath($path);

        if ($realpath !== false) {
            return $realpath;
        }

        $parent = dirname($path);

        if ($parent === $path || ! is_dir($parent)) {
            return null;
        }

        $parentReal = realpath($parent);

        if ($parentReal === false) {
            return null;
        }

        return $parentReal.DIRECTORY_SEPARATOR.basename($path);
    }
}
