package com.aps.service.Algorithm;

import com.aps.common.util.FileHelper;
import com.aps.common.util.SpringContextUtil;
import com.aps.entity.Algorithm.GAScheduleResult;
import com.aps.entity.Algorithm.ScheduleResultDetail;
import com.aps.entity.basic.Entry;
import com.aps.entity.basic.GlobalParam;
import com.aps.entity.basic.Machine;
import com.aps.entity.basic.SegmentType;
import com.aps.entity.basic.TimeSegment;
import com.aps.service.DiscreteParameterDurationService;
import com.aps.service.DiscreteParameterMatrixService;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;

/**
 * 倒排换型辅助类：集中处理 JIT 倒排时绿色/蓝色/黄色之间的换型计算、窗口校验和黄色旧换型替换。
 */
class BackwardChangeoverHelper {
    private static final String DEBUG_BACKWARD_SETUP_TIME_SECONDS_PROPERTY = "aps.debug.backwardSetupTimeSeconds";
    private static final String DEBUG_BACKWARD_CHANGEOVER_VERBOSE_PROPERTY = "aps.debug.backwardChangeoverVerbose";

    private final GlobalParam globalParam;
    private final LocalDateTime baseTime;
    private final MachineCalculator machineCalculator;
    private final boolean verboseLogging;

    BackwardChangeoverHelper(GlobalParam globalParam, LocalDateTime baseTime, MachineCalculator machineCalculator) {
        this.globalParam = globalParam;
        this.baseTime = baseTime;
        this.machineCalculator = machineCalculator;
        this.verboseLogging = Boolean.parseBoolean(System.getProperty(DEBUG_BACKWARD_CHANGEOVER_VERBOSE_PROPERTY, "false"));
    }

    boolean isVerboseLoggingEnabled() {
        return verboseLogging;
    }

    void writeVerboseLog(String message) {
        if (verboseLogging) {
            FileHelper.writeLogFile(message);
        }
    }

    private String convertTime(int second) {
        return baseTime.plusSeconds(second).format(DateTimeFormatter.ofPattern("YYYY-MM-dd HH:mm"));
    }

    private boolean isBackwardInsufficientTimeException(RuntimeException ex) {
        return ex instanceof IllegalStateException
                && ex.getMessage() != null
                && ex.getMessage().contains("\u8bbe\u5907\u53ef\u7528\u65f6\u95f4\u4e0d\u8db3");
    }
    // ==================== 倒排换型辅助方法开始 ====================

    GAScheduleResult getNextMachineTaskForBackward(CopyOnWriteArrayList<GAScheduleResult> machineTasks, int processEndTime) {
        if (machineTasks == null || machineTasks.isEmpty()) {
            return null;
        }
        return machineTasks.stream()
                .filter(t -> getProcessStartTime(t) >= processEndTime)
                .min(Comparator.comparingInt(this::getProcessStartTime))
                .orElse(null);
    }

    GAScheduleResult getPrevMachineTaskForBackward(CopyOnWriteArrayList<GAScheduleResult> machineTasks, int processStartTime) {
        if (machineTasks == null || machineTasks.isEmpty()) {
            return null;
        }
        return machineTasks.stream()
                .filter(t -> getProcessEndTime(t) <= processStartTime)
                .max(Comparator.comparingInt(this::getProcessEndTime))
                .orElse(null);
    }

    int getProcessStartTime(GAScheduleResult result) {
        if (result == null) {
            return 0;
        }
        return getProcessStartTime(result.getGeneDetails(), result.getStartTime());
    }

    int getProcessEndTime(GAScheduleResult result) {
        if (result == null) {
            return 0;
        }
        return getProcessEndTime(result.getGeneDetails(), result.getEndTime());
    }

    int getProcessStartTime(List<ScheduleResultDetail> details, int fallback) {
        if (details == null || details.isEmpty()) {
            return fallback;
        }
        return details.stream()
                .filter(detail -> !detail.isSetup())
                .mapToInt(ScheduleResultDetail::getStartTime)
                .min()
                .orElse(fallback);
    }

    int getProcessEndTime(List<ScheduleResultDetail> details, int fallback) {
        if (details == null || details.isEmpty()) {
            return fallback;
        }
        return details.stream()
                .filter(detail -> !detail.isSetup())
                .mapToInt(ScheduleResultDetail::getEndTime)
                .max()
                .orElse(fallback);
    }

