package com.aps.controller;

import com.aps.common.util.ParamValidator;
import com.aps.common.util.R;
import com.aps.macroplanner.MacroPlannerOptimizer;
import com.aps.macroplanner.data.DataValidator;
import com.aps.macroplanner.data.MacroPlannerDataConverter;
import com.aps.macroplanner.data.TestDataBuilder;
import com.aps.macroplanner.output.ResultWriter;
import com.aps.macroplanner.output.dto.*;
import com.aps.service.MacroPlannerResultService;
import com.google.ortools.Loader;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.*;
import java.util.stream.Collectors;

/**
 * MP排产结果前端接口 — 提供排产优化运行、结果查询的完整 API。
 *
 * <h3>接口一览</h3>
 * <table>
 *   <tr><th>方法</th><th>路径</th><th>说明</th></tr>
 *   <tr><td>POST</td><td>/macroResult/run?sceneId=xxx</td><td>触发排产优化: 转换→验证→建模→分层求解→保存JSON</td></tr>
 *   <tr><td>GET </td><td>/macroResult/salesDemands?sceneId=xxx</td><td>订单级需求满足明细</td></tr>
 *   <tr><td>GET </td><td>/macroResult/supplyChain?sceneId=xxx</td><td>产品级库存流转与销售满足汇总</td></tr>
 *   <tr><td>GET </td><td>/macroResult/productSummary?sceneId=xxx</td><td>产品-库位库存周期明细(不聚合)</td></tr>
 *   <tr><td>GET </td><td>/macroResult/unitCapacity?sceneId=xxx</td><td>设备产能使用情况(含利用率)</td></tr>
 *   <tr><td>GET </td><td>/macroResult/productNetwork?sceneId=xxx</td><td>BOM生产网络</td></tr>
 *   <tr><td>GET </td><td>/macroResult/summary?sceneId=xxx</td><td>KPI与求解统计</td></tr>
 * </table>
 *
 * <h3>典型调用流程</h3>
 * <ol>
 *   <li>POST /run → 触发求解, 返回 status:"SUCCESS"</li>
 *   <li>GET  /salesDemands → 查看订单需求满足</li>
 *   <li>GET  /productSummary → 查看库存周期明细</li>
 *   <li>GET  /unitCapacity → 查看设备产能使用</li>
 *   <li>GET  /productNetwork → 查看BOM网络</li>
 *   <li>GET  /summary → 查看KPI与求解统计</li>
 * </ol>
 */
@RestController
@RequestMapping("/macroResult")
@Tag(name = "MP排产结果", description = "MP宏观排产: 运行优化 & 查询物料供应链、产能、生产网络、KPI 结果")
public class MacroPlannerResultController {

    @Autowired
    private MacroPlannerResultService macroPlannerResultService;

    @Autowired
    private MacroPlannerDataConverter macroPlannerDataConverter;

    /**
     * 加载结果文件, 校验 sceneId 和文件存在性。
     * @return OptimizationResult, 或 null (需要调用方返回 R.failed)
     */
    private OptimizationResult loadResult(String sceneId) {
        if (sceneId == null || sceneId.trim().isEmpty()) {
            return null;
        }
        return macroPlannerResultService.getResult(sceneId.trim());
    }

    // ==================== 拆分接口 ====================

