<?php

/**
 * 文件功能：聊天室礼包（红包）控制器
 *
 * 提供两个核心接口：
 *  - config()：读取当前职务的默认礼包数量与份数
 *  - send()  ：拥有权限的职务用户按职务配置发出礼包（金币 or 经验）
 *  - claim() ：在线用户抢礼包（先到先得，每人一份）
 *
 * 接入 UserCurrencyService 记录所有货币变动流水。
 *
 * @author  ChatRoom Laravel
 *
 * @version 1.0.0
 */

namespace App\Http\Controllers;

use App\Enums\CurrencySource;
use App\Events\MessageSent;
use App\Events\RedPacketClaimed;
use App\Events\RedPacketSent;
use App\Jobs\SaveMessageJob;
use App\Models\RedPacketClaim;
use App\Models\RedPacketEnvelope;
use App\Models\User;
use App\Services\ChatStateService;
use App\Services\PositionPermissionService;
use App\Services\UserCurrencyService;
use App\Support\PositionPermissionRegistry;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;

/**
 * 类功能：处理聊天室礼包的发包、查状态与抢包流程
 *
 * 负责礼包主记录创建、Redis 拆包金额管理、领取入账以及实时广播。
 */
class RedPacketController extends Controller
{
    /** 礼包默认总数量 */
    private const DEFAULT_TOTAL_AMOUNT = 8888;

    /** 礼包默认份数 */
    private const DEFAULT_TOTAL_COUNT = 10;

    /** 礼包有效期（秒） */
    private const EXPIRE_SECONDS = 300;

    /**
     * 构造函数：注入依赖服务
     */
    public function __construct(
        private readonly ChatStateService $chatState,
        private readonly UserCurrencyService $currencyService,
        private readonly PositionPermissionService $positionPermissionService,
    ) {}

    /**
     * 获取当前用户可发出的礼包默认配置。
     *
     * 聊天室发包弹窗打开时调用，确保页面展示与最终发包数量同源。
     */
    public function config(): JsonResponse
    {
        $user = Auth::user();

        // 仅拥有礼包红包权限的在职职务可以读取发包配置。
        if (! $this->positionPermissionService->hasPermission($user, PositionPermissionRegistry::ROOM_RED_PACKET)) {
            return response()->json(['status' => 'error', 'message' => '当前职务无权发礼包红包'], 403);
        }

        $redPacketConfig = $this->redPacketConfigForUser($user);

        return response()->json([
            'status' => 'success',
            'amount' => $redPacketConfig['amount'],
            'count' => $redPacketConfig['count'],
            'expire_seconds' => self::EXPIRE_SECONDS,
        ]);
    }

