package com.aps.macroplanner.output;

import com.aps.entity.Algorithm.Chromosome;
import com.aps.entity.Schedule.SceneChromsome;
import com.aps.entity.Schedule.SceneDetail;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
import com.aps.macroplanner.output.dto.*;
import com.aps.service.plan.SceneService;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.google.ortools.linearsolver.MPSolver;
import com.google.ortools.linearsolver.MPVariable;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

/**
 * 优化结果写入器 — 将求解器变量值回写到业务对象 DTO 并序列化为 JSON。
 *
 * <h3>对应 Quintiq 原模型</h3>
 * <pre>
 *   PeriodTaskResult   ← CapacityPlanningAlgorithmHandleFeasibleOperationPeriodTask
 *   SalesDemandResult  ← CapacityPlanningAlgorithmHandleFeasibleSalesDemand
 *   PispipResult       ← CapacityPlanningAlgorithmHandleFeasibleProductInStockingPointInPeriod
 *   KpiResult          ← DisplayGoalValue + CreateKPISnapshot
 *   SolverStatistics   ← SnapshotMacroPlannerOptimizer
 * </pre>
 *
 * <h3>输出格式</h3>
 * <pre>
 *   optimization_result.json:
 *   {
 *     "metadata": { "timestamp": "...", "solver": "SCIP", "version": "1.0.0" },
 *     "periodTasks": [ { "operationId": "OP1", "periodIndex": 0, "productionQty": 100.0, ... } ],
 *     "salesDemands": [ { "salesDemandId": "SD1", "fulfilledQty": 80.0, "unmetQty": 20.0, ... } ],
 *     "pispips": [ { "productId": "PA", "spId": "SP1", "endingInventory": 50.0, ... } ],
 *     "kpis": { "objectiveValue": 1356.0, "entries": [ ... ] },
 *     "statistics": { "numVariables": 191, "numConstraints": 144, "elapsedSeconds": 0.05, ... }
 *   }
 * </pre>
 */
public class ResultWriter {

    /** 默认输出目录 */
    private static final String OUTPUT_DIR = "src/main/java/com/mp/log/";
    /** 默认输出文件名 */
    private static final String OUTPUT_FILE = "optimization_result.json";

    private final MacroPlannerModel model;
    private final TestDataBuilder data;
    private final long startTimeMs;
    /**
     * 是否使用 GZIP 压缩，默认 true
     */
    private boolean useCompression = false;

    ObjectMapper objectMapper = createObjectMapper();

    private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(SceneService.class);
    private String getFileExtension() {
        return useCompression ? ".json.gz" : ".json";
    }

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

    private File getResultDirectory() {
        File resultDir = new File("mp/result");
        if (!resultDir.exists()) {
            boolean created = resultDir.mkdirs();
            if (!created) {
                logger.warn("无法创建结果目录: {}", resultDir.getAbsolutePath());
                throw new RuntimeException("无法创建结果目录: " + resultDir.getAbsolutePath());
            }
        }
        return resultDir;
    }
    private File getOptimizationFile(String sceneId) {
        File resultDir = getResultDirectory();
        String fileName = "optimization_result_" + sceneId + getFileExtension();
        return new File(resultDir, fileName);
    }
    // ==================== 主入口 ====================

    /**
     * 将结果保存到 JSON 文件
     */
    public boolean saveResultToFile(String sceneId) {
        OptimizationResult result = buildResult();
        if (result == null) {
            logger.warn("对象不能为空");
            return false;
        }

        if (sceneId == null || sceneId.trim().isEmpty()) {
            logger.warn("场景ID不能为空");
            return false;
        }

        try {

            File file = getOptimizationFile(sceneId);
            File tempFile = new File(file.getParentFile(), file.getName() + ".tmp");

            if (useCompression) {
                try (FileOutputStream fos = new FileOutputStream(tempFile);
                     GZIPOutputStream gzos = new GZIPOutputStream(fos)) {
                    objectMapper.writeValue(gzos, result);
                }
            } else {
                objectMapper.writeValue(tempFile, result);
            }

            if (tempFile.length() == 0) {
                logger.error("写入的临时文件为空: {}", tempFile.getAbsolutePath());
                Files.deleteIfExists(tempFile.toPath());
                return false;
            }

            if (useCompression) {
                try (FileInputStream fis = new FileInputStream(tempFile);
                     GZIPInputStream gzis = new GZIPInputStream(fis)) {
                    OptimizationResult verifyChromosome = objectMapper.readValue(gzis, OptimizationResult.class);
                    if (verifyChromosome == null) {
                        throw new IOException("验证读取失败");
                    }
                }
            } else {
                try {
                    OptimizationResult verifyChromosome = objectMapper.readValue(tempFile, OptimizationResult.class);
                    if (verifyChromosome == null) {
                        throw new IOException("验证读取失败");
                    }
                } catch (Exception e) {
                    logger.error("验证染色体文件内容失败，文件: {}", tempFile.getAbsolutePath(), e);
                    Files.deleteIfExists(tempFile.toPath());
                    return false;
                }
            }

            if (file.exists()) {
                Files.deleteIfExists(file.toPath());
            }
            Files.move(tempFile.toPath(), file.toPath());

            logger.info("保存成功，场景ID: {}, 文件: {}", sceneId, file.getAbsolutePath());
            return true;
        } catch (Exception e) {
            logger.error("保存文件失败，场景ID: " + sceneId, e);
            return false;
        }
    }

