<?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\HostSystemUpdateExecutionStatus;
use App\Models\HostSystemAuthorization;
use App\Models\HostSystemUpdateExecution;
use App\Models\HostSystemUpdatePackage;
use Illuminate\Support\Facades\Log;
use Throwable;

/**
 * 将系统更新成功/失败结果回传官方 Support Center（失败不阻断本地回滚流程）。
 *
 * 契约对齐：results 使用 status=success；failures 使用 status=failed、failed_step、error_code、message_summary。
 */
final class HostSystemUpdateResultReporter
{
    private const REPORT_STATUS_SUCCESS = 'success';

    private const REPORT_STATUS_FAILED = 'failed';

    private const OUTCOME_SUCCEEDED = 'succeeded';

    private const OUTCOME_ROLLED_BACK = 'rolled_back';

    private const SKIP_REASON_MISSING_SITE_NO = 'missing_site_no';

    private const SKIP_REASON_INVALID_SUCCESS_PAYLOAD = 'invalid_success_payload';

    public function __construct(
        private readonly SupportCenterOpenApiTransport $transport,
        private readonly SupportCenterConnectivityGuard $connectivityGuard,
        private readonly SupportCenterRequestContextBuilder $contextBuilder,
        private readonly HostSystemUpdateResultPayloadSanitizer $payloadSanitizer,
        private readonly SupportCenterAuditLogger $auditLogger,
        private readonly HostSiteOnboardingStatusCacheStore $onboardingStatusCacheStore,
    ) {}

    public function reportSuccess(HostSystemUpdateExecution $execution): void
    {
        if (! $this->shouldReport()) {
            return;
        }

        $execution = $execution->fresh(['package']) ?? $execution;

        if ($this->resolveSiteNo() === null) {
            $this->skipReportDueToMissingSiteNo($execution, 'success');

            return;
        }

        try {
            $payload = $this->payloadSanitizer->sanitize(
                $this->buildSuccessPayload($execution),
            );

            $invalidFields = $this->invalidSuccessPayloadFields($payload);

            if ($invalidFields !== []) {
                $this->skipInvalidSuccessPayload($execution, $invalidFields);

                return;
            }

            $response = $this->transport->post(
                SupportCenterOpenHostEndpoints::SYSTEM_UPDATE_RESULTS,
                $payload,
                'host_system_update_result_report',
            );

            $this->auditLogger->logSystemUpdateResultReported($execution, 'success', $response);
        } catch (Throwable $exception) {
            $this->logReportFailure($execution, 'success', $exception);
        }
    }

    public function reportFailure(
        HostSystemUpdateExecution $execution,
        ?string $errorCode = null,
        ?string $userFacingMessage = null,
        ?string $failedStep = null,
    ): void {
        if (! $this->shouldReport()) {
            return;
        }

        $execution = $execution->fresh(['package']) ?? $execution;

        if ($this->resolveSiteNo() === null) {
            $this->skipReportDueToMissingSiteNo($execution, 'failure');

            return;
        }

        try {
            $payload = $this->payloadSanitizer->sanitize(
                $this->buildFailurePayload(
                    $execution,
                    $errorCode,
                    $userFacingMessage,
                    $failedStep,
                ),
            );

            $response = $this->transport->post(
                SupportCenterOpenHostEndpoints::SYSTEM_UPDATE_FAILURES,
                $payload,
                'host_system_update_failure_report',
            );

            $this->auditLogger->logSystemUpdateResultReported($execution, 'failure', $response);
        } catch (Throwable $exception) {
            $this->logReportFailure($execution, 'failure', $exception);
        }
    }

    private function shouldReport(): bool
    {
        if (! filter_var(config('dberp.system_update.report_results', true), FILTER_VALIDATE_BOOLEAN)) {
            return false;
        }

        return $this->connectivityGuard->canMakeRealCalls();
    }

    /**
     * @return array<string, mixed>
     */
    private function buildSuccessPayload(HostSystemUpdateExecution $execution): array
    {
        $package = $execution->package;
        $executionStatus = (string) ($execution->status ?? HostSystemUpdateExecutionStatus::Completed->value);

        return $this->filterPayload([
            ...$this->sharedIdentityFields($execution),
            'from_version' => $execution->from_version,
            'to_version' => $execution->to_version,
            'from_update_version' => $this->stringifyUpdateVersion($execution->from_update_version),
            'to_update_version' => $this->stringifyUpdateVersion($execution->to_update_version),
            'execution_no' => $execution->execution_no,
            'status' => self::REPORT_STATUS_SUCCESS,
            'outcome' => $executionStatus === HostSystemUpdateExecutionStatus::RolledBack->value
                ? self::OUTCOME_ROLLED_BACK
                : self::OUTCOME_SUCCEEDED,
            'package_type' => HostSystemUpdatePackageTypeGuard::INSTALL_PACKAGE_TYPE_FULL,
            'checksum_verified' => $this->resolveChecksumVerified($package),
            'signature_verified' => $this->resolveSignatureVerified($package),
            'manifest_version' => $this->resolveManifestVersion($package),
            'duration_seconds' => $this->resolveDurationSeconds($execution),
            'summary' => (string) __('dberp.system_update.report.success_summary'),
            'reported_at' => now()->utc()->toIso8601String(),
        ]);
    }

