/*
 * Copyright (c) 2018-2999 广州市蓝海创新科技有限公司 All rights reserved.
 *
 * https://www.mall4j.com/
 *
 * 未经允许，不可做商业用途！
 *
 * 版权所有，侵权必究！
 */

package com.yami.shop.api.listener;

import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.StrUtil;
import com.yami.shop.bean.app.dto.ShopCartItemDiscountDto;
import com.yami.shop.bean.app.dto.ShopCartItemDto;
import com.yami.shop.bean.app.dto.ShopCartOrderDto;
import com.yami.shop.bean.app.dto.ShopCartOrderMergerDto;
import com.yami.shop.bean.enums.OrderStatus;
import com.yami.shop.bean.event.SubmitOrderEvent;
import com.yami.shop.bean.model.*;
import com.yami.shop.bean.order.SubmitOrderOrder;
import com.yami.shop.common.constants.Constant;
import com.yami.shop.common.exception.YamiShopBindException;
import com.yami.shop.common.util.Arith;
import com.yami.shop.dao.*;
import com.yami.shop.security.api.util.SecurityUtils;
import com.yami.shop.service.ProductService;
import com.yami.shop.service.SkuService;
import com.yami.shop.service.UserAddrOrderService;
import lombok.AllArgsConstructor;
import cn.hutool.core.bean.BeanUtil;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.util.*;

/**
 * 确认订单信息时的默认操作
 * 该类主要用于处理在用户提交订单时的一系列默认业务逻辑，例如保存订单地址、计算订单金额、更新商品库存、生成订单相关信息以及插入订单结算数据等操作，
 * 同时还包含了对商品库存、商品状态等相关情况的校验，并在出现异常情况时抛出相应的异常。
 *
 * @author LGH
 */
@Component("defaultSubmitOrderListener")
@AllArgsConstructor
public class SubmitOrderListener {

    // 用于处理用户地址相关的订单服务，比如保存用户地址信息等操作
    private final UserAddrOrderService userAddrOrderService;
    // 用于处理商品相关业务逻辑的服务，例如获取商品信息等
    private final ProductService productService;
    // 用于处理商品库存单元（SKU）相关业务逻辑的服务，比如获取SKU信息等
    private final SkuService skuService;
    // 雪花算法工具类，用于生成唯一的订单编号等
    private final Snowflake snowflake;
    // 订单商品项数据访问层接口，用于操作订单商品项相关的数据，比如插入、更新等操作
    private final OrderItemMapper orderItemMapper;
    // SKU数据访问层接口，用于操作SKU相关的数据，比如更新库存等操作
    private final SkuMapper skuMapper;
    // 商品数据访问层接口，用于操作商品相关的数据，比如更新库存等操作
    private final ProductMapper productMapper;
    // 订单数据访问层接口，用于操作订单相关的数据，比如插入、更新订单信息等操作
    private final OrderMapper orderMapper;
    // 订单结算数据访问层接口，用于操作订单结算相关的数据，比如插入订单结算记录等操作
    private final OrderSettlementMapper orderSettlementMapper;
    // 购物车数据访问层接口，用于操作购物车相关的数据，比如删除购物车中的商品等操作
    private final BasketMapper basketMapper;

