package com.aps.macroplanner.output;

import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
import com.aps.macroplanner.objective.StrategyLevel;

import java.util.*;
import java.time.LocalDate;
import java.util.logging.Logger;

/**
 * 求解结果输出器 — 按日输出产线视图、产品视图、物料需求展开、期末库存。
 *
 * <p>输出格式参照供应链排产日志, 包含以下视图:</p>
 * <ul>
 *   <li>BOM 关系验证 — 展示产品间的消耗与被消耗关系</li>
 *   <li>产线视图 — 按设备分组, 展示各操作的生产量和产能利用率</li>
 *   <li>产品视图 — 汇总各产品库存、生产、销售情况</li>
 *   <li>物料需求展开 — 按 BOM 逐级展开物料消耗</li>
 *   <li>期末库存 — 简洁的库存快照</li>
 *   <li>KPI 汇总 — 目标函数各惩罚项明细</li>
 *   <li>求解统计 — 变量数、约束数、耗时</li>
 * </ul>
 */
public class SolutionPrinter {

    private static final Logger LOG = Logger.getLogger(SolutionPrinter.class.getName());

    private final MacroPlannerModel model;
    private final TestDataBuilder data;
    private final long startTimeMs;

    public SolutionPrinter(MacroPlannerModel model, TestDataBuilder data, long startTimeMs) {
        this.model = model;
        this.data = data;
        this.startTimeMs = startTimeMs;
    }

    // ==================== 主入口 ====================

    /** 输出完整求解结果 */
    public void printAll() {
        printBomStructure();
        printHeader();
        printDailyViews();
        printKpiSummary();
        printStatistics();
    }

    // ==================== BOM 关系验证 ====================

    private void printBomStructure() {
        System.out.println("📋 BOM关系验证:");
        for (Product prod : data.getProducts()) {
            System.out.printf("  %s → 消耗: %s, 被消耗于: %s%n",
                    prod.getId(),
                    formatConsumes(prod),
                    formatConsumedBy(prod));
        }
        System.out.println();
    }

    /** 某产品消耗了哪些物料 (BOM输入, 含库存点) */
    private String formatConsumes(Product prod) {
        List<String> items = new ArrayList<>();
        for (Operation op : data.getOperations()) {
            if (!op.producesProduct(prod.getId())) continue;
            int inputCount = 0;
            for (OperationInput input : data.getOperationInputs()) {
                if (!input.getOperation().getId().equals(op.getId())) continue;
                items.add(input.getInputProduct().getId() + "@" + input.getInputSp().getId()
                        + "×" + formatFactor(input.getFactor()));
                inputCount++;
            }
            if (inputCount > 0) {
                LOG.fine(String.format("[结果输出] BOM消耗: '%s'(产出%s) 消耗 %d 种物料",
                        op.getId(), formatOutputProducts(op), inputCount));
            }
        }
        return items.isEmpty() ? "[]" : "[" + String.join(", ", items) + "]";
    }

    /** 某产品被哪些产品消耗 (BOM输出, 含库存点) */
    private String formatConsumedBy(Product prod) {
        List<String> items = new ArrayList<>();
        for (OperationInput input : data.getOperationInputs()) {
            if (!input.getInputProduct().getId().equals(prod.getId())) continue;
            // 遍历消费者工序的所有产出 (支持联产品/副产品)
            List<OperationOutput> outputs = input.getOperation().getOutputs();
            if (outputs.size() > 1) {
                LOG.fine(String.format("[结果输出] 多产出消耗: '%s' 被工序 '%s' 消耗, 该工序有 %d 个产出",
                        prod.getId(), input.getOperation().getId(), outputs.size()));
            }
            for (OperationOutput oo : outputs) {
                items.add(oo.getProductId() + "@" + oo.getSpId()
                        + "×" + formatFactor(input.getFactor()));
            }
        }
        return items.isEmpty() ? "[]" : "[" + String.join(", ", items) + "]";
    }

    /** 格式化工序的所有产出产品 ID (用于显示) */
    private String formatOutputProducts(Operation op) {
        List<String> ids = new ArrayList<>();
        for (OperationOutput oo : op.getOutputs()) {
            ids.add(oo.getProductId());
        }
        if (ids.size() > 1) {
            LOG.fine(String.format("[结果输出] 多产出工序 '%s': 产出=%s",
                    op.getId(), String.join("+", ids)));
        }
        return ids.isEmpty() ? "-" : String.join("+", ids);
    }

