package com.tmerclub.cloud.coupon.manager;

import cn.hutool.core.collection.CollUtil;
import com.tmerclub.cloud.common.cache.bo.MultiDecrementBO;
import com.tmerclub.cloud.common.cache.config.MultiRedisConfig;
import com.tmerclub.cloud.common.cache.constant.LockCacheNames;
import com.tmerclub.cloud.common.cache.enums.MultiStockEnum;
import com.tmerclub.cloud.common.cache.manager.MultiRedisStockManager;
import com.tmerclub.cloud.common.cache.util.RedisUtil;
import com.tmerclub.cloud.common.constant.Constant;
import com.tmerclub.cloud.coupon.bo.mongo.MongoCouponUserBO;
import com.tmerclub.cloud.coupon.mapper.CouponMapper;
import com.tmerclub.cloud.coupon.model.Coupon;
import com.tmerclub.cloud.coupon.service.CouponUserService;
import com.tmerclub.cloud.coupon.vo.CouponUserVO;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author TRACK
 */
@Service
public class CouponStockManager {
    private static final Logger LOGGER = LoggerFactory.getLogger(CouponStockManager.class);

    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private CouponMapper couponMapper;
    @Autowired
    private MultiRedisStockManager multiRedisStockManager;
    @Autowired
    @Lazy
    private CouponUserService couponUserService;
    @Autowired
    private MultiRedisConfig multiRedisConfig;

    public Boolean decrStocks(MultiDecrementBO multiDecrementBO) {
        try {
            multiRedisStockManager.decrStocks(multiDecrementBO);
        } catch (Exception e) {
            LOGGER.error("优惠券：{}领券出现异常：{}", multiDecrementBO.getActivityId(), e.getMessage());
            return false;
        }
        return true;
    }

    public void saveLogAndDeleteLock(List<MongoCouponUserBO> couponUsers) {
        // 这里可能发生记录存储异常，可以通过扣减库存在redis存入的锁定记录来进行解锁，具体看定时任务unLockCouponStock
        couponUserService.saveBatchToMongodb(couponUsers);
        // 领券成功后删除锁定记录，删除不成功也没关系，有定时任务unLockCouponStock进行库存的解锁
        for (MongoCouponUserBO couponUser : couponUsers) {
            multiRedisStockManager.handleReceiveSuccessCouponStock(couponUser.getCouponUserId(), couponUser.getUserId(), couponUser.getCouponId());
        }
    }

    /**
     * 获取正在投放的优惠券的真实库存
     */
    public Integer getStockByCouponId(Long couponId) {
        int totalStock = 0;
        RLock lock = redissonClient.getLock(LockCacheNames.LOCK_COUPON_ZONE_PERFIX + couponId);
        lock.lock();
        try {
            Integer redisStock = multiRedisStockManager.getCouponStock(
                    new MultiDecrementBO(MultiStockEnum.COUPON, null, null,
                            couponId, couponId, null, null, null));
            if (Objects.isNull(redisStock)) {
                Coupon coupon = couponMapper.getByCouponId(couponId);
                // 总库存量，需要查一下领取数量，才知道到底还有多少库存
                int receiveCouponNum = couponUserService.countReceiveCoupon(couponId);
                // 剩余库存
                int stockByReceive = coupon.getTotalStock() - receiveCouponNum;
                // 看下通过领取数出来的库存，和原始库存那个比较少，那个少哪个就最接近“真实库存”。
                // 所谓的接近真实都是建立在缓存被删了，然后没办法恢复的状态，只能通过领取数量和历史库存进行查询的结果。
                // 正常情况下，缓存的库存就是真实库存。
                totalStock = Math.min(coupon.getStocks(), stockByReceive);
                // 把库存存入redis
                MultiDecrementBO multiDecrementBO = new MultiDecrementBO(MultiStockEnum.COUPON, null, null,
                        couponId, couponId, null, null, null);
                multiRedisStockManager.doCouponZone(multiDecrementBO,totalStock);
                return totalStock;
            }
            totalStock = redisStock;
        } finally {
            lock.unlock();
        }
        return totalStock;
    }

    public void couponStockPlacement(List<Long> couponIds, Boolean isCancelPut) {
        for (Long couponId : couponIds) {
            RLock lock = redissonClient.getLock(LockCacheNames.LOCK_COUPON_ZONE_PERFIX + couponId);
            lock.lock();
            try {
                if (isCancelPut) {
                    // 如果是取消投放 要把可能存在的领券异常处理先
                    List<CouponUserVO> couponUsers = scanLockStockCouponLog(couponId);
                    if (CollUtil.isNotEmpty(couponUsers)) {
                        unlockStockByException(couponUsers);
                    }
                }
                // 库存落库
                flushStockToDb(couponId, null);
            } finally {
                lock.unlock();
            }
        }
        if (isCancelPut) {
            // 取消投放删除用户领券缓存key、库存分片key
            multiRedisStockManager.removeCancelPutCouponKey(couponIds);
        }
    }

    public void flushStockToDb(Long couponId, Integer hasStock) {
        // 可用库存
        if (Objects.isNull(hasStock)) {
            hasStock = getStockByCouponId(couponId);
        }
        Coupon dbCoupon = couponMapper.getByCouponId(couponId);
        // 数据库的可用库存
        Integer dbStocks = dbCoupon.getStocks();
        // 改变的库存
        int changeStock = dbStocks - hasStock;
        if (changeStock == 0) {
            LOGGER.info("优惠券{}库存未改变，无需更新", couponId);
            return;
        }
        // 更新剩余库存
        couponMapper.updateStockById(couponId, changeStock);
    }

