package com.aps.macroplanner.constraint;

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

import java.util.Map;

/**
 * 需求缺口联动约束 (DemandSlack Linkage)
 *
 * <p>强制将 SalesDemandQty 与 DemandSlack 关联起来，确保未满足的销售需求
 * 必须通过 DemandSlack 变量暴露，从而触发目标函数中的惩罚。</p>
 *
 * <h3>问题背景</h3>
 * <p>在没有此约束时，求解器可以通过降低 SalesDemandQty 来"规避"需求缺口，
 * 而不是通过 DemandSlack 来"暴露"需求缺口。因为 DemandSlack 在物料平衡约束中
 * 位于流入侧，SalesDemandQty 在流出侧，求解器可以同时降低两者来平衡方程，
 * 从而避免触发 DemandSlack 的高额惩罚（权重 100）。</p>
 *
 * <h3>数学公式</h3>
 * <pre>
 * 对于每个 PISPIP (Product × StockingPoint × Period):
 *   Σ SalesDemandQty + DemandSlack >= Σ DemandQuantity
 *
 * 即: 实际销售量 + 需求缺口 >= 原始需求总量
 * 等价于: DemandSlack >= Σ(DemandQuantity - SalesDemandQty)
 * </pre>
 *
 * <h3>效果</h3>
 * <p>添加此约束后，当 SalesDemandQty < DemandQuantity 时，
 * DemandSlack 必须填补缺口，从而在目标函数中产生惩罚。
 * 求解器被激励去增加生产来满足需求，而非简单地降低销售量。</p>
 *
 * @see BalanceConstraint 物料平衡约束（DemandSlack 在流入侧）
 * @see DemandVariableBuilder 需求变量定义（SalesDemandQty, DemandSlack）
 */
public class DemandSlackLinkageConstraint {

    /**
     * 构建需求缺口联动约束。
     *
     * <p>对每个 PISPIP 创建一条约束:
     * Σ SalesDemandQty + DemandSlack >= Σ DemandQuantity</p>
     *
     * @param model 模型容器（提供 SalesDemandQty 和 DemandSlack 变量）
     * @param data  测试数据（提供 SalesDemand 列表）
     */
    public static void build(MacroPlannerModel model, TestDataBuilder data) {
        Map<String, MPVariable> sdVars = model.getSalesDemandQtyVars();
        Map<String, MPVariable> slackVars = model.getDemandSlackVars();

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

                for (Period p : data.getPeriods()) {
                    String pispipKey = prod.getId() + "_" + sp.getId() + "_" + p.getIndex();

                    double totalDemandQty = 0.0;
                    boolean hasDemand = false;

                    // 汇总该 PISPIP 的所有销售需求
                    for (SalesDemand sd : data.getSalesDemandsFor(prod, sp, p)) {
                        totalDemandQty += sd.getQuantity();
                        hasDemand = true;
                    }

                    // 只对存在销售需求的 PISPIP 创建约束
                    if (!hasDemand) continue;

                    // 约束: Σ SalesDemandQty + DemandSlack >= totalDemandQty
                    MPConstraint c = model.getSolver().makeConstraint(
                            totalDemandQty, MPSolver.infinity(),
                            "DSLink_" + pispipKey);

                    // + Σ SalesDemandQty
                    for (SalesDemand sd : data.getSalesDemandsFor(prod, sp, p)) {
                        MPVariable sdVar = sdVars.get(sd.getKey());
                        if (sdVar != null) c.setCoefficient(sdVar, 1.0);
                    }

                    // + DemandSlack
                    MPVariable slackVar = slackVars.get(pispipKey);
                    if (slackVar != null) c.setCoefficient(slackVar, 1.0);
                }
            }
        }
    }
}