    // ==================== 头部信息 ====================

    private void printHeader() {
        // 成品: 不被任何操作消耗的产品
        Set<String> consumedProducts = new HashSet<>();
        for (OperationInput input : data.getOperationInputs()) {
            consumedProducts.add(input.getInputProduct().getId());
        }
        List<String> finishedGoods = new ArrayList<>();
        List<String> producedGoods = new ArrayList<>();
        List<String> rawMaterials = new ArrayList<>();

        for (Product prod : data.getProducts()) {
            boolean hasOperation = false;
            boolean hasBomInput = false;
            for (Operation op : data.getOperations()) {
                if (op.producesProduct(prod.getId())) {
                    hasOperation = true;
                }
            }
            for (OperationInput input : data.getOperationInputs()) {
                if (input.getOperation().producesProduct(prod.getId())) {
                    hasBomInput = true;
                }
            }

            if (!consumedProducts.contains(prod.getId())) {
                finishedGoods.add(prod.getId());
            }
            if (hasOperation) {
                producedGoods.add(prod.getId());
            } else {
                rawMaterials.add(prod.getId());
            }
        }

        System.out.println("========== 网状BOM + MRP 排产求解 ==========");
        System.out.printf("成品(根节点) %d 种：%s%n", finishedGoods.size(),
                String.join(" ", finishedGoods));
        System.out.printf("生产品 %d 种：%s%n", producedGoods.size(),
                String.join(" ", producedGoods));
        if (!rawMaterials.isEmpty()) {
            System.out.printf("采购品 %d 种：%s%n", rawMaterials.size(),
                    String.join(" ", rawMaterials));
        }
        System.out.printf("周期 %d 天%n", data.getPeriods().size());
        System.out.println();
    }

    // ==================== 每日视图 ====================

    private void printDailyViews() {
        for (Period p : data.getPeriods()) {
            System.out.println();
            System.out.printf("═══════════════════ 第 %d 天 ═══════════════════%n",
                    p.getIndex() + 1);
            printLineView(p);
            printProductView(p);
            printBomExplosion(p);
            printEndingInventory(p);
        }
    }

    // --- 产线视图 ---

    private void printLineView(Period p) {
        System.out.println("  ▶ 产线视图（按产线分组）");

        // 收集所有 unit
        Set<String> unitIds = new LinkedHashSet<>();
        for (UnitPeriod up : data.getUnitPeriods()) {
            if (up.getPeriod().getIndex() == p.getIndex()) {
                unitIds.add(up.getUnitId());
            }
        }

        for (String unitId : unitIds) {
            UnitPeriod up = data.getUnitPeriod(unitId, p);
            if (up == null) continue;

            if (up.isUnlimited()) {
                System.out.printf("    【%s】产能无限 (供应商)%n", unitId);
            } else {
                String minStr = up.hasMinCapacity() ? String.format("最小%.0fh/", up.getMinCapacity()) : "";
                System.out.printf("    【%s】产能%s%.0fh %s%n", unitId, minStr, up.getMaxCapacity(),
                        up.hasMinCapacity() ? "(强制最小)" : "");
            }

            boolean hasProduction = false;
            for (Operation op : data.getOperations()) {
                for (UnitOperation uo : op.getUnitOperations()) {
                    if (!uo.getUnitId().equals(unitId)) continue;
                    String key = op.ptQtyKey(uo, p.getIndex());
                    double ptQty = solutionValue(model.getPtQtyVars(), key);
                    if (ptQty > 0.001) {
                        hasProduction = true;
                        double capacityUsed = ptQty * uo.getCapacityCoeff();
                        if (up.isUnlimited()) {
                            System.out.printf("      %s(%s): %.0f件 (耗时%.1fh)%n",
                                    op.getName(), formatOutputProducts(op), ptQty, capacityUsed);
                        } else {
                            double utilPercent = up.getMaxCapacity() > 0
                                    ? capacityUsed / up.getMaxCapacity() * 100 : 0;
                            System.out.printf("      %s(%s): %.0f件 (耗时%.1fh) %s%n",
                                    op.getName(), formatOutputProducts(op), ptQty, capacityUsed,
                                    uo.hasLotSize() ? String.format(", 批次%.0f", uo.getLotSize()) : "");
                            System.out.printf("      利用率: %.0f/%.0f h (%.0f%%)%n",
                                    capacityUsed, up.getMaxCapacity(), utilPercent);
                        }
                    }
                }
            }
            if (!hasProduction) {
                System.out.println("      （休息）");
            }
        }
    }

