<?php

/**
 * 文件功能：节日福利触发队列任务
 *
 * 由 Laravel 调度器每分钟检查并触发到期的节日活动：
 * 计算在线用户 → 分配金额 → 写入领取记录 → 广播 WebSocket 事件。
 * 通过 Horizon 队列异步执行，不阻塞 HTTP 请求。
 *
 * @author  ChatRoom Laravel
 *
 * @version 1.0.0
 */

namespace App\Jobs;

use App\Events\HolidayEventStarted;
use App\Events\MessageSent;
use App\Models\HolidayClaim;
use App\Models\HolidayEvent;
use App\Models\HolidayEventRun;
use App\Models\User;
use App\Services\ChatStateService;
use App\Services\HolidayEventScheduleService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;

/**
 * 类功能：按模板配置生成节日福利发放批次并广播给前端。
 */
class TriggerHolidayEventJob implements ShouldQueue
{
    use Queueable;

    /**
     * 最大重试次数（避免因为临时故障无限重试）。
     */
    public int $tries = 3;

    /**
     * @param  HolidayEvent  $event  节日活动模板
     * @param  bool  $manual  是否为管理员手动立即触发
     */
    public function __construct(
        public readonly HolidayEvent $event,
        public readonly bool $manual = false,
    ) {}

    /**
     * 执行任务：触发节日福利发放。
     */
    public function handle(
        ChatStateService $chatState,
        HolidayEventScheduleService $scheduleService,
    ): void {
        $run = $this->prepareRun($scheduleService);

        if (! $run) {
            return;
        }

        // 获取在线用户（满足目标用户条件），每一轮都按触发当时的在线状态重新计算。
        $onlineIds = $this->getEligibleOnlineUsers($run);

        if (empty($onlineIds)) {
            $run->update(['status' => 'completed', 'audience_count' => 0]);

            return;
        }

        // 按本批次快照中的 max_claimants 限制人数。
        if ($run->max_claimants > 0 && count($onlineIds) > $run->max_claimants) {
            shuffle($onlineIds);
            $onlineIds = array_slice($onlineIds, 0, $run->max_claimants);
        }

        // 计算每人的待领取金额。
        $amounts = $this->distributeAmounts($run, count($onlineIds));

        DB::transaction(function () use ($run, $onlineIds, $amounts): void {
            $claims = [];

            foreach ($onlineIds as $i => $userId) {
                $claims[] = [
                    'event_id' => $run->holiday_event_id,
                    'run_id' => $run->id,
                    'user_id' => $userId,
                    'amount' => $amounts[$i] ?? 0,
                    'claimed_at' => null,
                ];
            }

            // 一次性生成本轮全部待领取记录，claimed_at 默认为 null。
            HolidayClaim::insert($claims);

            $run->update(['audience_count' => count($claims)]);
        });

        // 广播本轮发放批次，前端将基于 run_id 领取。
        broadcast(new HolidayEventStarted($run->fresh()));

        // 向聊天室追加系统公告，提醒用户点击弹窗领取。
        $this->pushSystemMessage($run, count($onlineIds), $chatState);
    }

    /**
     * 生成批次并推进模板到下一次触发时间。
     */
    private function prepareRun(HolidayEventScheduleService $scheduleService): ?HolidayEventRun
    {
        /** @var HolidayEventRun|null $run */
        $run = DB::transaction(function () use ($scheduleService): ?HolidayEventRun {
            /** @var HolidayEvent|null $event */
            $event = HolidayEvent::query()
                ->whereKey($this->event->id)
                ->lockForUpdate()
                ->first();

            if (! $event || ! $event->enabled || $event->status === 'cancelled') {
                return null;
            }

            $now = now();
            $scheduledFor = $this->manual ? $now->copy() : $event->send_at;

            if (! $this->manual) {
                // 定时触发只允许处理真正到期且仍处于 pending 的模板。
                if ($event->status !== 'pending' || $scheduledFor === null || $scheduledFor->isFuture()) {
                    return null;
                }

                $nextSendAt = $scheduleService->advanceAfterTrigger($event);
                $event->update([
                    'send_at' => $nextSendAt,
                    'status' => $nextSendAt ? 'pending' : 'completed',
                    'triggered_at' => $now,
                    'expires_at' => $now->copy()->addMinutes($event->expire_minutes),
                    'claimed_count' => 0,
                    'claimed_amount' => 0,
                ]);
            } else {
                // 手动立即触发只更新最后触发信息，不改动既有 send_at 锚点。
                $event->update([
                    'triggered_at' => $now,
                    'expires_at' => $now->copy()->addMinutes($event->expire_minutes),
                ]);
            }

            return HolidayEventRun::query()->create([
                'holiday_event_id' => $event->id,
                'event_name' => $event->name,
                'event_description' => $event->description,
                'total_amount' => $event->total_amount,
                'max_claimants' => $event->max_claimants,
                'distribute_type' => $event->distribute_type,
                'min_amount' => $event->min_amount,
                'max_amount' => $event->max_amount,
                'fixed_amount' => $event->fixed_amount,
                'target_type' => $event->target_type,
                'target_value' => $event->target_value,
                'repeat_type' => $event->repeat_type,
                'scheduled_for' => $scheduledFor,
                'triggered_at' => $now,
                'expires_at' => $now->copy()->addMinutes($event->expire_minutes),
                'status' => 'active',
                'audience_count' => 0,
                'claimed_count' => 0,
                'claimed_amount' => 0,
            ]);
        });

        return $run;
    }

