package com.webchat.service;

import com.webchat.common.enums.ChatMessageTypeEnum;
import com.webchat.common.enums.RedPacketStatusEnum;
import com.webchat.common.enums.RedPacketTypeEnum;
import com.webchat.common.enums.RedisKeyEnum;
import com.webchat.common.enums.RoleCodeEnum;
import com.webchat.common.enums.WalletTransEventEnum;
import com.webchat.common.enums.WalletTransTypeEnum;
import com.webchat.common.exception.BusinessException;
import com.webchat.common.util.DateUtils;
import com.webchat.common.util.JsonUtil;
import com.webchat.common.util.TransactionSyncManagerUtil;
import com.webchat.controller.client.ChatWebSocket;
import com.webchat.domain.vo.dto.OpenRedPacketMessageDTO;
import com.webchat.domain.vo.request.SendRedPacketRequestVO;
import com.webchat.domain.vo.request.mess.ChatMessageRequestVO;
import com.webchat.domain.vo.response.OpenRedPacketVO;
import com.webchat.domain.vo.response.RedPacketDetailVO;
import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
import com.webchat.domain.vo.response.mess.ChatMessageResponseVO;
import com.webchat.repository.dao.IRedPacketDAO;
import com.webchat.repository.entity.RedPacketEntity;
import com.webchat.service.queue.OpenRedPacketQueue;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.unit.DataUnit;

import javax.websocket.Session;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author 程序员七七， https://www.coderutil.com网站作者
 * @date 2024/11/9 03:37
 */
@Service
public class RedPacketService {

    @Autowired
    private RedisService redisService;

    @Autowired
    private IRedPacketDAO redPacketDAO;
    @Autowired
    private UserWalletService userWalletService;
    @Autowired
    private UserService userService;
    @Autowired
    private ChatMessService chatMessService;
    @Autowired
    private OpenRedPacketQueue openRedPacketQueue;

    /**
     * 发红包
     *
     * @param sendRedPacketRequest
     * @return
     */
    @Transactional
    public Long sendRedPacket(SendRedPacketRequestVO sendRedPacketRequest) {

        // 红包发送人
        String sendUserId = sendRedPacketRequest.getSendUserId();
        // 取红包总金额
        BigDecimal totalMoney = sendRedPacketRequest.getTotalMoney();

        // 1. 钱包余额扣除，流转一条交易明细（支出类型doTrans方法会校验余额）
        boolean transResult = userWalletService.doTrans(WalletTransEventEnum.SEND_PACKET,
                                  sendUserId,
                                  sendRedPacketRequest.getReceiverUserId(),
                                  totalMoney);
        RedPacketEntity redPacket = null;
        RedPacketDetailVO redPacketDetailVO = null;
        try {
            // 2. 流转成功说明余额是够的，这里红包信息入库
            redPacket = this.saveRedPacket2DB(sendRedPacketRequest);

            // 3. 后续红包的信息要在客户端对话窗口内展示，为了保证请求性能我们这里先将红包信息刷入redis缓存
            // 抢红包是个高并发场景，红包信息的查询属于聚集性流量，不能直接将请求打到数据库
            redPacketDetailVO = this.refreshRedPacketDetailCache(redPacket);

            // 4. 注意这里我要先发消息通知，再发socket消息通知红包来了，避免用户未登录收不到提醒
            // 先发送红包来了消息通知
            ChatMessageRequestVO chatMessageRequest = this.buildChatMessageRequestVO(redPacketDetailVO);
            chatMessService.sendMess(chatMessageRequest);
            // 5. websocket发送红包消息通知
            RedPacketDetailVO finalRedPacketDetailVO = redPacketDetailVO;
            TransactionSyncManagerUtil.registerSynchronization(() -> {
                // 消息发送操作我们放到事务提交成功后异步执行
                this.doNotifyClientRedPacketMessage(finalRedPacketDetailVO);
            });
        } catch (Exception e) {
            if (transResult) {
                // 回滚trans钱包余额，或者清空钱包余额缓存
                // 这里场景是发红包，所以回滚一定加余额
                userWalletService.rollbackUserWalletBalanceCache(sendUserId, totalMoney);
            }
        }
        return redPacket == null ? null : redPacket.getId();
    }