    public List<CouponUserVO> scanLockStockCouponLog(Long couponId) {
        List<CouponUserVO> couponUsers = new ArrayList<>(Constant.INITIAL_CAPACITY);
        int redisCount = multiRedisConfig.getRedisCount();
        String couponLockKey = MultiStockEnum.COUPON.value() + "lock:";
        if (Objects.nonNull(couponId)) {
            // 单独搜索该优惠券
            couponLockKey = MultiStockEnum.COUPON.value() + "lock:" + couponId + Constant.UNDERLINE;
        }
        for (int i = 0; i < redisCount; i++) {
            StringRedisTemplate stringRedisTemplate = multiRedisConfig.getRedisByIndex(i);
            List<String> list = RedisUtil.hScan(stringRedisTemplate, couponLockKey);
            for (String key : list) {
                String couponInfo = key.replace(couponLockKey, "");
                String[] split = couponInfo.split(Constant.UNDERLINE);
                CouponUserVO couponUserVO = new CouponUserVO();
                couponUserVO.setCouponId(Long.parseLong(split[0]));
                couponUserVO.setUserId(Long.parseLong(split[1]));
                couponUserVO.setCouponUserId(Long.parseLong(split[2]));
                couponUsers.add(couponUserVO);
            }
        }
        return couponUsers;
    }

    /**
     * 因为添加领券记录异常，所以要删除锁定记录
     * 因为异常解锁，所以要删除领券记录
     * <p>
     * 优惠券库存扣减流程分析：
     * 1.用户领券/系统发放优惠券，扣除库存失败 - 无需处理
     * 2.用户领券/系统发放优惠券，成功扣除库存，领券记录添加失败 - 还原库存和删除redis中的优惠券库存锁定明细记录
     * 3.用户领券/系统发放优惠券，成功扣除库存，成功添加领券记录，解锁异常 - 删除redis中的优惠券库存锁定明细记录
     * <p>
     * 数据对应关系：
     * 1.redis中查询锁定记录， 如果没有锁定记录就代表是上面的步骤1，无需处理
     * 2.mongodb中查询领券记录，如果没有领券记录，就是上面的步骤2，还原库存和删除redis中的优惠券库存锁定明细记录
     * 3.mongodb中查询领券记录，有领券记录但库存锁定记录还在，就是上面的步骤3，删除redis中的优惠券库存锁定明细记录
     */
    public void unlockStockByException(List<CouponUserVO> couponUsers) {
        List<Long> couponUserIds = couponUsers.stream().map(CouponUserVO::getCouponUserId).collect(Collectors.toList());
        // 查询领券记录
        List<MongoCouponUserBO> successCouponUser = couponUserService.listCouponUserIdsByIds(couponUserIds);
        Set<Long> successCouponUserIds = successCouponUser.stream().map(MongoCouponUserBO::getCouponUserId).collect(Collectors.toSet());
        // 领券记录创建失败的数据列表
        List<CouponUserVO> receiveFailList = new ArrayList<>(Constant.INITIAL_CAPACITY);
        // 锁定记录删除失败的数据列表
        List<CouponUserVO> deleteFailList = new ArrayList<>(Constant.INITIAL_CAPACITY);
        if (CollUtil.isEmpty(successCouponUserIds)) {
            // 全部是领券记录存储失败的
            receiveFailList.addAll(couponUsers);
        } else {
            for (CouponUserVO couponUser : couponUsers) {
                if (successCouponUserIds.contains(couponUser.getCouponUserId())) {
                    // 锁定记录删除失败
                    deleteFailList.add(couponUser);
                    continue;
                }
                // 领券记录存储失败
                receiveFailList.add(couponUser);
            }
        }
        // 处理领券记录失败的--还原库存和删除redis中的优惠券库存锁定明细记录
        if (CollUtil.isNotEmpty(receiveFailList)) {
            for (CouponUserVO couponUserVO : receiveFailList) {
                MultiDecrementBO multiDecrementBO = new MultiDecrementBO(MultiStockEnum.COUPON, couponUserVO.getUserId(), couponUserVO.getCouponUserId(),
                        couponUserVO.getCouponId(), couponUserVO.getCouponId(), 1, null, null);
                RLock lock = redissonClient.getLock(LockCacheNames.LOCK_COUPON_ZONE_PERFIX + couponUserVO.getCouponId());
                lock.lock();
                try {
                    multiRedisStockManager.rollbackCouponStock(multiDecrementBO);
                } catch (Exception e) {
                    LOGGER.error("优惠券记录{}回退失败{}", couponUserVO.getCouponUserId(), e.getMessage());
                } finally {
                    lock.unlock();
                }
            }
        }
        // 处理锁定记录删除失败的--删除redis中的优惠券库存锁定明细记录
        if (CollUtil.isNotEmpty(deleteFailList)) {
            for (CouponUserVO couponUserVO : deleteFailList) {
                RLock lock = redissonClient.getLock(LockCacheNames.LOCK_COUPON_ZONE_PERFIX + couponUserVO.getCouponId());
                lock.lock();
                try {
                    multiRedisStockManager.handleReceiveSuccessCouponStock(couponUserVO.getCouponUserId(), couponUserVO.getUserId(), couponUserVO.getCouponId());
                } finally {
                    lock.unlock();
                }
            }
        }
    }
}