    /**
     * 获取满足当前批次条件的在线用户 ID 列表。
     *
     * @return array<int>
     */
    private function getEligibleOnlineUsers(HolidayEventRun $run): array
    {
        try {
            $users = Redis::hgetall('room:1:users');
            if (empty($users)) {
                return [];
            }

            $ids = [];
            $fallbackUsernames = [];

            foreach ($users as $username => $jsonInfo) {
                $info = json_decode($jsonInfo, true);

                if (isset($info['user_id'])) {
                    $ids[] = (int) $info['user_id'];

                    continue;
                }

                $fallbackUsernames[] = $username;
            }

            if (! empty($fallbackUsernames)) {
                $fallbackIds = User::query()
                    ->whereIn('username', $fallbackUsernames)
                    ->pluck('id')
                    ->map(fn ($id): int => (int) $id)
                    ->all();

                $ids = array_merge($ids, $fallbackIds);
            }

            $ids = array_values(array_unique($ids));

            // 目标用户范围以当前批次快照为准，避免模板后续编辑影响本轮名单。
            return match ($run->target_type) {
                'vip' => User::whereIn('id', $ids)->whereNotNull('vip_level_id')->pluck('id')->map(fn ($id) => (int) $id)->all(),
                'level' => User::whereIn('id', $ids)->where('user_level', '>=', (int) ($run->target_value ?? 1))->pluck('id')->map(fn ($id) => (int) $id)->all(),
                default => $ids,
            };
        } catch (\Throwable) {
            return [];
        }
    }

    /**
     * 按分配方式计算每人金额数组。
     *
     * @return array<int>
     */
    private function distributeAmounts(HolidayEventRun $run, int $count): array
    {
        if ($count <= 0) {
            return [];
        }

        if ($run->distribute_type === 'fixed') {
            // 定额模式：每人固定一个金额，优先使用模板快照中的 fixed_amount。
            $amount = $run->fixed_amount ?? (int) floor($run->total_amount / $count);

            return array_fill(0, $count, $amount);
        }

        // 随机模式沿用二倍均值算法，保证总金额恰好发完。
        $total = $run->total_amount;
        $min = max(1, $run->min_amount ?? 1);
        $max = $run->max_amount ?? (int) ceil($total * 2 / $count);

        $amounts = [];
        $remaining = $total;

        for ($i = 0; $i < $count - 1; $i++) {
            $remainingPeople = $count - $i;
            $avgDouble = (int) floor($remaining * 2 / $remainingPeople);
            $cap = max($min, min($max, $avgDouble - 1, $remaining - ($remainingPeople - 1) * $min));
            $amounts[] = random_int($min, max($min, $cap));
            $remaining -= end($amounts);
        }

        $amounts[] = max($min, $remaining);
        shuffle($amounts);

        return $amounts;
    }

    /**
     * 向聊天室推送系统公告消息并写入 Redis + 落库。
     */
    private function pushSystemMessage(HolidayEventRun $run, int $claimCount, ChatStateService $chatState): void
    {
        $typeLabel = $run->distribute_type === 'fixed' ? "每人固定 {$run->fixed_amount} 金币" : '随机分配';
        $content = "🎊 【{$run->event_name}】节日福利开始啦！总奖池 💰".number_format($run->total_amount)
            ." 金币，{$typeLabel}，共 {$claimCount} 名在线用户可领取！点击弹窗按钮立即领取！";

        $msg = [
            'id' => $chatState->nextMessageId(1),
            'room_id' => 1,
            'from_user' => '系统传音',
            'to_user' => '大家',
            'content' => $content,
            'is_secret' => false,
            'font_color' => '#f59e0b',
            'action' => '大声宣告',
            'sent_at' => now()->toDateTimeString(),
        ];

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