    /**
     * 获取订单级需求满足情况: 每条 SalesDemand 的需求量/满足量/满足率。
     *
     * <p>每条记录对应一个 SalesDemand (productId + spId + periodIndex 唯一键),
     * 即一条订单级需求。可按 productId / periodIndex 可选过滤。</p>
     *
     * <h3>返回字段说明</h3>
     * <table>
     *   <tr><td>totalCount</td><td>订单级需求总条数</td></tr>
     *   <tr><td>totalDemand / totalFulfilled / totalUnmet</td><td>全部订单需求量/满足量/缺口合计</td></tr>
     *   <tr><td>overallFulfillmentRate</td><td>整体满足率 = totalFulfilled / totalDemand</td></tr>
     *   <tr><td>orders</td><td>SalesDemandResult[] 明细</td></tr>
     *   <tr><td>orders[].salesDemandId</td><td>键: productId_spId_periodIndex</td></tr>
     *   <tr><td>orders[].demandQty / fulfilledQty / unmetQty</td><td>需求量/满足量/缺口</td></tr>
     *   <tr><td>orders[].fulfillmentRate</td><td>该订单满足率 (0.0~1.0)</td></tr>
     *   <tr><td>orders[].priority</td><td>需求优先级</td></tr>
     * </table>
     */
    @GetMapping("/salesDemands")
    @Operation(summary = "订单需求满足",
            description = "订单级需求满足明细: 每条SalesDemand的需求量/满足量/缺口/满足率/优先级。"
                    + "支持按 productId / periodIndex 可选过滤。")
    public R<Map<String, Object>> getSalesDemands(
            @RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId,
            @RequestParam(value = "productId", required = false) @Parameter(description = "按产品ID过滤(可选)") String productId,
            @RequestParam(value = "periodIndex", required = false) @Parameter(description = "按周期索引过滤(可选)") Integer periodIndex) {
        OptimizationResult result = loadResult(sceneId);
        if (result == null) {
            return R.failed("未找到场景 " + sceneId + " 的排产结果文件");
        }

        List<SalesDemandResult> all = result.getSalesDemands();

        // 可选过滤
        List<SalesDemandResult> filtered = all.stream()
                .filter(s -> productId == null || productId.isEmpty() || productId.equals(s.getProductId()))
                .filter(s -> periodIndex == null || periodIndex == s.getPeriodIndex())
                .collect(Collectors.toList());

        // 汇总统计
        double totalDemand = filtered.stream().mapToDouble(SalesDemandResult::getDemandQty).sum();
        double totalFulfilled = filtered.stream().mapToDouble(SalesDemandResult::getFulfilledQty).sum();
        double totalUnmet = filtered.stream().mapToDouble(SalesDemandResult::getUnmetQty).sum();

        Map<String, Object> data = new LinkedHashMap<>();
        data.put("totalCount", filtered.size());
        data.put("totalDemand", totalDemand);
        data.put("totalFulfilled", totalFulfilled);
        data.put("totalUnmet", totalUnmet);
        data.put("overallFulfillmentRate", totalDemand > 0 ? totalFulfilled / totalDemand : 1.0);
        data.put("orders", filtered);
        return R.ok(data);
    }

    /**
     * 获取产品级物料供应链视图: 各产品-库位-周期的库存流转(期初/期末/流入/流出/偏差)与按产品聚合的销售满足。
     *
     * <h3>返回字段说明</h3>
     * <table>
     *   <tr><td>totalPispipRecords</td><td>库存点库存总记录数</td></tr>
     *   <tr><td>productCount / productIds</td><td>产品数量与ID列表</td></tr>
     *   <tr><td>periodRange</td><td>周期范围 [min, max]</td></tr>
     *   <tr><td>productSummaries</td><td>按产品+库位聚合的跨周期库存汇总</td></tr>
     *   <tr><td>productSummaries[].initialInventory</td><td>首周期期初库存</td></tr>
     *   <tr><td>productSummaries[].finalInventory</td><td>末周期期末库存</td></tr>
     *   <tr><td>productSummaries[].totalInflow</td><td>总流入(生产到货+在途到货)</td></tr>
     *   <tr><td>productSummaries[].totalOutflow</td><td>总流出(销售消耗+BOM依赖消耗)</td></tr>
     *   <tr><td>productSummaries[].belowTarget</td><td>低于目标库存累计值</td></tr>
     *   <tr><td>productSummaries[].belowMin / aboveMax</td><td>违反最小/最大库存惩罚</td></tr>
     *   <tr><td>salesDemandCount</td><td>销售需求总记录数</td></tr>
     *   <tr><td>salesSummaries</td><td>按产品聚合的销售需求满足</td></tr>
     *   <tr><td>salesSummaries[].totalDemand / totalFulfilled / totalUnmet</td><td>需求量/满足量/缺口</td></tr>
     *   <tr><td>salesSummaries[].avgFulfillmentRate</td><td>平均满足率 (0.0~1.0)</td></tr>
     *   <tr><td>pispipDetails</td><td>全量库存点库存明细，前端可按周期过滤</td></tr>
     * </table>
     */
    @GetMapping("/supplyChain")
    @Operation(summary = "物料供应链",
            description = "各产品-库位-周期的库存流转(期初/期末/流入/流出)与按产品聚合的销售需求满足情况。"
                    + "数据来源: PispipResult(库存点库存) + SalesDemandResult(销售需求)。"
                    + "注: 如需订单级需求满足明细, 请使用 /salesDemands 接口。")
    public R<Map<String, Object>> getSupplyChain(
            @RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId) {
        OptimizationResult result = loadResult(sceneId);
        if (result == null) {
            return R.failed("未找到场景 " + sceneId + " 的排产结果文件");
        }
        return R.ok(buildSupplyChain(result));
    }

