package jnpf.attendance.schedule;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 将 {@link HalfHourPostDemandMatrix}（半小时×岗位目标人数）整理为「完整班次」块列表，供后续排班/展示。
 *
 * <p><b>整理规则（按岗位独立处理后再汇总）</b>
 * <ul>
 *   <li>若该岗位在当日有需求的时段内人数<strong>始终不变</strong>：生成<strong>一段固定排班</strong>，覆盖从首段有需求到末段有需求的整段连续时间，
 *       人数为该常数。</li>
 *   <li>若人数<strong>中途有变化</strong>：在「有需求」的索引区间内取<strong>最小需求人数</strong> {@code minNeed}，
 *       再取所有 {@code target==minNeed} 的<strong>最长连续半小时段</strong>（若有并列取<strong>最早</strong>的一段）作为<strong>固定排班</strong>，人数为 {@code minNeed}；</li>
 *   <li>其余时段的缺口用<strong>划线排班</strong>表达：固定段外侧用<strong>全额</strong>目标人数；固定段内侧用 {@code target-minNeed}。</li>
 * </ul>
 *
 * <p>输出：{@code Map<排班日, List<班次块>>}，每块含开始/结束分钟、是否固定、{@link ShiftPlanPostNeed} 列表（通常单岗位一条，合并后可能多岗位）。
 */
public final class HalfHourDemandBasedShiftPlanBuilder {

    private static final Logger log = LoggerFactory.getLogger(HalfHourDemandBasedShiftPlanBuilder.class);

    private HalfHourDemandBasedShiftPlanBuilder() {}

    /**
     * @param matrices key 为排班日，与 {@link SimilarDayDemandSteps4And5#buildPerScheduleDayDemandMatrices} 输出一致
     * @return 每个排班日按开始时间、再划线优先排序的班次块列表（已做同窗同类型合并）
     */
    public static Map<LocalDate, List<ShiftPlanBlock>> build(Map<LocalDate, HalfHourPostDemandMatrix> matrices) {
        if (matrices == null || matrices.isEmpty()) {
            return Collections.emptyMap();
        }
        List<LocalDate> days = new ArrayList<>();
        for (LocalDate d : matrices.keySet()) {
            if (d != null) {
                days.add(d);
            }
        }
        if (days.size() < matrices.size()) {
            log.error(
                    "ignored {} null LocalDate key(s) in matrices map (total keys={})",
                    matrices.size() - days.size(),
                    matrices.size());
        }
        Collections.sort(days);
        Map<LocalDate, List<ShiftPlanBlock>> out = new LinkedHashMap<>();
        for (LocalDate day : days) {
            HalfHourPostDemandMatrix m = matrices.get(day);
            if (day == null || m == null) {
                continue;
            }
            List<ShiftPlanBlock> raw = buildBlocksForOneDay(day, m);
            List<ShiftPlanBlock> merged = mergeCoextensiveBlocks(raw);
            merged.sort(
                    Comparator.comparingInt(ShiftPlanBlock::getStartMinuteInclusive)
                            .thenComparingInt(ShiftPlanBlock::getEndMinuteExclusive)
                            .thenComparing(b -> b.isFixedScheduling()));
            out.put(day, Collections.unmodifiableList(merged));
        }
        return Collections.unmodifiableMap(out);
    }

    private static List<ShiftPlanBlock> buildBlocksForOneDay(LocalDate day, HalfHourPostDemandMatrix matrix) {
        List<ShiftPlanBlock> all = new ArrayList<>();
        for (String postId : matrix.getPostIdsOrdered()) {
            all.addAll(buildBlocksForPost(day, matrix, postId));
        }
        return all;
    }

    private static List<ShiftPlanBlock> buildBlocksForPost(
            LocalDate day, HalfHourPostDemandMatrix matrix, String postId) {
        List<String> slots = matrix.getSlotRangeLabelsOrdered();
        int n = slots.size();
        if (n == 0) {
            return Collections.emptyList();
        }
        int[] t = new int[n];
        for (int i = 0; i < n; i++) {
            t[i] = matrix.getTarget(slots.get(i), postId);
        }
        int first = -1;
        int last = -1;
        for (int i = 0; i < n; i++) {
            if (t[i] > 0) {
                first = i;
                break;
            }
        }
        for (int i = n - 1; i >= 0; i--) {
            if (t[i] > 0) {
                last = i;
                break;
            }
        }
        if (first < 0) {
            return Collections.emptyList();
        }

        int minNeed = Integer.MAX_VALUE;
        for (int i = first; i <= last; i++) {
            if (t[i] > 0) {
                minNeed = Math.min(minNeed, t[i]);
            }
        }
        if (minNeed == Integer.MAX_VALUE) {
            minNeed = 0;
        }
        if (minNeed <= 0) {
            minNeed = minPositiveInRange(t, first, last);
        }
        if (minNeed <= 0) {
            return Collections.emptyList();
        }

        boolean allConstant = true;
        for (int i = first; i <= last; i++) {
            if (t[i] > 0 && t[i] != minNeed) {
                allConstant = false;
                break;
            }
        }

        List<ShiftPlanBlock> out = new ArrayList<>();
        if (allConstant) {
            int sm = slotStartMinute(slots, first);
            int em = slotEndMinuteExclusive(slots, last);
            if (em > sm) {
                out.add(
                        new ShiftPlanBlock(
                                day,
                                sm,
                                em,
                                true,
                                Collections.singletonList(new ShiftPlanPostNeed(postId, minNeed))));
            }
            return out;
        }

        int[] longest = longestContiguousRunWhereEquals(t, first, last, minNeed);
        int runStart = longest[0];
        int runEndExclusive = longest[1];
        if (runStart >= 0 && runEndExclusive > runStart) {
            int sm = slotStartMinute(slots, runStart);
            int em = slotEndMinuteExclusive(slots, runEndExclusive - 1);
            if (em > sm && minNeed > 0) {
                out.add(
                        new ShiftPlanBlock(
                                day,
                                sm,
                                em,
                                true,
                                Collections.singletonList(new ShiftPlanPostNeed(postId, minNeed))));
            }
        }

        int[] lineNeed = new int[n];
        for (int i = first; i <= last; i++) {
            if (i >= runStart && i < runEndExclusive) {
                lineNeed[i] = Math.max(0, t[i] - minNeed);
            } else {
                lineNeed[i] = Math.max(0, t[i]);
            }
        }

        int i = first;
        while (i <= last) {
            if (lineNeed[i] <= 0) {
                i++;
                continue;
            }
            int v = lineNeed[i];
            int j = i;
            while (j <= last && lineNeed[j] == v) {
                j++;
            }
            int sm = slotStartMinute(slots, i);
            int em = slotEndMinuteExclusive(slots, j - 1);
            if (em > sm && v > 0) {
                out.add(
                        new ShiftPlanBlock(
                                day,
                                sm,
                                em,
                                false,
                                Collections.singletonList(new ShiftPlanPostNeed(postId, v))));
            }
            i = j;
        }
        return out;
    }

