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

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.tmerclub.cloud.api.order.feign.OrderFeignClient;
import com.tmerclub.cloud.api.product.bo.SkuStockLockBO;
import com.tmerclub.cloud.api.product.dto.SkuStockLockDTO;
import com.tmerclub.cloud.api.product.dto.StockBillLogItemDTO;
import com.tmerclub.cloud.api.product.vo.SkuStockVO;
import com.tmerclub.cloud.api.seckill.feign.SeckillFeignClient;
import com.tmerclub.cloud.common.cache.constant.LuaOperateEnum;
import com.tmerclub.cloud.common.constant.Constant;
import com.tmerclub.cloud.common.constant.SysTypeEnum;
import com.tmerclub.cloud.common.exception.LuckException;
import com.tmerclub.cloud.common.leaf.manager.SegmentManager;
import com.tmerclub.cloud.common.order.vo.OrderItemVO;
import com.tmerclub.cloud.common.order.vo.OrderVO;
import com.tmerclub.cloud.common.order.vo.WarehouseVO;
import com.tmerclub.cloud.common.product.constant.SpuMold;
import com.tmerclub.cloud.common.product.constant.StockBillType;
import com.tmerclub.cloud.common.product.constant.StockType;
import com.tmerclub.cloud.common.product.constant.SupplierDeliveryType;
import com.tmerclub.cloud.common.product.vo.StockPointSkuVO;
import com.tmerclub.cloud.common.response.ResponseEnum;
import com.tmerclub.cloud.common.response.ServerResponseEntity;
import com.tmerclub.cloud.common.rocketmq.config.RocketMqConstant;
import com.tmerclub.cloud.common.util.BeanUtil;
import com.tmerclub.cloud.product.constant.DistributedIdKey;
import com.tmerclub.cloud.product.manager.StockManager;
import com.tmerclub.cloud.product.service.SkuStockLockService;
import com.tmerclub.cloud.product.service.StockBillLogService;
import com.tmerclub.cloud.product.service.StockPointSkuService;
import com.tmerclub.cloud.product.service.WarehouseService;
import com.tmerclub.cloud.product.util.StockUtil;
import org.apache.dubbo.config.annotation.DubboReference;
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.messaging.support.GenericMessage;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

/**
 * 库存锁定信息
 *
 * @author tmerclub
 * @date 2020-12-22 16:12:10
 */
@Service
public class SkuStockLockServiceImpl implements SkuStockLockService {
    private static final Logger LOG = LoggerFactory.getLogger(SkuStockLockServiceImpl.class);

    @Autowired
    private RocketMQTemplate outStockLogTemplate;
    @Autowired
    private StockBillLogService stockBillLogService;
    @Autowired
    private StockManager stockManager;
    @Autowired
    private SegmentManager segmentManager;
    @Autowired
    private WarehouseService warehouseService;
    @Autowired
    private StockPointSkuService stockPointSkuService;
    @DubboReference
    private OrderFeignClient orderFeignClient;
    @DubboReference
    private SeckillFeignClient seckillFeignClient;

