package com.tmerclub.cloud.common.cache.manager;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.tmerclub.cloud.common.cache.bo.MultiDecrementBO;
import com.tmerclub.cloud.common.cache.config.MultiRedisConfig;
import com.tmerclub.cloud.common.cache.enums.MultiStockEnum;
import com.tmerclub.cloud.common.cache.util.RedisLuaUtil;
import com.tmerclub.cloud.common.cache.util.RedisUtil;
import com.tmerclub.cloud.common.constant.Constant;
import com.tmerclub.cloud.common.exception.LuckException;
import com.tmerclub.cloud.common.util.Arith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author LGH
 */
@Component
@ConditionalOnProperty(value = "redis.multi.enable", havingValue = "true")
public class MultiRedisStockManager {

    private static final Logger LOG = LoggerFactory.getLogger(MultiRedisStockManager.class);

    @Autowired
    private MultiRedisConfig multiRedisConfig;

    public static final String STOCK = "stock:";
    public static final String RECEIVE = "receive:";

    /**
     * 库存不足
     */
    public static final String NOT_ENOUGH = "-1";
    /**
     * 超过购买上限
     */
    public static final String MAX_RESULT = "-2";

    /**
     * 秒杀sku扣减库存
     */
    public static final byte[] SECKILL_DECREMENT_STOCK_BYTES = RedisLuaUtil.initByte("lua/multistock/SeckillSkuStock.lua");

    /**
     * 秒杀sku合并扣减库存
     */
    public static final byte[] SECKILL_MERGE_STOCK_BYTES = RedisLuaUtil.initByte("lua/multistock/SeckillSkuStockMerge.lua");

    /**
     * 秒杀锁定库存
     */
    public static final byte[] SECKILL_ORDER_STOCK_BYTES = RedisLuaUtil.initByte("lua/multistock/DecrementStock.lua");
    /**
     * 秒杀订单取消，回退库存
     */
    public static final byte[] SECKILL_ORDER_CANCEL_STOCK_BYTES = RedisLuaUtil.initByte("lua/multistock/ReturnStock.lua");
    /**
     * 秒杀订单支付成功，删除锁定信息，删除订单
     */
    public static final byte[] SECKILL_ORDER_PAY_SUCCESS_STOCK_BYTES = RedisLuaUtil.initByte("lua/multistock/PaySuccessToStock.lua");

    /**
     * 优惠券扣减库存
     */
    public static final byte[] COUPON_DECREMENT_STOCK_BYTES = RedisLuaUtil.initByte("lua/multistock/coupon/DecrementCouponStock.lua");

    /**
     * 优惠券还原库存
     */
    public static final byte[] COUPON_ROLLBACK_BYTES = RedisLuaUtil.initByte("lua/multistock/coupon/ReturnCouponStock.lua");

    /**
     * 优惠券删除，还原库存
     */
    public static final byte[] COUPON_ADD_STOCK_BYTES = RedisLuaUtil.initByte("lua/multistock/coupon/AddCouponStock.lua");

