
package com.tmerclub.cloud.coupon.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.tmerclub.cloud.api.auth.bo.UidInfoBO;
import com.tmerclub.cloud.api.coupon.bo.CouponGiveBO;
import com.tmerclub.cloud.api.coupon.bo.CouponIdAndCountBO;
import com.tmerclub.cloud.api.coupon.constant.PutOnStatus;
import com.tmerclub.cloud.api.coupon.constant.UserCouponStatus;
import com.tmerclub.cloud.api.coupon.dto.BindCouponDTO;
import com.tmerclub.cloud.api.coupon.dto.ReceiveCouponDTO;
import com.tmerclub.cloud.api.coupon.vo.CouponUserCountDataVO;
import com.tmerclub.cloud.common.cache.bo.MultiDecrementBO;
import com.tmerclub.cloud.common.cache.enums.MultiStockEnum;
import com.tmerclub.cloud.common.cache.manager.MultiRedisStockManager;
import com.tmerclub.cloud.common.constant.Constant;
import com.tmerclub.cloud.common.database.dto.PageDTO;
import com.tmerclub.cloud.common.database.vo.PageVO;
import com.tmerclub.cloud.common.exception.LuckException;
import com.tmerclub.cloud.common.leaf.manager.SegmentManager;
import com.tmerclub.cloud.common.mongodb.util.MongoPageUtil;
import com.tmerclub.cloud.common.order.vo.CouponOrderVO;
import com.tmerclub.cloud.common.response.ResponseEnum;
import com.tmerclub.cloud.common.rocketmq.config.RocketMqConstant;
import com.tmerclub.cloud.common.security.AuthUserContext;
import com.tmerclub.cloud.common.util.BeanUtil;
import com.tmerclub.cloud.constant.DistributedIdKey;
import com.tmerclub.cloud.coupon.bo.AddCouponStockMsgBO;
import com.tmerclub.cloud.coupon.bo.mongo.MongoCouponUserBO;
import com.tmerclub.cloud.coupon.constant.CouponStatus;
import com.tmerclub.cloud.coupon.constant.ValidTimeType;
import com.tmerclub.cloud.coupon.dto.CouponUserDTO;
import com.tmerclub.cloud.coupon.manager.CouponStockManager;
import com.tmerclub.cloud.coupon.mapper.CouponGiveLogMapper;
import com.tmerclub.cloud.coupon.mapper.CouponLockMapper;
import com.tmerclub.cloud.coupon.mapper.CouponMapper;
import com.tmerclub.cloud.coupon.mapper.CouponUserMapper;
import com.tmerclub.cloud.coupon.model.Coupon;
import com.tmerclub.cloud.coupon.model.CouponGiveLog;
import com.tmerclub.cloud.coupon.model.CouponLock;
import com.tmerclub.cloud.coupon.model.CouponUser;
import com.tmerclub.cloud.coupon.service.CouponUserService;
import com.tmerclub.cloud.coupon.vo.CouponAppVO;
import com.tmerclub.cloud.coupon.vo.CouponVO;
import com.tmerclub.cloud.coupon.vo.mongo.MongoCouponOrderVO;
import com.tmerclub.cloud.coupon.vo.mongo.MongoCouponUserCountVO;
import com.tmerclub.cloud.coupon.vo.mongo.MongoCouponUserVO;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.rocketmq.client.producer.SendStatus;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.*;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.stream.Collectors;

/**
 * 优惠券用户关联信息
 * @author YXF
 */
@Service
public class CouponUserServiceImpl implements CouponUserService {
    private static final Logger logger = LoggerFactory.getLogger(CouponUserServiceImpl.class);

    @Autowired
    private CouponUserMapper couponUserMapper;
    @Autowired
    private CouponGiveLogMapper couponGiveLogMapper;
    @Autowired
    private CouponMapper couponMapper;
    @Autowired
    private CouponLockMapper couponLockMapper;
    @Autowired
    private CouponStockManager couponStockManager;
    @Autowired
    private MongoTemplate mongoTemplate;
    @Autowired
    private SegmentManager segmentManager;
    @Autowired
    private MongoPageUtil mongoPageUtil;
    @Autowired
    private MultiRedisStockManager multiRedisStockManager;
    @Autowired
    private RocketMQTemplate sendAddCouponStockTemplate;

    @Override
    public void saveToMongodb(MongoCouponUserBO mongoCouponUserBO) {
        if (Objects.isNull(mongoCouponUserBO) || Objects.isNull(mongoCouponUserBO.getCouponUserId())) {
            throw new LuckException("领券记录信息不全");
        }
        mongoCouponUserBO.setCreateTime(new Date());
        mongoCouponUserBO.setUpdateTime(new Date());
        logger.info("领券记录保存至mongodb{}", mongoCouponUserBO);
        mongoTemplate.save(mongoCouponUserBO);
    }

    @Override
    public void saveBatchToMongodb(List<MongoCouponUserBO> list) {
        logger.info("批量保存领券记录到mongodb{}", list);
        for (MongoCouponUserBO mongoCouponUserBO : list) {
            if (Objects.isNull(mongoCouponUserBO) || Objects.isNull(mongoCouponUserBO.getCouponUserId())) {
                throw new LuckException("领券记录信息不全");
            }
            mongoCouponUserBO.setCreateTime(new Date());
            mongoCouponUserBO.setUpdateTime(new Date());
        }
        mongoTemplate.insertAll(list);
    }