    JitBackwardScheduleResult scheduleJitBackwardWithChangeover(Machine machine,
                                                                Entry operation,
                                                                int latestEndTime,
                                                                int processingTimeTotal,
                                                                double processingTime,
                                                                CopyOnWriteArrayList<GAScheduleResult> machineTasks,
                                                                List<Entry> allOperations) {
        // 倒排换型主流程：
        // 1. 先不带换型试排当前工序，定位它后面的相邻任务；
        // 2. 计算“当前 -> 后任务”的后置换型，把当前加工结束时间往前压；
        // 3. 再计算“前任务 -> 当前”的前置换型，校验整块窗口能否插入；
        // 4. 如果倒排已经压过基准时间或设备可用时间不足，交给外层整单正排兜底。
        boolean backwardScheduled = false;
        boolean forwardFallbackRequired = false;
        int backwardLatestEndTime = latestEndTime;
        int backwardAttemptLimit = Math.max(1, (machineTasks == null ? 0 : machineTasks.size()) + 1);
        int setupTime = 0;
        CopyOnWriteArrayList<ScheduleResultDetail> geneDetails = new CopyOnWriteArrayList<>();
        int baselineTime = 0;

        for (int backwardAttempt = 0; backwardAttempt < backwardAttemptLimit; backwardAttempt++) {
            CopyOnWriteArrayList<TimeSegment> emptySetupSegments = new CopyOnWriteArrayList<>();
            Map<Integer,Object> previewResult;
            try {
                // 试排只用于拿当前加工段位置；试排会临时占用日历，拿到位置后马上释放。
                previewResult = machineCalculator.CreateScheduleResultBackward(machine, operation,
                        processingTimeTotal, backwardLatestEndTime, processingTime, operation.getQuantity(),
                        0, backwardLatestEndTime, backwardLatestEndTime, emptySetupSegments, false);
            } catch (RuntimeException ex) {
                if (isBackwardInsufficientTimeException(ex)) {
                    writeVerboseLog("[BackwardInsertWindow] switch to forward because backward preview has insufficient machine time"
                            + ", operationId=" + operation.getId()
                            + ", groupId=" + operation.getGroupId()
                            + ", machineId=" + machine.getId()
                            + ", attempt=" + backwardAttempt
                            + ", backwardLatestEndTime=" + backwardLatestEndTime
                            + ", backwardLatestEnd=" + convertTime(backwardLatestEndTime)
                            + ", processingSeconds=" + processingTimeTotal
                            + ", reason=" + ex.getMessage());
                    forwardFallbackRequired = true;
                    break;
                }
                throw ex;
            }
            CopyOnWriteArrayList<ScheduleResultDetail> previewGeneDetails =
                    (CopyOnWriteArrayList<ScheduleResultDetail>) previewResult.get(2);
            int previewProcessStartTime = previewGeneDetails.stream()
                    .mapToInt(ScheduleResultDetail::getStartTime)
                    .min()
                    .orElse(backwardLatestEndTime);
            int previewProcessEndTime = previewGeneDetails.stream()
                    .mapToInt(ScheduleResultDetail::getEndTime)
                    .max()
                    .orElse(backwardLatestEndTime);
            machineCalculator.AddMachineAvailable(machine, previewGeneDetails);

            GAScheduleResult nextGeneOnMachine = getNextMachineTaskForBackward(machineTasks, previewProcessEndTime);
            // 黄色任务旧换型可能占着“绿色 -> 黄色”的时间，先释放出来再判断蓝色能不能插进去。
            CopyOnWriteArrayList<ScheduleResultDetail> releasedNextSetupDetails =
                    releaseNextTaskSetupForBackward(nextGeneOnMachine, machine);
            int nextSetupLatestEndTime = nextGeneOnMachine == null
                    ? previewProcessEndTime
                    : Math.min(backwardLatestEndTime, getProcessStartTime(nextGeneOnMachine));

            Map<Integer,Object> nextSetupResult = calculateSetupTimeBackward(nextGeneOnMachine, operation, machine,
                    nextSetupLatestEndTime, processingTimeTotal, globalParam.is_smoothChangeOverInWeek(),
                    allOperations, machineTasks);
            // 后置换型属于后任务：它决定当前工序最晚只能加工到什么时候。
            int nextSetupTime = (int) nextSetupResult.get(1);
            int latestProcessEndTime = (int) nextSetupResult.get(4);
            int attemptProcessingTimeTotal = (int) nextSetupResult.get(5);
            boolean backwardChangeoverFeasible = true;
            if (Boolean.TRUE.equals(nextSetupResult.get(8))) {
                backwardChangeoverFeasible = false;
                writeVerboseLog("[BackwardInsertWindow] 后置换型可用时间不足，当前插入位置不可行"
                        + ", operationId=" + operation.getId()
                        + ", groupId=" + operation.getGroupId()
                        + ", machineId=" + machine.getId()
                        + ", setupTime=" + nextSetupTime
                        + ", availableSeconds=" + nextSetupResult.get(7));
            }

            CopyOnWriteArrayList<TimeSegment> noSetupSegments = new CopyOnWriteArrayList<>();
            Map<Integer,Object> candidateResult;
            try {
                // 按“扣掉后置换型后的最晚结束时间”重新倒排当前加工段。
                candidateResult = machineCalculator.CreateScheduleResultBackward(machine, operation,
                        attemptProcessingTimeTotal, latestProcessEndTime, processingTime, operation.getQuantity(),
                        0, latestProcessEndTime, latestProcessEndTime, noSetupSegments, false);
            } catch (RuntimeException ex) {
                if (isBackwardInsufficientTimeException(ex)) {
                    restoreNextTaskSetupForBackward(nextGeneOnMachine, machine, releasedNextSetupDetails);
                    writeVerboseLog("[BackwardInsertWindow] switch to forward because backward candidate has insufficient machine time"
                            + ", operationId=" + operation.getId()
                            + ", groupId=" + operation.getGroupId()
                            + ", machineId=" + machine.getId()
                            + ", attempt=" + backwardAttempt
                            + ", latestProcessEndTime=" + latestProcessEndTime
                            + ", latestProcessEnd=" + convertTime(latestProcessEndTime)
                            + ", processingSeconds=" + attemptProcessingTimeTotal
                            + ", reason=" + ex.getMessage());
                    forwardFallbackRequired = true;
                    break;
                }
                throw ex;
            }
            CopyOnWriteArrayList<ScheduleResultDetail> candidateGeneDetails =
                    (CopyOnWriteArrayList<ScheduleResultDetail>) candidateResult.get(2);

            int finalProcessStartTime = getProcessStartTime(candidateGeneDetails, previewProcessStartTime);
            int finalProcessEndTime = getProcessEndTime(candidateGeneDetails, previewProcessEndTime);
            GAScheduleResult prevGeneOnMachine = getPrevMachineTaskForBackward(machineTasks, finalProcessStartTime);
            if (finalProcessStartTime < baselineTime) {
                machineCalculator.AddMachineAvailable(machine, candidateGeneDetails);
                restoreNextTaskSetupForBackward(nextGeneOnMachine, machine, releasedNextSetupDetails);
                writeVerboseLog("[BackwardInsertWindow] switch to forward because backward start is before baseline"
                        + ", operationId=" + operation.getId()
                        + ", groupId=" + operation.getGroupId()
                        + ", machineId=" + machine.getId()
                        + ", attempt=" + backwardAttempt
                        + ", baselineTime=" + baselineTime
                        + ", baseline=" + convertTime(baselineTime)
                        + ", finalProcessStart=" + finalProcessStartTime
                        + ", finalProcessStartTime=" + convertTime(finalProcessStartTime));
                forwardFallbackRequired = true;
                break;
            }
            writeVerboseLog("[BackwardChangeover] check previous adjacent task, operationId=" + operation.getId()
                    + ", groupId=" + operation.getGroupId()
                    + ", execId=" + operation.getExecId()
                    + ", machineId=" + machine.getId()
                    + ", attempt=" + backwardAttempt
                    + ", finalProcessStart=" + finalProcessStartTime
                    + ", finalProcessStartTime=" + convertTime(finalProcessStartTime)
                    + ", prevTask=" + describeScheduleResult(prevGeneOnMachine));

            Map<Integer,Object> previousSetupResult = calculateSetupTimeBeforeBackward(prevGeneOnMachine, operation, machine,
                    finalProcessStartTime, attemptProcessingTimeTotal, globalParam.is_smoothChangeOverInWeek(),
                    allOperations, machineTasks);
            // 前置换型属于当前任务：要跟当前加工段一起作为一个 block 校验是否落在前后任务之间。
            int previousSetupTime = (int) previousSetupResult.get(1);
            int blockStartTime = finalProcessStartTime;
            if (previousSetupTime > 0) {
                blockStartTime = Math.min(blockStartTime, (int) previousSetupResult.get(2));
            }
            if (blockStartTime < baselineTime) {
                machineCalculator.AddMachineAvailable(machine, candidateGeneDetails);
                restoreNextTaskSetupForBackward(nextGeneOnMachine, machine, releasedNextSetupDetails);
                writeVerboseLog("[BackwardInsertWindow] switch to forward because backward block is before baseline"
                        + ", operationId=" + operation.getId()
                        + ", groupId=" + operation.getGroupId()
                        + ", machineId=" + machine.getId()
                        + ", attempt=" + backwardAttempt
                        + ", baselineTime=" + baselineTime
                        + ", baseline=" + convertTime(baselineTime)
                        + ", blockStart=" + blockStartTime
                        + ", blockStartTime=" + convertTime(blockStartTime));
                forwardFallbackRequired = true;
                break;
            }
            if (Boolean.TRUE.equals(previousSetupResult.get(8))) {
                backwardChangeoverFeasible = false;
                writeVerboseLog("[BackwardInsertWindow] 前置换型可用时间不足，当前插入位置不可行"
                        + ", operationId=" + operation.getId()
                        + ", groupId=" + operation.getGroupId()
                        + ", machineId=" + machine.getId()
                        + ", setupTime=" + previousSetupTime
                        + ", availableSeconds=" + previousSetupResult.get(7));
            }

            boolean canInsertBackwardWindow = backwardChangeoverFeasible
                    && validateBackwardInsertWindow(prevGeneOnMachine, nextGeneOnMachine,
                    operation, machine, finalProcessStartTime, finalProcessEndTime,
                    previousSetupResult, nextSetupResult);

            if (canInsertBackwardWindow) {
                // 插入成功后，当前任务保存前置换型；后任务保存新的后置换型。
                setupTime = previousSetupTime;
                operation.setChangeLineTime(setupTime);
                geneDetails = candidateGeneDetails;

                if (previousSetupTime > 0) {
                    CopyOnWriteArrayList<TimeSegment> previousSetupSegments =
                            (CopyOnWriteArrayList<TimeSegment>) previousSetupResult.get(6);
                    CopyOnWriteArrayList<ScheduleResultDetail> previousSetupDetails =
                            machineCalculator.CreateSetupDetailsBackward(machine, previousSetupTime,
                                    (int) previousSetupResult.get(2),
                                    (int) previousSetupResult.get(3),
                                    previousSetupSegments,
                                    globalParam.is_smoothChangeOverInWeek());
                    if (previousSetupDetails != null && !previousSetupDetails.isEmpty()) {
                        geneDetails.addAll(previousSetupDetails);
                        geneDetails.sort(Comparator.comparingInt(ScheduleResultDetail::getStartTime));
                    }
                }

                replaceNextTaskSetupForBackward(nextGeneOnMachine, machine, nextSetupTime, nextSetupResult);
                backwardScheduled = true;
                break;
            }

            machineCalculator.AddMachineAvailable(machine, candidateGeneDetails);
            restoreNextTaskSetupForBackward(nextGeneOnMachine, machine, releasedNextSetupDetails);
            int nextLatestEndTime;
            if (prevGeneOnMachine != null) {
                // 当前窗口放不下，就继续往更早的相邻任务之前找插入点。
                nextLatestEndTime = getProcessStartTime(prevGeneOnMachine);
            } else {
                nextLatestEndTime = Math.min(finalProcessStartTime, previewProcessStartTime) - 1;
            }
            if (nextLatestEndTime <= 0 || nextLatestEndTime >= backwardLatestEndTime) {
                break;
            }
            backwardLatestEndTime = nextLatestEndTime;
        }

        if (!backwardScheduled && forwardFallbackRequired) {
            writeVerboseLog("[BackwardInsertWindow] backward search passed schedule baseline, request whole order forward fallback"
                    + ", operationId=" + operation.getId()
                    + ", groupId=" + operation.getGroupId()
                    + ", machineId=" + machine.getId()
                    + ", baselineTime=" + baselineTime
                    + ", baseline=" + convertTime(baselineTime));
            return new JitBackwardScheduleResult(new CopyOnWriteArrayList<>(), 0, true);
        } else if (!backwardScheduled) {
            // 倒排换型插入不成功但没有触发基准线/设备时间兜底时，保留原倒排能力，不强行制造换型。
            writeVerboseLog("[BackwardInsertWindow] 所有倒排插入位置都放不下，回退为不带换型倒排"
                    + ", operationId=" + operation.getId()
                    + ", groupId=" + operation.getGroupId()
                    + ", machineId=" + machine.getId()
                    + ", latestEndTime=" + latestEndTime
                    + ", latestEnd=" + convertTime(latestEndTime));
            geneDetails = machineCalculator.getNextAvailableTime(machine, operation, latestEndTime, -1,
                    processingTimeTotal, machineTasks, operation.getIsInterrupt() != 1, true,
                    processingTime, operation.getQuantity(), true, true);
            setupTime = 0;
            operation.setChangeLineTime(setupTime);
        }

        return new JitBackwardScheduleResult(geneDetails, setupTime, false);
    }