    /**
     * 锁定库存
     * 纯redis的操作，不需要事务
     *
     * 具体步骤：
     * 1.发送库存解锁mq
     * 2.扣除库存，同时添加锁定记录
     * 3.填充扣除后的库存数量
     * 4.添加出库记录
     *
     * 存在问题及处理方案（解锁库存时处理）：
     * 1.发送mq后，扣除库存失败 - 无需处理
     * 2.发送mq后，成功扣除库存，出库记录添加失败 - 在定时任务unLockOrderSkuStock中还原库存和删除redis中的库存锁定明细记录
     * 3.发送mq后，成功扣除库存，成功添加出库记录，订单创建失败 - 在定时任务unLockOrderSkuStock中删除出库记录，还原库存和删除redis中的库存锁定明细记录
     * 4.发送mq后，成功扣除库存，成功添加出库记录，订单创建后取消 - 添加入库记录，还原库存删除redis中的库存锁定明细记录
     *
     * @Transactional(rollbackFor = Exception.class)
     * @param stockBillLogItems 参数
     * @return
     */
    @Override
    public ServerResponseEntity<?> lock(List<StockBillLogItemDTO> stockBillLogItems) {
        List<SkuStockLockBO> skuStockLocks = BeanUtil.mapAsList(stockBillLogItems, SkuStockLockBO.class);

        // 扣除库存，同时添加锁定记录
        String luaResult = stockManager.lockStock(skuStockLocks);
        // 如果没有下划线，就是库存不足的商品skuId
        if (!luaResult.contains(StrUtil.COMMA)) {
            LOG.info("lock sku stock data: {}", luaResult);
            Long id = Long.valueOf(luaResult);
            if (id < 0) {
                return ServerResponseEntity.fail(ResponseEnum.SHOP_CART_NOT_EXIST);
            }
            return ServerResponseEntity.fail(ResponseEnum.NOT_STOCK, Long.valueOf(luaResult));
        }

        // 扣除库存成功，填充库存数量到出入库商品项中
        Map<String, List<StockBillLogItemDTO>> itemMap = stockBillLogItems.stream().collect(Collectors.groupingBy(s -> s.getOrderId().toString() + s.getSkuId()));
        String[] skuList = luaResult.split(Constant.COMMA);
        for (int i = 0; i < skuList.length; i++) {
            String[] arrayStr = skuList[i].split(Constant.UNDERLINE);
            if(Objects.equals(arrayStr[1], "0")) {
                continue;
            }
            List<StockBillLogItemDTO> stockBillLogItemList = itemMap.get(arrayStr[0] + arrayStr[2]);
            for (StockBillLogItemDTO stockBillLogItemDTO : stockBillLogItemList) {
                stockBillLogItemDTO.setAfterStock(Integer.valueOf(arrayStr[3]));
                Long stockBillLogId = segmentManager.getSegmentId(DistributedIdKey.tmerclub_STOCK_BILL_LOG);
                stockBillLogItemDTO.setStockBillLogId(stockBillLogId);
            }
        }

        // 生成出库明细
        SendStatus sendStockStatus = outStockLogTemplate.syncSend(RocketMqConstant.OUT_STOCK_LOG_TOPIC, new GenericMessage<>(stockBillLogItems)).getSendStatus();
        if (!Objects.equals(sendStockStatus, SendStatus.SEND_OK)) {
            // 消息发不出去就抛异常，发的出去无所谓
            throw new LuckException(ResponseEnum.EXCEPTION);
        }

        LOG.info("lock sku stock end....");
        return ServerResponseEntity.success();
    }

    @Override
    public String lockPurchaseOrder(List<SkuStockLockDTO> skuStockLocks) {
        List<String[]> stockList = this.listStringArrayBySkuStockLocks(skuStockLocks);
        // 扣除库存，同时添加锁定记录
        return stockManager.executeLuk(stockList, StockManager.PURCHASE_LOCK_STOCK_BYTES);
    }


    @Override
    public String purchaseOrderReduceLockStock(List<SkuStockLockDTO> skuStockLocks) {
        List<String[]> stockList = this.listStringArrayBySkuStockLocks(skuStockLocks);
        // 扣除库存，同时添加锁定记录
        return stockManager.executeLuk(stockList, StockManager.PURCHASE_REDUCE_LOCK_STOCK_BYTES);
    }

    @Override
    public String lockAllotOrder(List<SkuStockLockDTO> skuStockLocks) {
        List<String[]> stockList = this.listStringArrayBySkuStockLocks(skuStockLocks);
        // 扣除库存，同时添加锁定记录
        return stockManager.executeLuk(stockList, StockManager.ALLOT_LOCK_STOCK_BYTES);
    }

    @Override
    public String reduceLockAllotOrder(List<SkuStockLockDTO> skuStockLocks) {
        List<String[]> stockList = this.listStringArrayBySkuStockLocks(skuStockLocks);
        // 减少锁定库存，同时删除锁定记录
        return stockManager.executeLuk(stockList, StockManager.ALLOT_REDUCE_LOCK_STOCK_BYTES);
    }


