package com.aps.service.Algorithm;

import com.aps.entity.basic.Material;
import com.aps.entity.basic.Order;
import com.google.ortools.Loader;
import com.google.ortools.sat.CpModel;
import com.google.ortools.sat.CpSolver;
import com.google.ortools.sat.CpSolverStatus;
import com.google.ortools.sat.IntVar;
import com.google.ortools.sat.LinearExpr;
import com.google.ortools.sat.LinearExprBuilder;
import com.google.ortools.sat.Literal;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 基于 OR-Tools CP-SAT 的订单合并求解器
 * 将兼容订单建模为 Bin Packing 问题：
 * 每个物料组内，最小化合并组数，同时满足 maxProduction 约束
 *
 * 作者：佟礼
 * 时间：2026-05-27
 */
public class CpSatOrderMergeService {

    private static boolean nativeLibAvailable = false;

    static {
        try {
            Loader.loadNativeLibraries();
            nativeLibAvailable = true;
        } catch (Exception ignored) {
            System.err.println("[CP-SAT OrderMerge] 原生库加载失败: " + ignored.getMessage());
        }
    }

    public static boolean isAvailable() {
        return nativeLibAvailable;
    }

    private static final int DEFAULT_DUE_DATE_RANGE_DAYS = 7;
    private static final int MAX_SOLVE_TIME_SECONDS = 10;

    /**
     * 使用 CP-SAT 求解最优订单合并方案
     *
     * @param orders           原始订单列表
     * @param materials        物料列表（用于 maxProduction）
     * @param dueDateRangeDays 允许合并的交付日期最大相差天数
     * @return entry: key=合并后的订单列表, value=originalOrderId→mergedOrder 映射表
     */
    public AbstractMap.SimpleEntry<List<Order>, Map<String, Order>> mergeOrders(
            List<Order> orders, List<Material> materials, int dueDateRangeDays) {

        if (!nativeLibAvailable) {
            System.err.println("[CP-SAT OrderMerge] 原生库不可用，回退到贪心合并");
            return mergeOrdersGreedy(orders, materials, dueDateRangeDays);
        }

        if (orders == null || orders.size() <= 1) {
            return new AbstractMap.SimpleEntry<>(orders, new HashMap<>());
        }

        Map<String, Material> materialMap = buildMaterialMap(materials);
        Map<String, Order> mergeMap = new HashMap<>();
        List<Order> mergedOrders = new ArrayList<>();

        List<Order> remaining = new ArrayList<>(orders);
        while (!remaining.isEmpty()) {
            Order base = remaining.remove(0);
            List<Order> cluster = new ArrayList<>();
            cluster.add(base);

            Material material = materialMap.get(base.getMaterialId());
            BigDecimal maxProduction = (material != null && material.getMaxProduction() != null
                    && material.getMaxProduction().compareTo(BigDecimal.ZERO) > 0)
                    ? material.getMaxProduction() : null;

            Iterator<Order> it = remaining.iterator();
            while (it.hasNext()) {
                Order candidate = it.next();
                if (canMergeHard(base, candidate, dueDateRangeDays)) {
                    cluster.add(candidate);
                    it.remove();
                }
            }

            if (cluster.size() == 1) {
                mergedOrders.add(base);
            } else {
                List<List<Order>> groups;
                if (maxProduction != null) {
                    groups = solveBinPacking(cluster, maxProduction);
                } else {
                    groups = new ArrayList<>();
                    groups.add(cluster);
                }

                for (List<Order> group : groups) {
                    if (group.size() == 1) {
                        mergedOrders.add(group.get(0));
                    } else {
                        Order merged = createMergedOrder(group);
                        mergedOrders.add(merged);
                        for (Order child : group) {
                            mergeMap.put(child.getOrderId(), merged);
                        }
                    }
                }
            }
        }

        return new AbstractMap.SimpleEntry<>(mergedOrders, mergeMap);
    }