    /**
     * 拥有权限的职务用户凭空发出礼包。
     *
     * 不扣发包人自身货币，礼包总量和份数读取当前在职职务配置。
     * type 参数决定本次发出的是金币（gold）还是经验（exp）。
     *
     * @param  Request  $request  需包含 room_id 和 type（gold / exp）
     */
    public function send(Request $request): JsonResponse
    {
        $request->validate([
            'room_id' => 'required|integer',
            'type' => 'required|in:gold,exp',
        ]);

        $user = Auth::user();
        $roomId = (int) $request->input('room_id');
        $type = $request->input('type'); // 'gold' 或 'exp'

        // 改为按职务权限码控制礼包发放。
        if (! $this->positionPermissionService->hasPermission($user, PositionPermissionRegistry::ROOM_RED_PACKET)) {
            return response()->json(['status' => 'error', 'message' => '当前职务无权发礼包红包'], 403);
        }

        $redPacketConfig = $this->redPacketConfigForUser($user);
        $totalAmount = $redPacketConfig['amount'];
        $totalCount = $redPacketConfig['count'];

        // 检查该用户在此房间是否有进行中的红包（防止刷包）
        $activeExists = RedPacketEnvelope::query()
            ->where('sender_id', $user->id)
            ->where('room_id', $roomId)
            ->where('status', 'active')
            ->where('expires_at', '>', now())
            ->exists();

        if ($activeExists) {
            return response()->json(['status' => 'error', 'message' => '您有一个礼包尚未领完，请稍后再发！'], 422);
        }

        // 随机拆分数量（二倍均值法，保证每份至少 1，总额精确等于职务配置总量）
        $amounts = $this->splitAmount($totalAmount, $totalCount);

        // 货币展示文案
        $typeLabel = $type === 'exp' ? '经验' : '金币';
        $typeIcon = $type === 'exp' ? '✨' : '💰';
        $btnBg = $type === 'exp'
            ? 'linear-gradient(135deg,#7c3aed,#4f46e5)'
            : 'linear-gradient(135deg,#dc2626,#ea580c)';

        // 事务：创建红包记录 + Redis 写入分额
        $envelope = DB::transaction(function () use ($user, $roomId, $type, $amounts, $totalAmount, $totalCount): RedPacketEnvelope {
            // 创建红包主记录（凭空发出，不扣发包人货币）
            $envelope = RedPacketEnvelope::create([
                'sender_id' => $user->id,
                'sender_username' => $user->username,
                'room_id' => $roomId,
                'type' => $type,
                'total_amount' => $totalAmount,
                'total_count' => $totalCount,
                'claimed_count' => 0,
                'claimed_amount' => 0,
                'status' => 'active',
                'expires_at' => now()->addSeconds(self::EXPIRE_SECONDS),
            ]);

            // 将拆分好的数量序列存入 Redis（List，LPOP 抢红包）
            $key = "red_packet:{$envelope->id}:amounts";
            foreach ($amounts as $amt) {
                \Illuminate\Support\Facades\Redis::rpush($key, $amt);
            }
            // 多留 60s，确保领完后仍可回查
            \Illuminate\Support\Facades\Redis::expire($key, self::EXPIRE_SECONDS + 60);

            return $envelope;
        });

        // 广播系统公告，含可点击「立即抢包」按钮
        // 注意这里不能死命传 self::EXPIRE_SECONDS，因为这句话会被存入数据库的历史记录。我们需要在取出来的时候能根据发包时间动态变化！
        // 啊等等！由于这条消息是直接静态写入 `chat_messages` 内容里的，这就意味着如果在这里计算，存进去的还是 300。
        // 所以我们还是传 `self::EXPIRE_SECONDS` 作为总寿命，在前端逻辑里利用 `Date.now()` 和消息的 `sent_at` 来算出真实剩余倒计时更为严谨！
        $btnHtml = '<button data-sent-at="'.time().'" onclick="showRedPacketModal('
            .$envelope->id
            .',\''.$user->username.'\','
            .$totalAmount.','
            .$totalCount.','
            .self::EXPIRE_SECONDS
            .',\''.$type.'\''
            .')" style="margin-left:8px;padding:2px 10px;background:'.$btnBg.';'
            .'color:#fff;border:none;border-radius:10px;cursor:pointer;font-size:12px;font-weight:bold;'
            .'box-shadow:0 2px 6px rgba(0,0,0,0.3);">'.$typeIcon.' 立即抢包</button>';

        $msg = [
            'id' => $this->chatState->nextMessageId($roomId),
            'room_id' => $roomId,
            'from_user' => '系统公告',
            'to_user' => '',
            'content' => "🧧 <b>{$user->username}</b> 发出了一个 <b>{$totalAmount}</b> {$typeLabel}的礼包！共 {$totalCount} 份，先到先得，快去抢！{$btnHtml}",
            'is_secret' => false,
            'font_color' => $type === 'exp' ? '#6d28d9' : '#b91c1c',
            'action' => '',
            'sent_at' => now()->toDateTimeString(),
        ];
        $this->chatState->pushMessage($roomId, $msg);
        broadcast(new MessageSent($roomId, $msg));
        SaveMessageJob::dispatch($msg);

        // 广播红包事件（触发前端弹出红包卡片）
        broadcast(new RedPacketSent(
            roomId: $roomId,
            envelopeId: $envelope->id,
            senderUsername: $user->username,
            totalAmount: $totalAmount,
            totalCount: $totalCount,
            expireSeconds: self::EXPIRE_SECONDS,
            type: $type,
        ));

        return response()->json([
            'status' => 'success',
            'message' => "🧧 {$typeLabel}礼包已发出！{$totalAmount} {$typeLabel} · {$totalCount} 份",
        ]);
    }

