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

import cn.hutool.core.util.StrUtil;
import com.alibaba.nacos.common.utils.CollectionUtils;
import com.tmerclub.cloud.api.order.feign.OrderFeignClient;
import com.tmerclub.cloud.api.product.feign.SkuStockFeignClient;
import com.tmerclub.cloud.api.product.feign.StockPointFeignClient;
import com.tmerclub.cloud.api.product.vo.SkuStockVO;
import com.tmerclub.cloud.common.cache.bo.MultiDecrementBO;
import com.tmerclub.cloud.common.cache.constant.CacheNames;
import com.tmerclub.cloud.common.cache.constant.LockCacheNames;
import com.tmerclub.cloud.common.cache.constant.SeckillCacheNames;
import com.tmerclub.cloud.common.cache.enums.MultiStockEnum;
import com.tmerclub.cloud.common.cache.manager.MultiRedisStockManager;
import com.tmerclub.cloud.common.cache.util.CacheManagerUtil;
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.product.dto.SkuStockPointSearchDTO;
import com.tmerclub.cloud.common.product.dto.StockPointSkuDTO;
import com.tmerclub.cloud.common.product.vo.StockPointSkuVO;
import com.tmerclub.cloud.common.util.BeanUtil;
import com.tmerclub.cloud.seckill.constant.SeckillConstant;
import com.tmerclub.cloud.seckill.dto.SeckillSkuDTO;
import com.tmerclub.cloud.seckill.dto.SeckillStockPointSkuDTO;
import com.tmerclub.cloud.seckill.mapper.SeckillMapper;
import com.tmerclub.cloud.seckill.mapper.SeckillSkuMapper;
import com.tmerclub.cloud.seckill.service.SeckillSkuService;
import com.tmerclub.cloud.seckill.service.SeckillStockPointSkuService;
import com.tmerclub.cloud.seckill.vo.SeckillSkuVO;
import com.tmerclub.cloud.seckill.vo.SeckillStockPointSkuVO;
import com.tmerclub.cloud.seckill.vo.SeckillVO;
import io.seata.spring.annotation.GlobalTransactional;
import org.apache.dubbo.config.annotation.DubboReference;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 秒杀活动sku
 *
 * @author lhd
 * @date 2021-03-30 14:53:09
 */
@Service
public class SeckillSkuServiceImpl implements SeckillSkuService {

    @Autowired
    private SeckillSkuMapper seckillSkuMapper;

    @Autowired
    private CacheManagerUtil cacheManagerUtil;

    @Autowired
    private RedissonClient redissonClient;

    @DubboReference
    private OrderFeignClient orderFeignClient;

    @Autowired
    private SeckillMapper seckillMapper;
    @Autowired
    private MultiRedisStockManager multiRedisStockManager;
    @Autowired
    private SeckillStockPointSkuService seckillStockPointSkuService;
    @DubboReference
    private SkuStockFeignClient skuStockFeignClient;
    @DubboReference
    private StockPointFeignClient stockPointFeignClient;

    @Override
    @Cacheable(cacheNames = SeckillCacheNames.SECKILL_SKU_BY_ID, key = "#seckillSkuId", sync = true)
    public SeckillSkuVO getBySeckillSkuId(Long seckillSkuId) {
        // 这里的库存不准的不要用这个方法获取库存，请使用getStockBySeckillSkuId 这个方法获取库存
        return seckillSkuMapper.getBySeckillSkuId(seckillSkuId);
    }

    @Override
    @Cacheable(cacheNames = SeckillCacheNames.SECKILL_SKU_BY_SECKILL_ID, key = "#seckillId", sync = true)
    public List<SeckillSkuVO> listSeckillSkuBySeckillId(Long seckillId) {

        List<SeckillSkuVO> seckillSkuVOS = seckillSkuMapper.selectListBySeckillId(seckillId);

        return seckillSkuVOS;
    }

