<?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;

/**
 * 按 manifest vendor 策略处理 vendor 目录更新（不执行 composer 命令）。
 */
final class HostSystemUpdateVendorReplacer
{
    public function __construct(
        private readonly HostSystemUpdateExecutionContextResolver $contextResolver,
        private readonly HostSystemUpdateVendorStrategyResolver $vendorStrategyResolver,
        private readonly HostSystemUpdateProtectedPathGuard $protectedPathGuard,
        private readonly HostSystemUpdateFileChecksumVerifier $checksumVerifier,
        private readonly HostSystemUpdateExecutionLogger $executionLogger,
        private readonly SupportCenterAuditLogger $auditLogger,
    ) {}

    /**
     * @return array{strategy: string, replaced_count: int, skipped_count: int, deferred: bool}
     */
    public function replaceVendor(HostSystemUpdateExecution $execution): array
    {
        $startedAt = now()->utc();
        $package = $this->contextResolver->resolveReadyPackage($execution);
        $manifest = $this->contextResolver->manifest($execution);
        $strategy = $this->vendorStrategyResolver->resolveStrategy($manifest);

        $this->auditLogger->logSystemUpdateVendorReplacementStarted($execution, $package, $strategy);
        $this->executionLogger->logStepStarted(
            $execution,
            HostSystemUpdateExecutionStep::ReplaceVendor->value,
            (string) __('dberp.system_update.file_execution.log.vendor_started', ['strategy' => $strategy]),
            ['vendor_strategy' => $strategy],
        );

        try {
            $summary = match ($strategy) {
                HostSystemUpdateVendorStrategyResolver::STRATEGY_INCLUDED_VENDOR => $this->applyIncludedVendor(
                    $execution,
                    $manifest,
                ),
                HostSystemUpdateVendorStrategyResolver::STRATEGY_COMPOSER_REQUIRED => $this->deferComposerRequired(
                    $execution,
                ),
                default => $this->skipVendorUpdate($execution),
            };

            $this->executionLogger->logStepCompleted(
                $execution,
                HostSystemUpdateExecutionStep::ReplaceVendor->value,
                (string) __('dberp.system_update.file_execution.log.vendor_completed', [
                    'strategy' => $strategy,
                ]),
                $summary,
                $startedAt,
            );

            if (($summary['deferred'] ?? false) === true) {
                $this->auditLogger->logSystemUpdateVendorReplacementDeferred($execution, $package, $strategy, $summary);
            } else {
                $this->auditLogger->logSystemUpdateVendorReplacementCompleted($execution, $package, $strategy, $summary);
            }

            return $summary;
        } catch (HostSystemUpdateFileExecutionException $exception) {
            $this->executionLogger->logStepFailed(
                $execution,
                HostSystemUpdateExecutionStep::ReplaceVendor->value,
                $exception->userFacingMessage(),
                $exception->errorCode(),
                $exception->userFacingMessage(),
                ['vendor_strategy' => $strategy],
                $startedAt,
            );
            $this->auditLogger->logSystemUpdateVendorReplacementFailed($execution, $package, $strategy, $exception);

            throw $exception;
        }
    }

    /**
     * @param  array<string, mixed>  $manifest
     * @return array{strategy: string, replaced_count: int, skipped_count: int, deferred: bool, file_results: list<array<string, string>>}
     */
    private function applyIncludedVendor(HostSystemUpdateExecution $execution, array $manifest): array
    {
        $stagingDirectory = $this->contextResolver->stagingDirectory($execution);
        $vendorFiles = $this->vendorStrategyResolver->resolveVendorFiles($manifest);

        if ($vendorFiles === []) {
            return [
                'strategy' => HostSystemUpdateVendorStrategyResolver::STRATEGY_INCLUDED_VENDOR,
                'replaced_count' => 0,
                'skipped_count' => 0,
                'deferred' => false,
                'file_results' => [],
            ];
        }

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

        foreach ($vendorFiles as $relativePath) {
            $fileResults[] = $this->replaceVendorManifestFile(
                $execution,
                $manifest,
                $stagingDirectory,
                $relativePath,
                $replacedCount > 0,
            );
            $replacedCount++;
        }

        return [
            'strategy' => HostSystemUpdateVendorStrategyResolver::STRATEGY_INCLUDED_VENDOR,
            'replaced_count' => $replacedCount,
            'skipped_count' => $skippedCount,
            'deferred' => false,
            'file_results' => $fileResults,
        ];
    }

    /**
     * @param  array<string, mixed>  $manifest
     * @return array{path: string, status: string}
     */
    public function replaceVendorManifestFile(
        HostSystemUpdateExecution $execution,
        array $manifest,
        string $stagingDirectory,
        string $relativePath,
        bool $partialChangesApplied,
    ): array {
        if (! str_starts_with(strtolower($relativePath), 'vendor/')) {
            throw HostSystemUpdateFileExecutionException::vendorFileInvalid($relativePath);
        }

        $this->protectedPathGuard->assertSafeUpdateTargetPath($relativePath, 'vendor');

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

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

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

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

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

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

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

    /**
     * @return array{strategy: string, replaced_count: int, skipped_count: int, deferred: bool}
     */
    private function deferComposerRequired(HostSystemUpdateExecution $execution): array
    {
        $message = (string) __('dberp.system_update.file_execution.log.vendor_composer_deferred');

        $this->executionLogger->logTransition(
            $execution,
            HostSystemUpdateExecutionStep::ReplaceVendor->value,
            $message,
            ['vendor_strategy' => HostSystemUpdateVendorStrategyResolver::STRATEGY_COMPOSER_REQUIRED, 'deferred' => true],
        );

        return [
            'strategy' => HostSystemUpdateVendorStrategyResolver::STRATEGY_COMPOSER_REQUIRED,
            'replaced_count' => 0,
            'skipped_count' => 0,
            'deferred' => true,
        ];
    }

    /**
     * @return array{strategy: string, replaced_count: int, skipped_count: int, deferred: bool}
     */
    private function skipVendorUpdate(HostSystemUpdateExecution $execution): array
    {
        $this->executionLogger->logTransition(
            $execution,
            HostSystemUpdateExecutionStep::ReplaceVendor->value,
            (string) __('dberp.system_update.file_execution.log.vendor_skipped'),
            ['vendor_strategy' => HostSystemUpdateVendorStrategyResolver::STRATEGY_NONE],
        );

        return [
            'strategy' => HostSystemUpdateVendorStrategyResolver::STRATEGY_NONE,
            'replaced_count' => 0,
            'skipped_count' => 0,
            'deferred' => false,
        ];
    }

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

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

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

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

        @chmod($temporaryPath, $mode);

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

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