    @Override
    public long getUserHasCouponCount(Long couponId, Long userId) {
        Integer receiveCount = multiRedisStockManager.getCouponReceiveCount(userId, couponId);
        if (Objects.isNull(receiveCount)) {
            // 兼容旧数据
            Criteria criteria = Criteria.where("couponId").is(couponId).and("userId").is(userId).and("isDelete").is(0);
            long count = mongoTemplate.count(new Query(criteria), MongoCouponUserBO.class);
            // 存入redis
            multiRedisStockManager.setCouponReceiveCount(userId, couponId, count);
            return count;
        }
        return receiveCount;
    }

    @Override
    public List<MongoCouponUserCountVO> getCouponsCountForUsers(List<Long> couponIds, List<Long> userIds) {
        Criteria criteria = Criteria.where("userId").in(userIds).and("couponId").in(couponIds).and("isDelete").is(0);
        GroupOperation groupOperation = Aggregation.group("userId", "couponId")
                .first("userId").as("userId")
                .first("couponId").as("couponId")
                .count().as("count");
        // 按顺序组合每一个聚合步骤
        TypedAggregation<MongoCouponUserCountVO> typedAggregation =
                Aggregation.newAggregation(
                        MongoCouponUserCountVO.class,
                        Aggregation.match(criteria),
                        groupOperation
                );

        // 执行聚合操作
        AggregationResults<MongoCouponUserCountVO> aggregationResults =
                mongoTemplate.aggregate(typedAggregation, MongoCouponUserCountVO.class);
        // 取出最终结果
        List<MongoCouponUserCountVO> list = aggregationResults.getMappedResults();
        if (CollUtil.isNotEmpty(list)) {
            for (MongoCouponUserCountVO mongoCouponUserCountVO : list) {
                Long userId = mongoCouponUserCountVO.getUserId();
                Long couponId = mongoCouponUserCountVO.getCouponId();
                Integer receiveCount = multiRedisStockManager.getCouponReceiveCount(userId, couponId);
                if (Objects.isNull(receiveCount)) {
                    // 初始化数据
                    multiRedisStockManager.setCouponReceiveCount(userId, couponId, mongoCouponUserCountVO.getCount().longValue());
                }
            }
        }
        return list;
    }

    @Override
    public void deleteUserCouponByCouponId(Long couponUserId) {
        Long userId = AuthUserContext.get().getUserId();
        Query query = new Query(Criteria.where("userId").is(userId).and("couponUserId").is(couponUserId));
        Update update = new Update();
        update.set("isDelete", 1);
        mongoTemplate.updateFirst(query, update, MongoCouponUserBO.class);
    }

    @Override
    public Map<String, Long> getCouponCountByStatus(Long userId) {
        Criteria criteria = Criteria.where("userId").is(userId).and("isDelete").is(0).and("status").is(UserCouponStatus.EFFECTIVE.getValue());
        ProjectionOperation projectionOperation = Aggregation.project()
                .and(ConditionalOperators.when(Criteria.where("shopId").is(Constant.PLATFORM_SHOP_ID)).then(1)
                        .otherwise(0)).as("platformCount")
                .and(ConditionalOperators.when(Criteria.where("shopId").ne(Constant.PLATFORM_SHOP_ID)).then(1)
                        .otherwise(0)).as("multishopCount");
        GroupOperation groupOperation = Aggregation.group()
                .sum("platformCount").as("platformCount")
                .sum("multishopCount").as("multishopCount")
                .count().as("allCount");
        // 按顺序组合每一个聚合步骤
        TypedAggregation<MongoCouponUserCountVO> typedAggregation =
                Aggregation.newAggregation(
                        MongoCouponUserCountVO.class,
                        Aggregation.match(criteria),
                        projectionOperation,
                        groupOperation
                );

        // 执行聚合操作
        AggregationResults<MongoCouponUserCountVO> aggregationResults =
                mongoTemplate.aggregate(typedAggregation, MongoCouponUserCountVO.class);
        // 取出最终结果
        List<MongoCouponUserCountVO> list = aggregationResults.getMappedResults();
        Map<String, Long> res = new HashMap<>(Constant.INITIAL_CAPACITY);
        if (CollUtil.isEmpty(list)) {
            res.put("allCount", 0L);
            res.put("platformCount", 0L);
            res.put("multishopCount", 0L);
            return res;
        }
        MongoCouponUserCountVO mongoCouponUserCountVO = list.get(0);
        res.put("allCount", mongoCouponUserCountVO.getAllCount());
        res.put("platformCount", mongoCouponUserCountVO.getPlatformCount());
        res.put("multishopCount", mongoCouponUserCountVO.getMultishopCount());
        return res;
    }

    @Override
    public void listByAndShopIdOrSpuId(List<CouponAppVO> coupons, List<Long> couponIds) {
        UidInfoBO uidInfoBO = AuthUserContext.get();
        if (CollectionUtil.isEmpty(coupons) || Objects.isNull(uidInfoBO)) {
            return;
        }
        Criteria criteria = Criteria.where("userId").is(uidInfoBO.getUserId()).and("couponId").in(couponIds);
        ProjectionOperation projectionOperation = Aggregation.project("couponId")
                .and(ConditionalOperators.when(Criteria.where("status").is(UserCouponStatus.EFFECTIVE.getValue())
                        .and("isDelete").is(0)).then(1).otherwise(0)).as("count");
        GroupOperation groupOperation = Aggregation.group("couponId")
                .first("couponId").as("couponId")
                .sum("count").as("count");
        // 按顺序组合每一个聚合步骤
        TypedAggregation<MongoCouponUserCountVO> typedAggregation =
                Aggregation.newAggregation(
                        MongoCouponUserCountVO.class,
                        Aggregation.match(criteria),
                        projectionOperation,
                        groupOperation
                );

        // 执行聚合操作
        AggregationResults<MongoCouponUserCountVO> aggregationResults =
                mongoTemplate.aggregate(typedAggregation, MongoCouponUserCountVO.class);
        // 取出最终结果
        List<MongoCouponUserCountVO> list = aggregationResults.getMappedResults();
        Map<Long, MongoCouponUserCountVO> couponUserMap = list.stream().collect(Collectors.toMap(MongoCouponUserCountVO::getCouponId, c -> c));
        for (CouponAppVO coupon : coupons) {
            coupon.setSpus(null);
            if (!couponUserMap.containsKey(coupon.getCouponId())) {
                continue;
            }
            MongoCouponUserCountVO couponUserCount = couponUserMap.get(coupon.getCouponId());
            if (Objects.nonNull(couponUserCount.getCount()) && couponUserCount.getCount() > 0) {
                coupon.setHasReceive(couponUserCount.getCount());
            }
        }
        coupons.sort(Comparator.comparing(CouponAppVO::getHasReceive));
    }