    @Override
    public void saveBatch(List<SeckillSkuDTO> seckillSkuList) {
        seckillSkuMapper.saveBatch(seckillSkuList);
        List<SeckillStockPointSkuDTO> stockPointSkus = new ArrayList<>(Constant.INITIAL_CAPACITY);
        Map<Long, List<String>> stockPointMap = new HashMap<>();
        for (SeckillSkuDTO seckillSkuDTO : seckillSkuList) {
            List<String> stockPointList = new ArrayList<>(seckillSkuDTO.getSeckillStockPointList().size());
            seckillSkuDTO.getSeckillStockPointList().forEach(s-> {
                s.setSeckillSkuId(seckillSkuDTO.getSeckillSkuId());
                s.setSkuId(seckillSkuDTO.getSkuId());
                stockPointList.add(s.getStockPointId().toString() + StrUtil.C_UNDERLINE + s.getStock());
            });
            stockPointMap.put(seckillSkuDTO.getSkuId(), stockPointList);
            stockPointSkus.addAll(seckillSkuDTO.getSeckillStockPointList());
        }
         seckillStockPointSkuService.batchSave(stockPointSkus);
        // 库存分片到redis
        for (SeckillSkuDTO seckillSku : seckillSkuList) {
            multiRedisStockManager.doZone(new MultiDecrementBO(MultiStockEnum.SECKILL,null,null,
                    seckillSku.getSeckillId(), seckillSku.getSkuId(), null,null,null, stockPointMap.get(seckillSku.getSkuId())));
        }
    }

    @Override
    @CacheEvict(cacheNames = SeckillCacheNames.SECKILL_SKU_BY_SECKILL_ID, key = "#seckillId")
    public void removeSeckillSkuCacheBySeckillId(Long seckillId) {
        List<SeckillSkuVO> seckillSkus = listSeckillSkuBySeckillId(seckillId);
        //清除缓存
        if (CollectionUtils.isNotEmpty(seckillSkus)) {
            for (SeckillSkuVO seckillSku : seckillSkus) {
                // 不能采用this清除缓存，因为这样没有aop
                cacheManagerUtil.evictCache(SeckillCacheNames.SECKILL_SKU_BY_ID, String.valueOf(seckillSku.getSeckillSkuId()));
            }
        }
    }

    @Override
    public Integer doZone(Long seckillSkuId, Integer totalStock) {
        // 分片数量
        int zoneNum = SeckillConstant.ZONE_NUM;
        // 可用分片数量
        int availableZone = 0;
        if (totalStock == 0) {
            RedisUtil.set(SeckillCacheNames.SECKILL_SKU_AVAILABLE_ZONE + seckillSkuId, availableZone, 0);
            // 分片库存数量归零
            for (int i = 0; i < zoneNum; i++) {
                RedisUtil.set(SeckillCacheNames.SECKILL_SKU_STOCK + seckillSkuId + CacheNames.UNION_KEY + i, 0, 0);
            }
            return availableZone;
        }

        // 每个分片至少的库存数量（目前是想分片出来的结果要么是这个数量，要么是0）
        // 比如 ( 10,10,5,0,0,0,0,) 这种结果
        int avgStock = getAvgStock(zoneNum, totalStock);
        // 获取要存进缓存的每个分片库存数量
        for (int i = 0; i < zoneNum; i++) {

            if (totalStock != 0) {
                availableZone = i + 1;
            }
            // 更新缓存
            if (totalStock - avgStock >= 0) {
                RedisUtil.set(SeckillCacheNames.SECKILL_SKU_STOCK + seckillSkuId + CacheNames.UNION_KEY + i, avgStock, 0);
                totalStock -= avgStock;
            } else {
                RedisUtil.set(SeckillCacheNames.SECKILL_SKU_STOCK + seckillSkuId + CacheNames.UNION_KEY + i, totalStock, 0);
                totalStock = 0;
            }
        }
        // 缓存可用分片数量
        RedisUtil.set(SeckillCacheNames.SECKILL_SKU_AVAILABLE_ZONE + seckillSkuId, availableZone, 0);
        return availableZone;
    }

    @Override
    public Integer getStockBySeckillSkuId(Long activityId,Long skuId) {
        int totalStock = 0;
        RLock rLock = redissonClient.getLock(LockCacheNames.LOCK_SECKILL_ZONE_PERFIX + skuId);
        rLock.lock();
        try {
            totalStock = multiRedisStockManager.getSeckillStock(
                    new MultiDecrementBO(MultiStockEnum.SECKILL, null, null,
                            activityId, skuId, null, null, null, null));
        } finally {
            rLock.unlock();
        }
        return totalStock;
    }

