<?php

/**
 * 文件功能：百家乐买单活动服务
 *
 * 统一处理活动创建、生命周期推进、下注关联、
 * 结算累计补偿、用户领取补偿以及聊天室通知广播。
 */

namespace App\Services;

use App\Enums\CurrencySource;
use App\Events\MessageSent;
use App\Jobs\AiClaimBaccaratLossCoverJob;
use App\Jobs\SaveMessageJob;
use App\Models\BaccaratBet;
use App\Models\BaccaratLossCoverEvent;
use App\Models\BaccaratLossCoverRecord;
use App\Models\User;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;

class BaccaratLossCoverService
{
    /**
     * 注入聊天室状态与金币服务。
     */
    public function __construct(
        private readonly ChatStateService $chatState,
        private readonly UserCurrencyService $currency,
    ) {}

    /**
     * 创建新的百家乐买单活动。
     *
     * @param  array<string, mixed>  $data
     */
    public function createEvent(User $operator, array $data): BaccaratLossCoverEvent
    {
        $startsAt = Carbon::parse((string) $data['starts_at']);
        $endsAt = Carbon::parse((string) $data['ends_at']);

        if ($this->hasOverlap($startsAt, $endsAt)) {
            throw new \RuntimeException('活动时间段与其他百家乐买单活动重叠，请调整后再试。');
        }

        $event = BaccaratLossCoverEvent::create([
            'title' => (string) $data['title'],
            'description' => $data['description'] ?: null,
            'status' => 'scheduled',
            'starts_at' => $startsAt,
            'ends_at' => $endsAt,
            'claim_deadline_at' => Carbon::parse((string) $data['claim_deadline_at']),
            'created_by_user_id' => $operator->id,
        ]);

        // 如果开始时间已到，则立即激活活动，避免用户等待下一次定时扫描。
        if ($event->starts_at->lte(now())) {
            $this->activateEvent($event);
        }

        return $event->fresh(['creator', 'closer']);
    }

    /**
     * 手动结束或取消活动。
     */
    public function forceCloseEvent(BaccaratLossCoverEvent $event, User $operator): BaccaratLossCoverEvent
    {
        if ($event->status === 'cancelled' || $event->status === 'completed') {
            return $event;
        }

        // 未开始前手动关闭视为取消，保留完整开启档案但不参与结算。
        if ($event->status === 'scheduled' && $event->starts_at->isFuture()) {
            $event->update([
                'status' => 'cancelled',
                'closed_by_user_id' => $operator->id,
            ]);

            return $event->fresh(['creator', 'closer']);
        }

        // 已开始的活动会被立即截断结束时间，然后走正常的结算/领取流转。
        $event->update([
            'ends_at' => now(),
            'closed_by_user_id' => $operator->id,
        ]);

        $this->transitionAfterEnd($event->fresh());

        return $event->fresh(['creator', 'closer']);
    }

    /**
     * 定时推进所有买单活动的生命周期。
     */
    public function tick(): void
    {
        $this->activateDueEvents();
        $this->closeDueActiveEvents();
        $this->finalizeSettlementPendingEvents();
        $this->expireClaimableEvents();
    }

    /**
     * 获取某个下注时间点命中的活动。
     *
     * 这里按管理员设定的开始/结束时间窗口判断，
     * 不强依赖后台状态已经及时切到 active，
     * 这样刚到开始时间的活动也能立即参与买单判定。
     */
    public function findEventForBetTime(?Carbon $betTime = null): ?BaccaratLossCoverEvent
    {
        $betTime = $betTime ?? now();

        return BaccaratLossCoverEvent::query()
            ->whereNotIn('status', ['cancelled', 'completed'])
            ->where('starts_at', '<=', $betTime)
            ->where('ends_at', '>=', $betTime)
            ->orderByDesc('id')
            ->first();
    }