    /**
     * 获取产品级库存明细: 按 (productId, spId) 分组, 保留每周期原始库存数据, 不跨周期聚合。
     *
     * <h3>返回字段说明</h3>
     * <table>
     *   <tr><td>totalCount</td><td>产品-库位组合总数</td></tr>
     *   <tr><td>totalPispipRecords</td><td>库存点库存原始记录数</td></tr>
     *   <tr><td>summaries</td><td>按产品+库位分组的库存数据</td></tr>
     *   <tr><td>summaries[].key</td><td>分组键: productId@spId</td></tr>
     *   <tr><td>summaries[].productId / spId</td><td>产品ID / 库存点ID</td></tr>
     *   <tr><td>summaries[].periodCount</td><td>该产品-库位的周期数</td></tr>
     *   <tr><td>summaries[].records</td><td>PispipResult[] 每周期原始明细</td></tr>
     *   <tr><td>records[].periodIndex</td><td>周期索引</td></tr>
     *   <tr><td>records[].openingInventory / endingInventory</td><td>期初/期末库存</td></tr>
     *   <tr><td>records[].totalInflow / totalOutflow</td><td>流入/流出</td></tr>
     *   <tr><td>records[].productionArrived</td><td>生产到货量</td></tr>
     *   <tr><td>records[].inTransitArrival</td><td>在途到货量</td></tr>
     *   <tr><td>records[].belowTarget / belowMin / aboveMax</td><td>库存偏差</td></tr>
     * </table>
     */
    @GetMapping("/productSummary")
    @Operation(summary = "产品库存明细",
            description = "按产品+库位分组, 保留每周期原始PispipResult明细, 不跨周期聚合。"
                    + "与 supplyChain 中的 productSummaries (跨周期求和) 不同, "
                    + "此接口返回每周期独立数据, 适合前端展开查看各周期详情。")
    public R<Map<String, Object>> getProductSummary(
            @RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId) {
        OptimizationResult result = loadResult(sceneId);
        if (result == null) {
            return R.failed("未找到场景 " + sceneId + " 的排产结果文件");
        }

        List<PispipResult> pispips = result.getPispips();

        // 按 productId@spId 分组，保留每周期明细，不跨周期聚合
        Map<String, List<PispipResult>> grouped = pispips != null
                ? pispips.stream().collect(Collectors.groupingBy(
                p -> p.getProductId() + "@" + p.getSpId(),
                LinkedHashMap::new,
                Collectors.toList()))
                : new LinkedHashMap<>();

        List<Map<String, Object>> summaries = new ArrayList<>();
        for (Map.Entry<String, List<PispipResult>> entry : grouped.entrySet()) {
            List<PispipResult> records = entry.getValue();
            Map<String, Object> s = new LinkedHashMap<>();
            s.put("key", entry.getKey());
            s.put("productId", records.get(0).getProductId());
            s.put("spId", records.get(0).getSpId());
            s.put("periodCount", records.size());
            s.put("records", records); // 每周期明细, 不聚合
            summaries.add(s);
        }

        Map<String, Object> data = new LinkedHashMap<>();
        data.put("totalCount", summaries.size());
        data.put("totalPispipRecords", pispips != null ? pispips.size() : 0);
        data.put("summaries", summaries);
        return R.ok(data);
    }