    /**
     * 拆红包
     *
     * @param redPacketId
     * @param userId
     * @return
     */
    public String openRedPacket(Long redPacketId, String userId) {

        // 前置流程校验：判断用户是否拆分过当前红包，避免多次拆
        boolean isOpen = this.isOpenRedPacketByCache(redPacketId, userId);
        Assert.isTrue(!isOpen, "你已经拆分过当前红包");

        // 1. 走缓存获取红包配置信息
        RedPacketDetailVO redPacket = this.getRedPacketDetailCache(redPacketId);
        Assert.isTrue(redPacket != null && redPacket.getExpireTime() > DateUtils.getCurrentTimeMillis(), "红包不存在或者已过期");
        // 1.1 校验红包拆分权限（发给自己，发给自己所在群聊）
        Assert.isTrue(redPacket.getReceiverUserId().equals(userId) ||
                                userService.getGroupUserIdsFromCache(redPacket.getReceiverUserId()).contains(userId),
                        "不是发给你的红包，没有拆分权限");
        // 2. 抢红包，基于redis原子性操作特性来避免超卖问题
        // 一共几个包
        Integer totalCount = redPacket.getCount();
        // 先抢红包，抢到在开红包，抢不到则直接返回（这里抛异常返回）
        this.gradRedPacket(totalCount, redPacketId, userId);

        // 3. 红包算法
        BigDecimal redPacketMoney;
        if (RedPacketTypeEnum.NORMAL.getType().equals(redPacket.getType())) {
            // 3.1 如果是普通红包，则全金额领取
            redPacketMoney = redPacket.getTotalMoney();
        } else if (RedPacketTypeEnum.RANDOM.getType().equals(redPacket.getType())) {
            // 3.2 如果是拼手气红包，则走红包算法实时计算应该领取的金额
            // 走红包算法拆
            redPacketMoney = this.getRedPacketMoneyByAlgorithm(redPacket, userId);
        } else {
            throw new BusinessException("暂不支持的红包类型！");
        }

        // 4. 拆包后缓存刷新
        // 4.1 记录用户已经领取当前红包
        this.addUserReceiveRedPacketCache(redPacketId, userId, redPacketMoney);

        // 5. webSocket实时通知作者红包领取信息
        // 没时间了，这里先不发实时通知了，等后面有时间再细化（在发包场景我们已经有了类似功能，可以学习到这里的处理方法即可）


        // 6. 走消息队列处理后续钱包金额变化、消息通知
        OpenRedPacketMessageDTO messageQueueData = new OpenRedPacketMessageDTO();
        messageQueueData.setUserId(userId);
        messageQueueData.setTargetUserId(redPacket.getSendUserId());
        messageQueueData.setMoney(redPacketMoney);
        messageQueueData.setRedPacketId(redPacketId);
        messageQueueData.setTime(DateUtils.getCurrentTimeMillis());
        openRedPacketQueue.submit(messageQueueData);
        return redPacketMoney.toString();
    }


    private void addUserReceiveRedPacketCache(Long redPacketId, String userId, BigDecimal money) {
        // 1. 记录拆分用户
        String key = RedisKeyEnum.RED_PACKET_RECEIVER_COUNT.getKey(String.valueOf(redPacketId));
        redisService.sadd(key, userId);
        // 这里我偷个懒，直接永久缓存，暂不考虑从数据库重新拉取
        redisService.expire(key, -1L);

        // 2. zset记录拆分时间线及拆分明细数据
        long time = new Date().getTime();
        OpenRedPacketVO openRedPacketVO = new OpenRedPacketVO();
        openRedPacketVO.setUserId(userId);
        openRedPacketVO.setMoney(money.toString());
        openRedPacketVO.setTime(time);
        String openTimelineKey = RedisKeyEnum.RED_PACKET_RECEIVER_TIMELINE_COUNT.getKey(String.valueOf(redPacketId));
        redisService.zadd(openTimelineKey, JsonUtil.toJsonString(openTimelineKey), time, -1L);
    }

    /**
     * 校验用户是否拆分过红包
     *
     * @param redPacketId
     * @param userId
     * @return
     */
    private boolean isOpenRedPacketByCache(Long redPacketId, String userId) {
        String key = RedisKeyEnum.RED_PACKET_RECEIVER_COUNT.getKey(String.valueOf(redPacketId));
        return redisService.sIsMember(key, userId);
    }

    /**
     * 查询红包拆分记录
     *
     * @param redPacketId
     * @return
     */
    public List<OpenRedPacketVO> getOpenRecordsFromCache(Long redPacketId) {
        String openTimelineKey = RedisKeyEnum.RED_PACKET_RECEIVER_TIMELINE_COUNT.getKey(String.valueOf(redPacketId));
        Set<String> caches = redisService.zreverseRangeByScore(openTimelineKey, Long.MAX_VALUE, 0, Integer.MAX_VALUE);
        if (CollectionUtils.isEmpty(caches)) {
            return new ArrayList<>();
        }
        return caches.stream().map(cache -> JsonUtil.fromJson(cache, OpenRedPacketVO.class)).collect(Collectors.toList());
    }

