package com.aps.macroplanner.data;

import com.aps.common.util.ParamValidator;
import com.aps.entity.*;
import com.aps.entity.basic.Material;
import com.aps.entity.basic.MaterialSupply;
import com.aps.mapper.EquipCapacityDefMapper;
import com.aps.mapper.EquipinfoMapper;
import com.aps.mapper.ErpPurchaseOrderMapper;
import com.aps.mapper.MaterialInfoMapper;
import com.aps.mapper.MaterialPurchaseMapper;
import com.aps.mapper.MesShiftWorkSchedMapper;
import com.aps.mapper.PlanResourceMapper;
import com.aps.mapper.ProdEquipSpecialCalMapper;
import com.aps.mapper.ProdLaunchOrderMapper;
import com.aps.mapper.PurchaseReceiptMapper;
import com.aps.mapper.RoutingDetailEquipMapper;
import com.aps.mapper.RoutingHeaderMapper;
import com.aps.mapper.RoutingsupportingMapper;
import com.aps.mapper.SjzPfWhStockMapper;
import com.aps.mapper.StockMapper;
import com.aps.service.ApsTimeConfigService;
import com.aps.service.LanuchService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
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;

/**
 * 数据库工艺物料类 → macroplanner 实体转换器。
 *
 * <p>将 {@code PlanResultService.execute2} 处理的数据库"工艺物料类"实体
 * （{@link RoutingHeader}/{@link RoutingDetail}/{@link Routingsupporting}/
 * {@link Material}/{@link Stock} 等）转换为 {@link RoutingTestDataBuilder}
 * 所用的 macroplanner 实体（{@link Product}/{@link Operation}/{@link Routing}/
 * {@link OperationInput}/{@link StockingPoint}/{@link InitialInventory}/
 * {@link InTransitSupply}），返回填充好的 {@link TestDataBuilder}，
 * 供 {@code MacroPlannerOptimizer} 直接消费。</p>
 *
 * <h3>设计要点</h3>
 * <pre>
 *   1. 独立的 Spring @Service，接收 sceneId 自行查询数据库
 *   2. 通过同包静态内部类 Sink extends TestDataBuilder 访问 protected 字段
 *   3. 利用 Routing.expand() 自动生成工序间 WIP，只手动补 MP 原材料投料
 *   4. 消耗因子 factor = Routingsupporting.spentQty / Routingsupporting.mainQty
 *   5. Period/UnitPeriod/SalesDemand/InventorySpec/SupplySpec/KPIWeights 用默认值或从订单推导
 * </pre>
 *
 * <h3>核心映射关系</h3>
 * <pre>
 *   Product          ← Material (MaterialInfo)
 *   StockingPoint    ← Stock.storeId / Routingsupporting.storeId
 *   Operation        ← RoutingDetail (无产出, 由 Routing.expand() 配置)
 *   Routing          ← RoutingHeader + RoutingDetail 列表 (调用 expand())
 *   OperationInput   ← Routingsupporting (仅 MP 原材料投料)
 *   InitialInventory ← Material.materialStocks (Stock)
 *   InTransitSupply  ← Material.InTransit (MaterialSupply)
 *   SalesDemand      ← ProdLaunchOrder
 * </pre>
 */
@Service
@Slf4j
public class MacroPlannerDataConverter {

    /** 默认周期数 (一周) */
    private static final int DEFAULT_PERIOD_COUNT = 7;
    /** 默认单元最大产能 (小时/天) */
    private static final double DEFAULT_UNIT_MAX_CAPACITY = 24.0;
    /** 默认宽松库存上限 */
    private static final double LOOSE_MAX = 999999.0;

    // ==================== 注入的 Mapper ====================
    @Autowired
    private ProdLaunchOrderMapper prodLaunchOrderMapper;
    @Autowired
    private RoutingHeaderMapper routingHeaderMapper;
    @Autowired
    private RoutingsupportingMapper routingsupportingMapper;
    @Autowired
    private MaterialInfoMapper materialInfoMapper;
    @Autowired
    private StockMapper stockMapper;
    @Autowired
    private MaterialPurchaseMapper materialPurchaseMapper;
    @Autowired
    private ErpPurchaseOrderMapper erpPurchaseOrderMapper;
    @Autowired
    private PurchaseReceiptMapper purchaseReceiptMapper;
    @Autowired
    private SjzPfWhStockMapper sjzPfWhStockMapper;
    @Autowired
    private RoutingDetailEquipMapper routingDetailEquipMapper;
    @Autowired
    private EquipinfoMapper equipinfoMapper;
    @Autowired
    private PlanResourceMapper planResourceMapper;
    @Autowired
    private ProdEquipSpecialCalMapper prodEquipSpecialCalMapper;
    @Autowired
    private MesShiftWorkSchedMapper mesShiftWorkSchedMapper;
    @Autowired
    private EquipCapacityDefMapper equipCapacityDefMapper;

    // ==================== 注入的 Service ====================
    @Autowired
    private LanuchService lanuchService;
    @Autowired
    private ApsTimeConfigService apsTimeConfigService;

    // ==================== 公开入口 ====================

    /**
     * 将指定场景的数据库工艺物料数据转换为 macroplanner 的 TestDataBuilder。
     *
     * @param sceneId 场景ID
     * @return 填充好的 TestDataBuilder, 可直接传给 MacroPlannerOptimizer
     */
    public TestDataBuilder convert(String sceneId) {
        log.info("开始转换场景数据到 macroplanner: sceneId={}", sceneId);

        ConvertContext ctx = loadRawData(sceneId);
        buildMaterials(ctx);
        buildDailyCapacityByUnitId(ctx);

        Sink sink = new Sink();
        sink.fill(ctx);

        log.info("转换完成: products={}, stockingPoints={}, operations={}, routings={}, operationInputs={}, "
                        + "initialInventories={}, inTransitSupplies={}, salesDemands={}",
                sink.getProducts().size(), sink.getStockingPoints().size(),
                sink.getOperations().size(), sink.getRoutings().size(),
                sink.getOperationInputs().size(), sink.getInitialInventories().size(),
                sink.getInTransitSupplies().size(), sink.getSalesDemands().size());

        return sink;
    }

    // ==================== 步骤1: 数据加载 ====================