    /**
     * 计算订单金额及处理相关订单业务逻辑的方法
     * 该方法作为事件监听器，在接收到提交订单事件（SubmitOrderEvent）时被触发，执行一系列与订单确认相关的默认操作，
     * 包括保存订单地址、生成订单、更新商品库存、删除购物车商品等操作，同时会对商品库存不足等异常情况进行处理。
     *
     * @param event 提交订单事件对象，包含了与提交订单相关的各种数据，如合并后的订单信息等
     */
    @EventListener(SubmitOrderEvent.class)
    @Order(SubmitOrderOrder.DEFAULT)
    public void defaultSubmitOrderListener(SubmitOrderEvent event) {
        // 获取当前时间，用于后续设置创建时间等时间相关的属性
        Date now = new Date();
        // 获取当前登录用户的ID，用于关联订单与用户等操作
        String userId = SecurityUtils.getUser().getUserId();

        // 获取合并后的订单信息对象，包含了多个店铺的订单以及相关的商品、地址等信息
        ShopCartOrderMergerDto mergerOrder = event.getMergerOrder();

        // 订单商品参数，即包含了各个店铺下的订单商品信息列表
        List<ShopCartOrderDto> shopCartOrders = mergerOrder.getShopCartOrders();

        // 用于存储需要删除的购物车商品ID列表，后续会根据这个列表删除购物车中的对应商品
        List<Long> basketIds = new ArrayList<>();
        // 以商品SKU的ID为键，需要更新的SKU对象为值的映射表，用于记录商品SKU库存等信息的变化，方便后续统一更新库存
        Map<Long, Sku> skuStocksMap = new HashMap<>(16);
        // 以商品的ID为键，需要更新的商品对象为值的映射表，用于记录商品库存等信息的变化，方便后续统一更新库存
        Map<Long, Product> prodStocksMap = new HashMap<>(16);

        // 把订单地址保存到数据库
        // 通过属性拷贝将合并订单中的用户地址信息复制到用户地址订单对象中
        UserAddrOrder userAddrOrder = BeanUtil.copyProperties(mergerOrder.getUserAddr(), UserAddrOrder.class);
        if (userAddrOrder == null) {
            // 如果地址信息为空，则抛出异常，提示用户需要填写收货地址
            throw new YamiShopBindException("请填写收货地址");
        }
        userAddrOrder.setUserId(userId);
        userAddrOrder.setCreateTime(now);
        // 调用服务层方法保存用户地址订单信息到数据库中
        userAddrOrderService.save(userAddrOrder);

        // 获取保存后的订单地址ID，后续订单信息会关联该地址ID
        Long addrOrderId = userAddrOrder.getAddrOrderId();

        // 每个店铺生成一个订单，遍历各个店铺的订单商品信息列表进行订单创建等操作
        for (ShopCartOrderDto shopCartOrderDto : shopCartOrders) {
            createOrder(event, now, userId, basketIds, skuStocksMap, prodStocksMap, addrOrderId, shopCartOrderDto);
        }

        // 如果存在需要删除的购物车商品ID，则调用购物车数据访问层方法删除购物车中的对应商品
        if (!basketIds.isEmpty()) {
            basketMapper.deleteShopCartItemsByBasketIds(userId, basketIds);
        }

        // 遍历SKU库存映射表，更新每个SKU的库存信息
        skuStocksMap.forEach((key, sku) -> {
            // 调用SKU数据访问层的方法更新库存，如果更新的行数为0，表示库存更新失败（可能库存不足等原因）
            if (skuMapper.updateStocks(sku) == 0) {
                // 如果库存更新失败，清除对应SKU的缓存（通过SKU服务层方法），并抛出库存不足的异常，提示用户对应商品库存不足
                skuService.removeSkuCacheBySkuId(key, sku.getProdId());
                throw new YamiShopBindException("商品：[" + sku.getProdName() + "]库存不足");
            }
        });

        // 遍历商品库存映射表，更新每个商品的库存信息
        prodStocksMap.forEach((prodId, prod) -> {
            // 调用商品数据访问层的方法更新库存，如果更新的行数为0，表示库存更新失败（可能库存不足等原因）
            if (productMapper.updateStocks(prod) == 0) {
                // 如果库存更新失败，清除对应商品的缓存（通过商品服务层方法），并抛出库存不足的异常，提示用户对应商品库存不足
                productService.removeProductCacheByProdId(prodId);
                throw new YamiShopBindException("商品：[" + prod.getProdName() + "]库存不足");
            }
        });
    }