    @Override
    public Long countCanUseCoupon(Long userId) {
        Criteria criteria = Criteria.where("isDelete").is(0).and("userId").is(userId).and("status").is(UserCouponStatus.EFFECTIVE.getValue());
        return mongoTemplate.count(new Query(criteria), MongoCouponUserBO.class);
    }

    @Override
    public CouponUserCountDataVO countCouponUserByUserId(Long userId, Long shopId) {
        Criteria criteria = Criteria.where("userId").is(userId).and("isDelete").is(0);
        if (!Objects.equals(shopId, Constant.PLATFORM_SHOP_ID)) {
            criteria.and("shopId").is(shopId);
        }
        ProjectionOperation projectionOperation = Aggregation.project()
                .and(ConditionalOperators.when(Criteria.where("status").is(UserCouponStatus.INVALID.getValue())).then(1)
                        .otherwise(0)).as("couponExpiredNums")
                .and(ConditionalOperators.when(Criteria.where("status").is(UserCouponStatus.EFFECTIVE.getValue())).then(1)
                        .otherwise(0)).as("couponUsableNums")
                .and(ConditionalOperators.when(Criteria.where("status").is(UserCouponStatus.USED.getValue())).then(1)
                        .otherwise(0)).as("couponUsedNums");
        GroupOperation groupOperation = Aggregation.group()
                .sum("couponExpiredNums").as("couponExpiredNums")
                .sum("couponUsableNums").as("couponUsableNums")
                .sum("couponUsedNums").as("couponUsedNums");
        // 按顺序组合每一个聚合步骤
        TypedAggregation<MongoCouponUserCountVO> typedAggregation =
                Aggregation.newAggregation(
                        MongoCouponUserCountVO.class,
                        Aggregation.match(criteria),
                        projectionOperation,
                        groupOperation
                );

        // 执行聚合操作
        AggregationResults<MongoCouponUserCountVO> aggregationResults =
                mongoTemplate.aggregate(typedAggregation, MongoCouponUserCountVO.class);
        // 取出最终结果
        List<MongoCouponUserCountVO> list = aggregationResults.getMappedResults();
        CouponUserCountDataVO couponUserCountDataVO = new CouponUserCountDataVO();
        if (CollUtil.isNotEmpty(list)) {
            MongoCouponUserCountVO mongoCouponUserCountVO = list.get(0);
            couponUserCountDataVO.setCouponExpiredNums(mongoCouponUserCountVO.getCouponExpiredNums());
            couponUserCountDataVO.setCouponUsableNums(mongoCouponUserCountVO.getCouponUsableNums());
            couponUserCountDataVO.setCouponUsedNums(mongoCouponUserCountVO.getCouponUsedNums());
        }
        return couponUserCountDataVO;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void giveCoupon(CouponGiveBO couponGiveBO) {
        Integer bizType = couponGiveBO.getBizType();
        Long bizId = couponGiveBO.getBizId();
        Integer count = couponGiveLogMapper.count(bizType, bizId);
        if (count > 0) {
            return;
        }
        Long userId = couponGiveBO.getUserId();
        List<CouponIdAndCountBO> couponInfos = couponGiveBO.getCouponInfos();
        // 优惠券id
        List<Long> couponIds = couponInfos.stream().map(CouponIdAndCountBO::getCouponId).collect(Collectors.toList());
        Map<Long, Integer> couponIdWithCountMap = couponInfos.stream().collect(Collectors.toMap(CouponIdAndCountBO::getCouponId, CouponIdAndCountBO::getCount));
        // 优惠券
        List<Coupon> coupons = couponMapper.getListByCouponIds(couponIds);
        Map<Long, Integer> limitNumMap = coupons.stream().collect(Collectors.toMap(Coupon::getCouponId, Coupon::getLimitNum));
        List<MongoCouponUserBO> couponUsers = new ArrayList<>(Constant.INITIAL_CAPACITY);
        for (Coupon coupon : coupons) {
            if (!Objects.equals(coupon.getStatus(), CouponStatus.NO_OVERDUE.value()) || !Objects.equals(coupon.getPutonStatus(), PutOnStatus.PUT_ON.value())) {
                return;
            }
            // 超出领券数量限制，就不要继续发给用户
            long couponCount = getUserHasCouponCount(coupon.getCouponId(), userId);
            if (couponCount >= coupon.getLimitNum()) {
                logger.warn("用户：{}，的优惠券：{}已达个人领取上限，无法继续领取", userId, coupon.getCouponId());
                continue;
            }
            // 还剩下可以领取的数量
            int subStock = coupon.getLimitNum() - (int) couponCount;
            // 判断优惠券当前数量，给用户发放优惠券
            Integer couponNum = couponIdWithCountMap.get(coupon.getCouponId());
            Integer couponStock = couponStockManager.getStockByCouponId(coupon.getCouponId());
            // 优惠券数量有限，送完即止
            if (couponStock < couponNum) {
                couponNum = couponStock;
            }
            // 只能领取这么多
            couponNum = Math.min(subStock, couponNum);
            // 根据赠送的数量给用户发优惠券
            for (int i = 0; i < couponNum; i++) {
                MongoCouponUserBO couponUser = getCouponUser(new Date(), userId, coupon);
                couponUsers.add(couponUser);
            }
        }

        // 这里有唯一索引，所以不用怕重复
        CouponGiveLog couponGiveLog = new CouponGiveLog();
        couponGiveLog.setBizId(bizId);
        couponGiveLog.setBizType(bizType);
        couponGiveLogMapper.save(couponGiveLog);
        if (CollUtil.isNotEmpty(couponUsers)) {
            Iterator<MongoCouponUserBO> iterator = couponUsers.iterator();
            while (iterator.hasNext()) {
                MongoCouponUserBO couponUser = iterator.next();
                Integer limitNum = limitNumMap.get(couponUser.getCouponId());
                MultiDecrementBO multiDecrementBO = new MultiDecrementBO(MultiStockEnum.COUPON, userId, couponUser.getCouponUserId(), couponUser.getCouponId(),
                        couponUser.getCouponId(), 1, limitNum, null, null);
                if (couponStockManager.decrStocks(multiDecrementBO)) {
                    // 库存扣减成功
                    continue;
                }
                iterator.remove();
            }
        }
        if (CollUtil.isEmpty(couponUsers)) {
            return;
        }
        // 存储领券记录和删除锁定记录
        couponStockManager.saveLogAndDeleteLock(couponUsers);
    }

    @Override
    public void deleteUnValidTimeCoupons(Date date) {
        Query query = new Query(Criteria.where("status").is(UserCouponStatus.INVALID.getValue()).and("userEndTime").lt(date));
        mongoTemplate.remove(query, MongoCouponUserBO.class);
    }

    @Override
    public void updateStatusByTime() {
        // 失效未使用的优惠券，已使用的不需要失效
        Date now = new Date();
        Query query = new Query(Criteria.where("status").is(UserCouponStatus.EFFECTIVE.getValue()).and("userEndTime").lt(now));
        Update update = new Update();
        update.set("status", UserCouponStatus.INVALID.getValue());
        mongoTemplate.updateMulti(query, update, MongoCouponUserBO.class);
    }

    @Override
    public PageVO<MongoCouponUserVO> getPageByUserId(PageDTO pageDTO, CouponUserDTO couponUserDTO) {
        Criteria criteria = Criteria.where("userId").is(couponUserDTO.getUserId()).and("isDelete").is(0);
        if (Objects.nonNull(couponUserDTO.getStatus())) {
            criteria.and("status").is(couponUserDTO.getStatus());
        }
        if (Objects.nonNull(couponUserDTO.getShopId())) {
            criteria.and("shopId").is(couponUserDTO.getShopId());
        }
        Query query = new Query(criteria).with(Sort.by(Sort.Direction.DESC, "receiveTime"));
        PageVO<MongoCouponUserVO> page = mongoPageUtil.doPage(MongoCouponUserVO.class, pageDTO, query);
        if (CollUtil.isNotEmpty(page.getList())) {
            List<Long> couponIds = page.getList().stream().map(MongoCouponUserVO::getCouponId).collect(Collectors.toList());
            List<Coupon> couponList = couponMapper.getListByCouponIds(couponIds);
            Map<Long, Coupon> couponMap = couponList.stream().collect(Collectors.toMap(Coupon::getCouponId, c -> c));
            for (MongoCouponUserVO mongoCouponUserVO : page.getList()) {
                Coupon coupon = couponMap.get(mongoCouponUserVO.getCouponId());
                if (Objects.isNull(coupon)) {
                    continue;
                }
                CouponVO couponVO = BeanUtil.map(coupon, CouponVO.class);
                mongoCouponUserVO.setCouponVO(couponVO);
            }
        }
        return page;
    }

    @Override
    public Collection<Long> batchBindCoupon(List<BindCouponDTO> bindCouponDTOList) {
        if (CollUtil.isEmpty(bindCouponDTOList)) {
            return null;
        }
        Set<Long> couponIds = new HashSet<>(Constant.INITIAL_CAPACITY);
        for (BindCouponDTO bindCouponDTO : bindCouponDTOList) {
            couponIds.addAll(bindCouponDTO.getCouponIds());
        }
        // 优惠券
        List<Coupon> coupons = couponMapper.getListByCouponIds(new ArrayList<>(couponIds));
        Map<Long, Coupon> couponMap = coupons.stream().collect(Collectors.toMap(Coupon::getCouponId, (k) -> k));

        // 统计用户已经领取了多少个优惠券（领券有限制）
        List<Long> userIds = bindCouponDTOList.stream().map(BindCouponDTO::getUserId).collect(Collectors.toList());
        List<MongoCouponUserCountVO> couponsCountForUsers = getCouponsCountForUsers(new ArrayList<>(couponIds), userIds);
        Map<Long, List<Long>> couponUserIdMap = new HashMap<>(Constant.INITIAL_CAPACITY);
        // 优惠券发了多少个
        Map<Long, Integer> couponSendMap = new HashMap<>(Constant.INITIAL_CAPACITY);
        for (BindCouponDTO bindCouponDTO : bindCouponDTOList) {
            Long userId = bindCouponDTO.getUserId();
            List<Long> userCouponIds = bindCouponDTO.getCouponIds();
            for (Long couponId : userCouponIds) {
                Coupon coupon = couponMap.get(couponId);
                // 超出领券数量限制，就不要继续发给用户
                boolean overLimit = judgeOverLimit(couponsCountForUsers, couponSendMap, coupon, userId, couponId);
                if (overLimit) {
                    continue;
                }
                if (couponSendMap.containsKey(couponId)) {
                    couponSendMap.put(couponId, couponSendMap.get(couponId) + 1);
                } else {
                    couponSendMap.put(couponId, 1);
                }
                // 添加优惠券与用户关联
                if (couponUserIdMap.containsKey(couponId)) {
                    List<Long> mapUserIds = couponUserIdMap.get(couponId);
                    mapUserIds.add(userId);
                    couponUserIdMap.put(couponId, mapUserIds);
                } else {
                    couponUserIdMap.put(couponId, Collections.singletonList(userId));
                }
            }
        }

        handleUpdateCouponStock(couponSendMap, couponUserIdMap, bindCouponDTOList, couponMap);
        // 返回需要清除缓存的优惠券id
        return couponSendMap.keySet();
    }

    private MongoCouponUserBO getCouponUser(Date now, Long userId, Coupon coupon) {
        MongoCouponUserBO couponUser = new MongoCouponUserBO();
        couponUser.setCouponUserId(segmentManager.getSegmentId(DistributedIdKey.tmerclub_COUPON_USER));
        couponUser.setCouponId(coupon.getCouponId());
        couponUser.setUserId(userId);
        couponUser.setShopId(coupon.getShopId());
        couponUser.setReceiveTime(now);
        couponUser.setStatus(UserCouponStatus.EFFECTIVE.getValue());
        couponUser.setIsDelete(0);
        if (Objects.equals(ValidTimeType.FIXED.value(), coupon.getValidTimeType())) {
            // 优惠券已过期
            if (coupon.getEndTime().getTime() < System.currentTimeMillis()) {
                couponUser.setStatus(UserCouponStatus.INVALID.getValue());
            }
            couponUser.setUserStartTime(coupon.getStartTime());
            couponUser.setUserEndTime(coupon.getEndTime());

        }
        // 生效时间类型为领取后生效
        if (Objects.equals(ValidTimeType.RECEIVE.value(), coupon.getValidTimeType())) {
            if (Objects.isNull(coupon.getAfterReceiveDays())) {
                coupon.setAfterReceiveDays(0);
            }
            if (Objects.isNull(coupon.getValidDays())) {
                coupon.setValidDays(0);
            }
            couponUser.setUserStartTime(DateUtils.addDays(DateUtil.beginOfDay(now), coupon.getAfterReceiveDays()));
            couponUser.setUserEndTime(DateUtils.addDays(couponUser.getUserStartTime(), coupon.getValidDays()));
        }
        return couponUser;
    }

    private boolean judgeOverLimit(List<MongoCouponUserCountVO> couponsCountForUsers, Map<Long, Integer> couponSendMap, Coupon coupon, Long userId, Long couponId) {
        if (Objects.isNull(coupon)) {
            return true;
        }
        if (!(Objects.equals(coupon.getStatus(), CouponStatus.NO_OVERDUE.value()) && Objects.equals(coupon.getPutonStatus(), PutOnStatus.PUT_ON.value()))) {
            return true;
        }
        coupon.setStocks(couponStockManager.getStockByCouponId(couponId));
        if (couponSendMap.containsKey(couponId) && couponSendMap.get(couponId) >= coupon.getStocks()) {
            return true;
        }
        // 超出领券数量限制
        int num = 0;
        if (CollUtil.isNotEmpty(couponsCountForUsers)) {
            MongoCouponUserCountVO couponUserCountDTO = couponsCountForUsers.stream()
                    .filter(item -> Objects.equals(userId, item.getUserId()) && Objects.equals(couponId, item.getCouponId())).findAny().orElse(null);
            if (Objects.nonNull(couponUserCountDTO)) {
                num = couponUserCountDTO.getCount();
            }
        }
        if (num >= coupon.getLimitNum()) {
            return true;
        }
        return couponSendMap.containsKey(couponId) && (couponSendMap.get(couponId) + num) >= coupon.getLimitNum();
    }

    /**
     * 将减少优惠券的map转为数组集合
     * @param couponSendMap 发送优惠券的数量
     */
    private void handleUpdateCouponStock(Map<Long, Integer> couponSendMap, Map<Long, List<Long>> couponUserIdMap,
                                         List<BindCouponDTO> bindCouponDTOList, Map<Long, Coupon> couponMap) {
        Date now = new Date();
        List<MongoCouponUserBO> couponUsers = new ArrayList<>(Constant.INITIAL_CAPACITY);
        for (Long couponId : couponSendMap.keySet()) {
            Integer count = Math.abs(couponSendMap.get(couponId));
            logger.info("用户绑定优惠券{}，扣减库存{}", couponId, count);
            List<Long> userIds = couponUserIdMap.get(couponId);
            Integer subStock = 0;
            for (Long userId : userIds) {
                Coupon coupon = couponMap.get(couponId);
                MongoCouponUserBO couponUser = getCouponUser(now, userId, coupon);
                MultiDecrementBO multiDecrementBO = new MultiDecrementBO(MultiStockEnum.COUPON, userId, couponUser.getCouponUserId(), couponId,
                        couponId, 1, coupon.getLimitNum(), null);
                if (couponStockManager.decrStocks(multiDecrementBO)) {
                    // 库存扣减成功
                    couponUsers.add(couponUser);
                    ++subStock;
                }
            }
            logger.info("用户绑定优惠券{}，实际扣减库存{}", couponId, subStock);
        }

        if (CollUtil.isEmpty(couponUsers)) {
            return;
        }
        // 存储领券记录和删除锁定记录
        couponStockManager.saveLogAndDeleteLock(couponUsers);
    }

    @Override
    public void addCouponStock(Long couponId, Integer stock, List<MongoCouponUserBO> userIdList) {
        for (MongoCouponUserBO couponUserBO : userIdList) {
            MultiDecrementBO multiDecrementBO = new MultiDecrementBO(MultiStockEnum.COUPON, couponUserBO.getUserId(), couponUserBO.getCouponUserId(), couponId,
                    couponId, 1, null, null);
            multiRedisStockManager.addCouponStock(multiDecrementBO);
        }
    }

    @Override
    public Collection<Long> batchDeleteUserCoupon(List<BindCouponDTO> bindCouponDTOList) {
        // 删除用户优惠券
        // 可能有很多的优惠券，把用户有效的未使用的优惠券删除了
        if (CollUtil.isEmpty(bindCouponDTOList)) {
            return null;
        }
        Set<Long> couponIds = new HashSet<>(Constant.INITIAL_CAPACITY);
        for (BindCouponDTO bindCouponDTO : bindCouponDTOList) {
            couponIds.addAll(bindCouponDTO.getCouponIds());
        }
        List<Long> userIds = bindCouponDTOList.stream().map(BindCouponDTO::getUserId).distinct().collect(Collectors.toList());
        // 将用户有效的优惠券筛选出来
        List<MongoCouponUserBO> list = getByCouponIdsAndUserIds(userIds, new ArrayList<>(couponIds));
        if (CollUtil.isEmpty(list)) {
            return null;
        }
        Map<Long, List<MongoCouponUserBO>> couponUserMap = list.stream().collect(Collectors.groupingBy(MongoCouponUserBO::getUserId));
        // 筛选出待确认，已锁定的优惠券是哪些
        List<Long> couponUserIdList = list.stream().map(MongoCouponUserBO::getCouponUserId).collect(Collectors.toList());
        List<CouponLock> couponLocks = couponLockMapper.getUnConfirmAndLockCoupon(couponUserIdList);
        List<Long> unCouponUserIds = couponLocks.stream().map(CouponLock::getCouponUserId).distinct().collect(Collectors.toList());
        // 筛选出可以被删除的优惠券
        List<Long> couponUserIds = couponUserIdList.stream().filter(item -> !unCouponUserIds.contains(item)).collect(Collectors.toList());
        if (CollUtil.isEmpty(couponUserIds)) {
            return null;
        }
        // 优惠券退还数量
        Map<Long, Integer> couponReturnStockMap = new HashMap<>(Constant.INITIAL_CAPACITY);
        // 优惠券与用户id
        Map<Long, List<MongoCouponUserBO>> couponUserIdMap = new HashMap<>(Constant.INITIAL_CAPACITY);
        // 处理需要删除用户的优惠券是那些
        Set<Long> couponUserIdDeleteList = new HashSet<>(Constant.INITIAL_CAPACITY);
        for (BindCouponDTO bindCouponDTO : bindCouponDTOList) {
            Long userId = bindCouponDTO.getUserId();
            List<Long> couponIdList = bindCouponDTO.getCouponIds();
            List<MongoCouponUserBO> couponUsers = couponUserMap.get(userId);
            if (CollUtil.isEmpty(couponUsers)) {
                continue;
            }
            for (MongoCouponUserBO couponUser : couponUsers) {
                Long couponId = couponUser.getCouponId();
                Long couponUserId = couponUser.getCouponUserId();
                // 优惠券不在已解锁状态，就不能删除，直接跳过
                if (!couponUserIds.contains(couponUserId)) {
                    continue;
                }
                if (couponIdList.contains(couponId)) {
                    couponUserIdDeleteList.add(couponUserId);
                    // 移除要被删除的优惠券的id列表中的其中一个
                    couponIdList.remove(couponId);
                    if (couponReturnStockMap.containsKey(couponId)) {
                        couponReturnStockMap.put(couponId, couponReturnStockMap.get(couponId) + 1);
                    } else {
                        couponReturnStockMap.put(couponId, 1);
                    }
                    // 添加优惠券与用户关联
                    if (couponUserIdMap.containsKey(couponId)) {
                        List<MongoCouponUserBO> mapUserIds = couponUserIdMap.get(couponId);
                        mapUserIds.add(couponUser);
                        couponUserIdMap.put(couponId, mapUserIds);
                    } else {
                        couponUserIdMap.put(couponId, Collections.singletonList(couponUser));
                    }
                }
            }
            List<MongoCouponUserBO> collect = couponUsers.stream().filter(item -> !couponUserIdDeleteList.contains(item.getCouponUserId())).collect(Collectors.toList());
            couponUserMap.put(userId, collect);
        }
        if (CollUtil.isEmpty(couponUserIdDeleteList)) {
            return null;
        }
        DateTime now = DateUtil.date();
        // 批量删除用户优惠券
        batchDeleteUserCoupon(new ArrayList<>(couponUserIdDeleteList), now);

        // mq保证事务 redis还原库存
        AddCouponStockMsgBO addCouponStockMsgBO = new AddCouponStockMsgBO();
        addCouponStockMsgBO.setCouponReturnStockMap(couponReturnStockMap);
        addCouponStockMsgBO.setCouponUserIdMap(couponUserIdMap);
        SendStatus sendStatus = sendAddCouponStockTemplate.syncSend(RocketMqConstant.ADD_COUPON_STOCK_TOPIC, new GenericMessage<>(addCouponStockMsgBO)).getSendStatus();
        if (!Objects.equals(sendStatus, SendStatus.SEND_OK)) {
            throw new LuckException(ResponseEnum.EXCEPTION);
        }
        // 返回需要清除缓存的优惠券id列表
        return couponReturnStockMap.keySet();
    }

    private void batchDeleteUserCoupon(List<Long> couponUserIds, Date now) {
        Query query = new Query(Criteria.where("_id").in(couponUserIds));
        Update update = new Update();
        update.set("isDelete", 1);
        update.set("updateTime", now);
        mongoTemplate.updateMulti(query, update, MongoCouponUserBO.class);
    }

    private List<MongoCouponUserBO> getByCouponIdsAndUserIds(List<Long> userIds, List<Long> couponIds) {
        Criteria criteria = Criteria.where("isDelete").is(0).and("status").is(UserCouponStatus.EFFECTIVE.getValue())
                .and("userId").in(userIds).and("couponId").in(couponIds);
        return mongoTemplate.find(new Query(criteria), MongoCouponUserBO.class);
    }

    @Override
    public long countUnUseCoupon(Long couponId) {
        Criteria criteria = Criteria.where("couponId").is(couponId).and("isDelete").is(0)
                .and("status").is(UserCouponStatus.EFFECTIVE.getValue());
        return mongoTemplate.count(new Query(criteria), MongoCouponUserBO.class);
    }

    @Override
    public List<Long> userReceiveCouponCount(ReceiveCouponDTO receiveCouponDTO) {
        Criteria criteria = Criteria.where("receiveTime").gte(receiveCouponDTO.getStartTime()).lte(receiveCouponDTO.getEndTime())
                .and("userId").in(receiveCouponDTO.getUserIds());
        GroupOperation groupOperation = Aggregation.group("userId").first("userId").as("userId");
        // 按顺序组合每一个聚合步骤
        TypedAggregation<MongoCouponUserCountVO> typedAggregation =
                Aggregation.newAggregation(
                        MongoCouponUserCountVO.class,
                        Aggregation.match(criteria),
                        groupOperation
                );

        // 执行聚合操作
        AggregationResults<MongoCouponUserCountVO> aggregationResults =
                mongoTemplate.aggregate(typedAggregation, MongoCouponUserCountVO.class);
        List<MongoCouponUserCountVO> mappedResults = aggregationResults.getMappedResults();
        if (CollUtil.isEmpty(mappedResults)) {
            return new ArrayList<>();
        }
        return mappedResults.stream().map(MongoCouponUserCountVO::getUserId).collect(Collectors.toList());
    }

    @Override
    public void batchUpdateUserCouponStatus(Integer status, List<Long> userCouponIds) {
        Criteria criteria = Criteria.where("_id").in(userCouponIds);
        Update update = new Update();
        update.set("status", status);
        mongoTemplate.updateMulti(new Query(criteria), update, MongoCouponUserBO.class);
    }

    @Override
    public void reductionCoupon(List<Long> couponUserIds) {
        Query query = new Query(Criteria.where("_id").in(couponUserIds).and("shopId").ne(Constant.PLATFORM_SHOP_ID));
        Update update = new Update();
        update.set("status", 1);
        mongoTemplate.updateFirst(query, update, MongoCouponUserBO.class);
    }

    @Override
    public List<Long> countMemberCouponByParam(Long shopId, Date startTime, Date endTime) {
        Criteria criteria = new Criteria();
        if (Objects.nonNull(shopId) && !Objects.equals(shopId, Constant.PLATFORM_SHOP_ID)) {
            criteria.and("shopId").is(shopId);
        }
        if (Objects.nonNull(startTime) && Objects.nonNull(endTime)) {
            criteria.and("receiveTime").gte(startTime).lte(endTime);
        }
        List<Long> userIds = mongoTemplate.findDistinct(new Query(criteria), "userId", MongoCouponUserBO.class, Long.class);
        if (Objects.isNull(userIds)) {
            userIds = new ArrayList<>();
        }
        return userIds;
    }

    @Override
    public PageVO<CouponAppVO> pageUserCoupon(PageDTO page, Long userId, Long type, Integer status) {
        PageVO<CouponAppVO> res = new PageVO<>();
        res.setList(new ArrayList<>());
        Long shopCoupon = 2L;
        Criteria criteria = Criteria.where("userId").is(userId).and("isDelete").is(0).and("status").is(status);
        ;
        if (Objects.equals(type, 1L)) {
            criteria.and("shopId").is(Constant.PLATFORM_SHOP_ID);
        } else if (Objects.equals(type, shopCoupon)) {
            criteria.and("shopId").ne(Constant.PLATFORM_SHOP_ID);
        }
        Query query = new Query(criteria);
        if (Objects.equals(status, UserCouponStatus.EFFECTIVE.getValue())) {
            query.with(Sort.by(Sort.Direction.DESC, "createTime"));
        } else {
            query.with(Sort.by(Sort.Direction.DESC, "updateTime"));
        }
        PageVO<MongoCouponUserBO> pageVO = mongoPageUtil.doPage(MongoCouponUserBO.class, page, query);
        if (CollUtil.isNotEmpty(pageVO.getList())) {
            List<CouponAppVO> list = new ArrayList<>(Constant.INITIAL_CAPACITY);
            List<CouponAppVO> couponAppList = BeanUtil.mapAsList(pageVO.getList(), CouponAppVO.class);
            List<Long> couponIds = couponAppList.stream().map(CouponAppVO::getCouponId).collect(Collectors.toList());
            List<Coupon> couponList = couponMapper.getListByCouponIds(couponIds);
            Map<Long, Coupon> couponMap = couponList.stream().collect(Collectors.toMap(Coupon::getCouponId, (k) -> k));
            Iterator<CouponAppVO> iterator = couponAppList.iterator();
            while (iterator.hasNext()) {
                CouponAppVO couponAppVO = iterator.next();
                Coupon coupon = couponMap.get(couponAppVO.getCouponId());
                if (Objects.isNull(coupon)) {
                    iterator.remove();
                }
                CouponAppVO couponData = BeanUtil.map(coupon, CouponAppVO.class);
                couponData.setCouponUserId(couponAppVO.getCouponUserId());
                couponData.setStatus(couponAppVO.getStatus());
                couponData.setReceiveTime(couponAppVO.getReceiveTime());
                list.add(couponData);
            }
            res.setList(list);
        }
        res.setTotal(pageVO.getTotal());
        res.setPages(pageVO.getPages());
        return res;
    }

    @Override
    public List<CouponAppVO> setCouponUserInfo(List<CouponAppVO> couponList, Long userId) {
        List<Long> couponIds = couponList.stream().map(CouponAppVO::getCouponId).collect(Collectors.toList());
        Criteria criteria = Criteria.where("userId").is(userId);
        ProjectionOperation projectionOperation = Aggregation.project("couponId")
                .and(ConditionalOperators.when(Criteria.where("status").is(UserCouponStatus.EFFECTIVE.getValue()).and("isDelete").is(0))
                        .then(1).otherwise(0)).as("hasReceive");
        GroupOperation groupOperation = Aggregation.group("couponId")
                .first("couponId").as("couponId")
                .sum("hasReceive").as("hasReceive");
        // 按顺序组合每一个聚合步骤
        TypedAggregation<MongoCouponUserCountVO> typedAggregation =
                Aggregation.newAggregation(
                        MongoCouponUserCountVO.class,
                        Aggregation.match(criteria),
                        projectionOperation,
                        groupOperation
                );

        // 执行聚合操作
        AggregationResults<MongoCouponUserCountVO> aggregationResults =
                mongoTemplate.aggregate(typedAggregation, MongoCouponUserCountVO.class);
        // 取出最终结果
        List<MongoCouponUserCountVO> list = aggregationResults.getMappedResults();
        Map<Long, MongoCouponUserCountVO> infoMap = list.stream().collect(Collectors.toMap(MongoCouponUserCountVO::getCouponId, c -> c));
        for (CouponAppVO couponAppVO : couponList) {
            MongoCouponUserCountVO mongoCouponUserCountVO = infoMap.get(couponAppVO.getCouponId());
            if (Objects.isNull(mongoCouponUserCountVO)) {
                couponAppVO.setHasReceive(0);
            } else {
                couponAppVO.setHasReceive(Objects.isNull(mongoCouponUserCountVO.getHasReceive()) ? 0 : mongoCouponUserCountVO.getHasReceive());
            }
        }
        return couponList;
    }

    @Override
    public List<CouponOrderVO> getCouponListByUserIdAndShopId(Long userId, Long shopId) {
        Criteria criteria = Criteria.where("userId").is(userId).and("shopId").is(shopId)
                .and("status").is(UserCouponStatus.EFFECTIVE.getValue()).and("isDelete").is(0);
        ProjectionOperation projectionOperation = Aggregation.project("couponId", "shopId")
                .and("_id").as("couponUserId")
                .and("userStartTime").as("startTime")
                .and("userEndTime").as("endTime");
        // 按顺序组合每一个聚合步骤
        TypedAggregation<MongoCouponOrderVO> typedAggregation =
                Aggregation.newAggregation(
                        MongoCouponOrderVO.class,
                        Aggregation.match(criteria),
                        projectionOperation
                );

        // 执行聚合操作
        AggregationResults<MongoCouponOrderVO> aggregationResults =
                mongoTemplate.aggregate(typedAggregation, MongoCouponOrderVO.class);
        // 取出最终结果
        List<MongoCouponOrderVO> list = aggregationResults.getMappedResults();
        return BeanUtil.mapAsList(list, CouponOrderVO.class);
    }

    @Override
    public void initCouponUserToMongodb() {
        int start = 0;
        int size = 10000;
        mongoTemplate.remove(MongoCouponUserBO.class);
        while (true) {
            // 批量以10000条处理
            List<CouponUser> couponUsers = couponUserMapper.listToMongodb(start, size);
            if (CollUtil.isEmpty(couponUsers)) {
                break;
            }
            List<MongoCouponUserBO> mongoUserScoreLogs = BeanUtil.mapAsList(couponUsers, MongoCouponUserBO.class);
            mongoTemplate.insertAll(mongoUserScoreLogs);
            if (couponUsers.size() <= size) {
                break;
            }
            start = start + size;
        }
    }

    @Override
    public int countReceiveCoupon(Long couponId) {
        Criteria criteria = Criteria.where("couponId").is(couponId).and("isDelete").is(0);
        long count = mongoTemplate.count(new Query(criteria), MongoCouponUserBO.class);
        return (int) count;
    }

    @Override
    public List<MongoCouponUserBO> listCouponUserIdsByIds(List<Long> couponUserIds) {
        Criteria criteria = Criteria.where("_id").in(couponUserIds);
        List<MongoCouponUserBO> couponUserList = mongoTemplate.find(new Query(criteria), MongoCouponUserBO.class);
        if (CollUtil.isEmpty(couponUserList)) {
            return new ArrayList<>();
        }
        return couponUserList;
    }
}