    // --- 产品视图 ---

    private void printProductView(Period p) {
        System.out.println("  ▶ 产品视图（汇总各产线产量）");

        for (Product prod : data.getProducts()) {
            for (StockingPoint sp : data.getStockingPointsForProduct(prod.getId())) {

                String invKey = prod.getId() + "_" + sp.getId() + "_" + p.getIndex();
                double invQty = solutionValue(model.getInvQtyVars(), invKey);

                // 期初库存 (第1周期=初始库存, 后续=上周期末库存)
                double openingInv;
                if (p.getIndex() == 0) {
                    openingInv = data.getInitialInventory(prod.getId(), sp.getId());
                } else {
                    String prevInvKey = prod.getId() + "_" + sp.getId() + "_" + (p.getIndex() - 1);
                    openingInv = solutionValue(model.getInvQtyVars(), prevInvKey);
                }

                // 库存规格
                InventorySpec spec = data.getInventorySpecFor(prod, sp, p);
                String specStr = formatInventorySpec(spec);

                // 生产量汇总 (到货: PTQty[productionDate], 生产日期 = p.startDate - leadTimeDays)
                // 关键: 只统计产出到当前库存点(sp)的操作, 避免多工序路由中跨库存点汇总
                StringBuilder prodStr = new StringBuilder();
                double totalArrived = 0;
                double totalInProduction = 0;
                int matchedUnitOpCount = 0;
                for (Operation op : data.getOperations()) {
                    if (!op.producesProductAtSp(prod.getId(), sp.getId())) continue;
                    int leadTimeDays = op.getLeadTimeDays();
                    // 到货: 在 productionDate = p.startDate - leadTimeDays 生产的此刻到货
                    Period arrivalSrcPeriod = data.getPeriodOffsetByDays(p, leadTimeDays);
                    for (UnitOperation uo : op.getUnitOperations()) {
                        matchedUnitOpCount++;
                        if (arrivalSrcPeriod != null) {
                            String ptKey = op.ptQtyKey(uo, arrivalSrcPeriod.getIndex());
                            double ptQty = solutionValue(model.getPtQtyVars(), ptKey);
                            if (ptQty > 0.001) {
                                prodStr.append(String.format("%s(到货%.0f件), ", uo.getUnitId(), ptQty));
                                totalArrived += ptQty;
                            }
                        }
                        // 生产中: PTQty[t] (当前正在生产, 将来到货)
                        String ptKey = op.ptQtyKey(uo, p.getIndex());
                        double ptQtyNow = solutionValue(model.getPtQtyVars(), ptKey);
                        if (ptQtyNow > 0.001) {
                            totalInProduction += ptQtyNow;
                        }
                    }
                }
                if (matchedUnitOpCount > 1) {
                    LOG.fine(String.format("[结果输出] 多Unit产出: %s@%s P%d 有 %d 个UnitOp产出到该库存点 (到货=%.0f, 生产中=%.0f)",
                            prod.getId(), sp.getId(), p.getIndex(), matchedUnitOpCount,
                            totalArrived, totalInProduction));
                }

                // 销售需求满足
                double totalSales = 0;
                double totalDemand = 0;
                for (SalesDemand sd : data.getSalesDemandsFor(prod, sp, p)) {
                    double sdQty = solutionValue(model.getSalesDemandQtyVars(), sd.getKey());
                    totalSales += sdQty;
                    totalDemand += sd.getQuantity();
                }

                // 需求缺口
                double demandSlack = solutionValue(model.getDemandSlackVars(), invKey);

                // 在途到货
                double inTransitQty = 0;
                for (InTransitSupply its : data.getInTransitSupplies()) {
                    if (its.getProduct().getId().equals(prod.getId())
                            && its.getStockingPoint().getId().equals(sp.getId())
                            && p.equals(data.getPeriodByDate(its.getArrivalDate()))) {
                        inTransitQty += its.getQuantity();
                    }
                }
                String inTransitStr = inTransitQty > 0.001
                        ? String.format(" | 📦在途到货%.0f件", inTransitQty) : "";

                // 期初库存显示
                String openingStr = p.getIndex() == 0
                        ? String.format("期初库存%.0f件 → ", openingInv)
                        : String.format("上期库存%.0f件 → ", openingInv);

                System.out.printf("    %s@%s: %s期末库存%.0f件 %s | %s%s%s | 销售%.0f/%.0f件",
                        prod.getId(), sp.getId(), openingStr, invQty, specStr,
                        totalArrived > 0.001
                                ? "到货共" + formatNum(totalArrived) + "件 ["
                                + prodStr.toString().replaceAll(", $", "") + "]"
                                + (totalInProduction > 0.001
                                ? " 生产中" + formatNum(totalInProduction) + "件"
                                : "")
                                : "无到货",
                        demandSlack > 0.001 ? " ⚠需求缺口" + formatNum(demandSlack) + "件" : "",
                        inTransitStr,
                        totalSales, totalDemand);

                double unmet = totalDemand - totalSales;
                if (unmet > 0.001) {
                    System.out.printf(" ⚠未满足%.0f件", unmet);
                    // 分析未满足原因
                    System.out.print(" " + analyzeUnmetReason(prod, sp, p, openingInv, totalArrived, unmet));
                }
                System.out.println();
            }
        }
    }