    /** @return int[2] {runStartInclusive, runEndExclusive} */
    private static int[] longestContiguousRunWhereEquals(int[] t, int first, int last, int value) {
        int bestStart = -1;
        int bestLen = 0;
        int a = first;
        while (a <= last) {
            if (t[a] != value) {
                a++;
                continue;
            }
            int b = a;
            while (b <= last && t[b] == value) {
                b++;
            }
            int len = b - a;
            if (len > bestLen || (len == bestLen && (bestStart < 0 || a < bestStart))) {
                bestLen = len;
                bestStart = a;
            }
            a = b;
        }
        if (bestStart < 0) {
            return new int[] {-1, -1};
        }
        return new int[] {bestStart, bestStart + bestLen};
    }

    private static int minPositiveInRange(int[] t, int first, int last) {
        int m = Integer.MAX_VALUE;
        for (int i = first; i <= last; i++) {
            if (t[i] > 0) {
                m = Math.min(m, t[i]);
            }
        }
        return m == Integer.MAX_VALUE ? 0 : m;
    }

    private static int slotStartMinute(List<String> slots, int slotIndex) {
        int[] se = HalfHourSlotLabel.startEndExclusiveMinutes(slots.get(slotIndex));
        return se[0];
    }

    private static int slotEndMinuteExclusive(List<String> slots, int slotIndexInclusive) {
        int[] se = HalfHourSlotLabel.startEndExclusiveMinutes(slots.get(slotIndexInclusive));
        return se[1];
    }

    private static List<ShiftPlanBlock> mergeCoextensiveBlocks(List<ShiftPlanBlock> blocks) {
        if (blocks == null || blocks.isEmpty()) {
            return new ArrayList<>();
        }
        List<ShiftPlanBlock> sorted = new ArrayList<>();
        for (ShiftPlanBlock b : blocks) {
            if (b != null) {
                sorted.add(b);
            }
        }
        if (sorted.isEmpty()) {
            return new ArrayList<>();
        }
        sorted.sort(
                Comparator.comparing((ShiftPlanBlock b) -> b.getScheduleDay())
                        .thenComparingInt(ShiftPlanBlock::getStartMinuteInclusive)
                        .thenComparingInt(ShiftPlanBlock::getEndMinuteExclusive)
                        .thenComparing(ShiftPlanBlock::isFixedScheduling));
        List<ShiftPlanBlock> out = new ArrayList<>();
        ShiftPlanBlock cur = sorted.get(0);
        for (int k = 1; k < sorted.size(); k++) {
            ShiftPlanBlock next = sorted.get(k);
            if (sameWindow(cur, next)) {
                cur = mergePostNeeds(cur, next);
            } else {
                out.add(cur);
                cur = next;
            }
        }
        out.add(cur);
        return out;
    }

    private static boolean sameWindow(ShiftPlanBlock a, ShiftPlanBlock b) {
        return a.getScheduleDay().equals(b.getScheduleDay())
                && a.getStartMinuteInclusive() == b.getStartMinuteInclusive()
                && a.getEndMinuteExclusive() == b.getEndMinuteExclusive()
                && a.isFixedScheduling() == b.isFixedScheduling();
    }

    private static ShiftPlanBlock mergePostNeeds(ShiftPlanBlock a, ShiftPlanBlock b) {
        Map<String, Integer> sum = new LinkedHashMap<>();
        for (ShiftPlanPostNeed n : a.getPostNeeds()) {
            if (n == null || n.getPostId() == null || n.getPostId().trim().isEmpty()) {
                continue;
            }
            sum.merge(n.getPostId(), n.getNeedCount(), Integer::sum);
        }
        for (ShiftPlanPostNeed n : b.getPostNeeds()) {
            if (n == null || n.getPostId() == null || n.getPostId().trim().isEmpty()) {
                continue;
            }
            sum.merge(n.getPostId(), n.getNeedCount(), Integer::sum);
        }
        List<ShiftPlanPostNeed> list = new ArrayList<>();
        for (Map.Entry<String, Integer> e : sum.entrySet()) {
            if (e.getValue() != null && e.getValue() > 0) {
                list.add(new ShiftPlanPostNeed(e.getKey(), e.getValue()));
            }
        }
        return new ShiftPlanBlock(
                a.getScheduleDay(),
                a.getStartMinuteInclusive(),
                a.getEndMinuteExclusive(),
                a.isFixedScheduling(),
                list,
                a.getShiftId());
    }
}
