package jnpf.attendance.schedule;

import java.time.LocalDate;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;

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

/**
 * 第四、五步：按排班日聚合相似历史半小时行 → (时间段,岗位) 稳健目标人数 → 半小时岗位需求矩阵。
 */
public final class SimilarDayDemandSteps4And5 {

    private static final Logger LOG = LoggerFactory.getLogger(SimilarDayDemandSteps4And5.class);

    /** 测试观测：单张矩阵内逐格解释（中位数/P75）最多输出条数，避免超大门店刷屏。 */
    private static final int FOR_TEST_CELL_DETAIL_CAP = 250;

    private SimilarDayDemandSteps4And5() {}

    /**
     * 对每个排班日生成一张 {@link HalfHourPostDemandMatrix}（仅包含有行数据的排班日；失败日无行则跳过该日不出现在 map 中）。
     *
     * @param rows                             须带 {@link SimilarHistoricalJobSlotRow#getTemplateScheduleDay()}
     * @param similarTemplateDaysByScheduleDay 与第三步同源，用于取相似历史日列表（补 0 顺序）
     * @param config                           中位数 / P75 与是否补 0
     */
    public static Map<LocalDate, HalfHourPostDemandMatrix> buildPerScheduleDayDemandMatrices(
            List<SimilarHistoricalJobSlotRow> rows,
            Map<LocalDate, ScheduleTemplateSimilarDaysResult> similarTemplateDaysByScheduleDay,
            SlotPostRobustTargetConfig config) {
        if (similarTemplateDaysByScheduleDay == null || similarTemplateDaysByScheduleDay.isEmpty()) {
            return Collections.emptyMap();
        }
        Objects.requireNonNull(config, "config");
        SchedulingForTestCheckLog.info(
                LOG,
                SchedulingForTestCheckLog.TEST_ISSUE_2_HALF_HOUR_DEMAND_MATRIX,
                "demand_matrix_global_config",
                String.format(
                        "algo=SimilarDayDemandSteps4And5.buildPerScheduleDayDemandMatrices | SlotPostRobustTargetConfig.maxMinusMinUseMedianThreshold=%d "
                                + "| imputeZeroForMissingSimilarDays=%s | ruleInterpretation=maxMinusMinGtThreshold_Then_P75_else_Median",
                        config.getMaxMinusMinUseMedianThreshold(),
                        config.isImputeZeroForMissingSimilarDays()));
        List<SimilarHistoricalJobSlotRow> safeRows = rows == null ? Collections.emptyList() : rows;

        Map<LocalDate, HalfHourPostDemandMatrix> out = new LinkedHashMap<>();
        for (Map.Entry<LocalDate, ScheduleTemplateSimilarDaysResult> e : similarTemplateDaysByScheduleDay.entrySet()) {
            LocalDate scheduleDay = e.getKey();
            ScheduleTemplateSimilarDaysResult sim = e.getValue();
            if (scheduleDay == null || sim == null || !sim.isSuccess()) {
                if (LOG.isErrorEnabled() && scheduleDay != null) {
                    SchedulingForTestCheckLog.info(
                            LOG,
                            SchedulingForTestCheckLog.TEST_ISSUE_2_HALF_HOUR_DEMAND_MATRIX,
                            "demand_matrix_skip_day",
                            "algo=SimilarDayDemandSteps4And5 | scheduleDay="
                                    + scheduleDay
                                    + " | skippedBecauseSimilarDaysNotSuccess="
                                    + (sim == null ? "null_sim" : Boolean.toString(sim.isSuccess()))
                                    + " | tier="
                                    + (sim == null ? "NONE" : sim.getTier())
                                    + " | msg="
                                    + (sim == null ? "" : sim.getMessage()));
                }
                continue;
            }
            List<SimilarHistoricalJobSlotRow> forDay = new ArrayList<>();
            for (SimilarHistoricalJobSlotRow r : safeRows) {
                if (r != null && scheduleDay.equals(r.getTemplateScheduleDay())) {
                    forDay.add(r);
                }
            }
            if (forDay.isEmpty()) {
                continue;
            }
            HalfHourPostDemandMatrix matrix =
                    buildMatrixForScheduleDay(scheduleDay, forDay, sim.getSimilarDates(), config);
            out.put(scheduleDay, matrix);
        }
        return Collections.unmodifiableMap(out);
    }

