<?php

declare(strict_types=1);

namespace App\Services\SupportCenter;

use App\Enums\SupportCenter\HostSystemUpdateExecutionStep;
use App\Exceptions\SupportCenter\HostSystemUpdateFileExecutionException;
use App\Exceptions\SupportCenter\HostSystemUpdateStagingException;
use App\Models\HostSystemUpdateExecution;

/**
 * 将 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');
        }

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

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

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

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

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