    /**
     * 获取unit产能使用情况: 按设备聚合各周期的产能占用和生产量。
     *
     * <h3>返回字段说明</h3>
     * <table>
     *   <tr><td>unitCount</td><td>设备总数</td></tr>
     *   <tr><td>totalTaskCount</td><td>生产任务总条数</td></tr>
     *   <tr><td>units</td><td>设备产能数组</td></tr>
     *   <tr><td>units[].unitId</td><td>设备ID (EQUIP_xxx)</td></tr>
     *   <tr><td>units[].operationCount / operationIds</td><td>工序数/工序ID列表</td></tr>
     *   <tr><td>units[].totalCapacityUsed</td><td>该设备全部周期产能占用合计(小时)</td></tr>
     *   <tr><td>units[].totalProduction</td><td>该设备全部周期产出合计</td></tr>
     *   <tr><td>units[].capacityByPeriod</td><td>每周期产能占用 Map{periodIndex: hours}</td></tr>
     *   <tr><td>units[].periodTasks</td><td>每个工序×周期的生产任务明细(PeriodTaskResult)</td></tr>
     * </table>
     */
    @GetMapping("/unitCapacity")
    @Operation(summary = "Unit产能",
            description = "按设备(unitId)聚合各周期的产能使用量(小时)与生产任务明细。"
                    + "数据来源: PeriodTaskResult(每工序-设备-周期的生产量和产能占比)。")
    public R<Map<String, Object>> getUnitCapacity(
            @RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId) {
        OptimizationResult result = loadResult(sceneId);
        if (result == null) {
            return R.failed("未找到场景 " + sceneId + " 的排产结果文件");
        }
        return R.ok(buildUnitCapacity(result));
    }

    /**
     * 获取产品生产网络: 成品向下递归展开完整 BOM 供应链树。
     *
     * <h3>返回字段说明 (ProductNetworkResult)</h3>
     * <table>
     *   <tr><td>finishedGoods</td><td>成品根节点列表 (SupplyChainNode[])</td></tr>
     *   <tr><td>finishedGoods[].productId / spId</td><td>产品ID / 库存点ID</td></tr>
     *   <tr><td>finishedGoods[].level</td><td>层级 (0=成品, 1=半成品, N=原材料)</td></tr>
     *   <tr><td>finishedGoods[].supplySources</td><td>供应来源 (PRODUCTION/INVENTORY/PURCHASE)</td></tr>
     *   <tr><td>finishedGoods[].consumers</td><td>被哪些上层产品消耗</td></tr>
     *   <tr><td>finishedGoods[].summary</td><td>{@link SupplySummary} 跨周期供应汇总</td></tr>
     *   <tr><td>finishedGoods[].children</td><td>BOM子节点 (childProductId, factor, node)</td></tr>
     *   <tr><td>allNodes</td><td>全部节点扁平索引 Map</td></tr>
     * </table>
     */
    @GetMapping("/productNetwork")
    @Operation(summary = "产品生产网络",
            description = "成品→半成品→原材料完整BOM供应链树。"
                    + "包含各层级的供应来源、消耗者、跨周期供应汇总。")
    public R<ProductNetworkResult> getProductNetwork(
            @RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId) {
        OptimizationResult result = loadResult(sceneId);
        if (result == null) {
            return R.failed("未找到场景 " + sceneId + " 的排产结果文件");
        }
        return R.ok(result.getProductNetwork());
    }

