package jnpf.attendance.schedule;

import jnpf.model.attendance.vo.DayShiftRevenueStatVo;
import jnpf.model.attendance.vo.ShiftPostHeadcountVo;
import jnpf.model.attendance.vo.ShiftPostStatVo;

import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeParseException;
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;
import java.util.TreeMap;

/**
 * 第六步：从相似历史日的排班数据中按岗位识别常见固定班次（排除划线班），统计并筛选候选池。
 */
public final class CommonFixedShiftDiscovery {

    private CommonFixedShiftDiscovery() {}

    /**
     * @param similarDatesOrdered 第三步给出的相似历史日（顺序保留，用于占比分母）
     * @param historyByDay        key 为 {@link DayShiftRevenueStatVo#getDay()}（yyyy-MM-dd）
     */
    public static Map<String, List<PostFixedShiftCandidate>> discoverCandidatesByPost(
            List<LocalDate> similarDatesOrdered,
            Map<String, DayShiftRevenueStatVo> historyByDay,
            CommonFixedShiftDiscoveryConfig cfg) {
        if (cfg == null) {
            throw new IllegalArgumentException("cfg");
        }
        List<LocalDate> sims = similarDatesOrdered == null ? Collections.emptyList() : similarDatesOrdered;
        int similarTotal = sims.size();
        Map<String, Map<LocalDate, Integer>> patternKeyToDayOcc =
                new LinkedHashMap<>(); // post|sm|em -> day -> count that day

        for (LocalDate d : sims) {
            if (d == null) {
                continue;
            }
            DayShiftRevenueStatVo vo = historyByDay == null ? null : historyByDay.get(d.toString());
            if (vo == null || vo.getShifts() == null) {
                continue;
            }
            for (ShiftPostStatVo shift : vo.getShifts()) {
                if (shift == null || Boolean.TRUE.equals(shift.getLineSchedule())) {
                    continue;
                }
                List<DayMinuteFragment> frags = splitToSameDayMinuteFragments(d, shift);
                for (DayMinuteFragment f : frags) {
                    int[] norm = ShiftHalfHourNormalizer.normalizeMinuteRange(f.startMinuteInclusive, f.endMinuteExclusive);
                    if (norm == null) {
                        continue;
                    }
                    int sm = norm[0];
                    int em = norm[1];
                    int dur = em - sm;
                    if (dur < cfg.getLegalMinShiftMinutes() || dur > cfg.getLegalMaxShiftMinutes()) {
                        continue;
                    }
                    if (!cfg.isWindowWithinBusiness(sm, em)) {
                        continue;
                    }
                    for (ShiftPostHeadcountVo p : emptyIfNull(shift.getPosts())) {
                        if (p == null || p.getHeadcount() == null || p.getHeadcount() <= 0) {
                            continue;
                        }
                        String postId = p.getPostId() == null ? "" : p.getPostId();
                        String shiftSid =
                                shift.getShiftId() == null || shift.getShiftId().isBlank()
                                        ? ""
                                        : shift.getShiftId().trim();
                        String key = postId + "|" + sm + "|" + em + "|" + shiftSid;
                        patternKeyToDayOcc
                                .computeIfAbsent(key, k -> new LinkedHashMap<>())
                                .merge(f.rowDate, 1, Integer::sum);
                    }
                }
            }
        }

        List<PostFixedShiftCandidate> raw = new ArrayList<>();
        for (Map.Entry<String, Map<LocalDate, Integer>> e : patternKeyToDayOcc.entrySet()) {
            String[] parsed = parsePatternKey(e.getKey());
            String postId = parsed[0];
            int sm = Integer.parseInt(parsed[1]);
            int em = Integer.parseInt(parsed[2]);
            String histRaw = parsed[3];
            String histShiftStored = histRaw == null || histRaw.isBlank() ? null : histRaw;
            Map<LocalDate, Integer> byDay = e.getValue();
            int dayCount = 0;
            int occ = 0;
            for (Integer c : byDay.values()) {
                if (c != null && c > 0) {
                    dayCount++;
                    occ += c;
                }
            }
            String label = ShiftHalfHourNormalizer.toLabel(sm, em);
            raw.add(
                    new PostFixedShiftCandidate(
                            postId,
                            label,
                            sm,
                            em,
                            dayCount,
                            occ,
                            similarTotal,
                            histShiftStored));
        }

        List<PostFixedShiftCandidate> pool = new ArrayList<>();
        for (PostFixedShiftCandidate c : raw) {
            if (passesInitialPool(c, cfg)) {
                pool.add(c);
            }
        }

        List<PostFixedShiftCandidate> finalList = new ArrayList<>();
        for (PostFixedShiftCandidate c : pool) {
            if (passesFinalTier(c, cfg)) {
                finalList.add(c);
            }
        }

        finalList.sort(
                Comparator.comparing(PostFixedShiftCandidate::getPostId)
                        .thenComparingInt(PostFixedShiftCandidate::getStartMinuteInclusive)
                        .thenComparingInt(PostFixedShiftCandidate::getEndMinuteExclusive));

        Map<String, List<PostFixedShiftCandidate>> byPost = new TreeMap<>();
        for (PostFixedShiftCandidate c : finalList) {
            byPost.computeIfAbsent(c.getPostId(), k -> new ArrayList<>()).add(c);
        }
        return Collections.unmodifiableMap(new LinkedHashMap<>(byPost));
    }