    /**
     * 在下注成功后登记用户的活动参与记录。
     */
    public function registerBet(BaccaratBet $bet): void
    {
        if (! $bet->loss_cover_event_id) {
            return;
        }

        // 首次命中活动的用户会创建聚合记录，并计入活动参与人数。
        $record = BaccaratLossCoverRecord::query()->firstOrCreate(
            [
                'event_id' => $bet->loss_cover_event_id,
                'user_id' => $bet->user_id,
            ],
            [
                'claim_status' => 'not_eligible',
            ],
        );

        if ($record->wasRecentlyCreated) {
            BaccaratLossCoverEvent::query()
                ->where('id', $bet->loss_cover_event_id)
                ->increment('participant_count');
        }

        // 每一笔命中活动的下注都要累加到账户活动统计中。
        $record->increment('total_bet_amount', $bet->amount);
    }

    /**
     * 在百家乐结算后同步更新活动用户记录。
     */
    public function registerSettlement(BaccaratBet $bet): void
    {
        if (! $bet->loss_cover_event_id) {
            return;
        }

        $record = BaccaratLossCoverRecord::query()->firstOrCreate(
            [
                'event_id' => $bet->loss_cover_event_id,
                'user_id' => $bet->user_id,
            ],
            [
                'claim_status' => 'not_eligible',
            ],
        );

        // 中奖只记录赔付统计，不影响补偿资格。
        if ($bet->status === 'won') {
            $record->increment('total_win_payout', $bet->payout);

            return;
        }

        if ($bet->status !== 'lost') {
            return;
        }

        // 输掉的金额就是后续可领取的补偿金额。
        $record->increment('total_loss_amount', $bet->amount);
        $record->increment('compensation_amount', $bet->amount);
        $record->update(['claim_status' => 'pending']);

        BaccaratLossCoverEvent::query()
            ->where('id', $bet->loss_cover_event_id)
            ->increment('total_loss_amount', $bet->amount);
    }

    /**
     * 用户领取某次活动的补偿金币。
     *
     * @return array{ok: bool, message: string, amount?: int}
     */
    public function claim(BaccaratLossCoverEvent $event, User $user): array
    {
        if (! $event->isClaimable()) {
            return ['ok' => false, 'message' => '当前活动暂未开放领取，或已超过领取时间。'];
        }

        $result = DB::transaction(function () use ($event, $user): array {
            $record = BaccaratLossCoverRecord::query()
                ->where('event_id', $event->id)
                ->where('user_id', $user->id)
                ->lockForUpdate()
                ->first();

            if (! $record || $record->compensation_amount <= 0) {
                return ['ok' => false, 'message' => '您在本次活动中暂无可领取补偿。'];
            }

            if ($record->claim_status === 'claimed') {
                return ['ok' => false, 'message' => '本次活动补偿您已经领取过了。'];
            }

            if ($record->claim_status === 'expired') {
                return ['ok' => false, 'message' => '本次活动补偿已过期，无法领取。'];
            }

            $amount = (int) $record->compensation_amount;

            // 领取成功时必须统一走金币服务，确保 user_currency_logs 自动落账。
            $this->currency->change(
                $user,
                'gold',
                $amount,
                CurrencySource::BACCARAT_LOSS_COVER_CLAIM,
                "百家乐买单活动 #{$event->id} 领取补偿",
            );

            $record->update([
                'claim_status' => 'claimed',
                'claimed_amount' => $amount,
                'claimed_at' => now(),
            ]);

            $event->increment('total_claimed_amount', $amount);

            // 所有待领取记录都被领完后，可提前结束活动，方便前台展示最终状态。
            if (! BaccaratLossCoverRecord::query()->where('event_id', $event->id)->where('claim_status', 'pending')->exists()) {
                $event->update(['status' => 'completed']);
            }

            return [
                'ok' => true,
                'message' => "已成功领取 {$amount} 金币补偿！",
                'amount' => $amount,
            ];
        });

        // 领取成功后，需要向聊天室广播一条同款百家乐风格消息，方便其他人快速领取。
        if (($result['ok'] ?? false) === true && isset($result['amount'])) {
            $this->pushClaimMessage($event->fresh(), $user, (int) $result['amount']);
        }

        return $result;
    }