    @Override
    public Map<Long, Integer> getPointStockBySeckillSkuId(Long activityId, Long skuId, List<Long> stockPointIds) {
        Map<Long, Integer> map = new HashMap<>(stockPointIds.size());
        for (Long stockPointId : stockPointIds) {
            int totalStock = 0;
            RLock rLock = redissonClient.getLock(LockCacheNames.LOCK_SECKILL_ZONE_PERFIX + skuId);
            rLock.lock();
            try {
                MultiDecrementBO decrementBO = new MultiDecrementBO(MultiStockEnum.SECKILL, null, null,
                        activityId, skuId, null, null, null, null);
                decrementBO.setStockPointId(stockPointId);
                totalStock = multiRedisStockManager.getSeckillStock(decrementBO);
            } finally {
                rLock.unlock();
            }
            map.put(stockPointId, totalStock);
        }
        return map;
    }

    @Override
    public Integer getStockBySeckillId(Long seckillId) {
        SeckillSkuServiceImpl seckillSkuService = (SeckillSkuServiceImpl) AopContext.currentProxy();
        List<SeckillSkuVO> skuList = seckillSkuService.listSeckillSkuBySeckillId(seckillId);
        int totalStock = 0;
        for (SeckillSkuVO seckillSkuVO : skuList) {
            totalStock += getStockBySeckillSkuId(seckillSkuVO.getSeckillId(),seckillSkuVO.getSkuId());
        }
        return totalStock;
    }