    private static boolean passesInitialPool(PostFixedShiftCandidate c, CommonFixedShiftDiscoveryConfig cfg) {
        double ratio = c.getAppearanceDayRatio();
        return c.getAppearanceDayCount() >= cfg.getPoolMinAppearanceDays()
                || c.getAppearanceCount() >= cfg.getPoolMinAppearanceCount()
                || ratio + 1e-9 >= cfg.getPoolMinDayRatio();
    }

    private static boolean passesFinalTier(PostFixedShiftCandidate c, CommonFixedShiftDiscoveryConfig cfg) {
        int n = c.getSimilarHistoryDayTotal();
        if (n >= cfg.getSampleLargeThresholdDays()) {
            return c.getAppearanceDayRatio() + 1e-9 >= cfg.getFinalMinDayRatioWhenSampleLarge();
        }
        if (n >= cfg.getSampleMediumLowDays() && n <= cfg.getSampleMediumHighDays()) {
            return c.getAppearanceDayCount() >= cfg.getFinalMinDaysWhenSampleMedium();
        }
        return true;
    }

    private static String[] parsePatternKey(String key) {
        String[] p = key.split("\\|", 4);
        if (p.length < 3) {
            return new String[] {"", "0", "0", ""};
        }
        String sidPart = p.length >= 4 ? p[3] : "";
        return new String[] {p[0], p[1], p[2], sidPart};
    }

    private static List<ShiftPostHeadcountVo> emptyIfNull(List<ShiftPostHeadcountVo> posts) {
        return posts == null ? Collections.emptyList() : posts;
    }

    /** 将一条班次拆成自然日内的 [start,end) 分钟片段；跨午夜拆为两段。 */
    static List<DayMinuteFragment> splitToSameDayMinuteFragments(LocalDate rowDate, ShiftPostStatVo shift) {
        List<DayMinuteFragment> out = new ArrayList<>();
        LocalTime s = parseHhMm(shift.getStartTime());
        LocalTime e = parseHhMm(shift.getEndTime());
        if (s == null || e == null) {
            return out;
        }
        int sm = HalfHourSlotLabel.localTimeToMinute(s);
        int em = HalfHourSlotLabel.localTimeToMinute(e);
        if (e.isAfter(s)) {
            out.add(new DayMinuteFragment(rowDate, sm, em));
            return out;
        }
        out.add(new DayMinuteFragment(rowDate, sm, 24 * 60));
        out.add(new DayMinuteFragment(rowDate.plusDays(1), 0, em));
        return out;
    }

    private static LocalTime parseHhMm(String hhMm) {
        if (hhMm == null) {
            return null;
        }
        String t = hhMm.trim();
        if (t.isEmpty()) {
            return null;
        }
        try {
            return LocalTime.parse(t);
        } catch (DateTimeParseException e) {
            return null;
        }
    }

    static final class DayMinuteFragment {
        final LocalDate rowDate;
        final int startMinuteInclusive;
        final int endMinuteExclusive;

        DayMinuteFragment(LocalDate rowDate, int startMinuteInclusive, int endMinuteExclusive) {
            this.rowDate = Objects.requireNonNull(rowDate);
            this.startMinuteInclusive = startMinuteInclusive;
            this.endMinuteExclusive = endMinuteExclusive;
        }
    }
}