    /**
     * helper 返回给 GeneticDecoder 的结果：
     * geneDetails/setupTime 表示本工序排程成功；forwardFallbackRequired 表示需要外层整单正排。
     */
    static class JitBackwardScheduleResult {
        private final CopyOnWriteArrayList<ScheduleResultDetail> geneDetails;
        private final int setupTime;
        private final boolean forwardFallbackRequired;

        JitBackwardScheduleResult(CopyOnWriteArrayList<ScheduleResultDetail> geneDetails, int setupTime,
                                  boolean forwardFallbackRequired) {
            this.geneDetails = geneDetails;
            this.setupTime = setupTime;
            this.forwardFallbackRequired = forwardFallbackRequired;
        }

        CopyOnWriteArrayList<ScheduleResultDetail> getGeneDetails() {
            return geneDetails;
        }

        int getSetupTime() {
            return setupTime;
        }

        boolean isForwardFallbackRequired() {
            return forwardFallbackRequired;
        }
    }

    boolean validateBackwardInsertWindow(GAScheduleResult prevGeneOnMachine,
                                              GAScheduleResult nextGeneOnMachine,
                                              Entry operation,
                                              Machine machine,
                                              int processStartTime,
                                              int processEndTime,
                                              Map<Integer,Object> previousSetupResult,
                                              Map<Integer,Object> nextSetupResult) {
        int prevBoundaryTime = prevGeneOnMachine == null
                ? Integer.MIN_VALUE
                : getProcessEndTime(prevGeneOnMachine);
        int nextBoundaryTime = nextGeneOnMachine == null
                ? Integer.MAX_VALUE
                : getProcessStartTime(nextGeneOnMachine);

        int blockStartTime = processStartTime;
        int previousSetupTime = getSetupTime(previousSetupResult);
        if (previousSetupTime > 0) {
            blockStartTime = Math.min(blockStartTime, (int) previousSetupResult.get(2));
        }

        int blockEndTime = processEndTime;
        int nextSetupTime = getSetupTime(nextSetupResult);
        if (nextSetupTime > 0) {
            blockEndTime = Math.max(blockEndTime, (int) nextSetupResult.get(3));
        }

        boolean previousSetupInsufficient = Boolean.TRUE.equals(previousSetupResult == null ? false : previousSetupResult.get(8));
        boolean nextSetupInsufficient = Boolean.TRUE.equals(nextSetupResult == null ? false : nextSetupResult.get(8));
        boolean canInsert = !previousSetupInsufficient
                && !nextSetupInsufficient
                && blockStartTime >= prevBoundaryTime
                && blockEndTime <= nextBoundaryTime;

        writeVerboseLog("[BackwardInsertWindow] operationId=" + operation.getId()
                + ", groupId=" + operation.getGroupId()
                + ", machineId=" + machine.getId()
                + ", prevTask=" + describeScheduleResult(prevGeneOnMachine)
                + ", nextTask=" + describeScheduleResult(nextGeneOnMachine)
                + ", prevBoundary=" + (prevGeneOnMachine == null ? "null" : convertTime(prevBoundaryTime))
                + ", nextBoundary=" + (nextGeneOnMachine == null ? "null" : convertTime(nextBoundaryTime))
                + ", blockStart=" + convertTime(blockStartTime)
                + ", blockEnd=" + convertTime(blockEndTime)
                + ", processStart=" + convertTime(processStartTime)
                + ", processEnd=" + convertTime(processEndTime)
                + ", previousSetupTime=" + previousSetupTime
                + ", nextSetupTime=" + nextSetupTime
                + ", canInsert=" + canInsert);

        return canInsert;
    }