    /**
     * 获取求解摘要: KPI 目标值与各子项明细 + 求解器运行统计。
     *
     * <h3>返回字段说明</h3>
     * <table>
     *   <tr><td>timestamp / solver / version</td><td>运行时间戳 / 求解器名称 / 版本</td></tr>
     *   <tr><td>kpis.objectiveValue</td><td>总目标值(所有KPI加权惩罚之和)</td></tr>
     *   <tr><td>kpis.entries[].name / rawValue / weight / penalty</td><td>KPI名称/原始值/权重/加权惩罚</td></tr>
     *   <tr><td>statistics.status</td><td>OPTIMAL / FEASIBLE / INFEASIBLE</td></tr>
     *   <tr><td>statistics.numVariables / numConstraints</td><td>变量数 / 约束数</td></tr>
     *   <tr><td>statistics.elapsedSeconds</td><td>求解耗时(秒)</td></tr>
     *   <tr><td>statistics.objectiveValue / bestBound / gap</td><td>目标值/下界/最优性间隙(0.0=最优)</td></tr>
     *   <tr><td>statistics.iterations</td><td>迭代次数</td></tr>
     * </table>
     */
    @GetMapping("/summary")
    @Operation(summary = "KPI与求解统计",
            description = "KPI目标值与各子项明细(需求缺口/产能超载/批次偏差/库存偏差等) + "
                    + "求解器状态/变量规模/约束规模/耗时/gap。")
    public R<Map<String, Object>> getSummary(
            @RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId) {
        OptimizationResult result = loadResult(sceneId);
        if (result == null) {
            return R.failed("未找到场景 " + sceneId + " 的排产结果文件");
        }
        return R.ok(buildSummary(result));
    }

