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 java.util.Objects;

/**
 * 将 {@link ScheduleDemandCoverResult}（第六～八步：固定班贪心、划线、剩余缺口）转为
 * {@link ShiftPlanBlock} 列表，供 {@link ShiftPlanAttendanceShiftIdResolver} 与
 * {@link ShiftPlanFinalStaffAssigner} 使用。
 *
 * <p>规则：
 * <ul>
 *   <li>固定班：同一岗位、同一分钟窗的多次贪心选取合并为一条 {@link ShiftPlanPostNeed}（人数=选本次数）；</li>
 *   <li>划线班：同一岗位、同一半小时索引窗的重复划线段合并计数；</li>
 *   <li>剩余缺口：{@link ScheduleDemandCoverResult#getRemainingNeedByPostAfterLine()} 按「连续相等且 &gt;0」
 *       的半小时序列收束为划线块（仍无法满足的缺口继续向下游可见）；</li>
 *   <li>同窗、同固定/划线类型、同时间窗的跨岗位块与 {@link HalfHourDemandBasedShiftPlanBuilder} 一样做合并。</li>
 * </ul>
 */
public final class ShiftPlanBlocksFromDemandCoverBuilder {

    private ShiftPlanBlocksFromDemandCoverBuilder() {}

    /**
     * @param coverByScheduleDay 与 {@code matrices} 中排班日 key 应对齐（通常来自
     *     {@link ScheduleDemandCoverSteps678#coverAllScheduleDays}）
     */
    public static Map<LocalDate, List<ShiftPlanBlock>> build(
            Map<LocalDate, ScheduleDemandCoverResult> coverByScheduleDay,
            Map<LocalDate, HalfHourPostDemandMatrix> matrices) {
        if (matrices == null || matrices.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<LocalDate, List<ShiftPlanBlock>> out = new LinkedHashMap<>();
        for (Map.Entry<LocalDate, HalfHourPostDemandMatrix> e : matrices.entrySet()) {
            LocalDate day = e.getKey();
            HalfHourPostDemandMatrix matrix = e.getValue();
            if (day == null || matrix == null) {
                continue;
            }
            ScheduleDemandCoverResult cov =
                    coverByScheduleDay == null ? null : coverByScheduleDay.get(day);
            if (cov == null) {
                continue;
            }
            out.put(day, Collections.unmodifiableList(buildForOneDay(day, cov, matrix)));
        }
        return Collections.unmodifiableMap(out);
    }

    private static List<ShiftPlanBlock> buildForOneDay(
            LocalDate day, ScheduleDemandCoverResult cov, HalfHourPostDemandMatrix matrix) {
        Objects.requireNonNull(day, "day");
        Objects.requireNonNull(cov, "cov");
        Objects.requireNonNull(matrix, "matrix");
        if (!day.equals(cov.getScheduleDay()) || !day.equals(matrix.getScheduleDay())) {
            throw new IllegalArgumentException(
                    "schedule day mismatch: day="
                            + day
                            + " covDay="
                            + cov.getScheduleDay()
                            + " matrixDay="
                            + matrix.getScheduleDay());
        }

        List<String> slots = matrix.getSlotRangeLabelsOrdered();
        List<ShiftPlanBlock> raw = new ArrayList<>();

        for (String postId : matrix.getPostIdsOrdered()) {
            if (postId == null) {
                continue;
            }

            FixedShiftCoverOutcome fo = cov.getFixedShiftCoverByPost().get(postId);
            if (fo != null) {
                Map<String, PostFixedShiftCandidate> rep = new LinkedHashMap<>();
                Map<String, Integer> counts = new LinkedHashMap<>();
                for (PostFixedShiftCandidate c : fo.getPicksInOrder()) {
                    if (c == null) {
                        continue;
                    }
                    String key = fixedPickKey(c);
                    rep.putIfAbsent(key, c);
                    counts.merge(key, 1, Integer::sum);
                }
                for (Map.Entry<String, Integer> en : counts.entrySet()) {
                    PostFixedShiftCandidate c = rep.get(en.getKey());
                    int n = en.getValue();
                    if (c == null || n <= 0) {
                        continue;
                    }
                    raw.add(
                            new ShiftPlanBlock(
                                    day,
                                    c.getStartMinuteInclusive(),
                                    c.getEndMinuteExclusive(),
                                    true,
                                    Collections.singletonList(new ShiftPlanPostNeed(postId, n)),
                                    null,
                                    c.getHistoricalAttendanceShiftId()));
                }
            }

            List<LineShiftPick> linePicks = cov.getLineShiftPicksByPost().get(postId);
            if (linePicks != null) {
                Map<String, Integer> lineCounts = new LinkedHashMap<>();
                Map<String, LineShiftPick> lineRep = new LinkedHashMap<>();
                for (LineShiftPick lp : linePicks) {
                    if (lp == null) {
                        continue;
                    }
                    String key =
                            postId
                                    + "|"
                                    + lp.getStartSlotIndexInclusive()
                                    + "|"
                                    + lp.getEndSlotIndexExclusive();
                    lineRep.putIfAbsent(key, lp);
                    lineCounts.merge(key, 1, Integer::sum);
                }
                for (Map.Entry<String, Integer> en : lineCounts.entrySet()) {
                    LineShiftPick lp = lineRep.get(en.getKey());
                    int n = en.getValue();
                    if (lp == null || n <= 0) {
                        continue;
                    }
                    int[] range = linePickToMinuteRange(slots, lp);
                    if (range[1] > range[0]) {
                        raw.add(
                                new ShiftPlanBlock(
                                        day,
                                        range[0],
                                        range[1],
                                        false,
                                        Collections.singletonList(new ShiftPlanPostNeed(postId, n))));
                    }
                }
            }

            int[] rem = cov.getRemainingNeedByPostAfterLine().get(postId);
            addRemainderLineBlocks(day, postId, rem, slots, raw);
        }

        List<ShiftPlanBlock> merged = mergeCoextensiveBlocks(raw);
        merged.sort(
                Comparator.comparingInt(ShiftPlanBlock::getStartMinuteInclusive)
                        .thenComparingInt(ShiftPlanBlock::getEndMinuteExclusive)
                        .thenComparing(b -> !b.isFixedScheduling()));
        return merged;
    }

    private static String fixedPickKey(PostFixedShiftCandidate c) {
        String hid = c.getHistoricalAttendanceShiftId();
        String hidSeg = hid == null ? "" : hid;
        return c.getPostId()
                + "|"
                + c.getStartMinuteInclusive()
                + "|"
                + c.getEndMinuteExclusive()
                + "|"
                + hidSeg;
    }

    private static int[] linePickToMinuteRange(List<String> slots, LineShiftPick lp) {
        int i = lp.getStartSlotIndexInclusive();
        int j = lp.getEndSlotIndexExclusive();
        if (slots == null || slots.isEmpty() || i < 0 || j <= i || i >= slots.size()) {
            return new int[] {0, 0};
        }
        int last = Math.min(j - 1, slots.size() - 1);
        int[] a = HalfHourSlotLabel.startEndExclusiveMinutes(slots.get(i));
        int[] b = HalfHourSlotLabel.startEndExclusiveMinutes(slots.get(last));
        return new int[] {a[0], b[1]};
    }

    /**
     * 将仍缺的半小时，按「连续、且缺口人数相同」收束为划线块。
     */
    private static void addRemainderLineBlocks(
            LocalDate day,
            String postId,
            int[] rem,
            List<String> slots,
            List<ShiftPlanBlock> out) {
        if (rem == null || slots == null || rem.length == 0 || slots.size() != rem.length) {
            return;
        }
        int i = 0;
        while (i < rem.length) {
            if (rem[i] <= 0) {
                i++;
                continue;
            }
            int v = rem[i];
            int j = i;
            while (j < rem.length && rem[j] == v) {
                j++;
            }
            int sm = HalfHourSlotLabel.startEndExclusiveMinutes(slots.get(i))[0];
            int em = HalfHourSlotLabel.startEndExclusiveMinutes(slots.get(j - 1))[1];
            if (em > sm && v > 0) {
                out.add(
                        new ShiftPlanBlock(
                                day,
                                sm,
                                em,
                                false,
                                Collections.singletonList(new ShiftPlanPostNeed(postId, v))));
            }
            i = j;
        }
    }

    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()));
            }
        }
        String mergedHistorical = mergeHistoricalAttendanceShiftIds(a, b);
        return new ShiftPlanBlock(
                a.getScheduleDay(),
                a.getStartMinuteInclusive(),
                a.getEndMinuteExclusive(),
                a.isFixedScheduling(),
                list,
                a.getShiftId(),
                mergedHistorical);
    }

    private static String mergeHistoricalAttendanceShiftIds(ShiftPlanBlock a, ShiftPlanBlock b) {
        String ha = a.getHistoricalAttendanceShiftId();
        String hb = b.getHistoricalAttendanceShiftId();
        if (ha == null || ha.isBlank()) {
            return hb == null || hb.isBlank() ? null : hb.trim();
        }
        if (hb == null || hb.isBlank()) {
            return ha.trim();
        }
        if (ha.trim().equals(hb.trim())) {
            return ha.trim();
        }
        return null;
    }
}
