<?php

namespace App\Services\Draw;

use Carbon\Carbon;
use App\Events\OddsUpdateBroadcast;
use App\Events\PlayCatalogUpdatedBroadcast;
use App\Events\PlayToggleBroadcast;
use App\Events\RiskSoldOutBroadcast;
use App\Events\RiskWarningBroadcast;
use App\Events\JackpotBurstBroadcast;
use App\Events\DrawCountdownBroadcast;
use App\Events\DrawStatusChangeBroadcast;
use App\Events\DrawResultPublishedBroadcast;
use Illuminate\Support\Facades\Cache;

/**
 * 对齐界面文档 §2.1：大厅公共频道广播（`lottery-hall`）。
 * 包含：draw.countdown、draw.status_change、result.published、
 *       risk.sold_out、risk.warning、play.toggle、odds.update、jackpot.burst
 */
final class LotteryHallRealtimeBroadcaster
{
    private const COUNTDOWN_FP_CACHE_KEY = 'lottery:hall:countdown:last-fingerprint';

    public function __construct(
        private readonly DrawHallSnapshotBuilder $snapshot,
    ) {}

    /** 每秒调度：边界变化立刻推送，完整快照按低频校准，避免仅本地倒计时无法切期。 */
    public function countdownPulse(): void
    {
        if (! $this->driverSupportsRealtime()) {
            return;
        }

        $nowUtc = Carbon::now()->utc();
        $ms = (int) floor(microtime(true) * 1000);
        $fingerprint = $this->snapshot->hallTargetFingerprint($nowUtc);
        $lastFingerprint = Cache::get(self::COUNTDOWN_FP_CACHE_KEY);
        $stateChanged = $this->fingerprintChanged($lastFingerprint, $fingerprint);
        $shouldSync = $this->shouldBroadcastSyncPulse($nowUtc);

        Cache::put(self::COUNTDOWN_FP_CACHE_KEY, $fingerprint, now()->addMinutes(10));

        if (! $stateChanged && ! $shouldSync) {
            return;
        }

        broadcast(new DrawCountdownBroadcast($this->snapshot->build($nowUtc), $ms));
    }

    /**
     * Tick 首尾对比：**数据库**当期指纹（{@see DrawHallSnapshotBuilder::hallTargetFingerprint}）变了再发，
     * 载荷仍为 {@see DrawHallSnapshotBuilder::build()}（含未到 tick 时对 `open` 的展示规范化）。
     */
    public function notifyStatusChangeIfHallDbChanged(?array $fpBefore, ?array $fpAfter, ?array $snapshotPayload): void
    {
        if (! $this->driverSupportsRealtime()) {
            return;
        }

        if (($fpBefore['draw_no'] ?? null) === ($fpAfter['draw_no'] ?? null)
            && ($fpBefore['status'] ?? null) === ($fpAfter['status'] ?? null)) {
            return;
        }

        $this->notifyStatusChange($snapshotPayload);
    }

    /** `draw.status_change`（管理端发布后等不与 tick 同路径时使用）。 */
    public function notifyStatusChange(?array $data): void
    {
        if (! $this->driverSupportsRealtime()) {
            return;
        }

        broadcast(new DrawStatusChangeBroadcast($data, (int) floor(microtime(true) * 1000)));
    }

    /** `result.published` */
    public function notifyResultPublished(?array $data): void
    {
        if (! $this->driverSupportsRealtime()) {
            return;
        }

        broadcast(new DrawResultPublishedBroadcast($data, (int) floor(microtime(true) * 1000)));
    }

    /** `risk.sold_out` —— 号码赔付池耗尽 */
    public function notifyRiskSoldOut(int $drawId, string $drawNo, string $normalizedNumber): void
    {
        if (! $this->driverSupportsRealtime()) {
            return;
        }

        broadcast(new RiskSoldOutBroadcast(
            $drawId,
            $drawNo,
            $normalizedNumber,
            (int) floor(microtime(true) * 1000),
        ));
    }

    /** `risk.warning` —— 号码赔付池占用超 80% 预警 */
    public function notifyRiskWarning(int $drawId, string $drawNo, string $normalizedNumber, float $usageRatio): void
    {
        if (! $this->driverSupportsRealtime()) {
            return;
        }

        broadcast(new RiskWarningBroadcast(
            $drawId,
            $drawNo,
            $normalizedNumber,
            $usageRatio,
            (int) floor(microtime(true) * 1000),
        ));
    }

    /** `play.toggle` —— 玩法开关变更 */
    public function notifyPlayToggle(string $playCode, bool $enabled, ?string $reason = null): void
    {
        if (! $this->driverSupportsRealtime()) {
            return;
        }

        broadcast(new PlayToggleBroadcast(
            $playCode,
            $enabled,
            $reason,
            (int) floor(microtime(true) * 1000),
        ));
    }

    /**
     * `play.catalog_updated` —— 玩法/赔率/封顶版本发布（全量目录变更）。
     *
     * @param  string  $module  play_config|odds|risk_cap
     */
    public function notifyPlayCatalogUpdated(
        string $module,
        int $versionId,
        string $versionLabel,
        ?array $meta = null,
    ): void {
        if (! $this->driverSupportsRealtime()) {
            return;
        }

        broadcast(new PlayCatalogUpdatedBroadcast(
            $module,
            $versionId,
            $versionLabel,
            $meta,
            (int) floor(microtime(true) * 1000),
        ));
    }

    /** `odds.update` —— 赔率变更 */
    public function notifyOddsUpdate(int $versionId, string $versionName, ?array $diff = null): void
    {
        if (! $this->driverSupportsRealtime()) {
            return;
        }

        broadcast(new OddsUpdateBroadcast(
            $versionId,
            $versionName,
            $diff,
            (int) floor(microtime(true) * 1000),
        ));
    }

    /** `jackpot.burst` —— Jackpot 爆池动画与浏览器通知 */
    public function notifyJackpotBurst(
        int $drawId,
        string $drawNo,
        string $firstPrizeNumber,
        string $currencyCode,
        int $totalPayoutAmount,
        int $winnerCount,
        string $triggerType,
        int $poolAmountAfter,
    ): void {
        if (! $this->driverSupportsRealtime()) {
            return;
        }

        broadcast(new JackpotBurstBroadcast(
            $drawId,
            $drawNo,
            $firstPrizeNumber,
            strtoupper($currencyCode),
            $totalPayoutAmount,
            $winnerCount,
            $triggerType,
            $poolAmountAfter,
            (int) floor(microtime(true) * 1000),
        ));
    }

    private function driverSupportsRealtime(): bool
    {
        $default = config('broadcasting.default');
        if ($default === null || $default === 'null') {
            return false;
        }

        $driver = config("broadcasting.connections.{$default}.driver") ?? $default;

        return ! in_array($driver, ['null', 'log'], true);
    }

    private function shouldBroadcastSyncPulse(Carbon $nowUtc): bool
    {
        $interval = match ((int) config('lottery.realtime_hall_countdown_sync_interval_seconds', 5)) {
            1, 2, 5, 10 => (int) config('lottery.realtime_hall_countdown_sync_interval_seconds', 5),
            default => 5,
        };

        return $interval === 1 || ((int) $nowUtc->format('s')) % $interval === 0;
    }

    private function fingerprintChanged(mixed $before, ?array $after): bool
    {
        if (! is_array($before)) {
            return $after !== null;
        }

        return ($before['draw_no'] ?? null) !== ($after['draw_no'] ?? null)
            || ($before['status'] ?? null) !== ($after['status'] ?? null);
    }
}
