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

/**
 * 将 staging 中已校验文件复制到项目根目录（不直接从 ZIP 写入）。
 */
final class HostSystemUpdateFileReplacer
{
    public function __construct(
        private readonly HostSystemUpdateExecutionContextResolver $contextResolver,
        private readonly HostSystemUpdateProtectedPathGuard $protectedPathGuard,
        private readonly HostSystemUpdateFileChecksumVerifier $checksumVerifier,
        private readonly HostSystemUpdateExecutionLogger $executionLogger,
        private readonly SupportCenterAuditLogger $auditLogger,
    ) {}

    /**
     * @return array{replaced_count: int, skipped_count: int, file_results: list<array<string, string>>}
     */
    public function replaceFiles(HostSystemUpdateExecution $execution): array
    {
        $startedAt = now()->utc();
        $package = $this->contextResolver->resolveReadyPackage($execution);
        $manifest = $this->contextResolver->manifest($execution);
        $stagingDirectory = $this->contextResolver->stagingDirectory($execution);

        $this->contextResolver->resolveCompletedFileSnapshotBackup($execution);

        $files = $manifest[HostSystemUpdateManifestContract::FILES] ?? [];

        if (! is_array($files)) {
            throw HostSystemUpdateFileExecutionException::prerequisitesNotMet('manifest_files_invalid');
        }

        $files = HostSystemUpdateReplaceFileOrderSorter::sortForReplace(
            array_values(array_filter($files, static fn (mixed $path): bool => is_string($path) && $path !== '')),
        );

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

        $replacedCount = 0;
        $skippedCount = 0;
        /** @var list<array<string, string>> $fileResults */
        $fileResults = [];

        try {
            foreach ($files as $path) {
                if (! is_string($path)) {
                    continue;
                }

                $result = $this->replaceManifestFile(
                    $execution,
                    $manifest,
                    $stagingDirectory,
                    $path,
                    $replacedCount > 0,
                );

                $fileResults[] = $result;

                if ($result['status'] === 'replaced') {
                    $replacedCount++;
                } else {
                    $skippedCount++;
                }
            }

            $summary = [
                'replaced_count' => $replacedCount,
                'skipped_count' => $skippedCount,
                'file_results' => $fileResults,
            ];

            $this->executionLogger->logStepCompleted(
                $execution,
                HostSystemUpdateExecutionStep::ReplaceFiles->value,
                (string) __('dberp.system_update.file_execution.log.replace_completed', [
                    'count' => $replacedCount,
                ]),
                $this->phaseSummaryContext($summary),
                $startedAt,
            );

            $this->auditLogger->logSystemUpdateFileReplacementCompleted($execution, $package, $summary);

            return $summary;
        } catch (HostSystemUpdateFileExecutionException $exception) {
            $this->executionLogger->logStepFailed(
                $execution,
                HostSystemUpdateExecutionStep::ReplaceFiles->value,
                $exception->userFacingMessage(),
                $exception->errorCode(),
                $exception->userFacingMessage(),
                [
                    'replaced_count' => $replacedCount,
                    'file_results' => $fileResults,
                ],
                $startedAt,
            );
            $this->auditLogger->logSystemUpdateFileReplacementFailed($execution, $package, $exception);

            if ($replacedCount > 0 && ! $exception->partialChangesApplied()) {
                throw new HostSystemUpdateFileExecutionException(
                    $exception->userFacingMessage(),
                    $exception->errorCode(),
                    true,
                    $exception,
                );
            }

            throw $exception;
        }
    }

    /**
     * @param  array<string, mixed>  $manifest
     * @return array{path: string, status: string}
     */
    public function replaceManifestFile(
        HostSystemUpdateExecution $execution,
        array $manifest,
        string $stagingDirectory,
        string $relativePath,
        bool $partialChangesApplied,
    ): array {
        $this->protectedPathGuard->assertSafeUpdateTargetPath($relativePath, 'files');

        $normalized = $this->protectedPathGuard->normalizeRelativePath($relativePath);

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

        // vendor 载荷只供 ReplaceVendor 阶段消费，不是项目根目录中的发布文件。
        if ($this->isVendorPayloadPath($manifest, $normalized)) {
            return ['path' => $normalized, 'status' => 'skipped_vendor_payload'];
        }

        $stagedPath = $this->contextResolver->stagedFilePath($stagingDirectory, $normalized);

        if (! is_file($stagedPath)) {
            throw HostSystemUpdateFileExecutionException::stagedSourceMissing($normalized);
        }

        $this->protectedPathGuard->assertPathWithinDirectory($stagedPath, $stagingDirectory);
        $this->checksumVerifier->verifyStagedFile($normalized, $stagedPath, $manifest, $partialChangesApplied);

        $targetPath = $this->protectedPathGuard->projectAbsolutePath($normalized);

        try {
            $this->copyFileAtomically($stagedPath, $targetPath);
        } catch (HostSystemUpdateFileExecutionException $exception) {
            throw HostSystemUpdateFileExecutionException::copyFailed(
                $normalized,
                $partialChangesApplied || $exception->partialChangesApplied(),
            );
        }

        return ['path' => $normalized, 'status' => 'replaced'];
    }

    /**
     * @param  array<string, mixed>  $summary
     * @return array<string, mixed>
     */
    private function phaseSummaryContext(array $summary): array
    {
        unset($summary['file_results']);

        return $summary;
    }

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

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

        $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 $normalizedPath === $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 $path) {
                if (is_string($path) && trim($path) !== '') {
                    $vendorFiles[] = str_replace('\\', '/', trim($path));
                }
            }
        }

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

    private function copyFileAtomically(string $sourcePath, string $targetPath): void
    {
        $directory = dirname($targetPath);

        if (! is_dir($directory) && ! mkdir($directory, 0755, true) && ! is_dir($directory)) {
            throw HostSystemUpdateFileExecutionException::copyFailed(basename($targetPath));
        }

        $mode = is_file($targetPath) ? (fileperms($targetPath) & 0777) : 0644;
        $temporaryPath = $targetPath.'.dberp-'.bin2hex(random_bytes(8)).'.tmp';

        if (! copy($sourcePath, $temporaryPath)) {
            throw HostSystemUpdateFileExecutionException::copyFailed(basename($targetPath));
        }

        @chmod($temporaryPath, $mode);

        if (! rename($temporaryPath, $targetPath)) {
            @unlink($temporaryPath);

            throw HostSystemUpdateFileExecutionException::copyFailed(basename($targetPath));
        }
    }
}