    /**
     * 运行MP排产优化: 数据库转换 → 验证 → 建模 → 分层求解 → 保存结果。
     *
     * <p>对应 {@code MacroPlannerDataConverterRunner} 的 Web API 版本。
     * 求解完成后结果自动保存到 {@code mp/result/optimization_result_{sceneId}.json}，
     * 前端可通过查询接口读取。</p>
     *
     * <h3>返回字段说明</h3>
     * <table>
     *   <tr><td>sceneId</td><td>场景ID</td></tr>
     *   <tr><td>dataSummary</td><td>数据规模: products/operations/routings/periods 等数量</td></tr>
     *   <tr><td>validation.valid</td><td>数据验证是否通过</td></tr>
     *   <tr><td>validation.errors / warnings</td><td>验证错误/警告详情</td></tr>
     *   <tr><td>status</td><td>SUCCESS / VALIDATION_FAILED / SAVE_FAILED / ERROR</td></tr>
     *   <tr><td>solveElapsedMs</td><td>求解耗时(毫秒)</td></tr>
     *   <tr><td>totalElapsedMs</td><td>总耗时(含转换+验证, 毫秒)</td></tr>
     *   <tr><td>resultFile</td><td>结果文件路径</td></tr>
     * </table>
     *
     * <h3>status 枚举</h3>
     * <ul>
     *   <li>SUCCESS — 求解完成, 结果已保存</li>
     *   <li>VALIDATION_FAILED — 数据验证未通过, 详见 validation.errors</li>
     *   <li>SAVE_FAILED — 求解完成但 JSON 写入失败</li>
     *   <li>ERROR — 运行时异常</li>
     * </ul>
     */
    @PostMapping("/run")
    @Operation(summary = "运行MP排产优化",
            description = "执行数据库转换→数据验证→MIP建模→分层求解→JSON结果保存的完整流程。"
                    + "耗时通常数秒到数分钟, 取决于数据规模。")
    public R<Map<String, Object>> runOptimization(
            @RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId) {
        if (sceneId == null || sceneId.trim().isEmpty()) {
            return R.failed("sceneId不能为空");
        }
        String sid = sceneId.trim();
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("sceneId", sid);
        long t0 = System.currentTimeMillis();

        try {
            // 1. 数据转换: 数据库工艺物料类 → macroplanner 实体
            TestDataBuilder data = macroPlannerDataConverter.convert(sid);
            result.put("dataSummary", buildDataSummary(data));

            // 2. 数据验证
            DataValidator validator = new DataValidator(data);
            boolean valid = validator.validate();
            result.put("validation", buildValidationResult(validator, valid));
            if (!valid) {
                result.put("status", "VALIDATION_FAILED");
                result.put("elapsedMs", System.currentTimeMillis() - t0);
                return R.failed(result, "数据验证失败, 请检查错误详情");
            }

            // 3. 加载 native libraries → 构建模型 → 分层求解
            Loader.loadNativeLibraries();
            long solveStart = System.currentTimeMillis();
            MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data);
            optimizer.buildModel();
            optimizer.solve();
            long solveEnd = System.currentTimeMillis();

            // 4. 保存结果到 JSON 文件
            ResultWriter writer = new ResultWriter(optimizer.getModel(), optimizer.getData(), solveStart);
            boolean saved = writer.saveResultToFile(sid);

            result.put("status", saved ? "SUCCESS" : "SAVE_FAILED");
            result.put("solveElapsedMs", solveEnd - solveStart);
            result.put("totalElapsedMs", System.currentTimeMillis() - t0);
            result.put("resultFile", "mp/result/optimization_result_" + sid + ".json");

            return R.ok(result);
        } catch (Exception e) {
            result.put("status", "ERROR");
            result.put("error", e.getMessage());
            result.put("elapsedMs", System.currentTimeMillis() - t0);
            return R.failed(result, "求解失败: " + e.getMessage());
        }
    }

    // ==================== 辅助构建方法 ====================

    /**
     * 构建供应链视图: 每个产品-库位的库存流转 + 销售满足情况。
     *
     * <p>数据来源:</p>
     * <ul>
     *   <li>{@link PispipResult} — 每周期期初/期末库存, 流入(生产到货+在途), 流出(销售+BOM消耗)</li>
     *   <li>{@link SalesDemandResult} — 销售需求量 vs 满足量, 满足率</li>
     * </ul>
     */
    private Map<String, Object> buildSupplyChain(OptimizationResult result) {
        Map<String, Object> sc = new LinkedHashMap<>();

        // 1.1 按产品汇总库存视图
        List<PispipResult> pispips = result.getPispips();
        sc.put("totalPispipRecords", pispips.size());

        // 提取产品列表
        Set<String> productIds = pispips.stream()
                .map(PispipResult::getProductId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        sc.put("productCount", productIds.size());
        sc.put("productIds", productIds);

        // 周期范围
        int minPeriod = pispips.stream().mapToInt(PispipResult::getPeriodIndex).min().orElse(0);
        int maxPeriod = pispips.stream().mapToInt(PispipResult::getPeriodIndex).max().orElse(0);
        sc.put("periodRange", new int[]{minPeriod, maxPeriod});

        // 1.2 按产品聚合库存汇总 (跨周期)
        List<Map<String, Object>> productSummaries = buildProductSummaries(pispips);
        sc.put("productSummaries", productSummaries);

        // 1.3 销售需求满足
        List<SalesDemandResult> sales = result.getSalesDemands();
        List<Map<String, Object>> salesSummaries = buildSalesSummaries(sales);
        sc.put("salesDemandCount", sales.size());
        sc.put("salesSummaries", salesSummaries);

        // 1.4 库存细节 (后端可返回全量, 前端可按需过滤)
        sc.put("pispipDetails", pispips);

        return sc;
    }

    /**
     * 按产品聚合跨周期库存汇总。
     */
    private List<Map<String, Object>> buildProductSummaries(List<PispipResult> pispips) {
        // 按 productId@spId 分组
        Map<String, List<PispipResult>> grouped = pispips.stream()
                .collect(Collectors.groupingBy(p -> p.getProductId() + "@" + p.getSpId()));

        List<Map<String, Object>> summaries = new ArrayList<>();
        for (Map.Entry<String, List<PispipResult>> entry : grouped.entrySet()) {
            List<PispipResult> records = entry.getValue();
            PispipResult first = records.get(0);

            double totalInflow = records.stream().mapToDouble(PispipResult::getTotalInflow).sum();
            double totalOutflow = records.stream().mapToDouble(PispipResult::getTotalOutflow).sum();
            double totalProduction = records.stream().mapToDouble(PispipResult::getProductionArrived).sum();
            double totalInTransit = records.stream().mapToDouble(PispipResult::getInTransitArrival).sum();
            double initialInv = records.stream()
                    .filter(r -> r.getPeriodIndex() == 0)
                    .mapToDouble(PispipResult::getOpeningInventory)
                    .findFirst().orElse(0);
            double finalInv = records.stream()
                    .filter(r -> r.getPeriodIndex() == records.size() - 1)
                    .mapToDouble(PispipResult::getEndingInventory)
                    .findFirst().orElse(0);
            double belowTarget = records.stream().mapToDouble(PispipResult::getBelowTarget).sum();
            double belowMin = records.stream().mapToDouble(PispipResult::getBelowMin).sum();
            double aboveMax = records.stream().mapToDouble(PispipResult::getAboveMax).sum();

            Map<String, Object> summary = new LinkedHashMap<>();
            summary.put("productId", first.getProductId());
            summary.put("spId", first.getSpId());
            summary.put("periodCount", records.size());
            summary.put("initialInventory", initialInv);
            summary.put("finalInventory", finalInv);
            summary.put("totalInflow", totalInflow);
            summary.put("totalOutflow", totalOutflow);
            summary.put("totalProduction", totalProduction);
            summary.put("totalInTransit", totalInTransit);
            summary.put("belowTarget", belowTarget);
            summary.put("belowMin", belowMin);
            summary.put("aboveMax", aboveMax);
            summaries.add(summary);
        }
        return summaries;
    }

    /**
     * 销售需求汇总: 按产品聚合需求量/满足量/满足率。
     */
    private List<Map<String, Object>> buildSalesSummaries(List<SalesDemandResult> sales) {
        // 按 productId 分组
        Map<String, List<SalesDemandResult>> grouped = sales.stream()
                .collect(Collectors.groupingBy(SalesDemandResult::getProductId));

        List<Map<String, Object>> summaries = new ArrayList<>();
        for (Map.Entry<String, List<SalesDemandResult>> entry : grouped.entrySet()) {
            List<SalesDemandResult> items = entry.getValue();
            double totalDemand = items.stream().mapToDouble(SalesDemandResult::getDemandQty).sum();
            double totalFulfilled = items.stream().mapToDouble(SalesDemandResult::getFulfilledQty).sum();
            double totalUnmet = items.stream().mapToDouble(SalesDemandResult::getUnmetQty).sum();
            double avgFulfillmentRate = items.stream()
                    .mapToDouble(SalesDemandResult::getFulfillmentRate)
                    .average().orElse(0);

            Map<String, Object> summary = new LinkedHashMap<>();
            summary.put("productId", entry.getKey());
            summary.put("demandCount", items.size());
            summary.put("totalDemand", totalDemand);
            summary.put("totalFulfilled", totalFulfilled);
            summary.put("totalUnmet", totalUnmet);
            summary.put("avgFulfillmentRate", avgFulfillmentRate);
            summaries.add(summary);
        }
        return summaries;
    }

    // ==================== 2. unit产能 ====================

    /**
     * 构建unit产能视图: 从 OptimisationResult.unitCapacities 读取,
     * 由 ResultWriter.buildUnitCapacities() 在求解后预计算。
     *按产品聚合库存汇总
     * <p>数据来源: {@link UnitCapacityResult} — 按设备聚合, 含 maxCapacity / usedCapacity / 利用率。</p>
     */
    private Map<String, Object> buildUnitCapacity(OptimizationResult result) {
        Map<String, Object> uc = new LinkedHashMap<>();

        List<UnitCapacityResult> unitCapacities = result.getUnitCapacities();
        if (unitCapacities == null || unitCapacities.isEmpty()) {
            uc.put("unitCount", 0);
            uc.put("totalTaskCount", 0);
            uc.put("units", Collections.emptyList());
            return uc;
        }

        List<Map<String, Object>> unitList = new ArrayList<>();
        int totalTasks = 0;
        for (UnitCapacityResult ucr : unitCapacities) {
            Map<String, Object> ud = new LinkedHashMap<>();
            ud.put("unitId", ucr.getUnitId());
            ud.put("operationIds", ucr.getOperationIds());
            ud.put("totalMaxCapacity", ucr.getTotalMaxCapacity());
            ud.put("totalCapacityUsed", ucr.getTotalCapacityUsed());
            ud.put("totalUtilizationRate", ucr.getTotalUtilizationRate());
            ud.put("totalProduction", ucr.getTotalProduction());

            // 周期明细 (含利用率)
            List<Map<String, Object>> pdList = new ArrayList<>();
            for (UnitCapacityResult.UnitPeriodDetail pd : ucr.getPeriodDetails()) {
                Map<String, Object> pdm = new LinkedHashMap<>();
                pdm.put("periodIndex", pd.periodIndex);
                pdm.put("periodStartDate", pd.periodStartDate);
                pdm.put("maxCapacity", pd.maxCapacity);
                pdm.put("capacityUsed", pd.capacityUsed);
                pdm.put("utilizationRate", pd.utilization);
                pdm.put("production", pd.production);
                pdm.put("periodTasks", pd.tasks);
                totalTasks += pd.tasks.size();
                pdList.add(pdm);
            }
            ud.put("periodDetails", pdList);

            unitList.add(ud);
        }

        uc.put("unitCount", unitCapacities.size());
        uc.put("totalTaskCount", totalTasks);
        uc.put("units", unitList);
        return uc;
    }

    // ==================== 3. 汇总: KPI + 求解器统计 ====================

    /**
     * 构建摘要: KPI 汇总 + 求解器运行统计。
     */
    private Map<String, Object> buildSummary(OptimizationResult result) {
        Map<String, Object> summary = new LinkedHashMap<>();

        // 元数据
        summary.put("timestamp", result.getTimestamp());
        summary.put("solver", result.getSolver());
        summary.put("version", result.getVersion());

        // KPI
        KpiResult kpis = result.getKpis();
        if (kpis != null) {
            Map<String, Object> kpiData = new LinkedHashMap<>();
            kpiData.put("objectiveValue", kpis.getObjectiveValue());
            List<Map<String, String>> kpiList = new ArrayList<>();
            for (KpiResult.KpiEntry e : kpis.getEntries()) {
                Map<String, String> entryMap = new LinkedHashMap<>();
                entryMap.put("name", e.name);
                entryMap.put("rawValue", String.format("%.2f", e.rawValue));
                entryMap.put("weight", String.format("%.1f", e.weight));
                entryMap.put("penalty", String.format("%.2f", e.penalty));
                kpiList.add(entryMap);
            }
            kpiData.put("entries", kpiList);
            summary.put("kpis", kpiData);
        }

        // 求解器统计
        SolverStatistics stats = result.getStatistics();
        if (stats != null) {
            Map<String, Object> statData = new LinkedHashMap<>();
            statData.put("status", stats.getStatus());
            statData.put("numVariables", stats.getNumVariables());
            statData.put("numConstraints", stats.getNumConstraints());
            statData.put("elapsedSeconds", stats.getElapsedSeconds());
            statData.put("objectiveValue", stats.getObjectiveValue());
            statData.put("bestBound", stats.getBestBound());
            statData.put("gap", stats.getGap());
            statData.put("iterations", stats.getIterations());
            summary.put("statistics", statData);
        }

        return summary;
    }

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

    /**
     * 构建数据转换后的规模摘要。
     */
    private Map<String, Object> buildDataSummary(TestDataBuilder data) {
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("products", data.getProducts().size());
        summary.put("stockingPoints", data.getStockingPoints().size());
        summary.put("operations", data.getOperations().size());
        summary.put("routings", data.getRoutings().size());
        summary.put("operationInputs", data.getOperationInputs().size());
        summary.put("initialInventories", data.getInitialInventories().size());
        summary.put("inTransitSupplies", data.getInTransitSupplies().size());
        summary.put("salesDemands", data.getSalesDemands().size());
        summary.put("periods", data.getPeriods().size());
        summary.put("unitPeriods", data.getUnitPeriods().size());
        return summary;
    }

    /**
     * 构建数据验证结果。
     */
    private Map<String, Object> buildValidationResult(DataValidator validator, boolean valid) {
        Map<String, Object> vr = new LinkedHashMap<>();
        vr.put("valid", valid);
        if (validator.hasErrors()) {
            vr.put("errorCount", validator.getErrors().size());
            vr.put("errors", validator.getErrors());
        }
        if (validator.hasWarnings()) {
            vr.put("warningCount", validator.getWarnings().size());
            vr.put("warnings", validator.getWarnings());
        }
        return vr;
    }
}