    /**
     * 试插蓝色前，先把黄色任务身上的旧后置换型释放出来。
     * 这里不永久删除业务含义，只是让设备日历先空出来，方便判断真实的绿色/黄色间隔。
     */
    CopyOnWriteArrayList<ScheduleResultDetail> releaseNextTaskSetupForBackward(GAScheduleResult nextGeneOnMachine,
                                                                                      Machine machine) {
        CopyOnWriteArrayList<ScheduleResultDetail> releasedSetupDetails = new CopyOnWriteArrayList<>();
        if (nextGeneOnMachine == null || nextGeneOnMachine.getGeneDetails() == null
                || nextGeneOnMachine.getGeneDetails().isEmpty()) {
            return releasedSetupDetails;
        }

        CopyOnWriteArrayList<ScheduleResultDetail> nextDetails =
                new CopyOnWriteArrayList<>(nextGeneOnMachine.getGeneDetails());
        nextDetails.stream()
                .filter(ScheduleResultDetail::isSetup)
                .forEach(releasedSetupDetails::add);
        if (releasedSetupDetails.isEmpty()) {
            return releasedSetupDetails;
        }

        machineCalculator.AddMachineAvailable(machine, releasedSetupDetails);
        nextDetails.removeIf(ScheduleResultDetail::isSetup);
        nextGeneOnMachine.setGeneDetails(nextDetails);
        nextGeneOnMachine.setChangeOverTime(0);
        refreshScheduleResultWindow(nextGeneOnMachine);
        writeVerboseLog("[BackwardInsertWindow] 临时释放黄色旧换型"
                + ", nextTask=" + describeScheduleResult(nextGeneOnMachine)
                + ", releasedSetupCount=" + releasedSetupDetails.size()
                + ", releasedSetupSeconds=" + sumSetupSeconds(releasedSetupDetails));
        return releasedSetupDetails;
    }

    /**
     * 当前绿色/黄色间隔放不下蓝色时，把刚才释放的黄色旧换型恢复回去。
     */
    void restoreNextTaskSetupForBackward(GAScheduleResult nextGeneOnMachine,
                                                 Machine machine,
                                                 List<ScheduleResultDetail> releasedSetupDetails) {
        if (releasedSetupDetails == null || releasedSetupDetails.isEmpty()) {
            return;
        }

        setMachineAvailabilityUsed(machine, releasedSetupDetails, true);
        if (nextGeneOnMachine != null) {
            CopyOnWriteArrayList<ScheduleResultDetail> nextDetails = nextGeneOnMachine.getGeneDetails() == null
                    ? new CopyOnWriteArrayList<>()
                    : new CopyOnWriteArrayList<>(nextGeneOnMachine.getGeneDetails());
            nextDetails.removeIf(ScheduleResultDetail::isSetup);
            nextDetails.addAll(releasedSetupDetails);
            nextDetails.sort(Comparator.comparingInt(ScheduleResultDetail::getStartTime));
            nextGeneOnMachine.setGeneDetails(nextDetails);
            nextGeneOnMachine.setChangeOverTime(sumSetupSeconds(releasedSetupDetails));
            refreshScheduleResultWindow(nextGeneOnMachine);
        }

        writeVerboseLog("[BackwardInsertWindow] 恢复黄色旧换型"
                + ", nextTask=" + describeScheduleResult(nextGeneOnMachine)
                + ", restoredSetupCount=" + releasedSetupDetails.size()
                + ", restoredSetupSeconds=" + sumSetupSeconds(releasedSetupDetails));
    }