    /**
     * 进行分片，分片是啥意思呢？就是将一个1000个库存的秒杀商品，分到n个redis当中，每个redis有1000/n个库存，用户进行抢购的时候，
     * 可以用到n个redis，性能相对一个redis提高n倍
     * @param totalStock 要分片的库存数量
     */
    public void doZone(MultiDecrementBO multiDecrementBO) {
        int zoneNum = multiRedisConfig.getRedisCount();
        List<String> stockPointList = multiDecrementBO.getStockPointList();
        // Map<对应redis编号, Map<仓库id, 库存数量>>，  Map<仓库id, 库存数量>就是每个sku区域库存分片后的库存map
        Map<Integer, Map<Long, Integer>> map = new HashMap<>();
        for (String stockPointSkuVO: stockPointList) {
            String[] split = stockPointSkuVO.split(String.valueOf(StrUtil.C_UNDERLINE));
            // 库存点id
            Long stockPointId = Long.parseLong(split[0]);
            // 库存点库存
            Integer totalStock = Integer.parseInt(split[1]);
            // 每个分片至少的库存数量（目前是想分片出来的结果要么是这个数量，要么是0）
            // 比如 ( 10,10,5,0,0,0,0,) 这种结果
            int avgStock = getAvgStock(zoneNum,totalStock);
            // 获取要存进缓存的每个分片库存数量
            for (int i = 0; i < zoneNum; i++) {
                Map<Long, Integer> stockMap = map.containsKey(i) ? map.get(i) : new HashMap<>();
                Integer stock = avgStock;
                // 更新缓存
                if (totalStock <= avgStock) {
                    stock = totalStock;
                }
                // 每个区域的分片库存
                stockMap.put(stockPointId, stock);
                map.put(i, stockMap);
            }
        }
        // 计算总库存
        for (int i = 0; i < zoneNum; i++) {
            Map<Long, Integer> stockMap = map.get(i);
            // 计算当前redis中的sku总库存数量
            AtomicInteger skuStock = new AtomicInteger();
            stockMap.forEach((key, stock) -> {
                skuStock.addAndGet(stock);
            });
            stockMap.put(0L, skuStock.intValue());
        }

        // 批量保存
        for (int i = 0; i < zoneNum; i++) {
            Map<Long, Integer> stockMap = map.get(i);
            // <skukey, 库存>
            Map<String, String> keyMap = new HashMap<>();
            // 转换为key，value
            stockMap.forEach((point, stock) -> {
                multiDecrementBO.setStockPointId(point);
                keyMap.put(getStockKey(multiDecrementBO), String.valueOf(stock));
            });
            getStockOpsForValue(i).multiSet(keyMap);
        }
    }

    /**
     * 进行分片，分片是啥意思呢？就是将一个1000个库存的秒杀商品，分到n个redis当中，每个redis有1000/n个库存，用户进行抢购的时候，
     * 可以用到n个redis，性能相对一个redis提高n倍
     * @param totalStock 要分片的库存数量
     */
    public void doCouponZone(MultiDecrementBO multiDecrementBO, Integer totalStock) {
        int zoneNum = multiRedisConfig.getRedisCount();

        // 每个分片至少的库存数量（目前是想分片出来的结果要么是这个数量，要么是0）
        // 比如 ( 10,10,5,0,0,0,0,) 这种结果
        int avgStock = getAvgStock(zoneNum, totalStock);
        // 获取要存进缓存的每个分片库存数量
        for (int i = 0; i < zoneNum; i++) {
            // 更新缓存
            if (totalStock - avgStock >= 0) {
                getStockOpsForValue(i).set(getStockKey(multiDecrementBO), String.valueOf(avgStock));
                totalStock -= avgStock;
            } else {
                getStockOpsForValue(i).set(getStockKey(multiDecrementBO), String.valueOf(totalStock));
                totalStock = 0;
            }
        }
    }

    private int getAvgStock(int zoneNum, Integer totalStock) {
        // 每个分片可以分配的库存数量，可能小于10（这里的平均库存并不是真正的平均库存，因为这里进行了线上取整）
        // 比如平均库存是 333.33 可以能会变成334
        int everyZoneStockRoundUp = Arith.divToRoundUp(totalStock, zoneNum);

        // 平均库存向上取整，因为就算是平均库存
        return Math.max(everyZoneStockRoundUp, 1);
    }