    /**
     * 分析需求未满足的原因。
     *
     * <p>从以下维度排查:</p>
     * <ol>
     *   <li>期初库存 + 到货量 是否足以覆盖需求?  否 → 库存耗尽</li>
     *   <li>所有生产该产品的单元是否满负荷?      是 → 产能瓶颈</li>
     *   <li>BOM 原材料是否耗尽?                  是 → 原材料短缺</li>
     *   <li>提前期是否导致生产来不及到货?         是 → 提前期限制</li>
     * </ol>
     */
    private String analyzeUnmetReason(Product prod, StockingPoint sp, Period p,
                                      double openingInv, double totalArrived, double unmet) {
        List<String> reasons = new ArrayList<>();

        // 1. 库存耗尽: 期初库存 + 到货 = 0, 或不足以覆盖需求
        double available = openingInv + totalArrived;
        // 用 totalDemand 来判断, 但这里只传了 unmet, 需要重新获取 totalDemand
        double totalDemand = 0;
        for (SalesDemand sd : data.getSalesDemandsFor(prod, sp, p)) {
            totalDemand += sd.getQuantity();
        }
        if (available < totalDemand) {
            reasons.add("库存耗尽(可用" + formatNum(available) + "件, 需求" + formatNum(totalDemand) + "件)");
        }

        // 2. 产能瓶颈: 所有生产该产品的单元都满负荷
        boolean allUnitsAtMax = true;
        boolean hasAnyUnit = false;
        for (Operation op : data.getOperations()) {
            if (!op.producesProductAtSp(prod.getId(), sp.getId())) continue;
            for (UnitOperation uo : op.getUnitOperations()) {
                hasAnyUnit = true;
                UnitPeriod up = data.getUnitPeriod(uo.getUnitId(), p);
                if (up == null) continue;
                double totalUsed = 0;
                for (Operation unitOp : data.getOperations()) {
                    for (UnitOperation unitUo : unitOp.getUnitOperations()) {
                        if (!unitUo.getUnitId().equals(uo.getUnitId())) continue;
                        String ptKey = unitOp.ptQtyKey(unitUo, p.getIndex());
                        double ptQty = solutionValue(model.getPtQtyVars(), ptKey);
                        totalUsed += ptQty * unitUo.getCapacityCoeff();
                    }
                }
                double utilPercent = up.getMaxCapacity() > 0
                        ? totalUsed / up.getMaxCapacity() * 100 : 0;
                if (utilPercent < 95.0) {  // 95% 以下不算满负荷
                    allUnitsAtMax = false;
                    break;
                }
            }
            if (!allUnitsAtMax) break;
        }
        if (hasAnyUnit && allUnitsAtMax) {
            reasons.add("产能瓶颈(所有产线满负荷)");
        }

        // 3. 原材料短缺: BOM 输入物料库存为 0 或处于最小值
        if (p.getIndex() > 0) {  // 第1周期可能只是初始库存不足, 不算原材料短缺
            for (Operation op : data.getOperations()) {
                if (!op.producesProduct(prod.getId())) continue;
                for (OperationInput input : data.getOperationInputs()) {
                    if (!input.getOperation().getId().equals(op.getId())) continue;
                    Product inputProd = input.getInputProduct();
                    StockingPoint inputSp = input.getInputSp();
                    String inputInvKey = inputProd.getId() + "_" + inputSp.getId() + "_" + (p.getIndex() - 1);
                    double prevInputInv = solutionValue(model.getInvQtyVars(), inputInvKey);
                    InventorySpec inputSpec = data.getInventorySpecFor(inputProd, inputSp, p);
                    double minInv = inputSpec != null ? inputSpec.getMinLevel() : 0;
                    if (prevInputInv <= minInv + 0.001) {
                        if (!reasons.contains("原材料短缺(" + inputProd.getId() + "库存见底)")) {
                            reasons.add("原材料短缺(" + inputProd.getId() + "库存见底)");
                        }
                    }
                }
            }
        }

        // 4. 提前期限制: 该产品有 leadTime, 当期生产无法当期到货
        if (p.getIndex() == 0) {
            for (Operation op : data.getOperations()) {
                if (!op.producesProduct(prod.getId())) continue;
                if (op.getLeadTimeDays() > 0) {
                    double totalProduced = 0;
                    for (UnitOperation uo : op.getUnitOperations()) {
                        String ptKey = op.ptQtyKey(uo, p.getIndex());
                        totalProduced += solutionValue(model.getPtQtyVars(), ptKey);
                    }
                    if (totalProduced > 0.001) {
                        reasons.add("提前期限制(第1周期生产" + formatNum(totalProduced) + "件, 需" + op.getLeadTimeDays() + "天后到货)");
                    }
                }
            }
        }

        // 5. 无生产工序: 该产品没有任何 Operation
        boolean hasOperation = false;
        for (Operation op : data.getOperations()) {
            if (op.producesProduct(prod.getId())) {
                hasOperation = true;
                break;
            }
        }
        if (!hasOperation) {
            reasons.add("无生产工序(纯采购品, 依赖库存/在途)");
        }

        if (reasons.isEmpty()) {
            reasons.add("供需缺口(可用库存不足)");
        }

        return "[" + String.join(", ", reasons) + "]";
    }