    /**
     * 按排程明细里的 key/usedSegment，把设备日历设置为占用或可用。
     * 恢复旧换型时用 true，释放时仍沿用 machineCalculator.AddMachineAvailable。
     */
    private void setMachineAvailabilityUsed(Machine machine,
                                            List<ScheduleResultDetail> geneDetails,
                                            boolean used) {
        if (machine == null || machine.getAvailability() == null
                || geneDetails == null || geneDetails.isEmpty()) {
            return;
        }

        Set<String> keys = new HashSet<>();
        for (ScheduleResultDetail detail : geneDetails) {
            if (detail == null) {
                continue;
            }
            if (detail.getUsedSegment() != null && !detail.getUsedSegment().isEmpty()) {
                detail.getUsedSegment().stream()
                        .map(TimeSegment::getKey)
                        .filter(Objects::nonNull)
                        .forEach(keys::add);
            } else if (detail.getKey() != null) {
                keys.add(detail.getKey());
            }
        }
        if (keys.isEmpty()) {
            return;
        }

        machine.getAvailability().stream()
                .filter(t -> keys.contains(t.getKey()))
                .forEach(t -> t.setUsed(used));
    }

    private int sumSetupSeconds(List<ScheduleResultDetail> setupDetails) {
        if (setupDetails == null || setupDetails.isEmpty()) {
            return 0;
        }
        return setupDetails.stream()
                .mapToInt(ScheduleResultDetail::getProcessingTime)
                .sum();
    }

    /**
     * 后置换型归属黄色任务。
     * 插入蓝色后，黄色原来的“绿色 -> 黄色”换型要释放掉，再替换成“蓝色 -> 黄色”的新换型。
     */
    void replaceNextTaskSetupForBackward(GAScheduleResult nextGeneOnMachine,
                                                 Machine machine,
                                                 int setupTime,
                                                 Map<Integer,Object> setupResult) {
        if (nextGeneOnMachine == null || setupResult == null) {
            return;
        }

        CopyOnWriteArrayList<ScheduleResultDetail> nextDetails = nextGeneOnMachine.getGeneDetails() == null
                ? new CopyOnWriteArrayList<>()
                : new CopyOnWriteArrayList<>(nextGeneOnMachine.getGeneDetails());

        List<ScheduleResultDetail> oldSetupDetails = nextDetails.stream()
                .filter(ScheduleResultDetail::isSetup)
                .collect(Collectors.toList());
        if (!oldSetupDetails.isEmpty()) {
            machineCalculator.AddMachineAvailable(machine, oldSetupDetails);
            nextDetails.removeIf(ScheduleResultDetail::isSetup);
        }

        nextGeneOnMachine.setChangeOverTime(0);
        if (setupTime > 0) {
            CopyOnWriteArrayList<TimeSegment> setupSegments =
                    (CopyOnWriteArrayList<TimeSegment>) setupResult.get(6);
            CopyOnWriteArrayList<ScheduleResultDetail> newSetupDetails =
                    machineCalculator.CreateSetupDetailsBackward(machine, setupTime,
                            (int) setupResult.get(2),
                            (int) setupResult.get(3),
                            setupSegments,
                            globalParam.is_smoothChangeOverInWeek());
            if (newSetupDetails != null && !newSetupDetails.isEmpty()) {
                nextDetails.addAll(newSetupDetails);
                nextDetails.sort(Comparator.comparingInt(ScheduleResultDetail::getStartTime));
            }
            nextGeneOnMachine.setChangeOverTime(setupTime);
        }

        nextGeneOnMachine.setGeneDetails(nextDetails);
        refreshScheduleResultWindow(nextGeneOnMachine);
    }

    /**
     * 根据明细刷新任务整体起止时间；加工开始/结束仍由 getProcessStartTime/getProcessEndTime 过滤 setup 得到。
     */
    private void refreshScheduleResultWindow(GAScheduleResult result) {
        if (result == null || result.getGeneDetails() == null || result.getGeneDetails().isEmpty()) {
            return;
        }
        result.setStartTime(result.getGeneDetails().stream()
                .mapToInt(ScheduleResultDetail::getStartTime)
                .min()
                .orElse(result.getStartTime()));
        result.setEndTime(result.getGeneDetails().stream()
                .mapToInt(ScheduleResultDetail::getEndTime)
                .max()
                .orElse(result.getEndTime()));
    }
    private int getSetupTime(Map<Integer,Object> setupResult) {
        if (setupResult == null || setupResult.get(1) == null) {
            return 0;
        }
        return (int) setupResult.get(1);
    }

    String describeScheduleResult(GAScheduleResult result) {
        if (result == null) {
            return "null";
        }
        return "{operationId=" + result.getOperationId()
                + ", groupId=" + result.getGroupId()
                + ", execId=" + result.getExecId()
                + ", start=" + result.getStartTime() + "(" + convertTime(result.getStartTime()) + ")"
                + ", end=" + result.getEndTime() + "(" + convertTime(result.getEndTime()) + ")"
                + ", processStart=" + getProcessStartTime(result) + "(" + convertTime(getProcessStartTime(result)) + ")"
                + ", processEnd=" + getProcessEndTime(result) + "(" + convertTime(getProcessEndTime(result)) + ")"
                + ", changeOverTime=" + result.getChangeOverTime()
                + "}";
    }