    /**
     * 扣减库存，返回扣减的分片id方便发mq的时候知道从哪里找到锁定的记录并且删除
     * <p>
     * 返回锁定记录所在的位置。因为redis有很多台，不知道这个锁定记录在哪里，所以要返回去，到时候生成订单成功的时候好把这条记录删掉
     */
    public void decrStocks(MultiDecrementBO multiDecrementBO) {
        // 获取当前库存所使用的redis
        int redisIndex = multiRedisConfig.getIndex(multiDecrementBO.getUserId());

        // 1.通过分片扣减，如果没有扣减成功会抛异常
        String result = "";
        if (Objects.equals(multiDecrementBO.getMultiStockEnum(), MultiStockEnum.SECKILL)) {
            result = reduceSeckillStockByZone(multiDecrementBO, redisIndex);
        }
        else if (Objects.equals(multiDecrementBO.getMultiStockEnum(), MultiStockEnum.COUPON)) {
            result = reduceCouponStockByZone(multiDecrementBO, redisIndex);
        }
        // 扣减成功直接return
        if (!NOT_ENOUGH.equals(result)) {
            return;
        }

        // 只有一台redis，没库存就是没库存了
        int redisCount = multiRedisConfig.getRedisCount();
        if (redisCount == 1) {
            LOG.info("只有一台redis，没库存就是没库存了");
            throw new LuckException("库存不足");
        }

        // 2. 因为秒杀绝大部分是一件一件的买的，所以先判断一下是不是要买一件
        if (Objects.equals(multiDecrementBO.getProdCount(), 1)) {
            LOG.info("扣减失败，尝试使用其他分片进行扣减，此次扣减数量为1，multiDecrementBO:{} ", multiDecrementBO);
            decrementOtherRedis(multiDecrementBO, redisCount, redisIndex);
            return;
        }

        LOG.info("扣减失败，聚合扣减进行，multiDecrementBO:{} ", multiDecrementBO);
        // 2.次分片无库存或扣减失败，聚合所有分片扣减库存
        // 2.1 统计所有库存数量还剩多少
        int totalStock = 0;
        // 遍历redis
        for (int i = 0; i < redisCount; i++) {
            // 就是因为库存扣减失败才会来到这里，所以不用再从自己这里获取库存了
            if (redisIndex == i) {
                continue;
            }
            int zoneStock = Integer.parseInt(Objects.requireNonNull(getStockOpsForValue(i).get(getStockKey(multiDecrementBO))));
            totalStock += zoneStock;
        }
        // 2.2 库存不够扣减了
        if (totalStock < multiDecrementBO.getProdCount()) {
            LOG.info("扣减失败，尝试使用所有分片进行扣减，此次扣减数量为{}，其他分片也无法完成，总库存剩余{} multiDecrementBO:{} ", multiDecrementBO.getProdCount(), totalStock, multiDecrementBO);
            throw new LuckException("库存不足");
        }

        // 进行全redis的扣减
        decrementAllRedis(multiDecrementBO, redisCount, redisIndex);
    }

    /**
     * 获取秒杀有多少库存
     */
    public Integer getSeckillStock(MultiDecrementBO multiDecrementBO) {
        int redisCount = multiRedisConfig.getRedisCount();

        LOG.info("扣减失败，聚合扣减进行，multiDecrementBO:{} ", multiDecrementBO);
        // 2.1 统计所有库存数量还剩多少
        int totalStock = 0;
        // 遍历redis
        for (int i = 0; i < redisCount; i++) {
            multiDecrementBO.setStockPointId(multiDecrementBO.getStockPointId() == null ? 0L : multiDecrementBO.getStockPointId());
            String stockStr = getStockOpsForValue(i).get(getStockKey(multiDecrementBO));
            int zoneStock;
            if (StrUtil.isNotBlank(stockStr)) {
                zoneStock = Integer.parseInt(stockStr);
            } else {
                zoneStock = 0;
            }
            totalStock += zoneStock;
        }
        return totalStock;
    }

    /**
     * 获取优惠券库存
     */
    public Integer getCouponStock(MultiDecrementBO multiDecrementBO) {
        int redisCount = multiRedisConfig.getRedisCount();

        int totalStock = 0;
        // 遍历redis
        for (int i = 0; i < redisCount; i++) {
            String stock = getStockOpsForValue(i).get(getStockKey(multiDecrementBO));
            if (Objects.isNull(stock)) {
                // 缓存未创建
                return null;
            }
            int zoneStock = Integer.parseInt(stock);
            totalStock += zoneStock;
        }
        return totalStock;
    }