    /**
     * @return array<string, mixed>
     */
    private function buildFailurePayload(
        HostSystemUpdateExecution $execution,
        ?string $errorCode,
        ?string $userFacingMessage,
        ?string $failedStep,
    ): array {
        $resolvedStep = $this->nullableString($failedStep)
            ?? $this->nullableString($execution->current_step)
            ?? 'unknown';
        $resolvedCode = $this->nullableString($errorCode)
            ?? $this->nullableString($execution->error_code)
            ?? 'update_failed';
        $resolvedMessage = $this->nullableString($userFacingMessage)
            ?? $this->nullableString($execution->error_message)
            ?? (string) __('dberp.system_update.report.failure_summary_fallback');

        return $this->filterPayload([
            ...$this->sharedIdentityFields($execution),
            'from_version' => $execution->from_version,
            'to_version' => $execution->to_version,
            'from_update_version' => $this->stringifyUpdateVersion($execution->from_update_version),
            'to_update_version' => $this->stringifyUpdateVersion($execution->to_update_version),
            'target_update_version' => $this->stringifyUpdateVersion($execution->to_update_version),
            'execution_no' => $execution->execution_no,
            'status' => self::REPORT_STATUS_FAILED,
            'failed_step' => $resolvedStep,
            'error_code' => $resolvedCode,
            'message_summary' => $resolvedMessage,
            'package_type' => HostSystemUpdatePackageTypeGuard::INSTALL_PACKAGE_TYPE_FULL,
            'duration_seconds' => $this->resolveDurationSeconds($execution),
            'reported_at' => now()->utc()->toIso8601String(),
            'context' => $this->buildFailureEnvironmentContext(),
        ]);
    }

    /**
     * @return array<string, mixed>
     */
    private function sharedIdentityFields(HostSystemUpdateExecution $execution): array
    {
        $package = $execution->package;
        $context = $this->contextBuilder->baseContext();

        return [
            'product_code' => $execution->product_code ?? 'dberp',
            'site_no' => $this->resolveSiteNo(),
            'current_domain' => $this->nullableString($context['current_domain'] ?? null),
            'license_no' => $this->resolveLicenseNo(),
            'package_id' => $this->resolveNumericPackageId($execution->package_id ?? $package?->package_id),
            'release_channel' => $execution->release_channel ?? $package?->release_channel,
            'instance_id' => $context['instance_id'] ?? null,
        ];
    }

    private function resolveSiteNo(): ?string
    {
        $authorization = HostSystemAuthorization::singleton();

        return $this->nullableString($authorization->site_no)
            ?? $this->nullableString(config('dberp.support_center.site_no'))
            ?? $this->readOnboardingSnapshotString('site_no')
            ?? $this->readOnboardingSnapshotString('official_site_no', fromData: true);
    }

    private function resolveLicenseNo(): ?string
    {
        $authorization = HostSystemAuthorization::singleton();

        return $this->nullableString($authorization->license_no)
            ?? $this->nullableString(config('dberp.support_center.license_no'))
            ?? $this->readOnboardingSnapshotString('license_no')
            ?? $this->readOnboardingSnapshotString('official_license_no', fromData: true);
    }

    private function readOnboardingSnapshotString(string $field, bool $fromData = false): ?string
    {
        $snapshot = $this->onboardingStatusCacheStore->get();

        if (! is_array($snapshot)) {
            return null;
        }

        if ($fromData) {
            $data = $snapshot['data'] ?? null;

            return is_array($data) ? $this->nullableString($data[$field] ?? null) : null;
        }

        return $this->nullableString($snapshot[$field] ?? null);
    }

    /**
     * @return array<string, bool>
     */
    private function siteNoSourcePresence(): array
    {
        $authorization = HostSystemAuthorization::singleton();
        $snapshot = $this->onboardingStatusCacheStore->get();
        $data = is_array($snapshot) && is_array($snapshot['data'] ?? null) ? $snapshot['data'] : [];

        return [
            'authorization_site_no' => $this->nullableString($authorization->site_no) !== null,
            'config_site_no' => $this->nullableString(config('dberp.support_center.site_no')) !== null,
            'cache_root_site_no' => $this->nullableString(is_array($snapshot) ? ($snapshot['site_no'] ?? null) : null) !== null,
            'cache_data_official_site_no' => $this->nullableString($data['official_site_no'] ?? null) !== null,
        ];
    }

