package com.aps.macroplanner.constraint;

import com.google.ortools.linearsolver.MPConstraint;
import com.google.ortools.linearsolver.MPVariable;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;

import java.util.List;
import java.util.Map;

/**
 * 工序产量一致性约束 (RoutingConstraint)
 *
 * <p>对于工艺路线中的每对相邻工序，强制每周期产量相等：
 * <pre>
 *   PTQty[op_i][t] = PTQty[op_{i+1}][t]   ∀ 相邻工序对, ∀ 周期 t
 * </pre>
 *
 * <h3>设计动机</h3>
 * 在多工序工艺路线 (Routing) 中，物料按序流经各道工序。
 * 如果仅靠 BOM 约束 (消耗 ≤ 产出)，求解器可能让上游工序
 * 过量生产，导致中间 WIP 库存异常堆积，产生难以解释的结果。
 *
 * <p>此约束强制所有工序保持相同节拍，确保：
 * <ul>
 *   <li>工序间 WIP 不异常累积</li>
 *   <li>排产结果直观可解释</li>
 *   <li>各工序产量一致，反映真实工艺路线约束</li>
 * </ul>
 *
 * <h3>对应 Quintiq 模型</h3>
 * Quintiq 中 RoutingStep 之间通过 PISPNodeInRouting 流转，
 * 配合 LeadTime 自然形成工序间产量一致性。
 *
 * <h3>约束位置</h3>
 * 在 BOM 约束之后、产能约束之前构建，确保工序间产量一致
 * 后再施加产能限制。
 */
public class RoutingConstraint {

    /**
     * 构建工序产量一致性约束。
     *
     * @param model 模型容器 (提供 PTQty 变量)
     * @param data  测试数据 (提供 Routing 列表)
     */
    public static void build(MacroPlannerModel model, TestDataBuilder data) {
        List<Routing> routings = data.getRoutings();
        if (routings.isEmpty()) {
            return;  // 无工艺路线, 无需构建
        }

        Map<String, MPVariable> ptQtyVars = model.getPtQtyVars();
        List<Period> periods = data.getPeriods();

        for (Routing routing : routings) {
            List<Operation> ops = routing.getOperations();
            if (ops.size() < 2) {
                continue;  // 单步路由无需工序间约束
            }

            // 对每对相邻工序 (i, i+1), 强制每周期产量相等
            // Σ PTQty[opCurrent][allUnits][t] - Σ PTQty[opNext][allUnits][t] = 0
            for (int i = 0; i < ops.size() - 1; i++) {
                Operation opCurrent = ops.get(i);
                Operation opNext = ops.get(i + 1);

                for (Period p : periods) {
                    MPConstraint con = model.getSolver().makeConstraint(0.0, 0.0,
                            "RoutingThru_" + routing.getId() + "_" + i + "_" + p.getIndex());
                    for (UnitOperation uoCur : opCurrent.getUnitOperations()) {
                        MPVariable ptCur = ptQtyVars.get(opCurrent.ptQtyKey(uoCur, p.getIndex()));
                        if (ptCur != null) con.setCoefficient(ptCur, 1.0);
                    }
                    for (UnitOperation uoNext : opNext.getUnitOperations()) {
                        MPVariable ptNext = ptQtyVars.get(opNext.ptQtyKey(uoNext, p.getIndex()));
                        if (ptNext != null) con.setCoefficient(ptNext, -1.0);
                    }
                }
            }
        }
    }
}