    String describeDiscreteParams(Entry operation) {
        if (operation == null || operation.getDiscreteParameter() == null || operation.getDiscreteParameter().isEmpty()) {
            return "[]";
        }
        return operation.getDiscreteParameter().stream()
                .map(p -> "{groupId=" + p.getGroupId()
                        + ", parameterId=" + p.getParameterId()
                        + ", parameterName=" + p.getParameterName()
                        + "}")
                .collect(Collectors.joining(",", "[", "]"));
    }

    /**
     * 临时测试开关：启动参数传 -Daps.debug.backwardSetupTimeSeconds=秒数 时，
     * 只覆盖倒排换型时间；不传或传负数时走原来的矩阵/时长计算。
     */
    private int overrideSetupTimeForDebug(int setupTime, String direction,
                                          Entry fromOperation, Entry toOperation, Machine machine) {
        String overrideValue = System.getProperty(DEBUG_BACKWARD_SETUP_TIME_SECONDS_PROPERTY);
        if (overrideValue == null || overrideValue.trim().isEmpty()) {
            return setupTime;
        }

        int debugSetupTime;
        try {
            debugSetupTime = Integer.parseInt(overrideValue.trim());
        } catch (NumberFormatException ex) {
            writeVerboseLog("[BackwardChangeoverDebug] 手动换型时间参数无效，继续使用原计算值"
                    + ", property=" + DEBUG_BACKWARD_SETUP_TIME_SECONDS_PROPERTY
                    + ", value=" + overrideValue
                    + ", originalSetupTime=" + setupTime);
            return setupTime;
        }
        if (debugSetupTime < 0) {
            return setupTime;
        }

        writeVerboseLog("[BackwardChangeoverDebug] 使用手动换型时间"
                + ", direction=" + direction
                + ", machineId=" + (machine == null ? null : machine.getId())
                + ", fromOp=" + (fromOperation == null ? null : fromOperation.getId())
                + ", toOp=" + (toOperation == null ? null : toOperation.getId())
                + ", originalSetupTime=" + setupTime
                + ", debugSetupTime=" + debugSetupTime);
        return debugSetupTime;
    }

    Map<Integer,Object> calculateSetupTimeBeforeBackward(GAScheduleResult prevGeneOnMachine, Entry operation,
                                                                 Machine machine, int processStartTime,
                                                                 int processingTimeTotal, boolean changeOverInWeek,
                                                                 List<Entry> allOperations,
                                                                 CopyOnWriteArrayList<GAScheduleResult> machineTasks) {
        Map<Integer,Object> result = new HashMap<>();
        CopyOnWriteArrayList<TimeSegment> setupSegments = new CopyOnWriteArrayList<>();
        result.put(1, 0);
        result.put(2, processStartTime);
        result.put(3, processStartTime);
        result.put(4, processStartTime);
        result.put(5, processingTimeTotal);
        result.put(6, setupSegments);
        result.put(7, 0);
        result.put(8, false);

        if (prevGeneOnMachine == null || operation == null || machine == null) {
            writeVerboseLog("[BackwardChangeover] skip previous changeover, prev/operation/machine is null"
                    + ", operationId=" + (operation == null ? null : operation.getId())
                    + ", machineId=" + (machine == null ? null : machine.getId())
                    + ", processStartTime=" + processStartTime
                    + ", processStart=" + convertTime(processStartTime)
                    + ", machineTaskCount=" + (machineTasks == null ? 0 : machineTasks.size()));
            return result;
        }

        Entry prevOperation = allOperations == null ? null : allOperations.stream()
                .filter(t -> Objects.equals(t.getExecId(), prevGeneOnMachine.getExecId()))
                .findFirst()
                .orElse(null);
        if (prevOperation == null) {
            writeVerboseLog("[BackwardChangeover] skip previous changeover, previous Entry not found"
                    + ", operationId=" + operation.getId()
                    + ", prevTask=" + describeScheduleResult(prevGeneOnMachine)
                    + ", allOperationCount=" + (allOperations == null ? 0 : allOperations.size()));
            return result;
        }

        DiscreteParameterMatrixService discreteParameterMatrixService = SpringContextUtil.getBean(DiscreteParameterMatrixService.class);
        DiscreteParameterDurationService discreteParameterDurationService = SpringContextUtil.getBean(DiscreteParameterDurationService.class);

        double durationValue = discreteParameterDurationService.calculateChangeoverTime(
                prevOperation, operation, machine, allOperations, processStartTime, machineTasks);
        double matrixValue = discreteParameterMatrixService.getDiscreteParameterMatrixValue(operation, prevOperation);
        int setupTime = (int) Math.max(durationValue, matrixValue);
        setupTime = overrideSetupTimeForDebug(setupTime, "prev->current", prevOperation, operation, machine);
        writeVerboseLog("[BackwardChangeover] previous changeover result prevOp=" + prevOperation.getId()
                + ", currentOp=" + operation.getId()
                + ", machineId=" + machine.getId()
                + ", prevParams=" + describeDiscreteParams(prevOperation)
                + ", currentParams=" + describeDiscreteParams(operation)
                + ", durationValue=" + durationValue
                + ", matrixValue=" + matrixValue
                + ", setupTime=" + setupTime
                + ", processStart=" + convertTime(processStartTime)
                + ", prevProcessEnd=" + convertTime(getProcessEndTime(prevGeneOnMachine)));

        result.put(1, setupTime);
        if (setupTime <= 0) {
            return result;
        }

        int prevBoundaryTime = getProcessEndTime(prevGeneOnMachine);
        LocalDateTime setupEnd = baseTime.plusSeconds(processStartTime);
        if (changeOverInWeek) {
            LocalDateTime setupStart = setupEnd.minusSeconds(setupTime);
            int setupStartTime = secondsFromBase(setupStart);
            setupSegments = collectAvailableSegmentsBetween(machine, setupStart, setupEnd);
            result.put(2, setupStartTime);
            result.put(3, processStartTime);
            result.put(4, processStartTime);
            result.put(6, setupSegments);
            result.put(7, Math.max(0, processStartTime - prevBoundaryTime));
            result.put(8, setupStartTime < prevBoundaryTime);
            writeVerboseLog("[BackwardChangeover] previous changeover window currentOp=" + operation.getId()
                    + ", setupStart=" + convertTime(setupStartTime)
                    + ", setupEnd=" + convertTime(processStartTime)
                    + ", prevBoundary=" + convertTime(prevBoundaryTime)
                    + ", requiredSeconds=" + setupTime
                    + ", availableCalendarSeconds=" + result.get(7)
                    + ", insufficient=" + result.get(8)
                    + ", segments=" + describeTimeSegments(setupSegments));
            return result;
        }

        BackwardSetupWindow setupWindow = collectBackwardSetupWindow(machine, setupEnd, setupTime,
                baseTime.plusSeconds(prevBoundaryTime));
        result.put(2, setupWindow.startTime);
        result.put(3, setupWindow.endTime);
        result.put(4, processStartTime);
        result.put(6, setupWindow.segments);
        result.put(7, setupWindow.availableSeconds);
        result.put(8, setupWindow.insufficient);
        writeVerboseLog("[BackwardChangeover] previous changeover window currentOp=" + operation.getId()
                + ", setupStart=" + convertTime(setupWindow.startTime)
                + ", setupEnd=" + convertTime(setupWindow.endTime)
                + ", prevBoundary=" + convertTime(prevBoundaryTime)
                + ", requiredSeconds=" + setupTime
                + ", availableSeconds=" + setupWindow.availableSeconds
                + ", insufficient=" + setupWindow.insufficient
                + ", segments=" + describeTimeSegments(setupWindow.segments));
        return result;
    }