    // --- 物料需求展开 ---

    private void printBomExplosion(Period p) {
        System.out.println("  ▶ 物料需求展开（按BOM逐级展开）");

        for (Operation op : data.getOperations()) {
            // 跨 UnitOperations 求和总产量
            double ptQty = 0;
            for (UnitOperation uo : op.getUnitOperations()) {
                String ptKey = op.ptQtyKey(uo, p.getIndex());
                ptQty += solutionValue(model.getPtQtyVars(), ptKey);
            }
            if (ptQty < 0.001) continue;

            String outputLabel = formatOutputProducts(op);
            System.out.printf("    %s(%s)生产%.0f件 →%n", op.getName(), outputLabel, ptQty);

            // 找到该操作的所有输入物料
            int inputMatched = 0;
            for (OperationInput input : data.getOperationInputs()) {
                if (!input.getOperation().getId().equals(op.getId())) continue;
                inputMatched++;

                Product inputProd = input.getInputProduct();
                StockingPoint inputSp = input.getInputSp();
                String invKey = inputProd.getId() + "_" + inputSp.getId() + "_" + p.getIndex();

                // 期初库存
                double prevInv;
                if (p.getIndex() > 0) {
                    String prevInvKey = inputProd.getId() + "_" + inputSp.getId() + "_"
                            + (p.getIndex() - 1);
                    prevInv = solutionValue(model.getInvQtyVars(), prevInvKey);
                } else {
                    prevInv = data.getInitialInventory(inputProd.getId(), inputSp.getId());
                }

                // 期末库存
                double endInv = solutionValue(model.getInvQtyVars(), invKey);

                // 生产量 (该物料自己的操作, 跨所有UnitOperations求和)
                double inputProdQty = 0;
                int matchedInputOps = 0;
                for (Operation inputOp : data.getOperations()) {
                    if (!inputOp.producesProductAtSp(inputProd.getId(), inputSp.getId())) continue;
                    matchedInputOps++;
                    for (UnitOperation uo : inputOp.getUnitOperations()) {
                        String key = inputOp.ptQtyKey(uo, p.getIndex());
                        inputProdQty += solutionValue(model.getPtQtyVars(), key);
                    }
                }
                if (matchedInputOps > 1) {
                    LOG.fine(String.format("[结果输出] 物料多源生产: %s@%s P%d 有 %d 个工序产出 (总量=%.0f)",
                            inputProd.getId(), inputSp.getId(), p.getIndex(),
                            matchedInputOps, inputProdQty));
                }

                // 操作需求 (该操作消耗的物料量)
                String opDemandKey = input.getKey() + "_" + p.getIndex();
                double opDemand = solutionValue(model.getOperationDemandQtyVars(), opDemandKey);

                // 外部销售需求
                double externalSales = 0;
                for (SalesDemand sd : data.getSalesDemandsFor(inputProd, inputSp, p)) {
                    externalSales += solutionValue(model.getSalesDemandQtyVars(), sd.getKey());
                }

                // 在途到货
                double inTransit = 0;
                for (InTransitSupply its : data.getInTransitSupplies()) {
                    if (its.getProduct().getId().equals(inputProd.getId())
                            && its.getStockingPoint().getId().equals(inputSp.getId())
                            && p.equals(data.getPeriodByDate(its.getArrivalDate()))) {
                        inTransit += its.getQuantity();
                    }
                }

                String inTransitNote = inTransit > 0.001
                        ? String.format("(+在途%.0f)", inTransit) : "";

                System.out.printf("    ─── 物料需求汇总 ───%n");
                System.out.printf("    %s(%s): 期初库存%.0f件, 今日生产%.0f件%s, 今日需求%.0f件, 期末库存%.0f件%n",
                        inputProd.getId(),
                        inputProd.getId().startsWith("R") ? "采购" : "生产",
                        prevInv, inputProdQty, inTransitNote,
                        opDemand + externalSales, endInv);
                System.out.printf("      └ %s需求%.0f件(由%s×%s系数)%n",
                        inputProd.getId(), opDemand,
                        formatOutputProducts(op), formatFactor(input.getFactor()));
            }
            if (inputMatched == 0) {
                LOG.fine(String.format("[结果输出] 无BOM输入: '%s'(产出=%s) P%d 生产%.0f件, 无物料消耗",
                        op.getId(), outputLabel, p.getIndex(), ptQty));
            }
        }
    }