    /**
     * 获取秒杀仓库有多少库存
     */
    public Map<Long, Integer> getSeckillStockPointMap(MultiDecrementBO multiDecrementBO) {
        int redisCount = multiRedisConfig.getRedisCount();
        Map<Long, Integer> pointMap = new HashMap<>();
        LOG.info("扣减失败，聚合扣减进行，multiDecrementBO:{} ", multiDecrementBO);
        // 2.1 统计所有库存数量还剩多少
        List<String> stockPointList = multiDecrementBO.getStockPointList();
        for (String pointId : stockPointList) {
            int totalStock = 0;
            // 遍历redis
            for (int i = 0; i < redisCount; i++) {
                multiDecrementBO.setStockPointId(Long.parseLong(pointId));
                int zoneStock = Integer.parseInt(Objects.requireNonNull(getStockOpsForValue(i).get(getStockKey(multiDecrementBO))));
                totalStock += zoneStock;
            }
            pointMap.put(Long.parseLong(pointId), totalStock);
        }
        return pointMap;
    }

    /**
     * 通过分片扣减秒杀商品库存并记录日志
     */
    public String reduceSeckillStockByZone(MultiDecrementBO multiDecrementBO, Integer redisIndex) {
        String result = RedisLuaUtil.executeList(multiRedisConfig.getRedisByIndex(redisIndex),
                SECKILL_ORDER_STOCK_BYTES,
                Arrays.asList(multiDecrementBO.getCachePrefix(),
                        multiDecrementBO.getOrderId().toString(),
                        multiDecrementBO.getActivityId().toString(),
                        multiDecrementBO.getProdCount().toString(),
                        multiDecrementBO.getUserId().toString(),
                        multiDecrementBO.getOrderInfo(),
                        multiDecrementBO.getMaxNum().toString(),
                        multiDecrementBO.getSkuId().toString(),
                        multiDecrementBO.getStockPointId().toString()
                )
        );

        if (Objects.equals(result, MAX_RESULT)) {
            throw new LuckException("已经到达购买上限");
        }
        return result;
    }

    /**
     * 通过分配扣减优惠券库存并记录日志
     */
    public String reduceCouponStockByZone(MultiDecrementBO multiDecrementBO, Integer redisIndex) {
        String result = RedisLuaUtil.executeList(multiRedisConfig.getRedisByIndex(redisIndex),
                COUPON_DECREMENT_STOCK_BYTES,
                Arrays.asList(multiDecrementBO.getCachePrefix(),
                        multiDecrementBO.getOrderId().toString(),
                        multiDecrementBO.getActivityId().toString(),
                        multiDecrementBO.getProdCount().toString(),
                        multiDecrementBO.getUserId().toString(),
                        multiDecrementBO.getMaxNum().toString()));

        if (Objects.equals(result, MAX_RESULT)) {
            throw new LuckException("该优惠券已达个人领取上限，无法继续领取");
        }
        return result;
    }