    static HalfHourPostDemandMatrix buildMatrixForScheduleDay(
            LocalDate scheduleDay,
            List<SimilarHistoricalJobSlotRow> rowsForScheduleDay,
            List<LocalDate> similarDatesOrdered,
            SlotPostRobustTargetConfig config) {
        // 历史自然日 → 时间段 → 岗位 → 当日合计（同日多班次先相加）
        Map<LocalDate, Map<String, Map<String, Integer>>> byDaySlotPost = new LinkedHashMap<>();
        Map<String, LocalTime> slotLabelToOrderStart = new LinkedHashMap<>();
        for (SimilarHistoricalJobSlotRow r : rowsForScheduleDay) {
            if (r == null) {
                continue;
            }
            String slot = r.getSlotRangeLabel();
            String post = r.getPostId();
            byDaySlotPost
                    .computeIfAbsent(r.getRowDate(), d -> new LinkedHashMap<>())
                    .computeIfAbsent(slot, s -> new LinkedHashMap<>())
                    .merge(post, r.getHeadcount(), Integer::sum);
            slotLabelToOrderStart.putIfAbsent(slot, r.getSlotStartInclusive());
        }

        Set<String> allSlots = new LinkedHashSet<>();
        Set<String> allPosts = new TreeSet<>();
        for (Map<String, Map<String, Integer>> slotMap : byDaySlotPost.values()) {
            for (Map.Entry<String, Map<String, Integer>> se : slotMap.entrySet()) {
                allSlots.add(se.getKey());
                if (se.getValue() != null) {
                    allPosts.addAll(se.getValue().keySet());
                }
            }
        }

        List<String> slotsOrdered = new ArrayList<>(allSlots);
        slotsOrdered.sort(Comparator.comparing(slotLabelToOrderStart::get));

        List<String> postsOrdered = new ArrayList<>(allPosts);

        Map<String, Map<String, Integer>> targetBySlot = new LinkedHashMap<>();
        List<LocalDate> similarDates =
                similarDatesOrdered == null ? Collections.emptyList() : similarDatesOrdered;

        int cellLogs = 0;
        for (String slot : slotsOrdered) {
            Map<String, Integer> row = new LinkedHashMap<>();
            for (String post : postsOrdered) {
                List<Integer> samples = buildSamplesForSlotPost(byDaySlotPost, slot, post, similarDates, config);
                int target = SlotPostTargetHeadcountCalculator.robustTargetFromSamples(samples, config);
                row.put(post, target);
                if (LOG.isErrorEnabled() && cellLogs < FOR_TEST_CELL_DETAIL_CAP) {
                    cellLogs++;
                    SchedulingForTestCheckLog.info(
                            LOG,
                            SchedulingForTestCheckLog.TEST_ISSUE_2_HALF_HOUR_DEMAND_MATRIX,
                            "demand_matrix_robust_cell",
                            "algo=SimilarDayDemandSteps4And5 | scheduleDay="
                                    + scheduleDay
                                    + " | slot="
                                    + slot
                                    + " | post="
                                    + post
                                    + " | similarDatesCount="
                                    + similarDates.size()
                                    + " | distinctHistoryCalendarDaysInRows="
                                    + byDaySlotPost.size()
                                    + " | "
                                    + SlotPostTargetHeadcountCalculator.describeRobustAggregation(samples, config));
                }
            }
            targetBySlot.put(slot, row);
        }
        if (LOG.isErrorEnabled() && postsOrdered.size() * slotsOrdered.size() > FOR_TEST_CELL_DETAIL_CAP) {
            SchedulingForTestCheckLog.info(
                    LOG,
                    SchedulingForTestCheckLog.TEST_ISSUE_2_HALF_HOUR_DEMAND_MATRIX,
                    "demand_matrix_robust_cell",
                    "algo=SimilarDayDemandSteps4And5 | scheduleDay="
                            + scheduleDay
                            + " | note=per_cell_detail_capped | cap="
                            + FOR_TEST_CELL_DETAIL_CAP
                            + " | totalCells="
                            + (postsOrdered.size() * slotsOrdered.size()));
        }

        HalfHourPostDemandMatrix matrix = new HalfHourPostDemandMatrix(scheduleDay, slotsOrdered, postsOrdered, targetBySlot);
        if (LOG.isErrorEnabled()) {
            SchedulingForTestCheckLog.info(
                    LOG,
                    SchedulingForTestCheckLog.TEST_ISSUE_2_HALF_HOUR_DEMAND_MATRIX,
                    "demand_matrix_tsv",
                    "algo=SimilarDayDemandSteps4And5 | scheduleDay="
                            + scheduleDay
                            + " | slotCount="
                            + slotsOrdered.size()
                            + " | postCount="
                            + postsOrdered.size()
                            + " | tsvSingleLine="
                            + SchedulingForTestCheckLog.singleLineTsv(matrix.toTsvTable(), 20000));
        }
        return matrix;
    }

    private static List<Integer> buildSamplesForSlotPost(
            Map<LocalDate, Map<String, Map<String, Integer>>> byDaySlotPost,
            String slot,
            String post,
            List<LocalDate> similarDates,
            SlotPostRobustTargetConfig config) {
        List<Integer> samples = new ArrayList<>();
        if (config.isImputeZeroForMissingSimilarDays() && !similarDates.isEmpty()) {
            for (LocalDate d : similarDates) {
                if (d == null) {
                    continue;
                }
                int v = 0;
                Map<String, Map<String, Integer>> slotMap = byDaySlotPost.get(d);
                if (slotMap != null) {
                    Map<String, Integer> postMap = slotMap.get(slot);
                    if (postMap != null && postMap.get(post) != null) {
                        v = postMap.get(post);
                    }
                }
                samples.add(v);
            }
        } else {
            TreeSet<LocalDate> days = new TreeSet<>(byDaySlotPost.keySet());
            for (LocalDate d : days) {
                Map<String, Map<String, Integer>> slotMap = byDaySlotPost.get(d);
                if (slotMap == null) {
                    continue;
                }
                Map<String, Integer> postMap = slotMap.get(slot);
                if (postMap == null || !postMap.containsKey(post)) {
                    continue;
                }
                samples.add(postMap.get(post));
            }
        }
        return samples;
    }
}