    Map<Integer,Object> calculateSetupTimeBackward(GAScheduleResult nextGeneOnMachine, Entry operation,
                                                           Machine machine, int latestEndTime,
                                                           int processingTimeTotal, boolean changeOverInWeek,
                                                           List<Entry> allOperations,
                                                           CopyOnWriteArrayList<GAScheduleResult> machineTasks) {
        Map<Integer,Object> result = new HashMap<>();
        CopyOnWriteArrayList<TimeSegment> setupSegments = new CopyOnWriteArrayList<>();
        result.put(1, 0);
        result.put(2, latestEndTime);
        result.put(3, latestEndTime);
        result.put(4, latestEndTime);
        result.put(5, processingTimeTotal);
        result.put(6, setupSegments);
        result.put(7, 0);
        result.put(8, false);

        if (nextGeneOnMachine == null || operation == null || machine == null) {
            writeVerboseLog("[倒排换型定位] 未计算换型：nextGeneOnMachine/operation/machine为空"
                    + ", operationId=" + (operation == null ? null : operation.getId())
                    + ", machineId=" + (machine == null ? null : machine.getId())
                    + ", latestEndTime=" + latestEndTime
                    + ", latestEnd=" + convertTime(latestEndTime)
                    + ", machineTaskCount=" + (machineTasks == null ? 0 : machineTasks.size()));
            return result;
        }

        Entry nextOperation = allOperations == null ? null : allOperations.stream()
                .filter(t -> Objects.equals(t.getExecId(), nextGeneOnMachine.getExecId()))
                .findFirst()
                .orElse(null);
        if (nextOperation == null) {
            writeVerboseLog("[倒排换型定位] 未计算换型：找不到后继工序Entry"
                    + ", operationId=" + operation.getId()
                    + ", nextTask=" + describeScheduleResult(nextGeneOnMachine)
                    + ", allOperationCount=" + (allOperations == null ? 0 : allOperations.size()));
            return result;
        }

        DiscreteParameterMatrixService discreteParameterMatrixService = SpringContextUtil.getBean(DiscreteParameterMatrixService.class);
        DiscreteParameterDurationService discreteParameterDurationService = SpringContextUtil.getBean(DiscreteParameterDurationService.class);

        double durationValue = discreteParameterDurationService.calculateChangeoverTime(
                operation, nextOperation, machine, allOperations, latestEndTime, machineTasks);
        double matrixValue = discreteParameterMatrixService.getDiscreteParameterMatrixValue(nextOperation, operation);
        int setupTime = (int) Math.max(durationValue, matrixValue);
        setupTime = overrideSetupTimeForDebug(setupTime, "current->next", operation, nextOperation, machine);
        nextOperation.setChangeLineTime(setupTime);
        int nextProcessStartTime = getProcessStartTime(nextGeneOnMachine);
        writeVerboseLog("[倒排换型定位] 换型查询结果 currentOp=" + operation.getId()
                + ", nextOp=" + nextOperation.getId()
                + ", machineId=" + machine.getId()
                + ", currentParams=" + describeDiscreteParams(operation)
                + ", nextParams=" + describeDiscreteParams(nextOperation)
                + ", durationValue=" + durationValue
                + ", matrixValue=" + matrixValue
                + ", setupTime=" + setupTime
                + ", latestEnd=" + convertTime(latestEndTime)
                + ", nextStart=" + convertTime(nextProcessStartTime));

        result.put(1, setupTime);
        if (setupTime <= 0) {
            return result;
        }

        int setupEndTime = Math.min(latestEndTime, nextProcessStartTime);
        LocalDateTime setupEnd = baseTime.plusSeconds(setupEndTime);

        if (changeOverInWeek) {
            LocalDateTime setupStart = setupEnd.minusSeconds(setupTime);
            setupSegments = collectAvailableSegmentsBetween(machine, setupStart, setupEnd);
            result.put(2, secondsFromBase(setupStart));
            result.put(3, setupEndTime);
            result.put(4, secondsFromBase(setupStart));
            result.put(6, setupSegments);
            result.put(7, sumCalendarSeconds(setupSegments));
            writeVerboseLog("[倒排换型定位] 换型窗口 currentOp=" + operation.getId()
                    + ", setupStart=" + convertTime(secondsFromBase(setupStart))
                    + ", setupEnd=" + convertTime(setupEndTime)
                    + ", latestProcessEnd=" + convertTime(secondsFromBase(setupStart))
                    + ", requiredSeconds=" + setupTime
                    + ", availableSeconds=" + result.get(7)
                    + ", segments=" + describeTimeSegments(setupSegments));
            return result;
        }

        BackwardSetupWindow setupWindow = collectBackwardSetupWindow(machine, setupEnd, setupTime);
        result.put(2, setupWindow.startTime);
        result.put(3, setupWindow.endTime);
        result.put(4, setupWindow.startTime);
        result.put(6, setupWindow.segments);
        result.put(7, setupWindow.availableSeconds);
        result.put(8, setupWindow.insufficient);
        writeVerboseLog("[倒排换型定位] 换型窗口 currentOp=" + operation.getId()
                + ", setupStart=" + convertTime(setupWindow.startTime)
                + ", setupEnd=" + convertTime(setupWindow.endTime)
                + ", latestProcessEnd=" + convertTime(setupWindow.startTime)
                + ", requiredSeconds=" + setupTime
                + ", availableSeconds=" + setupWindow.availableSeconds
                + ", insufficient=" + setupWindow.insufficient
                + ", segments=" + describeTimeSegments(setupWindow.segments));
        return result;
    }