    /**
     * 硬约束兼容性检查：materialCode + routingId + serie + 时间重叠 + 交付日期范围
     */
    static boolean canMergeHard(Order a, Order b, int dueDateRangeDays) {
        if (a == null || b == null) return false;
        if (a.getRoutingId() == null || b.getRoutingId() == null) return false;
        if (!a.getRoutingId().equals(b.getRoutingId())) return false;
        if (a.getMaterialCode() == null || b.getMaterialCode() == null) return false;
        if (!a.getMaterialCode().equals(b.getMaterialCode())) return false;
        if (a.getSerie() == null || b.getSerie() == null) return false;
        if (!a.getSerie().equals(b.getSerie())) return false;
        if (a.isMerged() || b.isMerged()) return false;
        if (a.getStartDate() == null || a.getDueDate() == null
                || b.getStartDate() == null || b.getDueDate() == null) return false;

        boolean overlap = !a.getDueDate().isBefore(b.getStartDate())
                && !b.getDueDate().isBefore(a.getStartDate());
        if (!overlap) return false;

        long daysDiff = Math.abs(ChronoUnit.DAYS.between(
                a.getDueDate().toLocalDate(), b.getDueDate().toLocalDate()));
        return daysDiff <= dueDateRangeDays;
    }

    /**
     * CP-SAT Bin Packing：将 cluster 中的订单分配到最少的分组，每个分组总数量 ≤ maxProduction
     */
    private List<List<Order>> solveBinPacking(List<Order> cluster, BigDecimal maxProduction) {
        int n = cluster.size();
        long maxProdLong = maxProduction.longValue();

        CpModel model = new CpModel();

        IntVar[][] x = new IntVar[n][n];
        IntVar[] y = new IntVar[n];

        for (int g = 0; g < n; g++) {
            y[g] = model.newBoolVar("y_" + g);
        }

        for (int i = 0; i < n; i++) {
            for (int g = 0; g < n; g++) {
                x[i][g] = model.newBoolVar("x_" + i + "_" + g);
            }
        }

        for (int i = 0; i < n; i++) {
            LinearExprBuilder sumRow = LinearExpr.newBuilder();
            for (int g = 0; g < n; g++) {
                sumRow.add(x[i][g]);
            }
            model.addEquality(sumRow, 1);
        }

        long[] quantities = new long[n];
        for (int i = 0; i < n; i++) {
            quantities[i] = (long) cluster.get(i).getQuantity();
        }

        for (int g = 0; g < n; g++) {
            LinearExprBuilder groupSum = LinearExpr.newBuilder();
            for (int i = 0; i < n; i++) {
                groupSum.addTerm(x[i][g], quantities[i]);
            }
            model.addLessOrEqual(groupSum, maxProdLong);
        }

        for (int g = 0; g < n; g++) {
            for (int i = 0; i < n; i++) {
                model.addLessOrEqual(x[i][g], y[g]);
            }
        }

        for (int g = 1; g < n; g++) {
            model.addLessOrEqual(y[g], y[g - 1]);
        }

        LinearExprBuilder objExpr = LinearExpr.newBuilder();
        for (int g = 0; g < n; g++) {
            objExpr.add(y[g]);
        }
        model.minimize(objExpr);

        CpSolver solver = new CpSolver();
        solver.getParameters().setMaxTimeInSeconds(MAX_SOLVE_TIME_SECONDS);
        solver.getParameters().setLogSearchProgress(false);

        CpSolverStatus status = solver.solve(model);

        List<List<Order>> groups = new ArrayList<>();
        if (status == CpSolverStatus.OPTIMAL || status == CpSolverStatus.FEASIBLE) {
            for (int g = 0; g < n; g++) {
                if (solver.value(y[g])>0) {
                    List<Order> group = new ArrayList<>();
                    for (int i = 0; i < n; i++) {
                        if (solver.value(x[i][g])>0) {
                            group.add(cluster.get(i));
                        }
                    }
                    if (!group.isEmpty()) {
                        groups.add(group);
                    }
                }
            }
        } else {
            groups.add(cluster);
        }

        return groups;
    }