    /**
     * 创建单个店铺订单的方法
     * 该方法用于为每个店铺生成对应的订单信息，包括生成订单编号、处理订单商品项、构建订单对象、插入订单结算数据等操作，
     * 同时会更新相关的商品库存映射表和购物车商品ID列表。
     *
     * @param event            提交订单事件对象，包含了与提交订单相关的各种数据
     * @param now              当前时间，用于设置订单相关的时间属性
     * @param userId           当前登录用户的ID，用于关联订单与用户
     * @param basketIds        用于存储需要删除的购物车商品ID列表
     * @param skuStocksMap     以商品SKU的ID为键，需要更新的SKU对象为值的映射表，用于记录商品SKU库存等信息的变化
     * @param prodStocksMap    以商品的ID为键，需要更新的商品对象为值的映射表，用于记录商品库存等信息的变化
     * @param addrOrderId      订单地址的ID，用于关联订单与收货地址
     * @param shopCartOrderDto 单个店铺的订单商品信息对象，包含了该店铺下的商品、金额等订单相关信息
     */
    private void createOrder(SubmitOrderEvent event, Date now, String userId, List<Long> basketIds, Map<Long, Sku> skuStocksMap, Map<Long, Product> prodStocksMap, Long addrOrderId, ShopCartOrderDto shopCartOrderDto) {
        // 使用雪花算法生成唯一的订单编号，并转换为字符串类型
        String orderNumber = String.valueOf(snowflake.nextId());
        shopCartOrderDto.setOrderNumber(orderNumber);

        // 获取店铺的ID，用于关联订单与店铺
        Long shopId = shopCartOrderDto.getShopId();

        // 用于构建订单商品名称字符串，将各个商品名称拼接起来，方便展示订单包含的商品信息
        StringBuilder orderProdName = new StringBuilder(100);

        // 用于存储该订单下的各个订单商品项信息，后续会将这些商品项关联到订单对象中
        List<OrderItem> orderItems = new ArrayList<>();

        // 获取店铺订单下的商品折扣信息列表，每个商品折扣信息对象包含了多个商品项信息
        List<ShopCartItemDiscountDto> shopCartItemDiscounts = shopCartOrderDto.getShopCartItemDiscounts();
        for (ShopCartItemDiscountDto shopCartItemDiscount : shopCartItemDiscounts) {
            // 获取每个商品折扣信息对象下的商品项信息列表
            List<ShopCartItemDto> shopCartItems = shopCartItemDiscount.getShopCartItems();
            for (ShopCartItemDto shopCartItem : shopCartItems) {
                // 检查并获取商品的SKU信息，同时更新SKU库存映射表（如果需要），并处理库存不足等异常情况
                Sku sku = checkAndGetSku(shopCartItem.getSkuId(), shopCartItem, skuStocksMap);
                // 检查并获取商品信息，同时更新商品库存映射表（如果需要），并处理库存不足等异常情况
                Product product = checkAndGetProd(shopCartItem.getProdId(), shopCartItem, prodStocksMap);

                // 根据获取到的商品、SKU以及其他相关信息构建订单商品项对象
                OrderItem orderItem = getOrderItem(now, userId, orderNumber, shopId, orderProdName, shopCartItem, sku, product);

                orderItems.add(orderItem);

                if (shopCartItem.getBasketId()!= null && shopCartItem.getBasketId()!= 0) {
                    basketIds.add(shopCartItem.getBasketId());
                }
            }
        }

        // 对订单商品名称字符串进行处理，截取合适的长度，并去除末尾多余的逗号（如果有）
        orderProdName.subSequence(0, Math.min(orderProdName.length() - 1, 100));
        if (orderProdName.lastIndexOf(Constant.COMMA) == orderProdName.length() - 1) {
            orderProdName.deleteCharAt(orderProdName.length() - 1);
        }

        // 根据订单相关信息构建订单对象，包括店铺ID、订单编号、商品名称、用户ID、金额等各种属性
        com.yami.shop.bean.model.Order order = getOrder(now, userId, addrOrderId, shopCartOrderDto, orderNumber, shopId, orderProdName, orderItems);
        event.getOrders().add(order);

        // 插入订单结算数据，构建订单结算对象并设置相关属性，如用户ID、是否结算、创建时间、订单编号、支付金额等，然后插入到数据库中
        OrderSettlement orderSettlement = new OrderSettlement();
        orderSettlement.setUserId(userId);
        orderSettlement.setIsClearing(0);
        orderSettlement.setCreateTime(now);
        orderSettlement.setOrderNumber(orderNumber);
        orderSettlement.setPayAmount(order.getActualTotal());
        orderSettlement.setPayStatus(0);
        orderSettlement.setVersion(0);
        orderSettlementMapper.insert(orderSettlement);
    }