    /**
     * 从文件中读取 Chromosome 对象，并拆分文件读取与反序列化耗时。
     */
    public OptimizationResult getResultToFile(String sceneId) {
        if (sceneId == null || sceneId.trim().isEmpty()) {
            logger.warn("场景ID不能为空");
            return null;
        }

        try {
            long totalStart = System.nanoTime();
            File file = getOptimizationFile(sceneId);

            if (!file.exists()) {
                logger.warn("文件不存在: {}", file.getAbsolutePath());
                return null;
            }

            long fileSize = file.length();
            if (fileSize == 0) {
                logger.warn("文件为空: {}", file.getAbsolutePath());
                return null;
            }

            logger.info("正在从文件加载: {}, sceneId={}, fileSize={} bytes",
                    file.getAbsolutePath(), sceneId, fileSize);

            long deserializeStart = System.nanoTime();
            OptimizationResult optimizationResult;
            if (useCompression) {
                try (FileInputStream fis = new FileInputStream(file);
                     GZIPInputStream gzis = new GZIPInputStream(fis)) {
                    optimizationResult = objectMapper.readValue(gzis, OptimizationResult.class);
                }
            } else {
                try (FileInputStream fis = new FileInputStream(file)) {
                    optimizationResult = objectMapper.readValue(fis, OptimizationResult.class);
                }
            }
            long deserializeMs = (System.nanoTime() - deserializeStart) / 1_000_000;
            long totalMs = (System.nanoTime() - totalStart) / 1_000_000;

            logger.info("加载成功，场景ID: {}, fileSize={} bytes, deserialize={} ms, total={} ms",
                    sceneId, fileSize, deserializeMs, totalMs);

            return optimizationResult;
        } catch (Exception e) {
            logger.error("加载文件失败，场景ID: " + sceneId, e);
            throw new RuntimeException("加载文件失败: " + e.getMessage(), e);
        }
    }