    // --- 期末库存 ---

    private void printEndingInventory(Period p) {
        System.out.print("  ▶ 期末库存");
        // 收集所有产品在本周期的库存
        List<String> items = new ArrayList<>();
        for (Product prod : data.getProducts()) {
            for (StockingPoint sp : data.getStockingPointsForProduct(prod.getId())) {
                String invKey = prod.getId() + "_" + sp.getId() + "_" + p.getIndex();
                double invQty = solutionValue(model.getInvQtyVars(), invKey);
                items.add(String.format("%s@%s=%.0f", prod.getId(), sp.getId(), invQty));
            }
        }
        System.out.println("    " + String.join("  ", items));
    }

    // ==================== KPI 汇总 ====================

    private void printKpiSummary() {
        System.out.println();
        System.out.println("═══════════════════ KPI 汇总 ═══════════════════");
        double objValue = model.getSolver().objective().value();
        System.out.printf("目标函数值 (最小总惩罚): %.2f%n", objValue);

        double fulfillment = model.getTotalFulfillment().solutionValue();
        double lotSize = model.getTotalLotSize().solutionValue();
        double maxInv = model.getTotalMaxInventoryLevel().solutionValue();
        double minInv = model.getTotalMinInventoryLevel().solutionValue();
        double targetInv = model.getTotalTargetInvLevel().solutionValue();
        double capacity = model.getTotalUnitCapacity().solutionValue();
        double supplyTarget = model.getTotalSupplyTarget().solutionValue();
        double minSupply = model.getTotalMinSupply().solutionValue();
        double maxSupply = model.getTotalMaxSupply().solutionValue();
        double salesPriority = model.getTotalSalesDemandPriority().solutionValue();

        KPIWeights w = data.getKpiWeights();
        System.out.printf("需求缺口惩罚:   %.2f (权重%.0f × %.2f)%n",
                fulfillment * w.getFulfillmentWeight(), w.getFulfillmentWeight(), fulfillment);
        System.out.printf("批次偏差惩罚:   %.2f (权重%.0f × %.2f)%n",
                lotSize * w.getLotSizeWeight(), w.getLotSizeWeight(), lotSize);
        System.out.printf("超库存惩罚:     %.2f (权重%.0f × %.2f)%n",
                maxInv * w.getMaxInventoryLevelWeight(), w.getMaxInventoryLevelWeight(), maxInv);
        System.out.printf("欠库存惩罚:     %.2f (权重%.0f × %.2f)%n",
                minInv * w.getMinInventoryLevelWeight(), w.getMinInventoryLevelWeight(), minInv);
        System.out.printf("目标库存偏差:   %.2f (权重%.0f × %.2f)%n",
                targetInv * w.getTargetInventoryLevelWeight(), w.getTargetInventoryLevelWeight(), targetInv);
        System.out.printf("产能超载惩罚:   %.2f (权重%.0f × %.2f)%n",
                capacity * w.getUnitCapacityWeight(), w.getUnitCapacityWeight(), capacity);
        System.out.printf("供应目标偏差:   %.2f (权重%.0f × %.2f)%n",
                supplyTarget * w.getSupplyTargetWeight(), w.getSupplyTargetWeight(), supplyTarget);
        System.out.printf("最小供应不足:   %.2f (权重%.0f × %.2f)%n",
                minSupply * w.getMinSupplyWeight(), w.getMinSupplyWeight(), minSupply);
        System.out.printf("最大供应超出:   %.2f (权重%.0f × %.2f)%n",
                maxSupply * w.getMaxSupplyWeight(), w.getMaxSupplyWeight(), maxSupply);
        System.out.printf("销售优先级(负): %.2f (权重%.0f × %.2f)%n",
                salesPriority * w.getSalesDemandPriorityWeight(),
                w.getSalesDemandPriorityWeight(), salesPriority);

        if (fulfillment > 0.001) {
            System.out.println();
            System.out.println("⚠ 注意：存在需求缺口，部分订单未满足。");
        }
    }