    private BackwardSetupWindow collectBackwardSetupWindow(Machine machine, LocalDateTime setupEnd, int setupTime) {
        return collectBackwardSetupWindow(machine, setupEnd, setupTime, null);
    }

    private BackwardSetupWindow collectBackwardSetupWindow(Machine machine, LocalDateTime setupEnd, int setupTime,
                                                           LocalDateTime earliestStart) {
        BackwardSetupWindow window = new BackwardSetupWindow();
        window.endTime = secondsFromBase(setupEnd);
        window.startTime = secondsFromBase(setupEnd);

        if (machine == null || machine.getAvailability() == null || setupTime <= 0) {
            window.insufficient = setupTime > 0;
            return window;
        }

        int remaining = setupTime;
        LocalDateTime cursor = setupEnd;
        List<TimeSegment> slots = machine.getAvailability().stream()
                .filter(slot -> !slot.isUsed())
                .filter(slot -> slot.getType() != SegmentType.MAINTENANCE)
                .filter(slot -> slot.getStart().isBefore(setupEnd))
                .filter(slot -> earliestStart == null || slot.getEnd().isAfter(earliestStart))
                .sorted(Comparator.comparing(TimeSegment::getEnd).reversed())
                .collect(Collectors.toList());

        for (TimeSegment slot : slots) {
            LocalDateTime effectiveEnd = slot.getEnd().isAfter(cursor) ? cursor : slot.getEnd();
            LocalDateTime effectiveSlotStart = earliestStart != null && slot.getStart().isBefore(earliestStart)
                    ? earliestStart
                    : slot.getStart();
            if (!effectiveSlotStart.isBefore(effectiveEnd)) {
                continue;
            }

            int availableSeconds = (int) ChronoUnit.SECONDS.between(effectiveSlotStart, effectiveEnd);
            int usedSeconds = Math.min(remaining, availableSeconds);
            LocalDateTime effectiveStart = effectiveEnd.minusSeconds(usedSeconds);

            window.segments.add(copySegment(slot, effectiveStart, effectiveEnd, true));
            window.availableSeconds += usedSeconds;
            remaining -= usedSeconds;
            cursor = effectiveStart;

            if (remaining <= 0) {
                break;
            }
        }

        window.segments.sort(Comparator.comparing(TimeSegment::getStart));
        window.startTime = secondsFromBase(cursor);
        window.insufficient = remaining > 0;
        return window;
    }

    private CopyOnWriteArrayList<TimeSegment> collectAvailableSegmentsBetween(Machine machine,
                                                                              LocalDateTime startTime,
                                                                              LocalDateTime endTime) {
        if (machine == null || machine.getAvailability() == null || !startTime.isBefore(endTime)) {
            return new CopyOnWriteArrayList<>();
        }

        return machine.getAvailability().stream()
                .filter(slot -> !slot.isUsed())
                .filter(slot -> slot.getType() != SegmentType.MAINTENANCE)
                .filter(slot -> slot.getStart().isBefore(endTime) && slot.getEnd().isAfter(startTime))
                .map(slot -> {
                    LocalDateTime effectiveStart = slot.getStart().isBefore(startTime) ? startTime : slot.getStart();
                    LocalDateTime effectiveEnd = slot.getEnd().isAfter(endTime) ? endTime : slot.getEnd();
                    return copySegment(slot, effectiveStart, effectiveEnd, true);
                })
                .filter(slot -> slot.getStart().isBefore(slot.getEnd()))
                .sorted(Comparator.comparing(TimeSegment::getStart))
                .collect(Collectors.toCollection(CopyOnWriteArrayList::new));
    }

    private TimeSegment copySegment(TimeSegment source, LocalDateTime startTime,
                                    LocalDateTime endTime, boolean used) {
        TimeSegment segment = new TimeSegment();
        segment.setStart(startTime);
        segment.setEnd(endTime);
        segment.setType(source.getType());
        segment.setHoliday(source.isHoliday());
        segment.setUsed(used);
        segment.setEfficiency(source.getEfficiency());
        return segment;
    }

    private int sumCalendarSeconds(List<TimeSegment> segments) {
        if (segments == null || segments.isEmpty()) {
            return 0;
        }
        return segments.stream()
                .mapToInt(segment -> (int) ChronoUnit.SECONDS.between(segment.getStart(), segment.getEnd()))
                .sum();
    }

    private String describeTimeSegments(List<TimeSegment> segments) {
        if (segments == null || segments.isEmpty()) {
            return "[]";
        }
        return segments.stream()
                .map(segment -> "[" + secondsFromBase(segment.getStart())
                        + "-" + secondsFromBase(segment.getEnd())
                        + "](" + segment.getStart()
                        + "-" + segment.getEnd()
                        + "," + ChronoUnit.SECONDS.between(segment.getStart(), segment.getEnd())
                        + ")")
                .collect(Collectors.joining(",", "[", "]"));
    }

    private int secondsFromBase(LocalDateTime time) {
        return (int) ChronoUnit.SECONDS.between(baseTime, time);
    }

    private static class BackwardSetupWindow {
        private int startTime;
        private int endTime;
        private int availableSeconds;
        private boolean insufficient;
        private CopyOnWriteArrayList<TimeSegment> segments = new CopyOnWriteArrayList<>();
    }

    // ==================== 倒排换型辅助方法结束 ====================

}