    @Override
    public List<SeckillSkuVO> listMaybeNeedStockPlacementActivity() {
        return seckillSkuMapper.listMaybeNeedStockPlacementActivity();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void flushStockToDb(Long seckillSkuId, Long seckillId) {
        SeckillSkuVO seckillSku = seckillSkuMapper.getBySeckillSkuId(seckillSkuId);

        // 可用库存
        Integer hasStock = getStockBySeckillSkuId(seckillId,seckillSku.getSkuId());

        // 数据库里面的还没更新的剩余库存
        Integer seckillStocks = seckillSku.getSeckillStocks();
        // 改变的库存
        Integer changeStock = seckillStocks - hasStock;
        // 数据就真的没有变过，不需要更新数据库
        if (changeStock == 0) {
            return;
        }
        // 更新数据库秒杀活动的剩余库存
        seckillMapper.updateStocksById(seckillId, changeStock);
        // 更新数据库sku剩余的库存
        seckillSkuMapper.updateStocks(seckillSkuId, changeStock);
    }

    @Override
    public List<SeckillSkuVO> listOverJustNow() {
        return seckillSkuMapper.listOverJustNow();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void rollbackSecKillStock(Long activityId, Long skuId, Integer count, Long orderId, Long userId, Long stockPointId) {
        SeckillSkuVO secKillSku = seckillSkuMapper.getBySkuIdAndSeckillId(activityId, skuId);
        if (Objects.isNull(secKillSku)) {
            throw new LuckException("秒杀商品信息不存在");
        }
        // 还原秒杀库存(redis)
        RLock rLock = redissonClient.getLock(LockCacheNames.LOCK_SECKILL_ZONE_PERFIX + secKillSku.getSkuId());
        rLock.lock();
        try {
//            // 随机对一个分片增加库存
//            Random random = new Random();
//            int zoneNo = random.nextInt(availableZone);
            // TODO 秒杀取消订单的应该很少，所以直接在userId那台redis处理就好
            multiRedisStockManager.rollbackSecKillStock(activityId, skuId, count, orderId, userId,null, stockPointId);
        } finally {
            rLock.unlock();
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @GlobalTransactional(rollbackFor = Exception.class)
    public void handSeckillPointSku(SeckillVO seckillVO) {
        if (seckillVO == null) {
            return;
        }
        if (Objects.equals(seckillVO.getReplenish(), 1)) {
            // 补充库存
            List<SeckillSkuVO> seckillSkuList = listSeckillSkuBySeckillId(seckillVO.getSeckillId());
            List<SeckillStockPointSkuVO> updateList = new ArrayList<>();
            for (SeckillSkuVO seckillSkuVO : seckillSkuList) {
                List<String> pointList = new ArrayList<>();
                seckillSkuVO.getSeckillStockPointList().forEach(s-> pointList.add(String.valueOf(s.getStockPointId())));
                // 获取各仓库库存
                Map<Long, Integer> pointMap = multiRedisStockManager.getSeckillStockPointMap(
                        new MultiDecrementBO(MultiStockEnum.SECKILL, null, null,
                                seckillVO.getSeckillId(), seckillSkuVO.getSkuId(), null, null, null, pointList));
                seckillSkuVO.getSeckillStockPointList().forEach(s-> {
                    s.setStock(pointMap.get(s.getStockPointId()));
                    s.setSkuId(seckillSkuVO.getSkuId());
                    s.setSpuId(seckillVO.getSpuId());
                    s.setStatus(1);
                });
                // 过滤掉已经为空的库存数据
                updateList.addAll(seckillSkuVO.getSeckillStockPointList().stream().filter(s-> s.getStock() > 0).collect(Collectors.toList()));
            }
            // 补充到仓库里面
            skuStockFeignClient.addAndUpdateStockPoint(seckillVO.getSpuId(), BeanUtil.mapAsList(updateList, StockPointSkuDTO.class));
        }
    }

    @Override
    public void initPointSeckillStock() {

        String key = MultiStockEnum.SECKILL.value() + MultiRedisStockManager.STOCK;
        Map<Integer, Map<String, String>> seckillSkuStockMap = multiRedisStockManager.scan(key);

        for (Map.Entry<Integer, Map<String, String>> keyEntry : seckillSkuStockMap.entrySet()) {
            Integer index = keyEntry.getKey();
            Map<String, String> keyMap = keyEntry.getValue();
            Set<Long> skuIdSet = new HashSet<>(Constant.INITIAL_CAPACITY);
            Set<String> existKeySet = new HashSet<>(Constant.INITIAL_CAPACITY);
            Map<String, SeckillSkuVO> stockVOMap = new HashMap<>(Constant.INITIAL_CAPACITY);
            for (Map.Entry<String, String> stockEntry : keyMap.entrySet()) {
                String stockKey = stockEntry.getKey();
                String replace = stockKey.replace(key, "");
                String[] split = replace.split(Constant.UNDERLINE);
                SeckillSkuVO skuStockVO = new SeckillSkuVO();
                skuStockVO.setSeckillId(Long.parseLong(split[0]));
                skuStockVO.setSkuId(Long.parseLong(split[1]));
                skuStockVO.setSeckillStocks(Integer.parseInt(stockEntry.getValue()));
                String seckillSkuKey = skuStockVO.getSeckillId() + Constant.UNDERLINE + skuStockVO.getSkuId();
                if (existKeySet.contains(seckillSkuKey)) {
                    continue;
                }
                // 已经有区域库存的sku不再插入
                if (split.length > 2) {
                    existKeySet.add(seckillSkuKey);
                    stockVOMap.remove(seckillSkuKey);
                    skuIdSet.remove(skuStockVO.getSkuId());
                    continue;
                }
                skuIdSet.add(skuStockVO.getSkuId());
                stockVOMap.put(seckillSkuKey, skuStockVO);
            }
            StockPointSkuDTO stockPointSkuDTO = new StockPointSkuDTO();
            stockPointSkuDTO.setSkuIds(new ArrayList<>(skuIdSet));
            stockPointSkuDTO.setType(0);
            // 获取要初始化的sku默认库存
            List<StockPointSkuVO> stockPointSkuVOList = stockPointFeignClient.listStockPointSku(stockPointSkuDTO);
            Map<Long, Long> stockPointMap = stockPointSkuVOList.stream().collect(Collectors.toMap(StockPointSkuVO::getSkuId, StockPointSkuVO::getStockPointId));

            Map<String, String> msetMap = new HashMap<>();
            for (Map.Entry<String, SeckillSkuVO> skuStockEntry : stockVOMap.entrySet()) {
                SeckillSkuVO seckillSkuVO = skuStockEntry.getValue();
                Long stockPointId = stockPointMap.get(seckillSkuVO.getSkuId());
                if (Objects.isNull(stockPointId)) {
                    continue;
                }
                msetMap.put(key + skuStockEntry.getKey() + Constant.UNDERLINE + stockPointId, seckillSkuVO.getSeckillStocks().toString());
            }
            multiRedisStockManager.mset(index, msetMap);
        }
    }

    private int getAvgStock(int zoneNum, Integer totalStock) {

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

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

    /**
     * 两个数相除然后向上取整
     *
     * @param a 除数
     * @param b 被除数
     * @return 返回值
     */
    private int divToRoundUp(int a, int b) {
        return new BigDecimal(a).divide(new BigDecimal(b), 0, RoundingMode.UP).intValue();
    }
}