    // ==================== 求解统计 ====================

    private void printStatistics() {
        long elapsedMs = System.currentTimeMillis() - startTimeMs;
        System.out.println();
        System.out.println("═══════════════════ 求解统计 ═══════════════════");
        System.out.printf("变量数：%d%n", model.getSolver().numVariables());
        System.out.printf("约束数：%d%n", model.getSolver().numConstraints());
        System.out.printf("耗时：%.3f 秒%n", elapsedMs / 1000.0);
        System.out.println();
    }

    // ==================== 分层优化汇总 ====================

    /**
     * 输出分层优化汇总 — 展示各层级的求解结果和 KPI 值。
     *
     * <p>在分层优化模式下, 每个层级的 KPI 依次求解,
     * 上层最优值作为下层约束。本方法展示各层求解结果,
     * 以及最终加权总惩罚 (等效于单目标函数值)。</p>
     *
     * @param levels         策略层级列表 (按优先级排序)
     * @param levelObjValues 各层级的最优目标值
     */
    public void printHierarchicalSummary(List<StrategyLevel> levels,
                                         List<Double> levelObjValues) {
        System.out.println();
        System.out.println("═══════════════════ 分层优化汇总 ═══════════════════");

        // 表头
        System.out.printf("%-6s %-14s %-12s %-12s %s%n",
                "层级", "名称", "松弛比例", "最优值", "包含KPI");

        double totalWeightedPenalty = 0.0;
        KPIWeights w = data.getKpiWeights();

        for (int i = 0; i < levels.size(); i++) {
            StrategyLevel level = levels.get(i);
            double objValue = (i < levelObjValues.size()) ? levelObjValues.get(i) : 0.0;

            // 构建 KPI 列表字符串
            List<String> kpiNames = new ArrayList<>();
            for (StrategyLevel.KPIEntry kpi : level.getKpis()) {
                kpiNames.add(kpi.name);
            }

            System.out.printf("%-6d %-14s %-12s %-12.2f %s%n",
                    level.getLevel(),
                    level.getName(),
                    String.format("%.0f%%", level.getRelativeGoalSlack() * 100),
                    objValue,
                    String.join(", ", kpiNames));
        }

        // 计算并输出加权总惩罚 (等价于单目标函数值)
        System.out.println();
        System.out.println("--- 最终 KPI 值 (加权总惩罚) ---");
        double fulfillment = model.getTotalFulfillment().solutionValue();
        double lotSize = model.getTotalLotSize().solutionValue();
        double maxInv = model.getTotalMaxInventoryLevel().solutionValue();
        double minInv = model.getTotalMinInventoryLevel().solutionValue();
        double targetInv = model.getTotalTargetInvLevel().solutionValue();
        double capacity = model.getTotalUnitCapacity().solutionValue();
        double supplyTarget = model.getTotalSupplyTarget().solutionValue();
        double minSupply = model.getTotalMinSupply().solutionValue();
        double maxSupply = model.getTotalMaxSupply().solutionValue();
        double salesPriority = model.getTotalSalesDemandPriority().solutionValue();

        double totalPenalty = fulfillment * w.getFulfillmentWeight()
                + lotSize * w.getLotSizeWeight()
                + maxInv * w.getMaxInventoryLevelWeight()
                + minInv * w.getMinInventoryLevelWeight()
                + targetInv * w.getTargetInventoryLevelWeight()
                + capacity * w.getUnitCapacityWeight()
                + supplyTarget * w.getSupplyTargetWeight()
                + minSupply * w.getMinSupplyWeight()
                + maxSupply * w.getMaxSupplyWeight()
                - salesPriority * w.getSalesDemandPriorityWeight();

        System.out.printf("  加权总惩罚: %.2f%n", totalPenalty);
        System.out.printf("  需求缺口:   %.2f (权重%.0f × %.2f)%n",
                fulfillment * w.getFulfillmentWeight(), w.getFulfillmentWeight(), fulfillment);
        System.out.printf("  产能超载:   %.2f (权重%.0f × %.2f)%n",
                capacity * w.getUnitCapacityWeight(), w.getUnitCapacityWeight(), capacity);
        System.out.printf("  批次偏差:   %.2f (权重%.0f × %.2f)%n",
                lotSize * w.getLotSizeWeight(), w.getLotSizeWeight(), lotSize);
        System.out.printf("  目标库存偏差: %.2f (权重%.0f × %.2f)%n",
                targetInv * w.getTargetInventoryLevelWeight(), w.getTargetInventoryLevelWeight(), targetInv);
        System.out.printf("  供应目标偏差: %.2f (权重%.0f × %.2f)%n",
                supplyTarget * w.getSupplyTargetWeight(), w.getSupplyTargetWeight(), supplyTarget);
        System.out.printf("  超库存:     %.2f (权重%.0f × %.2f)%n",
                maxInv * w.getMaxInventoryLevelWeight(), w.getMaxInventoryLevelWeight(), maxInv);
        System.out.printf("  欠库存:     %.2f (权重%.0f × %.2f)%n",
                minInv * w.getMinInventoryLevelWeight(), w.getMinInventoryLevelWeight(), minInv);
        System.out.printf("  最小供应不足: %.2f (权重%.0f × %.2f)%n",
                minSupply * w.getMinSupplyWeight(), w.getMinSupplyWeight(), minSupply);
        System.out.printf("  最大供应超出: %.2f (权重%.0f × %.2f)%n",
                maxSupply * w.getMaxSupplyWeight(), w.getMaxSupplyWeight(), maxSupply);
        System.out.printf("  销售优先级(负): %.2f (权重%.0f × %.2f)%n",
                salesPriority * w.getSalesDemandPriorityWeight(),
                w.getSalesDemandPriorityWeight(), salesPriority);
    }

