<?php

declare(strict_types=1);

namespace App\Services\SupportCenter;

use App\Exceptions\SupportCenter\SupportCenterException;
use App\Models\HostSystemUpdateCheck;
use App\Models\HostSystemUpdatePackage;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Throwable;

/**
 * 下载官方系统更新包至 storage/app/dberp/updates/packages，并执行整包校验。
 *
 * NOTE: 本步骤不解析 manifest.json，不解压 ZIP，不替换源码。
 */
final class HostSystemUpdatePackageDownloadService
{
    public function __construct(
        private readonly HostSystemUpdateDownloadTokenService $downloadTokenService,
        private readonly HostSystemUpdatePackageVerifier $packageVerifier,
        private readonly HostSystemUpdatePackageSignatureVerifier $signatureVerifier,
        private readonly SupportCenterConnectivityGuard $connectivityGuard,
        private readonly SupportCenterAuditLogger $auditLogger,
    ) {}

    public function downloadAndVerify(?HostSystemUpdateCheck $checkSnapshot = null): HostSystemUpdatePackage
    {
        $tokenResult = $this->downloadTokenService->requestToken($checkSnapshot);
        $package = $tokenResult->package;

        $this->auditLogger->logSystemUpdatePackageDownloadStarted(
            package: $package,
            requestId: $tokenResult->requestId,
        );

        $package->fill([
            'download_status' => HostSystemUpdatePackage::STATUS_DOWNLOADING,
            'download_message' => null,
            'download_error_code' => null,
        ]);
        $package->save();

        try {
            $this->connectivityGuard->assertRealCallsAllowed('host_system_update_package_download');

            $localPath = $this->downloadPackageFile(
                downloadUrl: $tokenResult->downloadUrl,
                downloadToken: $tokenResult->downloadToken,
                package: $package,
            );

            $package->fill([
                'local_path' => $localPath,
                'local_filename' => basename($localPath),
                'local_size' => is_file($localPath) ? (int) filesize($localPath) : null,
                'downloaded_at' => now()->utc(),
                'download_status' => HostSystemUpdatePackage::STATUS_DOWNLOADED,
                'download_message' => null,
                'download_error_code' => null,
                'download_request_id' => $tokenResult->requestId,
            ]);
            $package->save();

            $this->auditLogger->logSystemUpdatePackageDownloadCompleted($package->fresh() ?? $package);

            $verifiedPackage = $this->packageVerifier->verify($package->fresh() ?? $package);

            if (! $verifiedPackage->verified) {
                $this->auditLogger->logSystemUpdatePackageVerificationFailed($verifiedPackage);

                return $verifiedPackage;
            }

            $this->auditLogger->logSystemUpdatePackageVerificationCompleted($verifiedPackage);

            $signedPackage = $this->signatureVerifier->verifyIfConfigured($verifiedPackage->fresh() ?? $verifiedPackage);

            if (! $signedPackage->verified) {
                $this->auditLogger->logSystemUpdatePackageVerificationFailed($signedPackage);

                return $signedPackage;
            }

            return $signedPackage;
        } catch (SupportCenterException $exception) {
            $this->markDownloadFailed($package, $exception->userFacingMessage(), $exception->errorCode(), $tokenResult->requestId);
            $this->auditLogger->logSystemUpdatePackageDownloadFailed(
                packageId: (string) $package->package_id,
                version: $package->version,
                releaseChannel: $package->release_channel,
                exception: $exception,
                package: $package->fresh(),
            );

            throw $exception;
        } catch (Throwable $exception) {
            $message = (string) __('dberp.system_update.package.download_failed');
            $this->markDownloadFailed($package, $message, 'package_download_failed', $tokenResult->requestId);
            $this->auditLogger->logSystemUpdatePackageDownloadFailed(
                packageId: (string) $package->package_id,
                version: $package->version,
                releaseChannel: $package->release_channel,
                exception: SupportCenterException::remoteServiceUnavailable(
                    operation: 'host_system_update_package_download',
                    previous: $exception,
                ),
                package: $package->fresh(),
            );

            throw SupportCenterException::remoteServiceUnavailable(
                operation: 'host_system_update_package_download',
                previous: $exception,
            );
        }
    }

    private function downloadPackageFile(
        ?string $downloadUrl,
        string $downloadToken,
        HostSystemUpdatePackage $package,
    ): string {
        HostSystemUpdatePackageStorage::ensurePackagesDirectoryExists();

        $filename = HostSystemUpdatePackageStorage::buildSafeFilename(
            packageId: (string) $package->package_id,
            version: $package->version,
            originalFilename: $package->package_name,
        );
        $localPath = HostSystemUpdatePackageStorage::packagesDirectory().DIRECTORY_SEPARATOR.$filename;

        $url = $this->resolveDownloadUrl($downloadUrl);
        $timeout = max(30, (int) config('dberp.support_center.timeout', 10));
        $connectTimeout = max(5, (int) config('dberp.support_center.connect_timeout', 5));

        try {
            $response = Http::timeout($timeout)
                ->connectTimeout($connectTimeout)
                ->withOptions(['sink' => $localPath])
                ->accept('*/*')
                ->get($url, [
                    'download_token' => $downloadToken,
                ]);
        } catch (ConnectionException $exception) {
            $this->removePartialFile($localPath);
            Log::warning('support_center.system_update.package_download.connection_failed', [
                'package_id' => $package->package_id,
                'url' => $this->sanitizeUrlForLog($url),
            ]);

            throw SupportCenterException::networkTimeout('host_system_update_package_download', $exception);
        }

        if (! $response->successful()) {
            $this->removePartialFile($localPath);

            throw SupportCenterException::remoteServiceUnavailable(
                httpStatus: $response->status(),
                operation: 'host_system_update_package_download',
            );
        }

        if (! is_file($localPath) || filesize($localPath) === 0) {
            $this->removePartialFile($localPath);

            throw SupportCenterException::invalidResponseFormat(operation: 'host_system_update_package_download');
        }

        return $localPath;
    }

    private function resolveDownloadUrl(?string $downloadUrl): string
    {
        if (is_string($downloadUrl) && trim($downloadUrl) !== '') {
            return trim($downloadUrl);
        }

        $baseUrl = rtrim(trim((string) config('dberp.support_center.base_url')), '/');

        return $baseUrl.SupportCenterOpenHostEndpoints::SYSTEM_UPDATE_DOWNLOAD;
    }

    private function markDownloadFailed(
        HostSystemUpdatePackage $package,
        string $message,
        ?string $errorCode,
        ?string $requestId,
    ): void {
        $package->fill([
            'download_status' => HostSystemUpdatePackage::STATUS_FAILED,
            'download_message' => $message,
            'download_error_code' => $errorCode,
            'download_request_id' => $requestId,
            'verified' => false,
            'verified_at' => null,
        ]);
        $package->save();
    }

    private function removePartialFile(string $path): void
    {
        if (is_file($path)) {
            @unlink($path);
        }
    }

    private function sanitizeUrlForLog(string $url): string
    {
        $parts = parse_url($url);

        if (! is_array($parts)) {
            return 'invalid_url';
        }

        $path = $parts['path'] ?? '/';

        return ($parts['scheme'] ?? 'https').'://'.($parts['host'] ?? 'unknown').$path;
    }
}
