package com.sckw.transport.service.dashboard;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.sckw.core.common.enums.enums.DictEnum;
import com.sckw.core.model.constant.Global;
import com.sckw.core.model.enums.CarWaybillV1Enum;
import com.sckw.core.utils.CollectionUtils;
import com.sckw.transport.api.model.vo.RealtimeSalesGoodsSeriesVo;
import com.sckw.transport.api.model.vo.RealtimeSalesVolumeVo;
import com.sckw.transport.model.KwtLogisticsOrder;
import com.sckw.transport.model.KwtLogisticsOrderGoods;
import com.sckw.transport.model.KwtLogisticsOrderUnit;
import com.sckw.transport.model.KwtWaybillOrder;
import com.sckw.transport.model.KwtWaybillOrderSubtask;
import com.sckw.transport.repository.KwtLogisticsOrderGoodsRepository;
import com.sckw.transport.repository.KwtLogisticsOrderRepository;
import com.sckw.transport.repository.KwtLogisticsOrderUnitRepository;
import com.sckw.transport.repository.KwtWaybillOrderRepository;
import com.sckw.transport.repository.KwtWaybillOrderSubtaskRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

/**
 * 实时销量服务：按小时、按商品汇总已完成运单的净重（吨）
 * <p>
 * 主要逻辑：
 * 1. 确定最近12小时的时间窗口。
 * 2. 查询该窗口内已完成的运单。
 * 3. 关联查询运单子任务以获取净重，关联物流订单货物以获取商品信息。
 * 4. 将每个运单的贡献映射到对应的小时时间槽中。
 * 5. 聚合生成按商品分类的销量系列数据。
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class RealtimeSalesVolumeService {

    /**
     * 时区：亚洲/上海
     */
    private static final ZoneId ZONE = ZoneId.of("Asia/Shanghai");

    /**
     * 时间格式化器：仅保留小时和分钟 (HH:mm)
     */
    private static final DateTimeFormatter HM = DateTimeFormatter.ofPattern("HH:mm");

    /**
     * 任务结束时间的最小有效纪元毫秒值 (2000-01-01 00:00:00 UTC)，用于过滤无效时间
     */
    private static final long TASK_END_MIN_EPOCH_MS = 946684800000L;

    /**
     * Fixed enterprise scope for the public BI dashboard.
     */
    private static final List<Long> REPORT_ENT_IDS = List.of(
            538038314096136193L,
            538039617157337089L,
            538040297439891457L
    );

    private final KwtWaybillOrderRepository waybillOrderRepository;
    private final KwtWaybillOrderSubtaskRepository waybillOrderSubtaskRepository;
    private final KwtLogisticsOrderGoodsRepository logisticsOrderGoodsRepository;
    private final KwtLogisticsOrderRepository logisticsOrderRepository;
    private final KwtLogisticsOrderUnitRepository logisticsOrderUnitRepository;

    /**
     * 构建实时销量视图对象
     *
     * @return 包含时间标签和商品销量系列的视图对象
     */
    public RealtimeSalesVolumeVo build() {
        log.debug("开始构建实时销量数据...");

        // 1. 初始化时间窗口：当前时刻向前推12小时，截断到整点
        ZonedDateTime h0 = ZonedDateTime.now(ZONE).truncatedTo(ChronoUnit.HOURS);
        Date windowStart = Date.from(h0.minusHours(12).toInstant());
        Date windowEnd = Date.from(h0.toInstant());
        log.debug("计算时间窗口起点: {}, 终点: {}", windowStart, windowEnd);

        // 2. 生成12个时间槽的结束时间点及其索引映射
        // 时间槽从 h0-11h 到 h0，共12个小时段
        List<ZonedDateTime> slotEnds = IntStream.range(0, 12)
                .mapToObj(j -> h0.minusHours(11 - j))
                .toList();
        
        // 建立时间槽结束点 -> 索引 (0-11) 的映射，方便后续快速定位
        Map<ZonedDateTime, Integer> slotIndexByEnd = IntStream.range(0, slotEnds.size())
                .boxed()
                .collect(Collectors.toMap(slotEnds::get, i -> i));

        // 3. 初始化返回对象并设置时间标签
        RealtimeSalesVolumeVo vo = new RealtimeSalesVolumeVo();
        vo.setTimeLabels(slotEnds.stream().map(z -> z.format(HM)).collect(Collectors.toCollection(ArrayList::new)));
        log.debug("生成时间标签: {}", vo.getTimeLabels());

        // 4. 查询时间窗口内状态为“已完成”且未删除的运单
        Set<Long> reportLogisticsOrderIds = queryReportLogisticsOrderIds();
        if (CollectionUtils.isEmpty(reportLogisticsOrderIds)) {
            log.info("未找到匹配固定看板企业范围的物流订单，企业ID列表={}", REPORT_ENT_IDS);
            vo.setSeries(List.of());
            return vo;
        }

        List<KwtWaybillOrder> waybills = waybillOrderRepository.list(
                Wrappers.<KwtWaybillOrder>lambdaQuery()
                        .eq(KwtWaybillOrder::getStatus, CarWaybillV1Enum.COMPLETED.getCode())
                        .eq(KwtWaybillOrder::getDelFlag, Global.NO)
                        .in(KwtWaybillOrder::getLOrderId, reportLogisticsOrderIds)
                        .ge(KwtWaybillOrder::getUpdateTime, windowStart)
                        .lt(KwtWaybillOrder::getUpdateTime, windowEnd));

        if (CollectionUtils.isEmpty(waybills)) {
            log.info("在时间窗口 [{} - {}] 内未找到已完成的运单数据。", windowStart, windowEnd);
            vo.setSeries(new ArrayList<>());
            return vo;
        }
        log.debug("查询到符合条件的运单数量: {}", waybills.size());

        // 5. 提取运单ID和物流订单ID集合，用于后续关联查询
        Set<Long> wIds = waybills.stream().map(KwtWaybillOrder::getId).filter(Objects::nonNull).collect(Collectors.toSet());
        Set<Long> lOrderIds = waybills.stream().map(KwtWaybillOrder::getLOrderId).filter(Objects::nonNull).collect(Collectors.toSet());
        // 按物流订单计费方式决定净重取装货量还是卸货量，避免实时销量口径与结算口径不一致。
        Map<Long, String> billingModeByLogOrder = billingModeByLogOrder(lOrderIds);

        // 6. 查询运单子任务，并计算每个运单的净重总和
        Map<Long, BigDecimal> netByWaybill = netWeightByWaybill(
                waybillOrderSubtaskRepository.list(
                        Wrappers.<KwtWaybillOrderSubtask>lambdaQuery()
                                .in(KwtWaybillOrderSubtask::getWOrderId, wIds)
                                .eq(KwtWaybillOrderSubtask::getDelFlag, Global.NO)),
                billingModeByLogOrder);
        log.debug("计算得到 {} 个运单的净重数据", netByWaybill.size());

        // 7. 查询物流订单货物信息，并选取每个物流订单的主要商品
        Map<Long, KwtLogisticsOrderGoods> goodsByLogOrder = pickPrimaryGoods(
                logisticsOrderGoodsRepository.queryByLogOrderIds(new ArrayList<>(lOrderIds)));
        log.debug("匹配到 {} 个物流订单的商品信息", goodsByLogOrder.size());

        // 8. 处理每个运单，将其贡献累加到对应的商品和时间槽中
        Map<Long, RealtimeSalesGoodsSeriesVo> seriesMap = new HashMap<>();
        waybills.stream()
                .flatMap(wo -> contributionStream(wo, windowStart, windowEnd, slotIndexByEnd, netByWaybill, goodsByLogOrder))
                .forEach(c -> c.accumulate(seriesMap));

        // 9. 将结果转换为列表并按商品名称排序
        vo.setSeries(seriesMap.values().stream()
                .sorted(Comparator.comparing(RealtimeSalesGoodsSeriesVo::getGoodsName, Comparator.nullsLast(String::compareTo)))
                .collect(Collectors.toCollection(ArrayList::new)));

        log.info("实时销量构建完成：运单 {} 条，生成商品系列 {} 个。", waybills.size(), vo.getSeries().size());
        return vo;
    }

    /**
     * 生成单个运单的贡献流
     * <p>
     * 判断运单是否在时间窗口内，确定其所属的时间槽，并关联商品信息生成贡献对象。
     *
     * @param wo               运单对象
     * @param windowStart      时间窗口起始时间
     * @param windowEnd        时间窗口结束时间
     * @param slotIndexByEnd   时间槽结束点到索引的映射
     * @param netByWaybill     运单ID到净重的映射
     * @param goodsByLogOrder  物流订单ID到主要商品的映射
     * @return 包含贡献信息的流，如果无效则返回空流
     */
    /**
     * Query logistics order ids that belong to the fixed public dashboard enterprise scope.
     *
     * @return logistics order id set
     */
    private Set<Long> queryReportLogisticsOrderIds() {
        List<KwtLogisticsOrderUnit> units = logisticsOrderUnitRepository.queryByReportEntIds(REPORT_ENT_IDS);
        if (CollectionUtils.isEmpty(units)) {
            return Set.of();
        }
        return units.stream()
                .filter(Objects::nonNull)
                .map(KwtLogisticsOrderUnit::getLOrderId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
    }

    private static Stream<WaybillContribution> contributionStream(
            KwtWaybillOrder wo,
            Date windowStart,
            Date windowEnd,
            Map<ZonedDateTime, Integer> slotIndexByEnd,
            Map<Long, BigDecimal> netByWaybill,
            Map<Long, KwtLogisticsOrderGoods> goodsByLogOrder) {

        // 获取运单的实际完成时间
        Date completedAt = completionInstant(wo);
        
        // 校验完成时间是否在查询窗口内
        if (completedAt.before(windowStart) || !completedAt.before(windowEnd)) {
            log.trace("运单 ID: {} 完成时间 {} 不在窗口 [{} - {}] 内，跳过。", wo.getId(), completedAt, windowStart, windowEnd);
            return Stream.empty();
        }

        // 计算该完成时间所属的时间槽结束点（向上取整到下一小时整点）
        ZonedDateTime slotEnd = ZonedDateTime.ofInstant(completedAt.toInstant(), ZONE)
                .truncatedTo(ChronoUnit.HOURS)
                .plusHours(1);
        
        // 查找时间槽索引
        Integer slotIdx = slotIndexByEnd.get(slotEnd);
        if (slotIdx == null) {
            log.warn("运单 ID: {} 完成时间 {} 计算出的时间槽结束点 {} 未匹配到预定义时间槽，跳过。", wo.getId(), completedAt, slotEnd);
            return Stream.empty();
        }

        // 获取关联的商品信息
        return Optional.ofNullable(goodsByLogOrder.get(wo.getLOrderId()))
                .filter(g -> g.getGoodsId() != null) // 确保商品ID有效
                .map(g -> {
                    BigDecimal tons = netByWaybill.getOrDefault(wo.getId(), BigDecimal.ZERO);
                    log.trace("运单 ID: {} 贡献商品: {}, 重量: {} 吨, 时间槽索引: {}", wo.getId(), g.getGoodsName(), tons, slotIdx);
                    return Stream.of(new WaybillContribution(
                            g.getGoodsId(),
                            g.getGoodsName(),
                            slotIdx,
                            tons));
                })
                .orElseGet(() -> {
                    log.debug("运单 ID: {} (物流单: {}) 无有效关联商品或商品ID为空，跳过。", wo.getId(), wo.getLOrderId());
                    return Stream.empty();
                });
    }

    /**
     * 从物流订单货物列表中选取每个物流订单的主要商品
     * <p>
     * 策略：如果一个物流订单有多个货物，选择ID最小的那个作为代表（通常是最早添加的或主商品）。
     *
     * @param goodsRows 物流订单货物列表
     * @return 物流订单ID -> 主要商品对象的映射
     */
    private static Map<Long, KwtLogisticsOrderGoods> pickPrimaryGoods(List<KwtLogisticsOrderGoods> goodsRows) {
        if (CollectionUtils.isEmpty(goodsRows)) {
            log.debug("输入的物流订单货物列表为空，返回空映射。");
            return Map.of();
        }
        
        Map<Long, KwtLogisticsOrderGoods> result = goodsRows.stream()
                .filter(g -> g.getLOrderId() != null) // 过滤掉物流订单ID为空的记录
                .sorted(Comparator.comparing(KwtLogisticsOrderGoods::getId, Comparator.nullsLast(Long::compareTo))) // 按货物ID排序，确保确定性
                .collect(Collectors.toMap(
                        KwtLogisticsOrderGoods::getLOrderId, 
                        g -> g, 
                        (a, b) -> a // 冲突时保留第一个（即ID较小的）
                ));
        
        log.debug("从 {} 条货物记录中提取出 {} 个物流订单的主要商品。", goodsRows.size(), result.size());
        return result;
    }

    /**
     * 查询物流订单计费方式映射。
     *
     * @param lOrderIds 物流订单ID集合
     * @return 物流订单ID -> 计费方式
     */
    private Map<Long, String> billingModeByLogOrder(Set<Long> lOrderIds) {
        if (CollectionUtils.isEmpty(lOrderIds)) {
            log.debug("物流订单ID集合为空，跳过计费方式查询。");
            return Map.of();
        }
        List<KwtLogisticsOrder> logisticsOrders = logisticsOrderRepository.queryByLogOrderIds(lOrderIds);
        if (CollectionUtils.isEmpty(logisticsOrders)) {
            log.warn("未查询到物流订单计费方式，lOrderIds={}", lOrderIds);
            return Map.of();
        }
        Map<Long, String> result = logisticsOrders.stream()
                .filter(order -> order != null && order.getId() != null && order.getBillingMode() != null)
                .collect(Collectors.toMap(KwtLogisticsOrder::getId, KwtLogisticsOrder::getBillingMode, (left, right) -> left));
        log.debug("物流订单计费方式映射构建完成，共 {} 条。", result.size());
        return result;
    }

    /**
     * 获取运单的完成时间
     * <p>
     * 优先使用 taskEndTime，如果无效（如时间为0或过小），则使用 updateTime。
     *
     * @param wo 运单对象
     * @return 完成时间
     */
    private static Date completionInstant(KwtWaybillOrder wo) {
        return Optional.ofNullable(wo.getTaskEndTime())
                .filter(t -> t.getTime() > TASK_END_MIN_EPOCH_MS) // 过滤无效的早期时间戳
                .or(() -> Optional.ofNullable(wo.getUpdateTime())) // 降级使用更新时间
                .orElseGet(() -> {
                    log.warn("运单 ID: {} 既无有效的任务结束时间也无更新时间，使用默认时间。", wo.getId());
                    return new Date(0);
                });
    }

    /**
     * 计算每个运单的净重总和（吨）
     *
     * @param subtasks 运单子任务列表
     * @return 运单ID -> 净重总和的映射
     */
    private static Map<Long, BigDecimal> netWeightByWaybill(List<KwtWaybillOrderSubtask> subtasks, Map<Long, String> billingModeByLogOrder) {
        if (CollectionUtils.isEmpty(subtasks)) {
            return Map.of();
        }
        
        Map<Long, BigDecimal> result = subtasks.stream()
                .filter(st -> st != null && st.getWOrderId() != null)
                .collect(Collectors.groupingBy(
                        KwtWaybillOrderSubtask::getWOrderId,
                        Collectors.mapping(st -> subtaskNetTon(st, billingModeByLogOrder.get(st.getLOrderId())),
                                Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));
        
        log.debug("从 {} 条子任务记录中聚合出 {} 个运单的净重。", subtasks.size(), result.size());
        return result;
    }

    /**
     * 计算单个子任务的净重
     * <p>
     * billingMode=1 按装货量计算，billingMode=2 按卸货量计算，其它缺失或异常值默认按卸货量计算。
     *
     * @param st 子任务对象
     * @param billingMode 物流订单计费方式
     * @return 净重（吨），默认为0
     */
    static BigDecimal subtaskNetTon(KwtWaybillOrderSubtask st, String billingMode) {
        if (st == null) {
            return BigDecimal.ZERO;
        }
        if (DictEnum.CHARGING_TYPE_1.getValue().equals(billingMode)) {
            return validAmountOrZero(st.getLoadAmount());
        }
        return validAmountOrZero(st.getUnloadAmount());
    }

    /**
     * 过滤无效重量，避免空指针和负数污染统计结果。
     *
     * @param amount 原始重量
     * @return 有效重量；为空或小于等于0时返回0
     */
    private static BigDecimal validAmountOrZero(BigDecimal amount) {
        return Optional.ofNullable(amount)
                .filter(value -> value.compareTo(BigDecimal.ZERO) > 0)
                .orElse(BigDecimal.ZERO);
    }

    /**
     * 生成包含12个零值的列表，用于初始化每个商品的时间槽数据
     *
     * @return 包含12个 BigDecimal.ZERO 的列表
     */
    private static List<BigDecimal> twelveZeroSlots() {
        return IntStream.range(0, 12)
                .mapToObj(i -> BigDecimal.ZERO.setScale(4, RoundingMode.HALF_UP))
                .collect(Collectors.toCollection(ArrayList::new));
    }

    /**
     * 运单贡献记录
     * <p>
     * 封装了单个运单对某个商品在特定时间槽的重量贡献。
     */
    private record WaybillContribution(Long goodsId, String goodsName, int slotIndex, BigDecimal tons) {

        /**
         * 将当前贡献累加到系列映射中
         *
         * @param seriesMap 商品ID -> 销量系列视图的映射
         */
        void accumulate(Map<Long, RealtimeSalesGoodsSeriesVo> seriesMap) {
            // 如果该商品尚未在映射中，则创建一个新的系列对象
            RealtimeSalesGoodsSeriesVo series = seriesMap.computeIfAbsent(goodsId, RealtimeSalesVolumeService::emptySeries);
            
            // 设置商品名称（如果当前系列名称为空，则更新）
            Optional.ofNullable(goodsName)
                    .filter(n -> !n.isEmpty())
                    .filter(n -> series.getGoodsName() == null || series.getGoodsName().isEmpty())
                    .ifPresent(series::setGoodsName);
            
            // 累加对应时间槽的重量
            List<BigDecimal> tonsPerSlot = series.getTonsPerSlot();
            if (slotIndex >= 0 && slotIndex < tonsPerSlot.size()) {
                BigDecimal currentVal = tonsPerSlot.get(slotIndex);
                BigDecimal newVal = currentVal.add(tons).setScale(4, RoundingMode.HALF_UP);
                tonsPerSlot.set(slotIndex, newVal);
            } else {
                log.warn("时间槽索引 {} 超出范围 [0, {}]，无法累加商品 ID: {} 的重量。", slotIndex, tonsPerSlot.size() - 1, goodsId);
            }
        }
    }

    /**
     * 创建一个空的商品销量系列对象
     *
     * @param goodsId 商品ID
     * @return 初始化后的系列对象
     */
    private static RealtimeSalesGoodsSeriesVo emptySeries(Long goodsId) {
        RealtimeSalesGoodsSeriesVo s = new RealtimeSalesGoodsSeriesVo();
        s.setGoodsId(goodsId);
        s.setGoodsName(""); // 初始化为空字符串，后续会被填充
        s.setTonsPerSlot(twelveZeroSlots()); // 初始化12个小时槽的数据为0
        return s;
    }
}