    /**
     * 查询礼包当前状态（弹窗打开时实时刷新用）。
     *
     * 返回：剩余份数、是否已过期、当前用户是否已领取。
     *
     * @param  int  $envelopeId  红包 ID
     */
    public function status(int $envelopeId): JsonResponse
    {
        $envelope = RedPacketEnvelope::find($envelopeId);
        if (! $envelope) {
            return response()->json(['status' => 'error', 'message' => '红包不存在'], 404);
        }

        $user = Auth::user();
        $isExpired = $envelope->expires_at->isPast();
        $remainingCount = $envelope->remainingCount();
        $hasClaimed = RedPacketClaim::where('envelope_id', $envelopeId)
            ->where('user_id', $user->id)
            ->exists();

        // 若已过期但 status 尚未同步，顺手更新为 expired
        if ($isExpired && $envelope->status === 'active') {
            $envelope->update(['status' => 'expired']);
        }

        return response()->json([
            'status' => 'success',
            'remaining_count' => $remainingCount,
            'total_count' => $envelope->total_count,
            'envelope_status' => $isExpired ? 'expired' : $envelope->status,
            'is_expired' => $isExpired,
            'has_claimed' => $hasClaimed,
            'type' => $envelope->type ?? 'gold',
        ]);
    }

    /**
     * 用户抢礼包（先到先得）。
     *
     * 使用 Redis LPOP 原子操作获取数量，再写入数据库流水。
     * 重复领取通过 unique 约束保障幂等性。
     * 按红包 type 字段决定入账金币还是经验。
     *
     * @param  Request  $request  需包含 room_id
     * @param  int  $envelopeId  红包 ID
     */
    public function claim(Request $request, int $envelopeId): JsonResponse
    {
        $request->validate([
            'room_id' => 'required|integer',
        ]);

        $user = Auth::user();
        $roomId = (int) $request->input('room_id');

        // 加载红包记录
        $envelope = RedPacketEnvelope::find($envelopeId);
        if (! $envelope) {
            return response()->json(['status' => 'error', 'message' => '红包不存在'], 404);
        }

        // 检查红包是否可领
        if (! $envelope->isClaimable()) {
            return response()->json(['status' => 'error', 'message' => '红包已抢完或已过期'], 422);
        }

        // 检查是否已领取过
        $alreadyClaimed = RedPacketClaim::where('envelope_id', $envelopeId)
            ->where('user_id', $user->id)
            ->exists();
        if ($alreadyClaimed) {
            return response()->json(['status' => 'error', 'message' => '您已经领过这个礼包了'], 422);
        }

        // 从 Redis 原子 POP 一份数量
        $redisKey = "red_packet:{$envelopeId}:amounts";
        $amount = \Illuminate\Support\Facades\Redis::lpop($redisKey);
        if ($amount === null || $amount === false) {
            return response()->json(['status' => 'error', 'message' => '礼包已被抢完！'], 422);
        }
        $amount = (int) $amount;
        // 兼容旧记录（type 字段可能为 null）
        $envelopeType = $envelope->type ?? 'gold';

        // 事务：写领取记录 + 更新统计 + 货币入账
        try {
            DB::transaction(function () use ($envelope, $user, $amount, $roomId, $envelopeType): void {
                // 写领取记录（unique 约束保障不重复）
                RedPacketClaim::create([
                    'envelope_id' => $envelope->id,
                    'user_id' => $user->id,
                    'username' => $user->username,
                    'amount' => $amount,
                    'claimed_at' => now(),
                ]);

                // 更新红包统计
                $envelope->increment('claimed_count');
                $envelope->increment('claimed_amount', $amount);

                // 若已全部领完，关闭红包
                $envelope->refresh();
                if ($envelope->claimed_count >= $envelope->total_count) {
                    $envelope->update(['status' => 'completed']);
                }

                // 按类型入账（金币或经验）
                if ($envelopeType === 'exp') {
                    $this->currencyService->change(
                        $user,
                        'exp',
                        $amount,
                        CurrencySource::RED_PACKET_RECV_EXP,
                        "抢到礼包 {$amount} 经验（红包#{$envelope->id}）",
                        $roomId,
                    );
                } else {
                    $this->currencyService->change(
                        $user,
                        'gold',
                        $amount,
                        CurrencySource::RED_PACKET_RECV,
                        "抢到礼包 {$amount} 金币（红包#{$envelope->id}）",
                        $roomId,
                    );
                }
            });
        } catch (UniqueConstraintViolationException) {
            // 并发重复领取：将数量放回 Redis（补偿）
            \Illuminate\Support\Facades\Redis::rpush($redisKey, $amount);

            return response()->json(['status' => 'error', 'message' => '您已经领过这个礼包了'], 422);
        }

        // 重新读取红包统计，确保广播与响应使用的是最新剩余份数。
        $envelope->refresh();
        $remainingCount = $envelope->remainingCount();

        // 广播领取事件：房间内所有在线用户实时刷新剩余份数，领取者本人同步收到到账通知。
        broadcast(new RedPacketClaimed(
            claimer: $user,
            amount: $amount,
            envelopeId: $envelope->id,
            roomId: $envelope->room_id,
            remainingCount: $remainingCount,
            type: $envelopeType,
        ));

        // 在聊天室发送领取播报（所有人可见）
        $typeLabel = $envelopeType === 'exp' ? '经验' : '金币';
        $typeIcon = $envelopeType === 'exp' ? '✨' : '💰';
        $claimedMsg = [
            'id' => $this->chatState->nextMessageId($roomId),
            'room_id' => $roomId,
            'from_user' => '系统传音',
            'to_user' => '',
            'content' => "🧧 <b>{$user->username}</b> 抢到了 <b>{$amount}</b> {$typeLabel}礼包！{$typeIcon}",
            'is_secret' => false,
            'font_color' => $envelopeType === 'exp' ? '#6d28d9' : '#d97706',
            'action' => '',
            'sent_at' => now()->toDateTimeString(),
        ];
        $this->chatState->pushMessage($roomId, $claimedMsg);
        broadcast(new MessageSent($roomId, $claimedMsg));
        SaveMessageJob::dispatch($claimedMsg);

        $balanceField = $envelopeType === 'exp' ? 'exp_num' : 'jjb';
        $balanceNow = $user->fresh()->$balanceField;

        return response()->json([
            'status' => 'success',
            'amount' => $amount,
            'type' => $envelopeType,
            'remaining_count' => $remainingCount,
            'message' => "🧧 恭喜！您抢到了 {$amount} {$typeLabel}！当前{$typeLabel}：{$balanceNow}。",
        ]);
    }