    private ObjectMapper createObjectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.disable(SerializationFeature.INDENT_OUTPUT);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        return objectMapper;
    }

    /**
     * 收集所有结果并返回 JSON 字符串。
     */
    public String toJsonString() {
        return toJson(buildResult());
    }

    // ==================== 构建 OptimizationResult ====================

    private OptimizationResult buildResult() {
        OptimizationResult result = new OptimizationResult();

        // 元数据
        result.setTimestamp(LocalDateTime.now()
                .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        result.setSolver("SCIP (via Google OR-Tools)");
        result.setVersion("1.0.0");

        // 业务结果
        buildPeriodTasks(result);

        buildSalesDemands(result);
        buildPispips(result);
        result.setUnitCapacities(buildUnitCapacities());
        result.setProductNetwork(buildProductNetwork());
        result.setDemandSummary(buildDemandSummary());

        // KPI + 统计
        result.setKpis(buildKpis());
        result.setStatistics(buildStatistics());

        return result;
    }

    // ==================== PeriodTaskResult ====================

    private void buildPeriodTasks(OptimizationResult result) {
        for (Operation op : data.getOperations()) {
            for (UnitOperation uo : op.getUnitOperations()) {
                for (Period p : data.getPeriods()) {
                    String key = op.ptQtyKey(uo, p.getIndex());
                    double ptQty = solutionValue(model.getPtQtyVars(), key);
                    if (ptQty < 0.001) continue;

                    PeriodTaskResult pt = new PeriodTaskResult();
                    pt.setOperationId(op.getId());
                    pt.setOperationName(op.getName());
                    pt.setUnitId(uo.getUnitId());
                    pt.setPeriodIndex(p.getIndex());
                    pt.setPeriodStartDate(p.getStartDate().toString());
                    pt.setProductionQty(ptQty);
                    pt.setCapacityCoeff(uo.getCapacityCoeff());
                    pt.setCapacityUsed(ptQty * uo.getCapacityCoeff());
                    if (uo.hasLotSize()) {
                        pt.setLotSize(uo.getLotSize());
                    }
                    pt.setLotSizeOver(solutionValue(model.getPtLotSizeOverVars(), key));
                    pt.setLotSizeUnder(solutionValue(model.getPtLotSizeUnderVars(), key));

                    // 产出信息
                    for (OperationOutput oo : op.getOutputs()) {
                        PeriodTaskResult.OutputInfo oi = new PeriodTaskResult.OutputInfo();
                        oi.productId = oo.getProductId();
                        oi.spId = oo.getSpId();
                        oi.factor = 1.0; // 默认 1:1 产出
                        pt.getOutputs().add(oi);
                    }

                    result.getPeriodTasks().add(pt);
                }
            }
        }
    }


    // ==================== UnitCapacityResult ====================

    /**
     * 按设备聚合周期任务, 结合 UnitPeriod.maxCapacity 计算设备利用率。
     *
     * <p>利用率 = capacityUsed / maxCapacity (每周期和整体)。</p>
     */
    /**
     * 统计各 Unit 的产能使用情况: 每周期产能占用量、利用率、超载/未满足量。
     */
    // ==================== UnitCapacityResult ====================

    /**
     * 统计各 Unit 的产能使用情况: 每周期产能占用量、利用率、超载/未满足量。
     */
    private java.util.List<UnitCapacityResult> buildUnitCapacities() {
        java.util.Map<String, UnitCapacityResult> unitMap = new java.util.LinkedHashMap<>();

        for (UnitPeriod up : data.getUnitPeriods()) {
            String uid = up.getUnitId();
            UnitCapacityResult ucr = unitMap.computeIfAbsent(uid, k -> {
                UnitCapacityResult r = new UnitCapacityResult();
                r.setUnitId(k);
                return r;
            });

            // 计算该 unit 在此周期的实际产能占用量
            UnitCapacityResult.UnitPeriodDetail pe = new UnitCapacityResult.UnitPeriodDetail();
            pe.periodIndex = up.getPeriod().getIndex();
            pe.periodStartDate = up.getPeriod().getStartDate().toString();

            double capacityUsed = 0;
            for (Operation op : data.getOperations()) {
                for (UnitOperation uo : op.getUnitOperations()) {
                    if (!uo.getUnitId().equals(uid)) continue;
                    String ptKey = op.ptQtyKey(uo, up.getPeriod().getIndex());
                    double ptQty = solutionValue(model.getPtQtyVars(), ptKey);
                    if (ptQty < 0.001) continue;
                    double capUsed = ptQty * uo.getCapacityCoeff();
                    capacityUsed += capUsed;

                    // 工序级明细
                    UnitCapacityResult.UnitTaskInfo task = new UnitCapacityResult.UnitTaskInfo();
                    task.operationId = op.getId();
                    task.operationName = op.getName();
                    task.productionQty = ptQty;
                    task.capacityUsed = capUsed;
                    task.capacityCoeff = uo.getCapacityCoeff();
                    if (uo.hasLotSize()) {
                        task.lotSize = uo.getLotSize();
                        task.lotSizeOver = solutionValue(model.getPtLotSizeOverVars(), ptKey);
                        task.lotSizeUnder = solutionValue(model.getPtLotSizeUnderVars(), ptKey);
                    }
                    for (OperationOutput oo : op.getOutputs()) {
                        task.outputs.add(oo.getProductId() + "@" + oo.getSpId());
                    }
                    pe.tasks.add(task);
                }
            }

            double maxCap = up.getMaxCapacity();
            double utilization = maxCap > 0 ? capacityUsed / maxCap : 0;
            double overloaded = solutionValue(model.getCapacityOverloadedVars(), up.getKey());
            double notMet = solutionValue(model.getCapacityNotMetVars(), up.getKey());

            pe.maxCapacity = maxCap;
            pe.capacityUsed = capacityUsed;
            pe.utilization = utilization;
            pe.overloaded = overloaded;
            pe.notMet = notMet;
            ucr.getPeriodDetails().add(pe);

            // 累加汇总
            ucr.setTotalMaxCapacity(ucr.getTotalMaxCapacity() + maxCap);
            ucr.setTotalCapacityUsed(ucr.getTotalCapacityUsed() + capacityUsed);
            ucr.setTotalOverloaded(ucr.getTotalOverloaded() + overloaded);
            ucr.setTotalNotMet(ucr.getTotalNotMet() + notMet);
        }

        // 计算整体利用率
        for (UnitCapacityResult ucr : unitMap.values()) {
            if (ucr.getTotalMaxCapacity() > 0) {
                ucr.setOverallUtilization(
                        ucr.getTotalCapacityUsed() / ucr.getTotalMaxCapacity());
            }
        }

        return new java.util.ArrayList<>(unitMap.values());
    }


    // ==================== SalesDemandResult ====================

    private void buildSalesDemands(OptimizationResult result) {
        for (SalesDemand sd : data.getSalesDemands()) {
            SalesDemandResult sr = new SalesDemandResult();
            sr.setSalesDemandId(sd.getKey());
            sr.setProductId(sd.getProduct().getId());
            sr.setSpId(sd.getStockingPoint().getId());
            sr.setPeriodIndex(sd.getPeriod().getIndex());
            sr.setPeriodStartDate(sd.getPeriod().getStartDate().toString());
            sr.setDemandQty(sd.getQuantity());
            sr.setPriority(sd.getPriority());

            double fulfilled = solutionValue(model.getSalesDemandQtyVars(), sd.getKey());
            sr.setFulfilledQty(fulfilled);
            sr.setUnmetQty(Math.max(0, sd.getQuantity() - fulfilled));
            sr.setFulfillmentRate(sd.getQuantity() > 0 ? fulfilled / sd.getQuantity() : 1.0);

            // DemandSlack (PISPIP 级别)
            String invKey = sd.getProduct().getId() + "_" + sd.getStockingPoint().getId()
                    + "_" + sd.getPeriod().getIndex();
            sr.setDemandSlack(solutionValue(model.getDemandSlackVars(), invKey));

            result.getSalesDemands().add(sr);
        }
    }

    // ==================== DemandSummaryResult ====================

    /**
     * 按产品汇总需求量/满足量/满足率。
     */
    private java.util.List<DemandSummaryResult> buildDemandSummary() {
        java.util.Map<String, DemandSummaryResult> prodMap = new java.util.LinkedHashMap<>();

        for (SalesDemand sd : data.getSalesDemands()) {
            String prodId = sd.getProduct().getId();
            DemandSummaryResult dsr = prodMap.computeIfAbsent(prodId, k -> {
                DemandSummaryResult r = new DemandSummaryResult();
                r.setProductId(k);
                return r;
            });

            double fulfilled = solutionValue(model.getSalesDemandQtyVars(), sd.getKey());
            double demand = sd.getQuantity();
            double unmet = Math.max(0, demand - fulfilled);
            double rate = demand > 0 ? fulfilled / demand : 1.0;

            DemandSummaryResult.PeriodEntry pe = new DemandSummaryResult.PeriodEntry();
            pe.periodIndex = sd.getPeriod().getIndex();
            pe.periodStartDate = sd.getPeriod().getStartDate().toString();
            pe.demandQty = demand;
            pe.fulfilledQty = fulfilled;
            pe.unmetQty = unmet;
            pe.fulfillmentRate = rate;
            dsr.getPeriodEntries().add(pe);

            dsr.setTotalDemand(dsr.getTotalDemand() + demand);
            dsr.setTotalFulfilled(dsr.getTotalFulfilled() + fulfilled);
            dsr.setTotalUnmet(dsr.getTotalUnmet() + unmet);
        }

        for (DemandSummaryResult dsr : prodMap.values()) {
            if (dsr.getTotalDemand() > 0) {
                dsr.setFulfillmentRate(dsr.getTotalFulfilled() / dsr.getTotalDemand());
            } else {
                dsr.setFulfillmentRate(1.0);
            }
        }

        return new java.util.ArrayList<>(prodMap.values());
    }

    // ==================== PispipResult ====================

    private void buildPispips(OptimizationResult result) {
        for (Product prod : data.getProducts()) {
            for (StockingPoint sp : data.getStockingPointsForProduct(prod.getId())) {
                for (Period p : data.getPeriods()) {
                    PispipResult pr = new PispipResult();
                    pr.setProductId(prod.getId());
                    pr.setSpId(sp.getId());
                    pr.setPeriodIndex(p.getIndex());
                    pr.setPeriodStartDate(p.getStartDate().toString());

                    String invKey = prod.getId() + "_" + sp.getId() + "_" + p.getIndex();

                    // 期初库存
                    double openingInv;
                    if (p.getIndex() == 0) {
                        openingInv = data.getInitialInventory(prod.getId(), sp.getId());
                    } else {
                        String prevKey = prod.getId() + "_" + sp.getId() + "_" + (p.getIndex() - 1);
                        openingInv = solutionValue(model.getInvQtyVars(), prevKey);
                    }
                    pr.setOpeningInventory(openingInv);

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

                    // 库存规格
                    InventorySpec spec = data.getInventorySpecFor(prod, sp, p);
                    if (spec != null) {
                        if (spec.hasTarget()) pr.setTargetInventoryLevel(spec.getTargetLevel());
                        if (spec.hasTargetInDays()) pr.setTargetInventoryDays(spec.getTargetInDays());
                        if (spec.hasMinLevel()) pr.setMinInventoryLevel(spec.getMinLevel());
                        if (spec.hasMinLevelInDays()) pr.setMinInventoryDays(spec.getMinLevelInDays());
                        if (spec.hasMaxLevel()) pr.setMaxInventoryLevel(spec.getMaxLevel());
                        if (spec.hasMaxLevelInDays()) pr.setMaxInventoryDays(spec.getMaxLevelInDays());
                    }
                    pr.setBelowTarget(solutionValue(model.getInvQtyUnderTargetVars(), invKey));
                    pr.setBelowMin(solutionValue(model.getMinInvQtyUnderVars(), invKey));
                    pr.setAboveMax(solutionValue(model.getMaxInvQtyOverVars(), invKey));

                    // 需求满足量 (安全库存天数)
                    pr.setDemandFulfillment(solutionValue(model.getDemandFulfillmentVars(), invKey));

                    // 需求松弛
                    pr.setDemandSlack(solutionValue(model.getDemandSlackVars(), invKey));

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

                    // BOM 依赖需求
                    double totalDepDemand = 0;
                    String depKey = prod.getId() + "_" + sp.getId() + "_" + p.getIndex();
                    totalDepDemand = solutionValue(model.getDependentDemandVars(), depKey);
                    pr.setDependentDemandQty(totalDepDemand);

                    // 总流出
                    pr.setTotalOutflow(totalSales + totalDepDemand);

                    // 到货量 (考虑 lead time)
                    double totalArrived = 0;
                    double totalInProgress = 0;
                    for (Operation op : data.getOperations()) {
                        if (!op.producesProductAtSp(prod.getId(), sp.getId())) continue;

                        int leadTime = op.getLeadTimeDays();
                        Period arrivalSrcPeriod = data.getPeriodOffsetByDays(p, leadTime);
                        // 汇总所有产线
                        for (UnitOperation uo : op.getUnitOperations()) {
                            // 到货
                            if (arrivalSrcPeriod != null) {
                                String ptKey = op.ptQtyKey(uo, arrivalSrcPeriod.getIndex());
                                totalArrived += solutionValue(model.getPtQtyVars(), ptKey);
                            }

                            // 生产中
                            String ptKeyNow = op.ptQtyKey(uo, p.getIndex());
                            double ptQtyNow = solutionValue(model.getPtQtyVars(), ptKeyNow);
                            if (ptQtyNow > 0.001) {
                                totalInProgress += ptQtyNow;

                                // 生产明细
                                PispipResult.ProductionDetail pd = new PispipResult.ProductionDetail();
                                pd.operationId = op.getId();
                                pd.operationName = op.getName();
                                pd.unitId = uo.getUnitId();
                                pd.quantity = ptQtyNow;
                                pd.leadTimeDays = leadTime;

                                // BOM 消耗
                                for (OperationInput input : data.getOperationInputs()) {
                                    if (!input.getOperation().getId().equals(op.getId())) continue;
                                    PispipResult.BomConsumption bc = new PispipResult.BomConsumption();
                                    bc.inputProductId = input.getInputProduct().getId();
                                    bc.inputSpId = input.getInputSp().getId();
                                    bc.factor = input.getFactor();
                                    String opDemandKey = input.getKey() + "_" + p.getIndex();
                                    bc.consumedQty = solutionValue(
                                            model.getOperationDemandQtyVars(), opDemandKey);
                                    pd.bomConsumptions.add(bc);
                                }

                                pr.getProductionDetails().add(pd);
                            }
                        }
                    }
                    pr.setProductionArrived(totalArrived);
                    pr.setProductionInProgress(totalInProgress);

                    // 在途到货
                    double inTransit = 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()))) {
                            inTransit += its.getQuantity();
                        }
                    }
                    pr.setInTransitArrival(inTransit);

                    // 总流入
                    pr.setTotalInflow(totalArrived + inTransit);

                    result.getPispips().add(pr);
                }
            }
        }
    }

    // ==================== ProductNetworkResult ====================

    /**
     * 构建产品生产网络 — 从成品向下逐级展开到原材料, 形成完整 BOM 供应链视图。
     *
     * <h3>展开逻辑</h3>
     * <ol>
     *   <li>确定节点类型: 不被任何工序消耗 → 成品(根节点), 被消耗但也被生产 → 半成品, 只被消耗 → 原材料</li>
     *   <li>从成品开始, 通过 BOM 关系向下递归展开子物料</li>
     *   <li>每层记录: 供应源、消费者、库存汇总、BOM子节点</li>
     * </ol>
     */
    private ProductNetworkResult buildProductNetwork() {
        ProductNetworkResult network = new ProductNetworkResult();

        // Step 1: 找出被消耗的产品集合 (用于判断成品 vs 半成品 vs 原材料)
        java.util.Set<String> consumedProductIds = new java.util.HashSet<>();
        for (OperationInput input : data.getOperationInputs()) {
            consumedProductIds.add(input.getInputProduct().getId());
        }

        // Step 2: 判断每个产品是否有生产工序
        java.util.Map<String, Boolean> hasProduction = new java.util.LinkedHashMap<>();
        for (Product prod : data.getProducts()) {
            boolean found = false;
            for (Operation op : data.getOperations()) {
                if (op.producesProduct(prod.getId())) { found = true; break; }
            }
            hasProduction.put(prod.getId(), found);
        }

        // Step 3: 确定成品 (不被任何工序消耗且有生产)
        java.util.Set<String> expandedNodes = new java.util.HashSet<>();
        for (Product prod : data.getProducts()) {
            if (!consumedProductIds.contains(prod.getId())) {
                // 成品: 找一种库存点来展开
                for (StockingPoint sp : data.getStockingPointsForProduct(prod.getId())) {
                    SupplyChainNode root = network.getOrCreateNode(prod.getId(), sp.getId());
                    expandNode(root, network, 0, consumedProductIds, hasProduction,
                            expandedNodes);
                    network.getFinishedGoods().add(root);
                }
            }
        }

        // Step 4: 对每个节点计算供应/需求/库存汇总
        for (SupplyChainNode node : network.getAllNodes().values()) {
            fillNodeSummary(node);
        }

        return network;
    }

    /**
     * 递归展开 BOM 树: 为当前节点填充供应源、消费者, 并递归展开子物料。
     */
    private void expandNode(SupplyChainNode node, ProductNetworkResult network,
                            int level, java.util.Set<String> consumedProductIds,
                            java.util.Map<String, Boolean> hasProduction,
                            java.util.Set<String> expandedNodes) {
        String nodeKey = node.getProductId() + "@" + node.getSpId();
        if (!expandedNodes.add(nodeKey)) return; // 已展开过, 跳过防重复

        node.setLevel(level);

        String prodId = node.getProductId();
        String spId = node.getSpId();

        // --- 供应源: 哪些工序生产这个产品@库存点 ---
        java.util.Set<String> visitedSupplySources = new java.util.HashSet<>();
        for (Operation op : data.getOperations()) {
            if (!op.producesProductAtSp(prodId, spId)) continue;

            String srcKey = op.getId() + "_" + spId;
            if (!visitedSupplySources.add(srcKey)) continue; // 防重复

            // 计算总产量 (汇总所有产线)
            double totalProd = 0;
            double totalCap = 0;
            for (Period p : data.getPeriods()) {
                for (UnitOperation uo : op.getUnitOperations()) {
                    String key = op.ptQtyKey(uo, p.getIndex());
                    double qty = solutionValue(model.getPtQtyVars(), key);
                    totalProd += qty;
                    totalCap += qty * uo.getCapacityCoeff();
                }
            }

            SupplyChainNode.SupplySource src = new SupplyChainNode.SupplySource();
            src.type = "OPERATION";
            src.operationId = op.getId();
            src.operationName = op.getName();
            src.unitId = op.getUnitId();
            src.totalProduction = totalProd;
            src.capacityUsed = totalCap;
            if (op.hasLotSize()) src.lotSize = op.getLotSize();
            node.getSupplySources().add(src);
        }

        // 在途供应
        for (InTransitSupply its : data.getInTransitSupplies()) {
            if (its.getProduct().getId().equals(prodId)
                    && its.getStockingPoint().getId().equals(spId)) {
                SupplyChainNode.SupplySource src = new SupplyChainNode.SupplySource();
                src.type = "IN_TRANSIT";
                src.totalProduction = its.getQuantity();
                node.getSupplySources().add(src);
            }
        }

        // 外部采购 (无生产工序)
        if (node.getSupplySources().isEmpty()) {
            SupplyChainNode.SupplySource src = new SupplyChainNode.SupplySource();
            src.type = "EXTERNAL";
            src.totalProduction = 0;
            node.getSupplySources().add(src);
        }

        // --- 消费者: 哪些产品消耗这个节点 ---
        java.util.Set<String> visitedConsumers = new java.util.HashSet<>();
        for (OperationInput input : data.getOperationInputs()) {
            if (!input.getInputProduct().getId().equals(prodId)
                    || !input.getInputSp().getId().equals(spId)) continue;

            double totalConsumed = 0;
            for (Period p : data.getPeriods()) {
                String key = input.getKey() + "_" + p.getIndex();
                totalConsumed += solutionValue(model.getOperationDemandQtyVars(), key);
            }

            // 消费者 = 这个工序的产出产品
            for (OperationOutput oo : input.getOperation().getOutputs()) {
                String ciKey = oo.getProductId() + "@" + oo.getSpId()
                        + "_" + input.getOperation().getId();
                if (!visitedConsumers.add(ciKey)) continue; // 防重复

                SupplyChainNode.ConsumerInfo ci = new SupplyChainNode.ConsumerInfo();
                ci.consumerProductId = oo.getProductId();
                ci.consumerSpId = oo.getSpId();
                ci.operationId = input.getOperation().getId();
                ci.operationName = input.getOperation().getName();
                ci.factor = input.getFactor();
                ci.totalConsumed = totalConsumed;
                node.getConsumers().add(ci);
            }
        }

        // --- BOM 子物料: 这个产品的生产消耗什么 ---
        java.util.Set<String> expandedChildren = new java.util.HashSet<>();
        next_child:
        for (Operation op : data.getOperations()) {
            if (!op.producesProductAtSp(prodId, spId)) continue;

            for (OperationInput input : data.getOperationInputs()) {
                if (!input.getOperation().getId().equals(op.getId())) continue;

                String childKey = input.getInputProduct().getId()
                        + "@" + input.getInputSp().getId();
                if (expandedChildren.contains(childKey)) continue;
                expandedChildren.add(childKey);

                // 汇总该物料的总消耗量
                double totalConsumed = 0;
                for (Period p : data.getPeriods()) {
                    String key = input.getKey() + "_" + p.getIndex();
                    totalConsumed += solutionValue(model.getOperationDemandQtyVars(), key);
                }

                SupplyChainNode.BomChild child = new SupplyChainNode.BomChild();
                child.productId = input.getInputProduct().getId();
                child.spId = input.getInputSp().getId();
                child.totalFactor = input.getFactor();
                child.totalConsumedQty = totalConsumed;
                node.getChildren().add(child);

                // 递归展开子节点
                SupplyChainNode childNode = network.getOrCreateNode(
                        child.productId, child.spId);
                expandNode(childNode, network, level + 1,
                        consumedProductIds, hasProduction, expandedNodes);
            }
            break; // 只展开第一个匹配工序的 BOM (多 Routing 场景下各工序 BOM 相同)
        }
    }

    /** 填充节点的跨周期汇总数据 */
    private void fillNodeSummary(SupplyChainNode node) {
        String prodId = node.getProductId();
        String spId = node.getSpId();
        SupplySummary s = new SupplySummary();
        node.setSummary(s);

        int n = data.getPeriods().size();

        // 库存汇总
        double totalEndingInv = 0;
        for (Period p : data.getPeriods()) {
            String invKey = prodId + "_" + spId + "_" + p.getIndex();
            totalEndingInv += solutionValue(model.getInvQtyVars(), invKey);
        }
        s.setInitialInventory(data.getInitialInventory(prodId, spId));
        s.setFinalInventory(solutionValue(model.getInvQtyVars(),
                prodId + "_" + spId + "_" + (n - 1)));
        s.setAverageInventory(n > 0 ? totalEndingInv / n : 0);

        // 生产汇总
        double totalProd = 0;
        double totalArrived = 0;
        for (Operation op : data.getOperations()) {
            if (!op.producesProductAtSp(prodId, spId)) continue;
            for (Period p : data.getPeriods()) {
                for (UnitOperation uo : op.getUnitOperations()) {
                    String ptKey = op.ptQtyKey(uo, p.getIndex());
                    totalProd += solutionValue(model.getPtQtyVars(), ptKey);

                    // 到货 (考虑 lead time)
                    Period arrSrc = data.getPeriodOffsetByDays(p, op.getLeadTimeDays());
                    if (arrSrc != null) {
                        String arrKey = op.ptQtyKey(uo, arrSrc.getIndex());
                        totalArrived += solutionValue(model.getPtQtyVars(), arrKey);
                    }
                }
            }
        }
        s.setTotalProduction(totalProd);
        s.setTotalArrived(totalArrived);

        // 在途汇总
        double totalInTransit = 0;
        for (InTransitSupply its : data.getInTransitSupplies()) {
            if (its.getProduct().getId().equals(prodId)
                    && its.getStockingPoint().getId().equals(spId)) {
                totalInTransit += its.getQuantity();
            }
        }
        s.setTotalInTransit(totalInTransit);

        // 需求汇总
        double totalSalesDemand = 0, totalSalesFulfilled = 0;
        double totalDepDemand = 0, totalDemandFulf = 0, totalSlack = 0;
        for (Period p : data.getPeriods()) {
            String invKey = prodId + "_" + spId + "_" + p.getIndex();
            totalDepDemand += solutionValue(model.getDependentDemandVars(), invKey);
            totalDemandFulf += solutionValue(model.getDemandFulfillmentVars(), invKey);
            totalSlack += solutionValue(model.getDemandSlackVars(), invKey);

            for (SalesDemand sd : data.getSalesDemands()) {
                if (sd.getProduct().getId().equals(prodId)
                        && sd.getStockingPoint().getId().equals(spId)
                        && sd.getPeriod().getIndex() == p.getIndex()) {
                    totalSalesDemand += sd.getQuantity();
                    totalSalesFulfilled += solutionValue(model.getSalesDemandQtyVars(),
                            sd.getKey());
                }
            }
        }
        s.setTotalSalesDemand(totalSalesDemand);
        s.setTotalSalesFulfilled(totalSalesFulfilled);
        s.setTotalDependentDemand(totalDepDemand);
        s.setTotalDemandFulfillment(totalDemandFulf);
        s.setTotalDemandSlack(totalSlack);

        node.setTotalSalesDemand(totalSalesDemand);
        node.setTotalSalesFulfilled(totalSalesFulfilled);
        node.setTotalDependentDemand(totalDepDemand);

        // 库存规格偏差汇总
        double belowTarget = 0, belowMin = 0, aboveMax = 0;
        for (Period p : data.getPeriods()) {
            String invKey = prodId + "_" + spId + "_" + p.getIndex();
            belowTarget += solutionValue(model.getInvQtyUnderTargetVars(), invKey);
            belowMin += solutionValue(model.getMinInvQtyUnderVars(), invKey);
            aboveMax += solutionValue(model.getMaxInvQtyOverVars(), invKey);
        }
        s.setTotalBelowTarget(belowTarget);
        s.setTotalBelowMin(belowMin);
        s.setTotalAboveMax(aboveMax);
    }
    // ==================== KpiResult ====================

    private KpiResult buildKpis() {
        KpiResult kr = new KpiResult();
        KPIWeights w = data.getKpiWeights();

        double objValue = model.getSolver().objective().value();
        kr.setObjectiveValue(objValue);

        kr.addEntry("需求缺口(Fulfillment)",
                model.getTotalFulfillment().solutionValue(),
                w.getFulfillmentWeight(), false);
        kr.addEntry("批次偏差(LotSize)",
                model.getTotalLotSize().solutionValue(),
                w.getLotSizeWeight(), false);
        kr.addEntry("超库存(MaxInventory)",
                model.getTotalMaxInventoryLevel().solutionValue(),
                w.getMaxInventoryLevelWeight(), false);
        kr.addEntry("欠库存(MinInventory)",
                model.getTotalMinInventoryLevel().solutionValue(),
                w.getMinInventoryLevelWeight(), false);
        kr.addEntry("目标库存偏差(TargetInv)",
                model.getTotalTargetInvLevel().solutionValue(),
                w.getTargetInventoryLevelWeight(), false);
        kr.addEntry("产能超载(UnitCapacity)",
                model.getTotalUnitCapacity().solutionValue(),
                w.getUnitCapacityWeight(), false);
        kr.addEntry("供应目标偏差(SupplyTarget)",
                model.getTotalSupplyTarget().solutionValue(),
                w.getSupplyTargetWeight(), false);
        kr.addEntry("最小供应不足(MinSupply)",
                model.getTotalMinSupply().solutionValue(),
                w.getMinSupplyWeight(), false);
        kr.addEntry("最大供应超出(MaxSupply)",
                model.getTotalMaxSupply().solutionValue(),
                w.getMaxSupplyWeight(), false);
        kr.addEntry("销售优先级(SalesPriority)",
                model.getTotalSalesDemandPriority().solutionValue(),
                w.getSalesDemandPriorityWeight(), true);
        kr.addEntry("推迟惩罚(Postponement)",
                model.getTotalPostponementPenalty().solutionValue(),
                w.getPostponementPenaltyWeight(), false);
        kr.addEntry("过程最大量(ProcessMax)",
                model.getTotalProcessMaxQuantity().solutionValue(),
                w.getProcessMaxQuantityWeight(), false);

        return kr;
    }

    // ==================== SolverStatistics ====================

    private SolverStatistics buildStatistics() {
        SolverStatistics ss = new SolverStatistics();
        MPSolver solver = model.getSolver();

        ss.setStatus(solver.solve().toString());
        ss.setNumVariables(solver.numVariables());
        ss.setNumConstraints(solver.numConstraints());
        ss.setElapsedSeconds((System.currentTimeMillis() - startTimeMs) / 1000.0);
        ss.setObjectiveValue(solver.objective().value());
        ss.setIterations(solver.iterations());

        return ss;
    }

    // ==================== JSON 序列化 ====================

    private String toJson(OptimizationResult result) {
        JsonBuilder jb = new JsonBuilder();
        jb.obj();

        // metadata
        jb.key("metadata").obj()
                .key("timestamp").val(result.getTimestamp())
                .key("solver").val(result.getSolver())
                .key("version").val(result.getVersion())
                .endObj();

        // periodTasks
        jb.key("periodTasks").arr();
        for (PeriodTaskResult pt : result.getPeriodTasks()) {
            jb.obj();
            jb.key("operationId").val(pt.getOperationId());
            jb.key("operationName").val(pt.getOperationName());
            jb.key("unitId").val(pt.getUnitId());
            jb.key("periodIndex").val(pt.getPeriodIndex());
            jb.key("periodStartDate").val(pt.getPeriodStartDate());
            jb.key("productionQty").val(pt.getProductionQty());
            jb.key("capacityUsed").val(pt.getCapacityUsed());
            jb.key("capacityCoeff").val(pt.getCapacityCoeff());
            jb.key("lotSize").valOpt(pt.getLotSize());
            jb.key("lotSizeOver").val(pt.getLotSizeOver());
            jb.key("lotSizeUnder").val(pt.getLotSizeUnder());
            jb.key("outputs").arr();
            for (PeriodTaskResult.OutputInfo oi : pt.getOutputs()) {
                jb.obj()
                        .key("productId").val(oi.productId)
                        .key("spId").val(oi.spId)
                        .key("factor").val(oi.factor)
                        .endObj();
            }
            jb.endArr();
            jb.endObj();
        }
        jb.endArr();

        // salesDemands
        jb.key("salesDemands").arr();
        for (SalesDemandResult sr : result.getSalesDemands()) {
            jb.obj();
            jb.key("salesDemandId").val(sr.getSalesDemandId());
            jb.key("productId").val(sr.getProductId());
            jb.key("spId").val(sr.getSpId());
            jb.key("periodIndex").val(sr.getPeriodIndex());
            jb.key("periodStartDate").val(sr.getPeriodStartDate());
            jb.key("demandQty").val(sr.getDemandQty());
            jb.key("fulfilledQty").val(sr.getFulfilledQty());
            jb.key("unmetQty").val(sr.getUnmetQty());
            jb.key("demandSlack").val(sr.getDemandSlack());
            jb.key("fulfillmentRate").val(sr.getFulfillmentRate());
            jb.key("priority").val(sr.getPriority());
            jb.endObj();
        }
        jb.endArr();

        // pispips
        jb.key("pispips").arr();
        for (PispipResult pr : result.getPispips()) {
            jb.obj();
            jb.key("productId").val(pr.getProductId());
            jb.key("spId").val(pr.getSpId());
            jb.key("periodIndex").val(pr.getPeriodIndex());
            jb.key("periodStartDate").val(pr.getPeriodStartDate());

            jb.key("openingInventory").val(pr.getOpeningInventory());
            jb.key("endingInventory").val(pr.getEndingInventory());

            // 库存规格
            jb.key("targetInventoryLevel").valOpt(pr.getTargetInventoryLevel());
            jb.key("targetInventoryDays").valOpt(pr.getTargetInventoryDays());
            jb.key("belowTarget").val(pr.getBelowTarget());
            jb.key("minInventoryLevel").valOpt(pr.getMinInventoryLevel());
            jb.key("minInventoryDays").valOpt(pr.getMinInventoryDays());
            jb.key("belowMin").val(pr.getBelowMin());
            jb.key("maxInventoryLevel").valOpt(pr.getMaxInventoryLevel());
            jb.key("maxInventoryDays").valOpt(pr.getMaxInventoryDays());
            jb.key("aboveMax").val(pr.getAboveMax());

            // 流入
            jb.key("productionArrived").val(pr.getProductionArrived());
            jb.key("inTransitArrival").val(pr.getInTransitArrival());
            jb.key("totalInflow").val(pr.getTotalInflow());

            // 流出
            jb.key("salesDemandQty").val(pr.getSalesDemandQty());
            jb.key("salesFulfilledQty").val(pr.getSalesFulfilledQty());
            jb.key("dependentDemandQty").val(pr.getDependentDemandQty());
            jb.key("totalOutflow").val(pr.getTotalOutflow());

            // 需求满足
            jb.key("demandFulfillment").val(pr.getDemandFulfillment());
            jb.key("demandSlack").val(pr.getDemandSlack());

            // 生产明细
            jb.key("productionInProgress").val(pr.getProductionInProgress());
            jb.key("productionDetails").arr();
            for (PispipResult.ProductionDetail pd : pr.getProductionDetails()) {
                jb.obj();
                jb.key("operationId").val(pd.operationId);
                jb.key("operationName").val(pd.operationName);
                jb.key("unitId").val(pd.unitId);
                jb.key("quantity").val(pd.quantity);
                jb.key("leadTimeDays").val(pd.leadTimeDays);
                jb.key("bomConsumptions").arr();
                for (PispipResult.BomConsumption bc : pd.bomConsumptions) {
                    jb.obj()
                            .key("inputProductId").val(bc.inputProductId)
                            .key("inputSpId").val(bc.inputSpId)
                            .key("factor").val(bc.factor)
                            .key("consumedQty").val(bc.consumedQty)
                            .endObj();
                }
                jb.endArr();
                jb.endObj();
            }
            jb.endArr();

            jb.endObj();
        }
        jb.endArr();

        // kpis
        KpiResult kr = result.getKpis();
        jb.key("kpis").obj();
        jb.key("objectiveValue").val(kr.getObjectiveValue());
        jb.key("entries").arr();
        for (KpiResult.KpiEntry e : kr.getEntries()) {
            jb.obj()
                    .key("name").val(e.name)
                    .key("rawValue").val(e.rawValue)
                    .key("weight").val(e.weight)
                    .key("penalty").val(e.penalty)
                    .key("isBenefit").val(e.isBenefit)
                    .endObj();
        }
        jb.endArr();
        jb.endObj();

        // statistics
        SolverStatistics ss = result.getStatistics();
        jb.key("statistics").obj();
        jb.key("status").val(ss.getStatus());
        jb.key("numVariables").val(ss.getNumVariables());
        jb.key("numConstraints").val(ss.getNumConstraints());
        jb.key("elapsedSeconds").val(ss.getElapsedSeconds());
        jb.key("objectiveValue").val(ss.getObjectiveValue());
        jb.key("iterations").val(ss.getIterations());
        jb.endObj();

        jb.endObj();
        return jb.toString();
    }

    // ==================== 文件写入 ====================

    private String writeFile(String json) {
        try {
            Path dir = Paths.get(OUTPUT_DIR);
            if (!Files.exists(dir)) {
                Files.createDirectories(dir);
            }
            Path filePath = dir.resolve(OUTPUT_FILE);
            Files.write(filePath, json.getBytes(StandardCharsets.UTF_8));
            return filePath.toAbsolutePath().toString();
        } catch (IOException e) {
            System.err.println("[ERROR] JSON 文件写入失败: " + e.getMessage());
            return null;
        }
    }

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

    private static double solutionValue(Map<String, MPVariable> varMap, String key) {
        MPVariable var = varMap.get(key);
        if (var != null) {
            return var.solutionValue();
        }
        return 0.0;
    }
}