    /**
     * 红包算法
     *
     * 这里我们采用”二倍均值法“实时拆分：
     *
     * 使用二倍均值法生成的随机数，每次随机金额会在 0.01 ~ 剩余平均值*2 之间。
     * 假设当前红包剩余金额为 10 元，剩余个数为 5，10/5 = 2，则当前用户可以抢到的红包金额为：0.01 ~ 4 元之间
     *
     * @param redPacket
     * @param userId
     * @return
     */
    private synchronized BigDecimal getRedPacketMoneyByAlgorithm(RedPacketDetailVO redPacket, String userId) {
        Long redPacketId = redPacket.getId();
        if (redPacket.getCount().equals(1)) {
            // 如果拼手机红包就配置了一份, 则直接全额拆分
            return redPacket.getTotalMoney();
        }
        // 取红包剩余可拆分金额
        String key = RedisKeyEnum.RED_PACKET_BALANCE_COUNT.getKey();
        String redPacketBalance = redisService.hget(key, String.valueOf(redPacketId));
        BigDecimal redPacketBalanceMoney;
        if (StringUtils.isBlank(redPacketBalance)) {
            redPacketBalanceMoney = redPacket.getTotalMoney();
        } else {
            redPacketBalanceMoney = new BigDecimal(redPacketBalance);
        }
        Assert.isTrue(redPacketBalanceMoney.compareTo(new BigDecimal("0")) > 0, "红包已拆完！");
        // 取红包剩余可拆分份数
        long count = this.getRedPacketGradCount(redPacket.getCount(), redPacketId);
        Assert.isTrue(count > 0, "红包已拆完！");
        if (count == 1) {
            // 剩余一份
            return redPacketBalanceMoney;
        }
        BigDecimal minMoney = new BigDecimal("0.01");
        BigDecimal maxMoney = redPacketBalanceMoney.divide(new BigDecimal(count), 2, RoundingMode.HALF_UP);
        // 在 minMoney ~ maxMoney 之间随机一个红包金额:randomMoney
        Random random = new Random();
        BigDecimal range = maxMoney.subtract(minMoney);
        BigDecimal factor = new BigDecimal(random.nextDouble());
        BigDecimal randomMoney = minMoney.add(range.multiply(factor));
        randomMoney = randomMoney.setScale(2, RoundingMode.HALF_UP);
        redisService.hset(key, String.valueOf(redPacketId), redPacketBalanceMoney.subtract(randomMoney).toString(), -1L);
        return randomMoney;
    }


    private void gradRedPacket(Integer totalCount, Long redPacketId, String userId) {
        String key = RedisKeyEnum.RED_PACKET_GRAD_COUNT.getKey();
        Long totalGradCount = redisService.hincrex(key, String.valueOf(redPacketId));
        Assert.isTrue(totalGradCount <= totalCount, "来晚了，红包已抢完");
    }

    private Long getRedPacketGradCount(Integer totalCount, Long redPacketId) {
        String key = RedisKeyEnum.RED_PACKET_GRAD_COUNT.getKey();
        String totalGradCount = redisService.hget(key, String.valueOf(redPacketId));
        long totalGradCountValue = StringUtils.isBlank(totalGradCount) ? 0 : Long.valueOf(totalGradCount);
        return Math.max(totalCount - totalGradCountValue, 0L);
    }


    private ChatMessageRequestVO buildChatMessageRequestVO(RedPacketDetailVO redPacketDetailVO) {
        ChatMessageRequestVO chatMessageRequestVO = new ChatMessageRequestVO();
        chatMessageRequestVO.setSenderId(redPacketDetailVO.getSendUserId());
        chatMessageRequestVO.setReceiverId(redPacketDetailVO.getReceiverUserId());
        chatMessageRequestVO.setType(ChatMessageTypeEnum.RED_PACKET.getType());
        // 红包id作为消息
        chatMessageRequestVO.setMessage(String.valueOf(redPacketDetailVO.getId()));
        return chatMessageRequestVO;
    }