    /**
     * 随机拆分礼包数量。
     *
     * 使用「二倍均值法」：每次随机数量不超过剩余均值的 2 倍，
     * 保证每份至少 1 且总额精确等于 totalAmount。
     *
     * @param  int  $total  总数量
     * @param  int  $count  份数
     * @return int[] 每份数量数组
     */
    private function splitAmount(int $total, int $count): array
    {
        $amounts = [];
        $remaining = $total;

        for ($i = 1; $i < $count; $i++) {
            $leftCount = $count - $i;
            $max = min((int) floor($remaining / $leftCount * 2), $remaining - $leftCount);
            $max = max(1, $max);
            $amount = random_int(1, $max);
            $amounts[] = $amount;
            $remaining -= $amount;
        }

        // 最后一份为剩余全部
        $amounts[] = $remaining;

        // 打乱顺序，避免后来者必得少
        shuffle($amounts);

        return $amounts;
    }

    /**
     * 按当前在职职务解析礼包红包配置。
     *
     * @return array{amount: int, count: int}
     */
    private function redPacketConfigForUser(User $user): array
    {
        $position = $user->activePosition?->position;
        $amount = (int) ($position?->red_packet_amount ?? self::DEFAULT_TOTAL_AMOUNT);
        $count = (int) ($position?->red_packet_count ?? self::DEFAULT_TOTAL_COUNT);

        if ($amount < 1) {
            $amount = self::DEFAULT_TOTAL_AMOUNT;
        }

        if ($count < 1 || $count > 100) {
            $count = self::DEFAULT_TOTAL_COUNT;
        }

        if ($amount < $count) {
            $amount = self::DEFAULT_TOTAL_AMOUNT;
            $count = self::DEFAULT_TOTAL_COUNT;
        }

        return [
            'amount' => $amount,
            'count' => $count,
        ];
    }
}