    @Override
    public String changeStock(List<SkuStockVO> skuStocks) {
        List<String[]> stockList = new ArrayList<>(skuStocks.size());
        for (SkuStockVO skuStockLock : skuStocks) {
            stockList.add(StockUtil.loadQueryArray(
                    skuStockLock.getSkuId(),
                    StockManager.WAREHOUSE_ID,
                    skuStockLock.getStock(),
                    LuaOperateEnum.SKU_REPLACE));
        }
        // 扣除库存，同时添加锁定记录
        return stockManager.changeStock(stockList, Boolean.TRUE);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void unlockStockByCancel(List<Long> orderIds) {
        ServerResponseEntity<List<OrderVO>> ordersResponse = orderFeignClient.listOrderAndOrderItemByOrderIds(orderIds);
        if (!ordersResponse.isSuccess()) {
            throw new LuckException(ordersResponse.getMsg());
        }
        List<OrderVO> orderList = ordersResponse.getData();
        List<SkuStockLockBO> skuStocks = new ArrayList<>(Constant.INITIAL_CAPACITY);
        // 获取所有的非组合商品订单项的仓库和门店情况,如果门店是共享库存或者仓库已经被删除则库存回退到默认仓库
        List<Long> skuIds = new ArrayList<>(Constant.INITIAL_CAPACITY);
        Map<Long,Long> shopDefaultStockIdMap = new HashMap<>(Constant.INITIAL_CAPACITY);
        for (OrderVO orderVO : orderList) {
            skuIds.addAll(orderVO.getOrderItems().stream().filter(orderItemVO -> !Objects.equals(orderItemVO.getSpuMold(), SpuMold.COMBO.value()))
                    .map(OrderItemVO::getSkuId).toList());
            // 判断下是否为供应商商品,如果是商家代销供应商商品
            int sysType = SysTypeEnum.MULTISHOP.value();
            if (!Objects.equals(orderVO.getSupplierId(), 0L)) {
                sysType = SysTypeEnum.SUPPLIER.value();

            }
            // 此处直接用orderId作为key方便后续操作
            WarehouseVO  warehouseVO = warehouseService.getDefaultWarehouseByShopId(orderVO.getShopId(), sysType);
            if(Objects.nonNull(warehouseVO)){
                shopDefaultStockIdMap.put(orderVO.getOrderId(),warehouseVO.getWarehouseId());
            }
        }
        List<StockPointSkuVO> stockPointSkus = stockPointSkuService.listBySkuIds(skuIds);
        Map<Long, List<StockPointSkuVO>> skuStockPointMap = stockPointSkus.stream().collect(Collectors.groupingBy(StockPointSkuVO::getSkuId));
        for (OrderVO orderVO : orderList) {
            boolean supplierDelivery = Objects.equals(orderVO.getSupplierDeliveryType(), SupplierDeliveryType.SUPPLIER_DELIVERY.value());
            // 手动取消订单，解锁库存
            for (OrderItemVO orderItem : orderVO.getOrderItems()) {
                // 如果是组合商品主商品,不需要回退库存直接跳过
                if(Objects.equals(orderItem.getSpuMold(), SpuMold.COMBO.value())){
                    continue;
                }
                SkuStockLockBO skuStockLockBO = new SkuStockLockBO();
                if (supplierDelivery) {
                    skuStockLockBO.setSkuId(orderItem.getSupplierSkuId());
                } else {
                    skuStockLockBO.setSkuId(orderItem.getSkuId());
                }
                List<StockPointSkuVO> stockPointSku = skuStockPointMap.get(orderItem.getSkuId());
                List<Long> stockPointIds = stockPointSku.stream().map(StockPointSkuVO::getStockPointId).toList();
                skuStockLockBO.setStockPointId(orderItem.getStockPointId());
                // 如果不存在这个库存id,则使用默认库存id进行扣减
                if(!stockPointIds.contains(orderItem.getStockPointId())){
                    skuStockLockBO.setDefaultStockPointId(shopDefaultStockIdMap.get(orderVO.getOrderId()));
                }
                skuStockLockBO.setOrderId(orderVO.getOrderId());
                skuStockLockBO.setStockCount(orderItem.getCount());
                skuStocks.add(skuStockLockBO);
            }
        }
        if (CollUtil.isEmpty(skuStocks)) {
            return;
        }
        List<StockBillLogItemDTO> stockBillLogItems = new ArrayList<>(Constant.INITIAL_CAPACITY);
        Date now = new Date();
        // 解锁库存，并且将更新后的库存set到stockBillLogItems
        Map<Long, Integer> stockMap = stockManager.unLockStock(skuStocks);
        if (CollUtil.isEmpty(stockMap)) {
            LOG.info("没有可解锁的库存，订单ids:{}", orderIds);
            return;
        }

        for (OrderVO orderVO : orderList) {
            boolean supplierDelivery = Objects.equals(orderVO.getSupplierDeliveryType(), SupplierDeliveryType.SUPPLIER_DELIVERY.value());
            // 手动取消订单，解锁库存
            for (OrderItemVO orderItem : orderVO.getOrderItems()) {
                long skuId = supplierDelivery ? orderItem.getSupplierSkuId() : orderItem.getSkuId();
                if (!stockMap.containsKey(skuId)) {
                    continue;
                }
                Integer stock = stockMap.get(skuId);
                StockBillLogItemDTO stockBillLogItem = new StockBillLogItemDTO(
                        orderItem.getSpuId(),
                        orderItem.getSkuId(),
                        orderItem.getShopId(),
                        orderItem.getOrderId(),
                        orderItem.getSpuName(),
                        orderItem.getSkuName(),
                        orderItem.getCount(),
                        orderItem.getPrice(),
                        orderItem.getPartyCode(),
                        orderItem.getPic()
                );
                stockBillLogItem.setAfterStock(stock);
                List<StockPointSkuVO> stockPointSku = skuStockPointMap.get(orderItem.getSkuId());
                List<Long> stockPointIds = stockPointSku.stream().map(StockPointSkuVO::getStockPointId).toList();
                // 如果不存在这个库存id,则使用默认库存id进行后续操作
                if(!stockPointIds.contains(orderItem.getStockPointId())){
                    stockBillLogItem.setStockPointId(shopDefaultStockIdMap.get(orderVO.getOrderId()));
                }
                stockBillLogItem.setSupplierDeliveryType(orderVO.getSupplierDeliveryType());
                stockBillLogItem.setSupplierId(orderVO.getSupplierId());
                stockBillLogItem.setCreateTime(now);
                stockBillLogItem.setUpdateTime(now);
                stockBillLogItem.setSpuName(orderItem.getOrderItemLangList().get(0).getSpuName());
                stockBillLogItem.setSkuName(orderItem.getOrderItemLangList().get(0).getSkuName());
                stockBillLogItem.setStockBillType(StockBillType.ORDER_CANCEL.value());
                stockBillLogItem.setStocktype(StockType.WAREHOUSING.value());
                stockBillLogItems.add(stockBillLogItem);
            }
        }

        // 生成出入库明细信息
        stockBillLogService.generateStockLogOfOrderList(stockBillLogItems);
    }

    /**
     * 因为异常解锁，所以要删除库存记录
     *
     * 库存锁定流程分析：
     * 1.库存解锁发送mq后，扣除库存失败 - 无需处理
     * 2.库存解锁发送mq后，成功扣除库存，出库记录添加失败 - 还原库存和删除redis中的库存锁定明细记录
     * 3.库存解锁发送mq后，成功扣除库存，成功添加出库记录，订单创建失败 - 删除出库记录，还原库存和删除redis中的库存锁定明细记录
     * 4.库存解锁发送mq后，成功扣除库存，成功添加出库记录，订单创建后取消 - 添加入库记录，还原库存删除redis中的库存锁定明细记录
     * 5.库存解锁发送mq后，成功扣除库存，成功添加出库记录，订单支付成功 - 解锁库存、增加商品销量并删除redis中的库存锁定明细记录
     *
     * 数据对应关系：
     * 1.redis中查询锁定记录， 如果没有锁定记录就代表是上面的步骤1，无需处理
     * 2.mongodb中查询出库记录，如果没有出库记录，就是上面的步骤2，还原库存和删除redis中的库存锁定明细记录
     * 3.mongodb中查询出库记录，有出库记录但没有生成订单，就是上面的步骤3，删除出库记录、还原库存和删除redis中的库存锁定明细记录
     * 4.查询订单状态，如果订单已支付，就是上面的步骤5，还原库存、增加销量并删除redis中的库存锁定明细记录
     * 5.如果订单状态不是已取消且为待支付，就先不处理
     *
     * 订单取消和订单退款时，会解锁库存，所以不在该方法处理
     *
     *
     */
    @Override
    public void unlockStockByException(List<SkuStockVO> skuStockList) {
        if (CollUtil.isEmpty(skuStockList)) {
            return;
        }
        // 先查询订单信息，判断订单状态 - 如果能查询到订单，代表库存扣除和出库记录都已成功
        Set<Long> orderSet = skuStockList.stream().map(SkuStockVO::getOrderId).collect(Collectors.toSet());
        // 从订单服务中查询订单信息
        List<Long> orderIds = new ArrayList<>(orderSet);
        Map<Long, OrderVO> orderMap = orderFeignClient.listOrderByOrderIds(orderIds)
                .stream().collect(Collectors.toMap(OrderVO::getOrderId, Function.identity()));
        // 根据订单id对sku库存列表进行分组
        Map<Long, List<SkuStockVO>> orderStockMap = skuStockList.stream().collect(Collectors.groupingBy(SkuStockVO::getOrderId));
        // 订单支付成功的数据列表
        List<String[]> paySuccessList = new ArrayList<>();
        // 创建失败的数据列表
        List<String[]> createFailList = new ArrayList<>();
        // 未支付订单列表
        List<Long> unPayOrderIds = new ArrayList<>();
        // 处理的结束时间为前一天23点之前的订单
        long endTime = DateUtil.offsetHour(DateUtil.beginOfDay(new Date()), -1).getTime();
        orderIds.clear();
        // 获取sku库存记录
        // 提交订单时有事务，如果失败就是全部创建失败了，不存在一部分创建成功一部分创建失败的情况
        // 所以只要订单列表的数据不为空，就代表订单提交成功，以下是订单提交成功的处理
        for (Long orderId : orderStockMap.keySet()) {
            List<SkuStockVO> skuStocks = orderStockMap.get(orderId);
            // 没有查到订单数据，代表订单创建失败，需要解锁库存和删除sku出入库记录
            if (!orderMap.containsKey(orderId)) {
                orderIds.add(orderId);
                this.addSkuStock(createFailList, skuStocks);
                continue;
            }
            // 1.创建时间为23点之后的订单，先不用处理
            // 2.订单未支付，不处理-- 订单存在但未支付，此时距离订单提交已过去一个小时以上，而订单状态不取消也不支付，基本就是程序执行出问题了，所以等
            OrderVO orderVO = orderMap.get(orderId);
            boolean unPay = !Objects.equals(orderVO.getIsPayed(), 1);
            if (orderVO.getCreateTime().getTime() > endTime || unPay) {
                if (unPay) {
                    unPayOrderIds.add(orderId);
                } else {
                    LOG.info("订单创建时间不足一个小时，不处理，下单时间:{}", DateUtil.format(orderVO.getCreateTime(), DatePattern.NORM_DATETIME_PATTERN));
                }
                continue;
            }
            this.addSkuStock(paySuccessList, skuStocks);
        }
        System.out.println();

        if (CollUtil.isNotEmpty(unPayOrderIds)) {
            LOG.info("订单未支付，不处理，订单id:{}", unPayOrderIds);
        }


        if (CollUtil.isNotEmpty(paySuccessList)) {
            // 解除锁定库存并添加销量
            String stock = stockManager.executeLuk(paySuccessList, StockManager.ORDER_BATCH_MAKER_USE_STOCK_BYTES);
            if (!stock.contains(Constant.UNDERLINE)) {
                throw new LuckException("解锁库存, 增加销量失败，数据异常:{}", stock);
            } else {
                LOG.info("解锁库存, 增加销量成功，数据:{}", stock);
            }
        }

        // ===================== 如果生成订单失败，为了库存记录和订单数据的一致性，需要解锁库存并删除相关的记录。 =====================
        if (CollUtil.isNotEmpty(createFailList)) {
            // 删除sku入库记录
            stockBillLogService.removeByOrderIds(orderIds, StockBillType.PURCHASE_OUTBOUND.value());

            // 解锁库存，并且将更新后的库存set到stockBillLogItems
            // 解锁库存要放在最后执行，出入库记录又事务可以回滚， 但库存解锁是redis操作，无法回滚
            // 秒杀订单也会进入这里，但是因为没有生成对应的key，所以不用担心秒杀订单的库存解锁
            String stock = stockManager.executeLuk(createFailList, StockManager.ORDER_BATCH_UNLOCK_STOCK_BYTES);
            if (!stock.contains(Constant.UNDERLINE)) {
                throw new LuckException("解锁库存，增加可售库存失败，数据异常:{}", stock);
            } else {
                LOG.info("解锁库存，增加可售库存成功，sku数据:{}", stock);
            }
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void markerStockUse(List<Long> orderIds) {
        // 获取订单数据
        List<OrderVO> orderList = orderFeignClient.listOrderAndOrderItemByOrderIds(orderIds).getData();

        // 区分sku和供应商sku，进行库存处理
        Map<Long, SkuStockLockBO> stockLockMap = new HashMap<>(Constant.INITIAL_CAPACITY);
        for (OrderVO orderVO : orderList) {
            for (OrderItemVO orderItem : orderVO.getOrderItems()) {
                if (Objects.equals(orderItem.getSpuMold(), SpuMold.COMBO.value())) {
                    // 组合商品的主商品，不需要更改库存
                    continue;
                }
                long skuId;
                if (Objects.equals(orderVO.getSupplierDeliveryType(), SupplierDeliveryType.SUPPLIER_DELIVERY.value())) {
                    // 供应发货的商品，库存操作使用供应商的skuId
                    skuId = orderItem.getSupplierSkuId();
                } else {
                    // 非供应商商品或者供应商商品的商家自行发货，skuId
                    skuId = orderItem.getSkuId();
                }
                SkuStockLockBO skuStockLockBO = stockLockMap.get(skuId);
                if (skuStockLockBO == null) {
                    skuStockLockBO = new SkuStockLockBO();
                    skuStockLockBO.setSkuId(skuId);
                    skuStockLockBO.setOrderId(orderVO.getOrderId());
                    skuStockLockBO.setStockCount(orderItem.getCount());
                    skuStockLockBO.setStockPointId(orderItem.getStockPointId());
                    stockLockMap.put(skuId, skuStockLockBO);
                    continue;
                }
                skuStockLockBO.setStockCount(orderItem.getCount() + skuStockLockBO.getStockCount());
            }
        }

        //  ==============库存扣除=============
        if (MapUtil.isNotEmpty(stockLockMap)) {
            String s = stockManager.makerUseStock(new ArrayList<>(stockLockMap.values()));
        }
    }

    private List<String[]> listStringArrayBySkuStockLocks(List<SkuStockLockDTO> skuStockLocks) {
        List<String[]> stockList = new ArrayList<>(skuStockLocks.size());
        for (SkuStockLockDTO skuStockLock : skuStockLocks) {
            stockList.add(StockUtil.loadQueryArray(
                    skuStockLock.getOrderId(),
                    Objects.isNull(skuStockLock.getWarehouseId()) ? StockManager.WAREHOUSE_ID : skuStockLock.getWarehouseId(),
                    skuStockLock.getSkuId(),
                    skuStockLock.getCount()
            ));
        }
        return stockList;
    }
    private void addSkuStock(List<String[]> list, List<SkuStockVO> skuStocks) {
        for (SkuStockVO skuStock : skuStocks) {
            list.add(StockUtil.loadQueryArray(skuStock.getOrderId(), StockManager.WAREHOUSE_ID, skuStock.getSkuId(), skuStock.getStock()));
        }
    }
}