    private void doNotifyClientRedPacketMessage(RedPacketDetailVO redPacketDetailVO) {
        String sendUserId = redPacketDetailVO.getSendUserId();
        String receiveUserId = redPacketDetailVO.getReceiverUserId();
        UserBaseResponseInfoVO sender = userService.getUserBaseInfoByUserId(sendUserId);
        UserBaseResponseInfoVO receiver = userService.getUserBaseInfoByUserId(receiveUserId);
        Integer roleCode = receiver.getRoleCode();
        Set<String> receiverUsers = new HashSet<>();
        ChatMessageResponseVO chatMessageResponse = new ChatMessageResponseVO();
        chatMessageResponse.setSenderId(sendUserId);
        chatMessageResponse.setSender(sender);
        chatMessageResponse.setType(ChatMessageTypeEnum.RED_PACKET.getType());
        chatMessageResponse.setRedPacketDetail(redPacketDetailVO);
        if (RoleCodeEnum.GROUP.getCode().equals(roleCode)) {
            // 接收人是群组，需要那群组代理红包发送人发送红包消息
            chatMessageResponse.setSenderId(receiveUserId);
            chatMessageResponse.setSender(receiver);
            chatMessageResponse.setProxySenderId(sendUserId);
            chatMessageResponse.setProxySender(sender);
            List<String> userIds = userService.getGroupUserIdsFromCache(receiveUserId);
            if (CollectionUtils.isNotEmpty(userIds)) {
                receiverUsers.addAll(userIds);
            }
        } else {
            receiverUsers.add(receiveUserId);
        }
        // 消息排除红包发送人
        receiverUsers.remove(sendUserId);
        for (String realReceiverUserId : receiverUsers) {
            Session session = ChatWebSocket.clients.get(realReceiverUserId).getSession();
            session.getAsyncRemote().sendText(JsonUtil.toJsonString(chatMessageResponse));
        }
    }

    private RedPacketDetailVO refreshRedPacketDetailCache(RedPacketEntity redPacketEntity) {
        // entity转红包缓存对象
        RedPacketDetailVO redPacketDetail = new RedPacketDetailVO();
        BeanUtils.copyProperties(redPacketEntity, redPacketDetail);
        redPacketDetail.setSendTime(redPacketEntity.getCreateDate().getTime());
        redPacketDetail.setExpireTime(redPacketEntity.getExpireDate().getTime());
        // 缓存
        String key = RedisKeyEnum.RED_PACKET_DETAIL_CACHE.getKey(String.valueOf(redPacketEntity.getId()));
        redisService.set(key, JsonUtil.toJsonString(redPacketDetail), RedisKeyEnum.RED_PACKET_DETAIL_CACHE.getExpireTime());
        return redPacketDetail;
    }

    /**
     * 查询红包详情
     *
     * @param id
     * @return
     */
    public RedPacketDetailVO getRedPacketDetailCache(Long id) {
        String key = RedisKeyEnum.RED_PACKET_DETAIL_CACHE.getKey(String.valueOf(id));
        String cache = redisService.get(key);
        if (StringUtils.isNotBlank(cache)) {
            return JsonUtil.fromJson(cache, RedPacketDetailVO.class);
        }
        return this.refreshRedPacketDetailCache(id);
    }

    public RedPacketDetailVO refreshRedPacketDetailCache(Long id) {
        RedPacketEntity redPacketEntity = redPacketDAO.findById(id).orElse(null);
        return refreshRedPacketDetailCache(redPacketEntity);
    }

    /**
     * 保存红包信息入库
     *
     * @param sendRedPacketRequest
     * @return
     */
    private RedPacketEntity saveRedPacket2DB(SendRedPacketRequestVO sendRedPacketRequest) {
        RedPacketEntity redPacketEntity = this.convertEntity(sendRedPacketRequest);
        return redPacketDAO.save(redPacketEntity);
    }

    private RedPacketEntity convertEntity(SendRedPacketRequestVO sendRedPacketRequest) {
        RedPacketEntity redPacketEntity = new RedPacketEntity();
        redPacketEntity.setSendUserId(sendRedPacketRequest.getSendUserId());
        redPacketEntity.setReceiverUserId(sendRedPacketRequest.getReceiverUserId()  );
        redPacketEntity.setType(sendRedPacketRequest.getType());
        redPacketEntity.setTotalMoney(sendRedPacketRequest.getTotalMoney());
        redPacketEntity.setCount(sendRedPacketRequest.getCount());
        redPacketEntity.setStatus(RedPacketStatusEnum.NORMAL.getStatus());
        redPacketEntity.setCreateBy(sendRedPacketRequest.getSendUserId());
        Date now = new Date();
        redPacketEntity.setCreateDate(now);
        // 默认24小时后过期 （不一定有时间实现，交给学生们来实现吧）
        redPacketEntity.setExpireDate(DateUtils.getNextHourTime(now, 24));
        return redPacketEntity;
    }
}