    /**
     * 根据相关信息构建订单对象的方法
     * 该方法根据传入的各种参数，如时间、用户ID、地址ID、店铺订单信息、订单编号、店铺ID、订单商品名称以及订单商品项列表等，构建一个完整的订单对象，
     * 设置订单对象的各个属性，包括店铺ID、订单编号、商品名称、用户ID、商品总额、实际总额、订单状态、时间、支付状态等各种属性。
     *
     * @param now              当前时间，用于设置订单的创建时间和更新时间等属性
     * @param userId           当前登录用户的ID，用于设置订单的用户ID属性
     * @param addrOrderId      订单地址的ID，用于设置订单的关联地址ID属性
     * @param shopCartOrderDto 单个店铺的订单商品信息对象，包含了该店铺下的商品、金额等订单相关信息，用于设置订单的商品总额、实际总额等属性
     * @param orderNumber      订单编号，用于设置订单的订单编号属性
     * @param shopId           店铺的ID，用于设置订单的店铺ID属性
     * @param orderProdName    订单商品名称字符串，用于设置订单的商品名称属性
     * @param orderItems       订单商品项列表，用于设置订单的商品项关联属性
     * @return 构建好的订单对象，包含了各种设置好的订单相关属性信息
     */
    private com.yami.shop.bean.model.Order getOrder(Date now, String userId, Long addrOrderId, ShopCartOrderDto shopCartOrderDto, String orderNumber, Long shopId, StringBuilder orderProdName, List<OrderItem> orderItems) {
        com.yami.shop.bean.model.Order order = new com.yami.shop.bean.model.Order();

        order.setShopId(shopId);
        order.setOrderNumber(orderNumber);
        // 设置订单商品名称属性
        order.setProdName(orderProdName.toString());
        // 设置用户ID属性
        order.setUserId(userId);
        // 设置商品总额属性，从店铺订单信息中获取
        order.setTotal(shopCartOrderDto.getTotal());
        // 设置实际总额属性，从店铺订单信息中获取
        order.setActualTotal(shopCartOrderDto.getActualTotal());
        // 设置订单状态为未支付状态（通过枚举值表示）
        order.setStatus(OrderStatus.UNPAY.value());
        order.setUpdateTime(now);
        order.setCreateTime(now);
        order.setIsPayed(0);
        order.setDeleteStatus(0);
        order.setProductNums(shopCartOrderDto.getTotalCount());
        order.setAddrOrderId(addrOrderId);
        // 计算并设置优惠金额属性，通过商品总额、运费和实际总额进行计算
        order.setReduceAmount(Arith.sub(Arith.add(shopCartOrderDto.getTotal(), shopCartOrderDto.getTransfee()), shopCartOrderDto.getActualTotal()));
        order.setFreightAmount(shopCartOrderDto.getTransfee());
        order.setRemarks(shopCartOrderDto.getRemarks());

        order.setOrderItems(orderItems);
        return order;
    }

    /**
     * 根据相关信息构建订单商品项对象的方法
     * 该方法根据传入的各种参数，如时间、用户ID、订单编号、店铺ID、订单商品名称、购物车商品项信息、商品SKU信息以及商品信息等，构建一个订单商品项对象，
     * 设置订单商品项对象的各个属性，包括店铺ID、订单编号、商品ID、SKU ID、SKU名称、商品数量、商品名称、图片、价格、用户ID、商品总额、时间等各种属性。
     *
     * @param now              当前时间，用于设置订单商品项的接收时间等属性
     * @param userId           当前登录用户的ID，用于设置订单商品项的用户ID属性
     * @param orderNumber      订单编号，用于设置订单商品项的订单编号属性
     * @param shopId           店铺的ID，用于设置订单商品项的店铺ID属性
     * @param orderProdName    订单商品名称字符串，用于拼接商品名称（在循环中不断追加）
     * @param shopCartItem     购物车商品项信息