    /**
     * 扫描并激活到时的活动。
     */
    public function activateDueEvents(): void
    {
        BaccaratLossCoverEvent::query()
            ->where('status', 'scheduled')
            ->where('starts_at', '<=', now())
            ->orderBy('starts_at')
            ->get()
            ->each(function (BaccaratLossCoverEvent $event): void {
                $this->activateEvent($event);
            });
    }

    /**
     * 扫描已到结束时间的进行中活动。
     */
    public function closeDueActiveEvents(): void
    {
        BaccaratLossCoverEvent::query()
            ->where('status', 'active')
            ->where('ends_at', '<=', now())
            ->orderBy('ends_at')
            ->get()
            ->each(function (BaccaratLossCoverEvent $event): void {
                $this->transitionAfterEnd($event);
            });
    }

    /**
     * 扫描等待结算完成的活动并尝试开放领取。
     */
    public function finalizeSettlementPendingEvents(): void
    {
        BaccaratLossCoverEvent::query()
            ->where('status', 'settlement_pending')
            ->orderBy('ends_at')
            ->get()
            ->each(function (BaccaratLossCoverEvent $event): void {
                $this->transitionAfterEnd($event);
            });
    }

    /**
     * 扫描补偿领取过期的活动并收尾。
     */
    public function expireClaimableEvents(): void
    {
        BaccaratLossCoverEvent::query()
            ->where('status', 'claimable')
            ->where('claim_deadline_at', '<=', now())
            ->orderBy('claim_deadline_at')
            ->get()
            ->each(function (BaccaratLossCoverEvent $event): void {
                // 超过领取时间后，未领取的记录统一标记为过期。
                BaccaratLossCoverRecord::query()
                    ->where('event_id', $event->id)
                    ->where('claim_status', 'pending')
                    ->update(['claim_status' => 'expired']);

                $event->update(['status' => 'completed']);
            });
    }

    /**
     * 判断指定时间窗是否与历史或进行中的活动冲突。
     */
    public function hasOverlap(Carbon $startsAt, Carbon $endsAt): bool
    {
        return BaccaratLossCoverEvent::query()
            ->where('status', '!=', 'cancelled')
            ->where('starts_at', '<', $endsAt)
            ->where('ends_at', '>', $startsAt)
            ->exists();
    }

    /**
     * 激活单个活动并发送开始通知。
     */
    private function activateEvent(BaccaratLossCoverEvent $event): void
    {
        $updated = BaccaratLossCoverEvent::query()
            ->where('id', $event->id)
            ->where('status', 'scheduled')
            ->update(['status' => 'active']);

        if (! $updated) {
            return;
        }

        $event = $event->fresh(['creator']);

        // 只发送一次开始通知，避免调度重复触发。
        if ($event && $event->started_notice_sent_at === null) {
            $creatorName = $event->creator?->username ?? '管理员';
            $content = "🎉 【{$event->title}】活动开始啦！开启人：<b>{$creatorName}</b>，时间：{$event->starts_at?->format('m-d H:i')} ~ {$event->ends_at?->format('m-d H:i')}。活动期间参与百家乐，赢的归个人，输的活动结束后可领取补偿。";
            $this->pushRoomMessage($content, '#16a34a');
            $event->update(['started_notice_sent_at' => now()]);
        }
    }