    /**
     * 获取用户的秒杀商品购买数量
     */
    public Integer getSeckillProdBuyCount(MultiDecrementBO multiDecrementBO) {
        // 获取当前key所使用的redis
        int redisIndex = multiRedisConfig.getIndex(multiDecrementBO.getUserId());
        String key = MultiStockEnum.SECKILL.value() + "buy:" + multiDecrementBO.getActivityId() + StrUtil.C_UNDERLINE + multiDecrementBO.getUserId();
        StringRedisTemplate stringRedisTemplate = multiRedisConfig.getRedisByIndex(redisIndex);
        String count = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(count)) {
            return 0;
        }
        return Integer.valueOf(count);
    }

    /**
     * 获取用户优惠券领券数量
     */
    public Integer getCouponReceiveCount(Long userId, Long activityId) {
        // 获取当前key所使用的redis
        int redisIndex = multiRedisConfig.getIndex(userId);
        String key = MultiStockEnum.COUPON.value() + RECEIVE + activityId + StrUtil.C_UNDERLINE + userId;
        StringRedisTemplate stringRedisTemplate = multiRedisConfig.getRedisByIndex(redisIndex);
        String count = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(count)) {
            return null;
        }
        return Integer.valueOf(count);
    }

    /**
     * 初始用户优惠券领券数量，兼容旧数据
     */
    public void setCouponReceiveCount(Long userId, Long activityId, Long receiveCount) {
        int redisIndex = multiRedisConfig.getIndex(userId);
        String key = MultiStockEnum.COUPON.value() + RECEIVE + activityId + StrUtil.C_UNDERLINE + userId;
        getStockOpsForValue(redisIndex).set(key, String.valueOf(receiveCount));
    }

    /**
     * 扣减其他redis的库存，但是扣减记录和订单的记录还是在userId的hash上
     */
    private void decrementOtherRedis(MultiDecrementBO multiDecrementBO, int redisCount, int redisIndex) {
        // 遍历redis
        for (int i = 0; i < redisCount; i++) {
            // 就是因为库存扣减失败才会来到这里，所以不用再从自己这里扣库存了
            if (redisIndex == i) {
                continue;
            }
            // 扣减其他redis的库存
            String hasStock = RedisLuaUtil.executeList(multiRedisConfig.getRedisByIndex(i),
                    SECKILL_DECREMENT_STOCK_BYTES,
                    Arrays.asList(
                            // TODO stock sku总库存和sku区域库存的key
                            getStockKey(multiDecrementBO),
                            getStockKey(multiDecrementBO),
                            // 要扣除的库存数量
                            multiDecrementBO.getProdCount().toString()
                    ));

            if (StrUtil.isBlank(hasStock) && Long.parseLong(hasStock) < 0) {
                continue;
            }
//            // dev
//            // 扣减其他redis的库存
//            Long hasStock = multiRedisConfig.getRedisByIndex(i).execute(new DefaultRedisScript<>(RedisLuaUtil.DECREMENT_SCRIPT, Long.class),
//                    Collections.singletonList(getStockKey(multiDecrementBO)),
//                    multiDecrementBO.getProdCount().toString());
//            if (hasStock != null && hasStock < 0) {
//                continue;
//            }
            LOG.info("扣减失败，尝试使用其他分片进行扣减，此次扣减数量为1，在redisIndex:{} 完成扣减 multiDecrementBO: {}", redisIndex, multiDecrementBO);
            try {
                // 但是扣减记录和订单的记录还是在userId的hash上
                // 此处应该要把商品数量变为0，因为在上一步已经扣除过库存了
                multiDecrementBO.setProdCount(0);
                if (Objects.equals(multiDecrementBO.getMultiStockEnum(), MultiStockEnum.SECKILL)) {
                    reduceSeckillStockByZone(multiDecrementBO, redisIndex);
                }
                else if (Objects.equals(multiDecrementBO.getMultiStockEnum(), MultiStockEnum.COUPON)) {
                    reduceCouponStockByZone(multiDecrementBO, redisIndex);
                }
            } catch (Exception e) {
                e.printStackTrace();
                // 因为这个秒杀并发很高，所以很可能根本就没扣库存，就库存不足了，所以只还原已经扣了库存的redis
                RedisLuaUtil.executeList(multiRedisConfig.getRedisByIndex(i),
                        SECKILL_DECREMENT_STOCK_BYTES,
                        Arrays.asList(
                                // TODO stock sku总库存和sku区域库存的key
                                getStockKey(multiDecrementBO),
                                getStockKey(multiDecrementBO),
                                // 增加回库存，所以是负数
                                String.valueOf(-1)
                        ));
                throw e;
            }
            return;
        }
        LOG.info("扣减失败，尝试使用其他分片进行扣减，此次扣减数量为1，其他分片也无法完成，均无库存 multiDecrementBO: {}", multiDecrementBO);
        throw new LuckException("库存不足");
    }


    /**
     * 逐个扣减redis的库存，但是扣减记录和订单的记录还是在userId的hash上
     */
    private void decrementAllRedis(MultiDecrementBO multiDecrementBO, int redisCount, int redisIndex) {
        //1.执行多个分片的扣除扣减，对该商品的库存操作上锁，保证原子性
        Map<Integer, Integer> fallbackMap = new HashMap<>(redisCount);
        // 记录下来需要扣减的总库存，后面要使用，用int类型，防止对象被改变
        int totalNeedDecrementStock = multiDecrementBO.getProdCount();
        Integer prodCount = multiDecrementBO.getProdCount();
        try {
            // 开始循环扣减库存
            for (int i = 0; i < redisCount; i++) {

                // 负数代表还欠多少库存没扣减
                String hasStock = RedisLuaUtil.executeList(multiRedisConfig.getRedisByIndex(i),
                        SECKILL_MERGE_STOCK_BYTES,
                        Arrays.asList(
                                // TODO stock sku总库存和sku区域库存的key
                                getStockKey(multiDecrementBO),
                                getStockKey(multiDecrementBO),
                                prodCount.toString()
                        )
                );
                // 如果这个分片本身的库存为0，这里有问题？
                // 库存足够了
                if (Objects.nonNull(hasStock) && Integer.parseInt(hasStock) >= 0) {
                    // dev
//                if (hasStock >= 0) {
                    LOG.info("扣减失败，尝试使用所有分片进行扣减，总共需要扣减{} 此次扣减数量为{}，扣减足够结束扣减。 multiDecrementBO: {}",totalNeedDecrementStock, prodCount, multiDecrementBO);
                    // 记住当前扣减的库存数量，防止最终程序出现异常进行回滚
                    fallbackMap.put(i, prodCount);
                    prodCount = 0;
                    break;
                }

                // 剩余需要扣的库存数量
                int needDecrementStock = -Integer.parseInt(hasStock);

                // 记录日志，
                LOG.info("扣减失败，尝试使用所有分片进行扣减，总共需要扣减{} 此次扣减数量为{}，不足扣减，剩余扣减数量为{}。 multiDecrementBO: {}", totalNeedDecrementStock, prodCount, needDecrementStock, multiDecrementBO);
                // 记住当前扣减的库存数量，发现不够扣的时候可以还原回滚
                fallbackMap.put(i, prodCount - needDecrementStock);

                prodCount = needDecrementStock;
            }

            // 成功通过多个redis扣减完全部的库存
            if (prodCount == 0) {
                multiDecrementBO.setProdCount(0);
                // 但是扣减记录和订单的记录还是在userId的hash上
                if (Objects.equals(multiDecrementBO.getMultiStockEnum(), MultiStockEnum.SECKILL)) {
                    reduceSeckillStockByZone(multiDecrementBO, redisIndex);
                }
                else if (Objects.equals(multiDecrementBO.getMultiStockEnum(), MultiStockEnum.COUPON)) {
                    reduceCouponStockByZone(multiDecrementBO, redisIndex);
                }
                return;
            }

            throw new LuckException("库存不足");

        } catch (Exception e) {
            e.printStackTrace();
            // 完全扣除所有的分片库存后，还是未清零，则回退库存返回各自分区
            fallbackMap.forEach((k, v) -> {
                // 因为这个秒杀并发很高，所以很可能根本就没扣库存，就库存不足了，所以只还原已经扣了库存的redis
                if (v != 0){
                    RedisLuaUtil.executeList(multiRedisConfig.getRedisByIndex(k),
                            SECKILL_DECREMENT_STOCK_BYTES,
                            Arrays.asList(
                                    // TODO stock sku总库存和sku区域库存的key
                                    getStockKey(multiDecrementBO),
                                    getStockKey(multiDecrementBO),
                                    // 增加回库存，所以是负数
                                    String.valueOf(-v)
                            )
                    );
                }
//                // dev
//                // 因为这个秒杀并发很高，所以很可能根本就没扣库存，就库存不足了，所以只还原已经扣了库存的redis
//                if (v != 0) {
//                    multiRedisConfig.getRedisByIndex(k).execute(new DefaultRedisScript<>(RedisLuaUtil.DECREMENT_SCRIPT, Long.class),
//                            Collections.singletonList(getStockKey(multiDecrementBO)),
//                            // 增加回库存，所以是负数
//                            String.valueOf(-v));
//                }
            });
            throw e;
        }
    }

    /**
     * 因为库存需要长期增减，所以用到所有的redis，每个redis都扣减或者新增
     */
    private ValueOperations<String, String> getStockOpsForValue(int i) {
        return multiRedisConfig.getRedisByIndex(i).opsForValue();
    }


    /**
     * 获取库存的key
     */
    private static String getStockKey(MultiDecrementBO multiDecrementBO) {
        String key = multiDecrementBO.getCachePrefix() + STOCK + multiDecrementBO.getActivityId() + StrUtil.C_UNDERLINE + multiDecrementBO.getSkuId();
        if (Objects.nonNull(multiDecrementBO.getStockPointId()) && multiDecrementBO.getStockPointId() > 0) {
            key =  key + StrUtil.C_UNDERLINE + multiDecrementBO.getStockPointId();
        }
        return key;
    }

    /**
     * 取消订单，还原秒杀库存
     */
    public void rollbackSecKillStock(Long activityId, Long skuId, Integer count, Long orderId, Long userId, Integer zoneNo, Long stockPointId) {
        // 幂等保证,还是写成lua比较好
        // 获取当前库存所使用的redis
        int redisIndex = multiRedisConfig.getIndex(userId);
        RedisLuaUtil.executeList(multiRedisConfig.getRedisByIndex(redisIndex),
                SECKILL_ORDER_CANCEL_STOCK_BYTES,
                Arrays.asList(MultiStockEnum.SECKILL.value(),
                        orderId.toString(),
                        activityId.toString(),
                        count.toString(),
                        userId.toString(),
                        skuId.toString(),
                        stockPointId.toString()));
    }

    /**
     * 领券失败，还原优惠券库存
     */
    public void rollbackCouponStock(MultiDecrementBO multiDecrementBO) {
        int redisIndex = multiRedisConfig.getIndex(multiDecrementBO.getUserId());
        RedisLuaUtil.executeList(multiRedisConfig.getRedisByIndex(redisIndex),
                COUPON_ROLLBACK_BYTES,
                Arrays.asList(multiDecrementBO.getCachePrefix(),
                        multiDecrementBO.getOrderId().toString(),
                        multiDecrementBO.getActivityId().toString(),
                        multiDecrementBO.getProdCount().toString(),
                        multiDecrementBO.getUserId().toString(),
                        multiDecrementBO.getSkuId().toString()));
    }

    /**
     * 订单支付成功时，删除订单和订单锁定信息
     * @param count
     * @param orderId
     * @param userId
     */
    public void handlePaySuccessSecKillStock(Integer count, Long orderId, Long userId) {
        int redisIndex = multiRedisConfig.getIndex(userId);
        RedisLuaUtil.executeList(multiRedisConfig.getRedisByIndex(redisIndex),
                SECKILL_ORDER_PAY_SUCCESS_STOCK_BYTES,
                Arrays.asList(MultiStockEnum.SECKILL.value(),
                        orderId.toString(),
                        count.toString(),
                        userId.toString()));
    }

    /**
     * 领券记录存储成功，删除优惠券锁定信息
     */
    public void handleReceiveSuccessCouponStock(Long orderId, Long userId, Long couponId) {
        int redisIndex = multiRedisConfig.getIndex(userId);
        String key = MultiStockEnum.COUPON.value() + "lock:" + couponId + Constant.UNDERLINE + userId + Constant.UNDERLINE + orderId;
        // 删除锁定记录
        try {
            multiRedisConfig.getRedisByIndex(redisIndex).delete(key);
        } catch (Exception e) {
            LOG.error("锁定记录删除失败{},原因{}", key, e.getMessage());
        }
    }

    /**
     * 删除优惠券，还原优惠券库存
     */
    public void addCouponStock(MultiDecrementBO multiDecrementBO) {
        int redisIndex = multiRedisConfig.getIndex(multiDecrementBO.getUserId());
        RedisLuaUtil.executeList(multiRedisConfig.getRedisByIndex(redisIndex),
                COUPON_ADD_STOCK_BYTES,
                Arrays.asList(multiDecrementBO.getCachePrefix(),
                        multiDecrementBO.getOrderId().toString(),
                        multiDecrementBO.getActivityId().toString(),
                        multiDecrementBO.getProdCount().toString(),
                        multiDecrementBO.getUserId().toString(),
                        multiDecrementBO.getSkuId().toString()));
    }

    public Set<String> handleOutTimeSeckillOrder(String orderKeyCachePrefix, StringRedisTemplate stringRedisTemplate) {
        List<String> list = new ArrayList<>();
        Integer count = 0;
        while (true) {
            List<Object> result = stringRedisTemplate.execute(new DefaultRedisScript<>(RedisLuaUtil.GET_CANCEL_ORDER_SCRIPT, List.class),
                    Collections.singletonList(orderKeyCachePrefix),
                    count.toString());
            // 第一个是count，第二个是list，如果直接每次都是从0开始scan这里就有问题，所以在使用scan命令的时候，需要迭代的遍历，
            // 每次调用都需要使用上一次这个调用返回的游标作为该次调用的游标参数，以此来延续之前的迭代过程。
            count = Integer.valueOf(result.get(0).toString());
            list = (List<String>) result.get(1);
            if (count == 0 || list.size() == 0) {
                break;
            }
        }
        // 可能有重复的，去除一下
        return new HashSet<>(list);
    }

    /**
     * 优惠券取消投放，删除缓存
     */
    public void removeCancelPutCouponKey(List<Long> couponIds) {
        int redisCount = multiRedisConfig.getRedisCount();
        String couponReceiveKey = MultiStockEnum.COUPON.value() + RECEIVE;
        for (int i = 0; i < redisCount; i++) {
            List<String> keys = new ArrayList<>(Constant.INITIAL_CAPACITY);
            StringRedisTemplate stringRedisTemplate = multiRedisConfig.getRedisByIndex(i);
            for (Long couponId : couponIds) {
                // 库存key
                String couponStockKey = MultiStockEnum.COUPON.value() + STOCK + couponId + StrUtil.C_UNDERLINE + couponId;
                keys.add(couponStockKey);
                // 用户领券数量key
                List<String> list = RedisUtil.hScan(stringRedisTemplate, couponReceiveKey + couponId + Constant.UNDERLINE);
                if (CollUtil.isEmpty(list)) {
                    continue;
                }
                keys.addAll(list);
            }
            stringRedisTemplate.delete(keys);
        }
    }
    public Map<Integer, Map<String, String>> scan(String key) {
        Map<Integer, Map<String, String>> stockMap = new HashMap<>();
        int redisCount = multiRedisConfig.getRedisCount();
        for (int i = 0; i < redisCount; i++) {
            Map<String, String> keyMap = new HashMap<>();
            stockMap.put(i, keyMap);
            StringRedisTemplate stringRedisTemplate = multiRedisConfig.getRedisByIndex(i);

            Cursor<String> cursor = null;
            try {
                List<String> keys = new ArrayList<>();
                ScanOptions options = ScanOptions.scanOptions()
                        .count(1000)
                        .match(key + "*")
                        .build();
                RedisSerializer<String> redisSerializer = (RedisSerializer<String>) stringRedisTemplate.getKeySerializer();
                cursor = stringRedisTemplate.executeWithStickyConnection(redisConnection ->
                        new ConvertingCursor<>(redisConnection.keyCommands().scan(options), redisSerializer::deserialize));
                if(Objects.isNull(cursor)){
                    continue;
                }
                while (cursor.hasNext()) {
                    keys.add(cursor.next());
                }
                if (keys.isEmpty()) {
                    continue;
                }
                List<String> values = stringRedisTemplate.opsForValue().multiGet(keys);
                for (int j = 0; j < keys.size(); j++) {
                    String stock = values.get(j);
                    if (StrUtil.isNotBlank(stock)) {
                        keyMap.put(keys.get(j), stock);
                    }
                }
            } finally {
                if(Objects.nonNull(cursor)) {
                    cursor.close();
                }
            }
        }
        return stockMap;
    }

    public void mset(Integer index, Map<String, String> msetMap) {
        if (MapUtil.isEmpty(msetMap)) {
            return;
        }
        StringRedisTemplate stringRedisTemplate = multiRedisConfig.getRedisByIndex(index);
        stringRedisTemplate.opsForValue().multiSet(msetMap);
    }
}