    private function skipReportDueToMissingSiteNo(HostSystemUpdateExecution $execution, string $kind): void
    {
        $sources = $this->siteNoSourcePresence();

        Log::warning('support_center.system_update.result_report_skipped_missing_site_no', [
            'execution_no' => $execution->execution_no,
            'report_kind' => $kind,
            'identity_source_presence' => $sources,
        ]);

        $this->auditLogger->logSystemUpdateResultReportSkipped(
            $execution,
            $kind,
            self::SKIP_REASON_MISSING_SITE_NO,
            $sources,
        );
    }

    /** @param list<string> $invalidFields */
    private function skipInvalidSuccessPayload(HostSystemUpdateExecution $execution, array $invalidFields): void
    {
        Log::warning('support_center.system_update.result_report_skipped_invalid_payload', [
            'execution_no' => $execution->execution_no,
            'report_kind' => 'success',
            'invalid_fields' => $invalidFields,
        ]);

        $this->auditLogger->logSystemUpdateResultReportSkipped(
            $execution,
            'success',
            self::SKIP_REASON_INVALID_SUCCESS_PAYLOAD,
            array_fill_keys($invalidFields, false),
        );
    }

    /**
     * 与 Support Center HostSystemUpdateResultController 的 required/type 规则保持一致。
     *
     * @param  array<string, mixed>  $payload
     * @return list<string>
     */
    private function invalidSuccessPayloadFields(array $payload): array
    {
        $invalid = [];

        foreach (['product_code', 'site_no', 'current_domain', 'from_update_version', 'to_update_version', 'status'] as $field) {
            if (! is_string($payload[$field] ?? null) || trim((string) $payload[$field]) === '') {
                $invalid[] = $field;
            }
        }

        if (! is_int($payload['package_id'] ?? null) || $payload['package_id'] < 1) {
            $invalid[] = 'package_id';
        }

        return $invalid;
    }

    /**
     * @param  array<string, mixed>  $payload
     * @return array<string, mixed>
     */
    private function filterPayload(array $payload): array
    {
        return array_filter(
            $payload,
            static fn (mixed $value): bool => $value !== null && $value !== '',
        );
    }

    private function resolveNumericPackageId(mixed $packageId): ?int
    {
        if (is_int($packageId) && $packageId > 0) {
            return $packageId;
        }

        if (! is_string($packageId)) {
            return null;
        }

        $trimmed = trim($packageId);

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

        $numeric = (int) $trimmed;

        return $numeric > 0 ? $numeric : null;
    }

    private function stringifyUpdateVersion(mixed $value): ?string
    {
        if ($value === null) {
            return null;
        }

        if (is_int($value) || is_float($value)) {
            return (string) (int) $value;
        }

        if (! is_string($value)) {
            return null;
        }

        $trimmed = trim($value);

        return $trimmed === '' ? null : $trimmed;
    }

    private function resolveDurationSeconds(HostSystemUpdateExecution $execution): ?int
    {
        $startedAt = $execution->started_at;
        $finishedAt = $execution->finished_at ?? now();

        if ($startedAt === null) {
            return null;
        }

        return max(0, $finishedAt->diffInSeconds($startedAt));
    }

    private function resolveChecksumVerified(?HostSystemUpdatePackage $package): bool
    {
        return (bool) ($package?->verified ?? false);
    }

    private function resolveSignatureVerified(?HostSystemUpdatePackage $package): ?bool
    {
        if ($package === null) {
            return null;
        }

        $signature = $this->nullableString($package->signature);

        if ($signature === null) {
            return filter_var(
                config('dberp.system_update.require_package_signature', false),
                FILTER_VALIDATE_BOOLEAN,
            ) ? false : null;
        }

        if (! filter_var(config('dberp.system_update.verify_package_signature', false), FILTER_VALIDATE_BOOLEAN)) {
            return null;
        }

        return (bool) $package->verified;
    }

    /**
     * @return array<string, mixed>
     */
    private function buildFailureEnvironmentContext(): array
    {
        return array_filter([
            'php_version' => PHP_VERSION,
            'os' => PHP_OS_FAMILY,
            'db_driver' => config('database.default'),
        ], static fn (mixed $value): bool => is_string($value) && $value !== '');
    }

    private function resolveManifestVersion(?HostSystemUpdatePackage $package): ?string
    {
        if ($package === null) {
            return null;
        }

        $manifest = $package->manifest;

        if (! is_array($manifest)) {
            return null;
        }

        foreach (['manifest_version', 'version', 'target_version'] as $key) {
            $value = $this->nullableString($manifest[$key] ?? null);

            if ($value !== null) {
                return $value;
            }
        }

        return null;
    }

    private function logReportFailure(HostSystemUpdateExecution $execution, string $kind, Throwable $exception): void
    {
        Log::warning('support_center.system_update.result_report_failed', [
            'execution_no' => $execution->execution_no,
            'kind' => $kind,
            'exception' => $exception::class,
        ]);

        $this->auditLogger->logSystemUpdateResultReportFailed($execution, $kind, $exception);
    }

    private function nullableString(mixed $value): ?string
    {
        if (! is_string($value)) {
            return null;
        }

        $trimmed = trim($value);

        return $trimmed === '' ? null : $trimmed;
    }
}