    private ConvertContext loadRawData(String sceneId) {
        ConvertContext ctx = new ConvertContext();

        // 1. 订单 (按 sceneId 过滤)
        ctx.prodLaunchOrders = prodLaunchOrderMapper.selectList(
                new LambdaQueryWrapper<ProdLaunchOrder>()
                        .eq(ProdLaunchOrder::getSceneId, sceneId));
        log.info("加载订单: {} 条", ctx.prodLaunchOrders.size());

        // 2. 收集 materialIds
        Set<String> materialIds = ctx.prodLaunchOrders.stream()
                .map(ProdLaunchOrder::getMaterialId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toSet());
        List<Integer> routingIds=null;
        // 3. 工艺路线头表
        if (!materialIds.isEmpty()) {
            ctx.routingHeaders = routingHeaderMapper.selectList(
                    new LambdaQueryWrapper<RoutingHeader>()
                            .in(RoutingHeader::getMaterialId, materialIds));

             routingIds = ctx.routingHeaders.stream()
                    .map(RoutingHeader::getId)
                    .filter(Objects::nonNull)
                    .distinct()
                    .collect(Collectors.toList());
        }
        log.info("加载工艺路线: {} 条", ctx.routingHeaders.size());

        // 4. 工序 (通过 LanuchService 批量查询)
        if (!routingIds.isEmpty()) {
            List<Long> routingIdsLong = routingIds.stream()
                    .map(Long::valueOf)
                    .collect(Collectors.toList());
            List<RoutingDetail> details = lanuchService.getRoutingDetails(routingIdsLong);
            if (details != null) {
                ctx.routingDetails = details;
            }
        }
        log.info("加载工序: {} 条", ctx.routingDetails.size());

        // 5. 工艺物料消耗 (BOM)
        if (!routingIds.isEmpty()) {
            ctx.routingsupportings = routingsupportingMapper.selectList(
                    new LambdaQueryWrapper<Routingsupporting>()
                            .in(Routingsupporting::getRoutingHeaderId, routingIds)
                            .eq(Routingsupporting::getIsdeleted, 0));
        }
        log.info("加载工艺物料消耗: {} 条", ctx.routingsupportings.size());

        // 6. 收集所有 materialId (订单 + 工艺物料消耗 + 工艺路线头表)


        ctx.routingsupportings.forEach(rs -> {
            if (rs.getMaterialId() != null) materialIds.add(rs.getMaterialId());
        });


        // 7. 物料主数据
        if (!materialIds.isEmpty()) {
            ctx.materialInfos = materialInfoMapper.selectList(
                    new LambdaQueryWrapper<MaterialInfo>()
                            .in(MaterialInfo::getId, materialIds));
        }
        log.info("加载物料主数据: {} 条", ctx.materialInfos.size());

        // 8. 库存、采购、在途 (按 materialId 过滤, 避免全表扫描)
        if (!materialIds.isEmpty()) {
            ctx.stocks = stockMapper.selectList(
                    new LambdaQueryWrapper<Stock>()
                            .in(Stock::getMaterialId, materialIds)
                            .eq(Stock::getIsdeleted, 0));
            ctx.materialPurchases = materialPurchaseMapper.selectList(
                    new LambdaQueryWrapper<MaterialPurchase>()
                            .in(MaterialPurchase::getMaterialId, materialIds)
                            .eq(MaterialPurchase::getIsdeleted, 0));
            ctx.erpPurchaseOrders = erpPurchaseOrderMapper.selectList(
                    new LambdaQueryWrapper<ErpPurchaseOrder>()
                            .in(ErpPurchaseOrder::getMaterialId, materialIds)
                            .eq(ErpPurchaseOrder::getIsdeleted, 0));
            ctx.purchaseReceipts = purchaseReceiptMapper.selectList(
                    new LambdaQueryWrapper<PurchaseReceipt>()
                            .in(PurchaseReceipt::getMaterialid, materialIds)
                            .eq(PurchaseReceipt::getIsdeleted, 0));
            ctx.sjzPfWhStocks = sjzPfWhStockMapper.selectList(
                    new LambdaQueryWrapper<SjzPfWhStock>()
                            .in(SjzPfWhStock::getMaterialid, materialIds)
                            .eq(SjzPfWhStock::getIsdeleted, 0));
        }
        log.info("加载库存: {}, 采购: {}, ERP采购订单: {}, 待验: {}, 半成品在途: {}",
                ctx.stocks.size(), ctx.materialPurchases.size(),
                ctx.erpPurchaseOrders.size(), ctx.purchaseReceipts.size(), ctx.sjzPfWhStocks.size());

        // 9. RoutingDetailEquip (工序-设备关联), 按 routingDetailId 加载
        if (!ctx.routingDetails.isEmpty()) {
            List<Long> detailIds = ctx.routingDetails.stream()
                    .map(RoutingDetail::getId)
                    .filter(Objects::nonNull)
                    .distinct()
                    .collect(Collectors.toList());
            if (!detailIds.isEmpty()) {
                ctx.routingDetailEquips = routingDetailEquipMapper.selectList(
                        new LambdaQueryWrapper<RoutingDetailEquip>()
                                .in(RoutingDetailEquip::getRoutingDetailId, detailIds)
                                .eq(RoutingDetailEquip::getIsdeleted, 0));
            }
        }
        log.info("加载工序设备关联: {} 条", ctx.routingDetailEquips.size());

        // 10. 设备数据: Equipinfo + PlanResource (按 RoutingDetailEquip.equipId 过滤)
        Set<Long> equipIds = ctx.routingDetailEquips.stream()
                .map(RoutingDetailEquip::getEquipId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (!equipIds.isEmpty()) {
            ctx.planResources = planResourceMapper.selectList(
                    new LambdaQueryWrapper<PlanResource>()
                            .in(PlanResource::getId, equipIds)
                            .eq(PlanResource::getIsdeleted, false));
        }
        // 通过 PlanResource.referenceId 查询 Equipinfo
        Set<Integer> equipinfoIds = ctx.planResources.stream()
                .map(PlanResource::getReferenceId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (!equipinfoIds.isEmpty()) {
            ctx.equipinfos = equipinfoMapper.selectList(
                    new LambdaQueryWrapper<Equipinfo>()
                            .in(Equipinfo::getId, equipinfoIds)
                            .eq(Equipinfo::getIsdeleted, false));
        }
        log.info("加载设备资源: {}, 设备信息: {}", ctx.planResources.size(), ctx.equipinfos.size());

        // 11. 设备日历: ProdEquipSpecialCal + MesShiftWorkSched + EquipCapacityDef
        ctx.prodEquipSpecialCals = prodEquipSpecialCalMapper.selectList(
                new LambdaQueryWrapper<ProdEquipSpecialCal>()
                        .eq(ProdEquipSpecialCal::getSceneId, sceneId));
        ctx.mesShiftWorkScheds = mesShiftWorkSchedMapper.selectList(
                new LambdaQueryWrapper<MesShiftWorkSched>()
                        .eq(MesShiftWorkSched::getIsdeleted, 0));
        // EquipCapacityDef 按 PlanResource.referenceId 筛选
        if (!equipinfoIds.isEmpty()) {
            ctx.equipCapacityDefs = equipCapacityDefMapper.selectList(
                    new LambdaQueryWrapper<EquipCapacityDef>()
                            .in(EquipCapacityDef::getReferenceId, equipinfoIds)
                            .eq(EquipCapacityDef::getIsDeleted, 0));
        }
        log.info("加载设备日历: 特殊日历={}, 班次模板={}, 产能定义={}",
                ctx.prodEquipSpecialCals.size(), ctx.mesShiftWorkScheds.size(), ctx.equipCapacityDefs.size());

        // 12. baseTime
        ApsTimeConfig timeConfig = apsTimeConfigService.getOne(new LambdaQueryWrapper<>());
        ctx.baseTime = (timeConfig != null && timeConfig.getBaseTime() != null)
                ? timeConfig.getBaseTime() : LocalDateTime.now();

        return ctx;
    }

    // ==================== 步骤2: 构建 Material 业务对象 ====================
    // 参考 PlanResultService.getMaterials() (PlanResultService.java:2700-2849), 去掉缓存写入

    private void buildMaterials(ConvertContext ctx) {
        // 构建索引
        Map<String, List<Stock>> stocksByMaterialId = ctx.stocks.stream()
                .filter(s -> s.getMaterialId() != null)
                .collect(Collectors.groupingBy(Stock::getMaterialId));
        Map<String, List<MaterialPurchase>> purchasesByMaterialId = ctx.materialPurchases.stream()
                .filter(p -> p.getMaterialId() != null)
                .collect(Collectors.groupingBy(MaterialPurchase::getMaterialId));
        Map<String, List<PurchaseReceipt>> receiptsByMaterialId = ctx.purchaseReceipts.stream()
                .filter(r -> r.getMaterialid() != null)
                .collect(Collectors.groupingBy(PurchaseReceipt::getMaterialid));
        Map<String, List<ErpPurchaseOrder>> erpOrdersByMaterialId = ctx.erpPurchaseOrders.stream()
                .filter(e -> e.getMaterialId() != null)
                .collect(Collectors.groupingBy(ErpPurchaseOrder::getMaterialId));
        Map<String, List<SjzPfWhStock>> sjzByMaterialId = ctx.sjzPfWhStocks.stream()
                .filter(s -> s.getMaterialid() != null)
                .collect(Collectors.groupingBy(SjzPfWhStock::getMaterialid));

        for (MaterialInfo m : ctx.materialInfos) {
            if (m.getMaterialTypeName() == null) {
                // 无物料类型名, 跳过 (参照 getMaterials 逻辑)
                continue;
            }

            Material material = new Material();
            material.setId(m.getId());
            material.setMaterialType(m.getMaterialType());
            material.setMaterialTypeName(m.getMaterialTypeName());
            material.setCode(m.getCode());
            material.setName(m.getName());
            material.setMaxProduction(m.getMaxProduction());
            material.setMinProduction(m.getMinProduction());

            // 库存
            List<Stock> materialStocks = stocksByMaterialId.getOrDefault(m.getId(), Collections.emptyList());
            if (!materialStocks.isEmpty()) {
                double stock = materialStocks.stream().mapToDouble(Stock::getTotal).sum();
                material.setCurrentStock(stock);
                material.setMaterialStocks(new ArrayList<>(materialStocks));
            }

            // 采购供应商
            List<MaterialPurchase> purchases = purchasesByMaterialId.getOrDefault(m.getId(), Collections.emptyList());
            if (!purchases.isEmpty()) {
                material.setMaterialPurchases(new ArrayList<>(purchases));
            }

            // 在途物料 (区分 MP 原材料 vs 半成品/成品)
            List<MaterialSupply> inTransit = new ArrayList<>();
            if ("MP".equals(m.getMaterialTypeName())) {
                // 原材料: 待验 (PurchaseReceipt) + 采购订单 (ErpPurchaseOrder, 加检验周期)
                buildMpInTransit(inTransit, m.getId(),
                        receiptsByMaterialId.getOrDefault(m.getId(), Collections.emptyList()),
                        erpOrdersByMaterialId.getOrDefault(m.getId(), Collections.emptyList()),
                        purchases);
            } else {
                // 半成品/成品: SjzPfWhStock
                buildNonMpInTransit(inTransit, m.getId(),
                        sjzByMaterialId.getOrDefault(m.getId(), Collections.emptyList()));
            }
            material.setInTransit(inTransit);

            ctx.materialByMaterialId.put(m.getId(), material);
        }
        log.info("构建 Material 业务对象: {} 个", ctx.materialByMaterialId.size());
    }

    /**
     * 构建 MP 原材料的在途物料 (待验 + 采购订单)。
     * 参考 PlanResultService.getMaterials() 第 2783-2826 行。
     */
    private void buildMpInTransit(List<MaterialSupply> inTransit, String materialId,
                                  List<PurchaseReceipt> receipts,
                                  List<ErpPurchaseOrder> erpOrders,
                                  List<MaterialPurchase> purchases) {
        // 待验
        for (PurchaseReceipt pr : receipts) {
            LocalDateTime dt = pr.getExp5();
            if (dt == null && pr.getExp1() != null) {
                try {
                    dt = ParamValidator.parseDateTime(pr.getExp1() + " 00:00:00", "");
                } catch (Exception e) {
                    log.warn("解析待验到货时间失败: materialId={}, exp1={}", materialId, pr.getExp1());
                    continue;
                }
            }
            if (dt == null) {
                continue;
            }
            MaterialSupply ms = new MaterialSupply();
            ms.setArrivalTime(dt);
            ms.setQuantity(pr.getFjl1Sl());
            inTransit.add(ms);
        }
        // 采购订单 (可用时间 = 到货时间 + 检验周期)
        for (ErpPurchaseOrder epo : erpOrders) {
            if (epo.getArrivalDate() == null) {
                continue;
            }
            int checkDay = 0;
            if (epo.getManufacturerId() != null && purchases != null) {
                String manufacturerIdStr = String.valueOf(epo.getManufacturerId());
                Optional<MaterialPurchase> matched = purchases.stream()
                        .filter(p -> p.getSupplyId() != null && p.getSupplyId().equals(manufacturerIdStr))
                        .findFirst();
                if (matched.isPresent() && matched.get().getInspectionCycle() != null) {
                    checkDay = matched.get().getInspectionCycle();
                }
            }
            MaterialSupply ms = new MaterialSupply();
            ms.setArrivalTime(epo.getArrivalDate().plusDays(checkDay));
            ms.setQuantity(epo.getPurchaseQty());
            inTransit.add(ms);
        }
    }

    /**
     * 构建半成品/成品的在途物料 (SjzPfWhStock)。
     * 参考 PlanResultService.getMaterials() 第 2827-2840 行。
     */
    private void buildNonMpInTransit(List<MaterialSupply> inTransit, String materialId,
                                     List<SjzPfWhStock> sjzStocks) {
        for (SjzPfWhStock sjz : sjzStocks) {
            LocalDateTime dt = null;
            if (sjz.getExp1() != null) {
                try {
                    dt = ParamValidator.parseDateTime(sjz.getExp1() + " 00:00:00", "");
                } catch (Exception e) {
                    log.warn("解析半成品在途时间失败: materialId={}, exp1={}", materialId, sjz.getExp1());
                    continue;
                }
            }
            if (dt == null) {
                continue;
            }
            MaterialSupply ms = new MaterialSupply();
            ms.setArrivalTime(dt);
            ms.setQuantity(sjz.getFjl1Sl());
            inTransit.add(ms);
        }
    }

    // ==================== 步骤3: 结合设备日历计算每个 PlanResource (设备) 的日产能 ====================

    /**
     * 按 PlanResource.id 计算每台设备的日产能 (小时), 结合班次+节假日+效率系数。
     *
     * <p>映射链路:</p>
     * <pre>
     *   RoutingDetailEquip.equipId → PlanResource.id → unitId = "EQUIP_" + id
     *   PlanResource.referenceId → Equipinfo.id → Equipinfo.xxx
     *   ProdEquipSpecialCal(referenceType=1) → MesShiftWorkSched → 班次时间
     *   ProdEquipSpecialCal(referenceType=2) → 节假日日期
     *   EquipCapacityDef → 效率系数
     * </pre>
     *
     * <p>结果存储到 {@code ctx.dailyCapacityByUnitId} 和节假日映射。</p>
     */
    private void buildDailyCapacityByUnitId(ConvertContext ctx) {
        // 计算排产周期数 (从 ApsTimeConfig)
        ApsTimeConfig timeConfig = apsTimeConfigService.getOne(new LambdaQueryWrapper<>());
        if (timeConfig != null && timeConfig.getStartCount() != null && timeConfig.getEndCount() != null) {
            long horizonSeconds = timeConfig.getEndCount().longValue()
                    - timeConfig.getStartCount().longValue();
            if (horizonSeconds > 0) {
                ctx.periodCount = (int) Math.max(1, horizonSeconds / 86400);
            }
        }
        log.info("排产周期数: {}", ctx.periodCount);

        // 构建索引
        Map<Integer, PlanResource> planResourceById = ctx.planResources.stream()
                .collect(Collectors.toMap(PlanResource::getId, pr -> pr, (a, b) -> a));
        Map<Integer, Equipinfo> equipinfoById = ctx.equipinfos.stream()
                .collect(Collectors.toMap(Equipinfo::getId, e -> e, (a, b) -> a));

        // MesShiftWorkSched: weekWorkSchedId → List
        Map<Integer, List<MesShiftWorkSched>> shiftSchedByWeekId = ctx.mesShiftWorkScheds.stream()
                .filter(s -> s.getWeekWorkSchedId() != null)
                .collect(Collectors.groupingBy(MesShiftWorkSched::getWeekWorkSchedId));

        // ProdEquipSpecialCal: planResourceId → List (referenceType=1 班次)
        Map<Long, List<ProdEquipSpecialCal>> shiftCalsByPrId = ctx.prodEquipSpecialCals.stream()
                .filter(c -> c.getPlanResourceId() != null && c.getReferenceType() != null
                        && c.getReferenceType() == 1)
                .collect(Collectors.groupingBy(ProdEquipSpecialCal::getPlanResourceId));

        // ProdEquipSpecialCal: planResourceId → List (referenceType=2 节假日)
        Map<Long, List<ProdEquipSpecialCal>> holidayCalsByPrId = ctx.prodEquipSpecialCals.stream()
                .filter(c -> c.getPlanResourceId() != null && c.getReferenceType() != null
                        && c.getReferenceType() == 2)
                .collect(Collectors.groupingBy(ProdEquipSpecialCal::getPlanResourceId));

        // EquipCapacityDef: equipinfo.id → EquipCapacityDef
        Map<Long, EquipCapacityDef> capDefByRefId = ctx.equipCapacityDefs.stream()
                .filter(d -> d.getReferenceId() != null)
                .collect(Collectors.toMap(EquipCapacityDef::getReferenceId, d -> d, (a, b) -> a));

        double DEFAULT_DAILY_HOURS = 16.0;

        // 收集所有 unitId (从 RoutingDetailEquip)
        Set<String> allUnitIds = new HashSet<>();
        for (RoutingDetailEquip rde : ctx.routingDetailEquips) {
            if (rde.getEquipId() != null) {
                allUnitIds.add("EQUIP_" + rde.getEquipId());
            }
        }

        for (String unitId : allUnitIds) {
            String idStr = unitId.substring(6); // 去掉 "EQUIP_" 前缀
            long planResourceId;
            try {
                planResourceId = Long.parseLong(idStr);
            } catch (NumberFormatException e) {
                ctx.dailyCapacityByUnitId.put(unitId, DEFAULT_DAILY_HOURS);
                continue;
            }

            PlanResource pr = planResourceById.get(planResourceId);
            if (pr == null) {
                ctx.dailyCapacityByUnitId.put(unitId, DEFAULT_DAILY_HOURS);
                continue;
            }

            Equipinfo eq = (pr.getReferenceId() != null) ? equipinfoById.get(pr.getReferenceId()) : null;

            // 效率系数: EquipCapacityDef > ProdEquipSpecialCal > 默认 1.0
            double efficiency = 1.0;
            if (eq != null) {
                EquipCapacityDef capDef = capDefByRefId.get(eq.getId().longValue());
                if (capDef != null && capDef.getEfficiencyCoeff() != null) {
                    efficiency = capDef.getEfficiencyCoeff();
                }
            }
            if (efficiency == 1.0) {
                List<ProdEquipSpecialCal> shiftCals = shiftCalsByPrId.getOrDefault(pr.getId(), Collections.emptyList());
                for (ProdEquipSpecialCal cal : shiftCals) {
                    if ( cal.getEfficiencyCoeff() > 0) {
                        efficiency = cal.getEfficiencyCoeff();
                        break;
                    }
                }
            }

            // 收集班次时间
            List<MesShiftWorkSched> allShifts = new ArrayList<>();
            List<ProdEquipSpecialCal> shiftCals = shiftCalsByPrId.getOrDefault(pr.getId(), Collections.emptyList());
            for (ProdEquipSpecialCal cal : shiftCals) {
                if (cal.getReferenceId() != null) {
                    List<MesShiftWorkSched> scheds = shiftSchedByWeekId.get(cal.getReferenceId().intValue());
                    if (scheds != null) {
                        allShifts.addAll(scheds);
                    }
                }
            }
            // 备选: 通过 PlanResource.workSchedId
            if (allShifts.isEmpty() && pr.getWorkSchedId() != null) {
                List<MesShiftWorkSched> scheds = shiftSchedByWeekId.get(pr.getWorkSchedId().intValue());
                if (scheds != null) {
                    allShifts.addAll(scheds);
                }
            }

            double dailyHours;
            if (!allShifts.isEmpty()) {
                dailyHours = computeDailyHoursFromShifts(allShifts);
            } else {
                // 无班次: 使用 capabilityValue 回退
                if (pr.getCapabilityValue() != null) {
                    dailyHours = pr.getCapabilityValue().doubleValue();
                } else if (eq != null && eq.getCapabilityValue() != null) {
                    dailyHours = eq.getCapabilityValue().doubleValue();
                } else {
                    dailyHours = DEFAULT_DAILY_HOURS;
                }
            }
            dailyHours *= efficiency;
            ctx.dailyCapacityByUnitId.put(unitId, Math.max(0, dailyHours));
        }

        // 构建节假日映射: unitId → Set<LocalDate>
        for (Map.Entry<Long, List<ProdEquipSpecialCal>> entry : holidayCalsByPrId.entrySet()) {
            String unitId = "EQUIP_" + entry.getKey();
            Set<LocalDate> dates = ctx.holidayDatesByUnitId.computeIfAbsent(unitId, k -> new HashSet<>());
            for (ProdEquipSpecialCal cal : entry.getValue()) {
                if (cal.getStartDate() == null || cal.getEndDate() == null) {
                    continue;
                }
                LocalDate start = cal.getStartDate().toLocalDate();
                LocalDate end = cal.getEndDate().toLocalDate();
                for (LocalDate d = start; !d.isAfter(end); d = d.plusDays(1)) {
                    dates.add(d);
                }
            }
        }

        log.info("构建日产能(含日历+节假日): {} 台设备, {} 台有节假日, 默认={}h",
                ctx.dailyCapacityByUnitId.size(), ctx.holidayDatesByUnitId.size(), DEFAULT_DAILY_HOURS);
    }

    /**
     * 从 MesShiftWorkSched 列表计算平均日工作小时数。
     * 遍历每周 7 天, 累加匹配的班次时间, 除以 7 得到日均值。
     */
    private double computeDailyHoursFromShifts(List<MesShiftWorkSched> shifts) {
        double[] hoursByDay = new double[7]; // 0=Sunday, 1=Monday, ..., 6=Saturday
        for (MesShiftWorkSched s : shifts) {
            if (s.getShiftStart() == null || s.getShiftEnd() == null) {
                continue;
            }
            LocalTime startTime = s.getShiftStart().toLocalTime();
            LocalTime endTime = s.getShiftEnd().toLocalTime();
            double shiftHours = calculateShiftHours(startTime, endTime);

            int startDay = s.getStartWeekDay() != null ? s.getStartWeekDay() : 1;
            int endDay = s.getEndWeekDay() != null ? s.getEndWeekDay() : startDay;
            int mappedStart = (startDay % 7); // 数据库: 1=Mon→1, 7=Sun→0
            int mappedEnd = (endDay % 7);
            if (mappedStart == mappedEnd) {
                hoursByDay[mappedStart] += shiftHours;
            } else {
                int day = mappedStart;
                while (day != mappedEnd) {
                    hoursByDay[day] += shiftHours;
                    day = (day + 1) % 7;
                }
                hoursByDay[mappedEnd] += shiftHours;
            }
        }
        double totalWeekHours = 0;
        for (double h : hoursByDay) {
            totalWeekHours += h;
        }
        return totalWeekHours / 7.0;
    }

    /**
     * 计算班次时长 (小时), 处理跨天 (如 22:00-06:00)。
     */
    private double calculateShiftHours(LocalTime start, LocalTime end) {
        long minutes;
        if (end.isAfter(start)) {
            minutes = Duration.between(start, end).toMinutes();
        } else {
            minutes = Duration.between(start, LocalTime.MAX).toMinutes()
                    + Duration.between(LocalTime.MIN, end).toMinutes();
        }
        return minutes / 60.0;
    }

    // ==================== Sink: 继承 TestDataBuilder, 直接操作 protected 字段 ====================

    /**
     * TestDataBuilder 的同包子类, 通过 protected 字段直接填充数据。
     * 与 {@link ComprehensiveTestDataBuilder} / {@link RoutingTestDataBuilder} 模式一致。
     */
    private static class Sink extends TestDataBuilder {

        Sink() {
            super(true); // 跳过父类默认 build
        }

        void fill(ConvertContext ctx) {
            fillProductsAndStockingPoints(ctx);
            fillOperationsAndRoutings(ctx);
            fillOperationInputs(ctx);
            fillInitialInventoryAndInTransit(ctx);
            fillPeriodsAndUnitPeriods(ctx);
            fillSalesDemands(ctx);
            fillInventorySpecsAndSupplySpecs(ctx);
            fillKpiWeights();
        }

        // ---------- 步骤3: Product + StockingPoint ----------

        private void fillProductsAndStockingPoints(ConvertContext ctx) {
            // 3.1 为每个 Material 创建 Product
            for (Material m : ctx.materialByMaterialId.values()) {
                String name = pickName(m.getName(), m.getCode(), m.getId());
                Product p = new Product(m.getId(), name);
                products.add(p);
                ctx.productByMaterialId.put(m.getId(), p);
            }

            // 3.2 收集所有 storeId (库存 + 投料仓库), 建立 storeId → storeName 映射
            Map<Long, String> storeNameMap = new HashMap<>();
            for (Material m : ctx.materialByMaterialId.values()) {
                if (m.getMaterialStocks() != null) {
                    for (Stock s : m.getMaterialStocks()) {
                        if (s.getStoreId() != null && s.getStoreName() != null) {
                            storeNameMap.putIfAbsent(s.getStoreId(), s.getStoreName());
                        }
                    }
                }
            }
            for (Routingsupporting rs : ctx.routingsupportings) {
                if (rs.getStoreId() != null && rs.getStoreName() != null) {
                    storeNameMap.putIfAbsent(rs.getStoreId(), rs.getStoreName());
                }
            }

            Set<Long> storeIds = new HashSet<>();
            storeIds.addAll(storeNameMap.keySet());
            // 补充只有 Stock 有但没 name 的 storeId
            for (Material m : ctx.materialByMaterialId.values()) {
                if (m.getMaterialStocks() != null) {
                    for (Stock s : m.getMaterialStocks()) {
                        if (s.getStoreId() != null) storeIds.add(s.getStoreId());
                    }
                }
            }
            for (Routingsupporting rs : ctx.routingsupportings) {
                if (rs.getStoreId() != null) storeIds.add(rs.getStoreId());
            }

            for (Long sid : storeIds) {
                String spName = storeNameMap.getOrDefault(sid, "仓库_" + sid);
                StockingPoint sp = new StockingPoint("SP_" + sid, spName);
                stockingPoints.add(sp);
                ctx.spByStoreId.put(sid, sp);
            }

            // 3.3 ProductSpMapping + 记录每个物料的成品库 (首个库存点)
            for (Material m : ctx.materialByMaterialId.values()) {
                Product p = ctx.productByMaterialId.get(m.getId());
                StockingPoint firstSp = null;
                if (m.getMaterialStocks() != null) {
                    for (Stock s : m.getMaterialStocks()) {
                        if (s.getStoreId() == null) {
                            continue;
                        }
                        StockingPoint sp = ctx.spByStoreId.get(s.getStoreId());
                        if (sp != null) {
                            productSpMappings.add(new ProductSpMapping(p, sp));
                            if (firstSp == null) {
                                firstSp = sp;
                            }
                        }
                    }
                }
                if (firstSp == null) {
                    // 无库存记录, 建默认成品库
                    firstSp = new StockingPoint("SP_FG_" + m.getId(),
                            "成品库_" + pickName(m.getName(), m.getId()));
                    stockingPoints.add(firstSp);
                    productSpMappings.add(new ProductSpMapping(p, firstSp));
                }
                ctx.finalSpByMaterialId.put(m.getId(), firstSp);
            }
            log.info("构建 Product: {}, StockingPoint: {}", products.size(), stockingPoints.size());
        }

        // ---------- 步骤4: Operation + Routing (调用 expand) ----------

        private void fillOperationsAndRoutings(ConvertContext ctx) {
            // 4.1 工序按 routingHeaderId 分组, 按 taskSeq 升序排序
            Map<Integer, List<RoutingDetail>> detailsByHeader = new HashMap<>();
            for (RoutingDetail rd : ctx.routingDetails) {
                if (rd.getRoutingHeaderId() == null) {
                    continue;
                }
                int hid = rd.getRoutingHeaderId().intValue();
                detailsByHeader.computeIfAbsent(hid, k -> new ArrayList<>()).add(rd);
            }
            detailsByHeader.values().forEach(list ->
                    list.sort(Comparator.comparing(rd ->
                            rd.getTaskSeq() == null ? Long.MAX_VALUE : rd.getTaskSeq())));

            // 4.2 每个 RoutingHeader 构建 Routing
            for (RoutingHeader rh : ctx.routingHeaders) {
                if (Boolean.TRUE.equals(rh.getIsDeleted())) {
                    continue;
                }
                if (rh.getMaterialId() == null) {
                    continue;
                }
                Product product = ctx.productByMaterialId.get(rh.getMaterialId());
                if (product == null) {
                    log.warn("跳过工艺路线 {}: materialId={} 对应的物料不存在", rh.getId(), rh.getMaterialId());
                    continue;
                }
                StockingPoint finalSp = ctx.finalSpByMaterialId.get(rh.getMaterialId());
                if (finalSp == null) {
                    log.warn("跳过工艺路线 {}: 找不到成品库", rh.getId());
                    continue;
                }

                String routingName = pickName(rh.getName(), rh.getCode(), "工艺路线_" + rh.getId());
                Routing routing = new Routing("R_" + rh.getId(), routingName, product, finalSp);

                List<RoutingDetail> details = detailsByHeader.getOrDefault(rh.getId(), Collections.emptyList());
                // 构建索引: routingDetailId → List<RoutingDetailEquip>
                Map<Long, List<RoutingDetailEquip>> equipsByDetailId = ctx.routingDetailEquips.stream()
                        .filter(e -> e.getRoutingDetailId() != null)
                        .collect(Collectors.groupingBy(RoutingDetailEquip::getRoutingDetailId));

                for (RoutingDetail rd : details) {
                    if (rd.getIsDeleted() != null && rd.getIsDeleted() != 0) {
                        continue;
                    }
                    String opName = pickName(rd.getName(), "工序" + rd.getTaskSeq());

                    // 从 RoutingDetailEquip 创建 UnitOperation 列表 (每个设备一个 UnitOperation)
                    List<RoutingDetailEquip> equips = equipsByDetailId.getOrDefault(rd.getId(), Collections.emptyList());
                    List<UnitOperation> unitOps;
                    if (!equips.isEmpty()) {
                        unitOps = equips.stream()
                                .filter(e -> e.getEquipId() != null)
                                .map(e -> {
                                    String unitId = "EQUIP_" + e.getEquipId();
                                    double capCoeff = (e.getDuration() != null) ? e.getDuration().doubleValue() : 1.0;
                                    boolean lotSize = e.getOneBatchQuantity() != null
                                            && e.getOneBatchQuantity().compareTo(BigDecimal.ZERO) > 0;
                                    double lotSizeVal = lotSize ? e.getOneBatchQuantity().doubleValue() : 0.0;
                                    return new UnitOperation(unitId, capCoeff, lotSize, lotSizeVal, 1.0);
                                })
                                .collect(Collectors.toList());
                    } else {
                        // 无 RoutingDetailEquip: 回退到 equipTypeId
                        String unitId = "EQUIP_" + (rd.getEquipTypeId() != null ? rd.getEquipTypeId() : "DEFAULT");
                        double capCoeff = (rd.getRuntime() != null) ? rd.getRuntime().doubleValue() : 1.0;
                        unitOps = Arrays.asList(new UnitOperation(unitId, capCoeff, false, 0.0, 1.0));
                    }

                    Operation op = new Operation(
                            "OP_" + rh.getId() + "_" + rd.getId(),
                            opName, unitOps,
                            1.0, 0);
                    routing.addOperation(op);
                    ctx.operationByRoutingDetailId.put(rd.getId(), op);
                }

                if (routing.getOperations().isEmpty()) {
                    log.warn("跳过工艺路线 {}: 无有效工序", rh.getId());
                    continue;
                }

                routings.add(routing);
                operations.addAll(routing.getOperations());
                ctx.routingByHeaderId.put(rh.getId(), routing);

                // 调用 expand() 自动生成中间 WIP 库存点 + BOM 输入 + 工序产出配置
                try {
                    RoutingExpansion expansion = routing.expand();
                    stockingPoints.addAll(expansion.getStockingPoints());
                    productSpMappings.addAll(expansion.getProductSpMappings());
                    initialInventories.addAll(expansion.getInitialInventories());
                    operationInputs.addAll(expansion.getOperationInputs());
                } catch (Exception e) {
                    log.error("工艺路线 {} 展开失败: {}", rh.getId(), e.getMessage(), e);
                }
            }
            log.info("构建 Routing: {}, Operation: {}", routings.size(), operations.size());
        }

        // ---------- 步骤5: OperationInput (仅 MP 原材料投料) ----------

        private void fillOperationInputs(ConvertContext ctx) {
            // 仅 MP 原材料作为投料; 半成品由各自 Routing 产出 + RoutingConstraint 管理, 不重复投料
            for (Routingsupporting rs : ctx.routingsupportings) {
                Operation op = ctx.operationByRoutingDetailId.get(rs.getRoutingDetailId());
                if (op == null) {
                    continue;
                }
                if (rs.getMaterialId() == null) {
                    continue;
                }

                Material inputMaterial = ctx.materialByMaterialId.get(rs.getMaterialId());
                if (inputMaterial == null) {
                    continue;
                }
                if (!"MP".equals(inputMaterial.getMaterialTypeName())) {
                    // 半成品/成品不作为投料, 由各自 Routing 产出
                    continue;
                }

                if (rs.getMainQty() == null || rs.getMainQty().compareTo(BigDecimal.ZERO) == 0) {
                    log.warn("跳过主量为0的BOM项: routingDetailId={}, materialId={}",
                            rs.getRoutingDetailId(), rs.getMaterialId());
                    continue;
                }
                if (rs.getSpentQty() == null) {
                    throw new IllegalStateException(
                            "BOM消耗量为空: " + rs.getMaterialNumber() + ", routingDetailId=" + rs.getRoutingDetailId());
                }
                double factor = rs.getSpentQty().doubleValue() / rs.getMainQty().doubleValue();

                Product inputProduct = ctx.productByMaterialId.get(rs.getMaterialId());
                StockingPoint inputSp = resolveInputSp(ctx, rs, inputProduct);

                operationInputs.add(new OperationInput(op, inputProduct, inputSp, factor));
            }

            // 为无产出工序的 MP 原材料补充"采购 Operation"
            for (Material m : ctx.materialByMaterialId.values()) {
                if (!"MP".equals(m.getMaterialTypeName())) {
                    continue;
                }
                Product p = ctx.productByMaterialId.get(m.getId());
                if (p == null) {
                    continue;
                }
                boolean hasProducer = operations.stream()
                        .anyMatch(op -> op.producesProduct(p.getId()));
                if (!hasProducer) {
                    StockingPoint sp = ctx.finalSpByMaterialId.get(m.getId());
                    if (sp == null) {
                        sp = new StockingPoint("SP_PROCURE_" + m.getId(),
                                "采购库_" + pickName(m.getName(), m.getId()));
                        stockingPoints.add(sp);
                        productSpMappings.add(new ProductSpMapping(p, sp));
                        ctx.finalSpByMaterialId.put(m.getId(), sp);
                    }
                    int leadTimeDays = m.getPurchaseLeadTime() > 0 ? m.getPurchaseLeadTime() : 1;
                    Operation procureOp = new Operation(
                            "OP_PROCURE_" + m.getId(),
                            "采购-" + pickName(m.getName(), m.getId()),
                            "UNIT_PROCURE_" + m.getId(),
                            new OperationOutput(p, sp),
                            0.5, 1.0, false, 0.0, 1.0, leadTimeDays);
                    operations.add(procureOp);
                    log.info("为 MP 原材料 {} 补充采购 Operation (leadTime={}天)", m.getId(), leadTimeDays);
                }
            }
            log.info("构建 OperationInput: {}", operationInputs.size());
        }

        /**
         * 解析投料来源库存点: 优先用 Routingsupporting.storeId, 找不到则用物料默认 SP。
         */
        private StockingPoint resolveInputSp(ConvertContext ctx, Routingsupporting rs, Product inputProduct) {
            if (rs.getStoreId() != null) {
                StockingPoint sp = ctx.spByStoreId.get(rs.getStoreId());
                if (sp != null) {
                    return sp;
                }
                // 投料仓库未在库存中出现, 新建
                String spName = rs.getStoreName() != null ? rs.getStoreName() : "仓库_" + rs.getStoreId();
                sp = new StockingPoint("SP_" + rs.getStoreId(), spName);
                stockingPoints.add(sp);
                ctx.spByStoreId.put(rs.getStoreId(), sp);
                productSpMappings.add(new ProductSpMapping(inputProduct, sp));
                return sp;
            }
            // 无仓库信息, 用该物料的默认 SP
            StockingPoint sp = ctx.finalSpByMaterialId.get(rs.getMaterialId());
            if (sp == null) {
                sp = new StockingPoint("SP_DEFAULT_" + rs.getMaterialId(),
                        "默认库_" + rs.getMaterialId());
                stockingPoints.add(sp);
                productSpMappings.add(new ProductSpMapping(inputProduct, sp));
                ctx.finalSpByMaterialId.put(rs.getMaterialId(), sp);
            }
            return sp;
        }

        // ---------- 步骤6: InitialInventory + InTransitSupply ----------

        private void fillInitialInventoryAndInTransit(ConvertContext ctx) {
            // 已有的 (productId, spId) 集合 (来自 expand 的 0 库存 WIP)
            Set<String> existingKeys = new HashSet<>();
            for (InitialInventory ii : initialInventories) {
                existingKeys.add(ii.getProduct().getId() + "_" + ii.getStockingPoint().getId());
            }

            for (Material m : ctx.materialByMaterialId.values()) {
                Product p = ctx.productByMaterialId.get(m.getId());
                if (p == null) {
                    continue;
                }
                // 初始库存
                if (m.getMaterialStocks() != null) {
                    for (Stock s : m.getMaterialStocks()) {
                        if (s.getStoreId() == null) {
                            continue;
                        }
                        StockingPoint sp = ctx.spByStoreId.get(s.getStoreId());
                        if (sp == null) {
                            continue;
                        }
                        String key = p.getId() + "_" + sp.getId();
                        if (existingKeys.contains(key)) {
                            continue; // 已有 (WIP 0库存), 跳过避免重复
                        }
                        initialInventories.add(new InitialInventory(p, sp, s.getTotal()));
                        existingKeys.add(key);
                    }
                }

                // 在途供应
                StockingPoint defaultSp = ctx.finalSpByMaterialId.get(m.getId());
                if (defaultSp == null || m.getInTransit() == null) {
                    continue;
                }
                for (MaterialSupply ms : m.getInTransit()) {
                    if (ms.getArrivalTime() == null) {
                        continue;
                    }
                    inTransitSupplies.add(new InTransitSupply(
                            p, defaultSp, ms.getArrivalTime().toLocalDate(), ms.getQuantity()));
                }
            }
            log.info("构建 InitialInventory: {}, InTransitSupply: {}",
                    initialInventories.size(), inTransitSupplies.size());
        }

        // ---------- 步骤7: Period + UnitPeriod (结合设备日历和周期长度) ----------

        private void fillPeriodsAndUnitPeriods(ConvertContext ctx) {
            LocalDate baseDate = ctx.baseTime.toLocalDate();
            int periodCount = ctx.periodCount;

            // 创建周期 (1天/周期)
            for (int i = 0; i < periodCount; i++) {
                periods.add(new Period(i, "P" + (i + 1), 1.0, baseDate.plusDays(i)));
            }

            // 收集所有 unitId (遍历每个 Operation 的每个 UnitOperation)
            Set<String> unitIds = new LinkedHashSet<>();
            for (Operation op : operations) {
                for (UnitOperation uo : op.getUnitOperations()) {
                    unitIds.add(uo.getUnitId());
                }
            }

            double DEFAULT_DAILY_HOURS = 16.0;

            for (Period p : periods) {
                LocalDate periodStart = p.getStartDate();
                LocalDate periodEnd = periodStart.plusDays((long) p.getDurationInDays());

                for (String unitId : unitIds) {
                    double dailyCapacity = ctx.dailyCapacityByUnitId.getOrDefault(unitId, DEFAULT_DAILY_HOURS);

                    // 节假日扣减
                    Set<LocalDate> holidays = ctx.holidayDatesByUnitId.getOrDefault(unitId, Collections.emptySet());
                    long holidaysInPeriod = 0;
                    for (LocalDate h : holidays) {
                        if (!h.isBefore(periodStart) && h.isBefore(periodEnd)) {
                            holidaysInPeriod++;
                        }
                    }
                    long periodDays = (long) p.getDurationInDays();
                    long effectiveDays = Math.max(0, periodDays - holidaysInPeriod);
                    double periodMaxCapacity = dailyCapacity * effectiveDays;

                    if (periodMaxCapacity <= 0) {
                        periodMaxCapacity = DEFAULT_DAILY_HOURS * effectiveDays;
                    }
                    unitPeriods.add(new UnitPeriod(unitId, p, 0.0, periodMaxCapacity, false));
                }
            }
            log.info("构建 Period: {} (共{}天), UnitPeriod: {} (基于设备日历+节假日)",
                    periods.size(), periodCount, unitPeriods.size());
        }

        // ---------- 步骤8: SalesDemand (从订单推导) ----------

        private void fillSalesDemands(ConvertContext ctx) {
            if (periods.isEmpty()) {
                return;
            }
            for (ProdLaunchOrder plo : ctx.prodLaunchOrders) {
                if (plo.getMaterialId() == null) {
                    continue;
                }
                Product p = ctx.productByMaterialId.get(plo.getMaterialId());
                if (p == null) {
                    continue;
                }
                StockingPoint sp = ctx.finalSpByMaterialId.get(plo.getMaterialId());
                if (sp == null) {
                    continue;
                }

                Period targetPeriod = null;
                if (plo.getEndDate() != null) {
                    LocalDate dueDate = plo.getEndDate().toLocalDate();
                    for (Period per : periods) {
                        if (per.contains(dueDate)) {
                            targetPeriod = per;
                            break;
                        }
                    }
                }
                if (targetPeriod == null) {
                    // 找不到匹配周期, 用最后一个周期
                    targetPeriod = periods.get(periods.size() - 1);
                }

                double priority = plo.getOrderPriority() != null ? plo.getOrderPriority() : 1;
                salesDemands.add(new SalesDemand(p, sp, targetPeriod, plo.getQuantity(), priority));
            }
            log.info("构建 SalesDemand: {}", salesDemands.size());
        }

        // ---------- 步骤9: InventorySpec + SupplySpec (默认宽松) ----------

        private void fillInventorySpecsAndSupplySpecs(ConvertContext ctx) {
            // InventorySpec: 每个 (Product, StockingPoint) × Period, 默认宽松约束
            Set<String> invSpecKeys = new HashSet<>();
            for (ProductSpMapping m : productSpMappings) {
                for (Period p : periods) {
                    String key = m.getProduct().getId() + "_" + m.getStockingPoint().getId() + "_" + p.getIndex();
                    if (invSpecKeys.add(key)) {
                        inventorySpecs.add(new InventorySpec(m.getProduct(), m.getStockingPoint(), p,
                                0.0, 0.0, LOOSE_MAX, true, true, true));
                    }
                }
            }

            // SupplySpec: 每个 Routing 的最后一道工序 (产出成品) + 采购 Operation
            for (Routing r : routings) {
                List<Operation> ops = r.getOperations();
                if (ops.isEmpty()) {
                    continue;
                }
                Operation lastOp = ops.get(ops.size() - 1);
                supplySpecs.add(new SupplySpec("Supply-" + r.getId(),
                        0.0, 0.0, LOOSE_MAX, false, Collections.singletonList(lastOp)));
            }
            for (Operation op : operations) {
                if (op.getId().startsWith("OP_PROCURE_")) {
                    supplySpecs.add(new SupplySpec("Supply-" + op.getId(),
                            0.0, 0.0, LOOSE_MAX, false, Collections.singletonList(op)));
                }
            }
            log.info("构建 InventorySpec: {}, SupplySpec: {}", inventorySpecs.size(), supplySpecs.size());
        }

        // ---------- 步骤10: KPIWeights (默认值, 复用 RoutingTestDataBuilder) ----------

        private void fillKpiWeights() {
            kpiWeights = new KPIWeights(
                    100.0,  // fulfillmentWeight
                    10.0,   // lotSizeWeight
                    5.0,    // maxInventoryLevelWeight
                    5.0,    // minInventoryLevelWeight
                    8.0,    // targetInventoryLevelWeight
                    3.0,    // unitCapacityWeight
                    8.0,    // supplyTargetWeight
                    5.0,    // minSupplyWeight
                    5.0,    // maxSupplyWeight
                    1.0,    // salesDemandPriorityWeight
                    20.0,   // postponementPenaltyWeight
                    5.0     // processMaxQuantityWeight
            );
        }

        // ---------- 工具方法 ----------

        /**
         * 按优先级选取第一个非空字符串。
         */
        private static String pickName(String... candidates) {
            for (String c : candidates) {
                if (c != null && !c.isEmpty()) {
                    return c;
                }
            }
            return "unknown";
        }
    }

    // ==================== ConvertContext: 承载所有加载的数据库实体 + 转换中间映射 ====================

    private static class ConvertContext {
        // 数据库加载的原始实体
        List<ProdLaunchOrder> prodLaunchOrders = new ArrayList<>();
        List<RoutingHeader> routingHeaders = new ArrayList<>();
        List<RoutingDetail> routingDetails = new ArrayList<>();
        List<Routingsupporting> routingsupportings = new ArrayList<>();
        List<MaterialInfo> materialInfos = new ArrayList<>();
        List<Stock> stocks = new ArrayList<>();
        List<MaterialPurchase> materialPurchases = new ArrayList<>();
        List<ErpPurchaseOrder> erpPurchaseOrders = new ArrayList<>();
        List<PurchaseReceipt> purchaseReceipts = new ArrayList<>();
        List<SjzPfWhStock> sjzPfWhStocks = new ArrayList<>();
        List<RoutingDetailEquip> routingDetailEquips = new ArrayList<>();
        List<Equipinfo> equipinfos = new ArrayList<>();
        List<PlanResource> planResources = new ArrayList<>();
        List<ProdEquipSpecialCal> prodEquipSpecialCals = new ArrayList<>();
        List<MesShiftWorkSched> mesShiftWorkScheds = new ArrayList<>();
        List<EquipCapacityDef> equipCapacityDefs = new ArrayList<>();
        LocalDateTime baseTime;

        // 转换过程的中间映射
        /** materialId → Material 业务对象 */
        Map<String, Material> materialByMaterialId = new HashMap<>();
        /** materialId → macroplanner Product */
        Map<String, Product> productByMaterialId = new HashMap<>();
        /** storeId → StockingPoint */
        Map<Long, StockingPoint> spByStoreId = new HashMap<>();
        /** routingHeaderId → macroplanner Routing */
        Map<Integer, Routing> routingByHeaderId = new HashMap<>();
        /** routingDetailId → macroplanner Operation */
        Map<Long, Operation> operationByRoutingDetailId = new HashMap<>();
        /** materialId → 成品/半成品库 StockingPoint (首个库存点) */
        Map<String, StockingPoint> finalSpByMaterialId = new HashMap<>();
        /** unitId ("EQUIP_" + PlanResource.id) → 日产能 (小时), 含日历+效率系数 */
        Map<String, Double> dailyCapacityByUnitId = new HashMap<>();
        /** unitId → 节假日日期集合 */
        Map<String, Set<LocalDate>> holidayDatesByUnitId = new HashMap<>();
        /** 排产周期数 (天), 从 ApsTimeConfig 计算 */
        int periodCount = 7;
    }
}