    private Map<String, Material> buildMaterialMap(List<Material> materials) {
        if (materials == null || materials.isEmpty()) {
            return Collections.emptyMap();
        }
        return materials.stream()
                .filter(m -> m.getId() != null)
                .collect(Collectors.toMap(Material::getId, m -> m, (e, r) -> e));
    }

    private Order createMergedOrder(List<Order> group) {
        Order merged = new Order();
        Order first = group.get(0);

        merged.setOrderId(first.getOrderId());
        merged.setOrderCode(first.getOrderCode() + "-MG");
        merged.setMaterialCode(first.getMaterialCode());
        merged.setMaterialName(first.getMaterialName());
        merged.setMaterialId(first.getMaterialId());
        merged.setSerie(first.getSerie());
        merged.setRoutingId(first.getRoutingId());
        merged.setRoutingCode(first.getRoutingCode());
        merged.setProductId(first.getProductId());
        merged.setMerged(true);

        double totalQty = group.stream().mapToDouble(Order::getQuantity).sum();
        merged.setQuantity(totalQty);
        merged.setSYQuantity(totalQty);

        LocalDateTime maxStartDate = group.stream()
                .map(Order::getStartDate)
                .filter(Objects::nonNull)
                .max(LocalDateTime::compareTo)
                .orElse(null);
        merged.setStartDate(maxStartDate);

        LocalDateTime minDueDate = group.stream()
                .map(Order::getDueDate)
                .filter(Objects::nonNull)
                .min(LocalDateTime::compareTo)
                .orElse(null);
        merged.setDueDate(minDueDate);

        int minPriority = group.stream()
                .mapToInt(Order::getPriority)
                .min()
                .orElse(first.getPriority());
        merged.setPriority(minPriority);
        merged.setActualPriority(minPriority);

        List<String> childIds = group.stream()
                .map(Order::getOrderId)
                .collect(Collectors.toList());
        merged.setMergedChildOrderIds(childIds);

        return merged;
    }

    /**
     * 贪心合并回退方案（当 OR-Tools 原生库不可用时使用）
     */
    private AbstractMap.SimpleEntry<List<Order>, Map<String, Order>> mergeOrdersGreedy(
            List<Order> orders, List<Material> materials, int dueDateRangeDays) {

        if (orders == null || orders.size() <= 1) {
            return new AbstractMap.SimpleEntry<>(orders, new HashMap<>());
        }

        Map<String, Material> materialMap = buildMaterialMap(materials);
        Map<String, Order> mergeMap = new HashMap<>();
        List<Order> mergedOrders = new ArrayList<>();
        List<Order> remaining = new ArrayList<>(orders);

        while (!remaining.isEmpty()) {
            Order base = remaining.remove(0);
            List<Order> mergeGroup = new ArrayList<>();
            mergeGroup.add(base);
            double groupQty = base.getQuantity();

            Material material = materialMap.get(base.getMaterialId());
            BigDecimal maxProduction = (material != null && material.getMaxProduction() != null
                    && material.getMaxProduction().compareTo(BigDecimal.ZERO) > 0)
                    ? material.getMaxProduction() : null;

            Iterator<Order> it = remaining.iterator();
            while (it.hasNext()) {
                Order candidate = it.next();
                if (!canMergeHard(base, candidate, dueDateRangeDays)) {
                    continue;
                }
                double newQty = groupQty + candidate.getQuantity();
                if (maxProduction != null && BigDecimal.valueOf(newQty).compareTo(maxProduction) > 0) {
                    continue;
                }
                mergeGroup.add(candidate);
                groupQty = newQty;
                it.remove();
            }

            if (mergeGroup.size() == 1) {
                mergedOrders.add(base);
            } else {
                Order merged = createMergedOrder(mergeGroup);
                mergedOrders.add(merged);
                for (Order child : mergeGroup) {
                    mergeMap.put(child.getOrderId(), merged);
                }
            }
        }

        return new AbstractMap.SimpleEntry<>(mergedOrders, mergeMap);
    }
}