    /**
     * 根据活动内是否仍有未结算下注，推进结束后的状态。
     */
    private function transitionAfterEnd(BaccaratLossCoverEvent $event): void
    {
        if (! in_array($event->status, ['active', 'settlement_pending'], true)) {
            return;
        }

        $hasPendingBet = BaccaratBet::query()
            ->where('loss_cover_event_id', $event->id)
            ->where('status', 'pending')
            ->exists();

        // 仍有活动内下注未开奖时，先进入等待结算状态。
        if ($hasPendingBet) {
            $event->update(['status' => 'settlement_pending']);

            return;
        }

        // 全部结算完成后，活动正式进入可领取状态。
        $compensableCount = BaccaratLossCoverRecord::query()
            ->where('event_id', $event->id)
            ->where('claim_status', 'pending')
            ->count();

        $event->update([
            'status' => $compensableCount > 0 ? 'claimable' : 'completed',
            'compensable_user_count' => $compensableCount,
        ]);

        if ($event->ended_notice_sent_at !== null) {
            return;
        }

        if ($compensableCount > 0) {
            $button = '<button onclick="claimBaccaratLossCover('.$event->id.')" style="margin-left:8px;padding:3px 12px;background:#16a34a;color:#fff;border:none;border-radius:12px;cursor:pointer;font-size:12px;font-weight:bold;">领取补偿</button>';
            $content = "📣 【{$event->title}】活动已结束并完成结算！本次共有 <b>{$compensableCount}</b> 位玩家可领取补偿，截止时间：{$event->claim_deadline_at?->format('m-d H:i')}。{$button}";
        } else {
            $content = "📣 【{$event->title}】活动已结束！本次活动没有产生可领取补偿的记录。";
        }

        $this->pushRoomMessage($content, '#7c3aed');
        $event->update(['ended_notice_sent_at' => now()]);

        // 活动开放领取后，为 AI小班长补发一次自动领取任务。
        $this->dispatchAiAutoClaimJob($event->fresh());
    }

    /**
     * 向房间广播一条系统公告消息。
     */
    private function pushRoomMessage(string $content, string $fontColor): void
    {
        $message = [
            'id' => $this->chatState->nextMessageId(1),
            'room_id' => 1,
            'from_user' => '系统传音',
            'to_user' => '大家',
            'content' => $content,
            'is_secret' => false,
            'font_color' => $fontColor,
            'action' => '',
            'sent_at' => now()->toDateTimeString(),
        ];

        $this->chatState->pushMessage(1, $message);
        broadcast(new MessageSent(1, $message));
        SaveMessageJob::dispatch($message);
    }

    /**
     * 在用户领取补偿成功后，向聊天室广播领取播报。
     */
    private function pushClaimMessage(?BaccaratLossCoverEvent $event, User $user, int $amount): void
    {
        if (! $event) {
            return;
        }

        $formattedAmount = number_format($amount);
        $button = $event->status === 'claimable'
            ? ' <button onclick="claimBaccaratLossCover('.$event->id.')" style="margin-left:8px;padding:3px 12px;background:#16a34a;color:#fff;border:none;border-radius:12px;cursor:pointer;font-size:12px;font-weight:bold;">领取补偿</button>'
            : '';

        // 领取成功的公屏格式复用百家乐参与播报风格，保证聊天室感知一致。
        $content = "🌟 🎲 <b>{$user->username}</b> 领取了 <b>{$formattedAmount}</b> 金币补偿！✨{$button}";
        $message = [
            'id' => $this->chatState->nextMessageId(1),
            'room_id' => 1,
            'from_user' => '系统传音',
            'to_user' => '大家',
            'content' => $content,
            'is_secret' => false,
            'font_color' => '#d97706',
            'action' => '',
            'sent_at' => now()->toDateTimeString(),
        ];

        $this->chatState->pushMessage(1, $message);
        broadcast(new MessageSent(1, $message));
        SaveMessageJob::dispatch($message);
    }

    /**
     * 在活动可领取后，为 AI小班长派发自动领取补偿任务。
     */
    private function dispatchAiAutoClaimJob(?BaccaratLossCoverEvent $event): void
    {
        if (! $event || $event->status !== 'claimable') {
            return;
        }

        $aiUserId = User::query()->where('username', 'AI小班长')->value('id');
        if (! $aiUserId) {
            return;
        }

        $hasPendingRecord = BaccaratLossCoverRecord::query()
            ->where('event_id', $event->id)
            ->where('user_id', $aiUserId)
            ->where('claim_status', 'pending')
            ->where('compensation_amount', '>', 0)
            ->exists();

        // 只有 AI小班长确实存在待领取补偿时，才派发自动领取任务。
        if (! $hasPendingRecord) {
            return;
        }

        AiClaimBaccaratLossCoverJob::dispatch($event->id);
    }
}