    // ==================== 辅助方法 ====================

    /** 获取变量的解值, 缺失时返回 0 */
    private static double solutionValue(Map<String, ?> varMap, String key) {
        Object var = varMap.get(key);
        if (var instanceof com.google.ortools.linearsolver.MPVariable) {
            return ((com.google.ortools.linearsolver.MPVariable) var).solutionValue();
        }
        return 0.0;
    }

    /** 格式化 BOM 因子 (整数时不显示小数) */
    private static String formatFactor(double factor) {
        if (factor == Math.floor(factor) && !Double.isInfinite(factor)) {
            return String.format("%.0f", factor);
        }
        return String.format("%.1f", factor);
    }

    /** 格式化数量 (整数时不显示小数) */
    private static String formatNum(double val) {
        if (Math.abs(val - Math.round(val)) < 0.001) {
            return String.format("%.0f", val);
        }
        return String.format("%.1f", val);
    }

    /** 格式化库存规格 */
    private String formatInventorySpec(InventorySpec spec) {
        if (spec == null) return "";
        StringBuilder sb = new StringBuilder("[");
        if (spec.hasTarget()) {
            sb.append("目标").append(formatNum(spec.getTargetLevel()));
        } else if (spec.hasTargetInDays()) {
            sb.append("目标").append(formatNum(spec.getTargetInDays())).append("天");
        }
        if (spec.hasMinLevel()) {
            sb.append("/最小").append(formatNum(spec.getMinLevel()));
        } else if (spec.hasMinLevelInDays()) {
            sb.append("/最小").append(formatNum(spec.getMinLevelInDays())).append("天");
        }
        if (spec.hasMaxLevel()) {
            sb.append("/最大").append(formatNum(spec.getMaxLevel()));
        } else if (spec.hasMaxLevelInDays()) {
            sb.append("/最大").append(formatNum(spec.getMaxLevelInDays())).append("天");
        }
        sb.append("]");
        return sb.toString();
    }
}