package jnpf.attendance.schedule;

import jnpf.model.attendance.vo.PeriodNameVO;
import jnpf.model.attendance.vo.ShiftPeriodVo;
import jnpf.model.attendance.vo.attendance.ShiftNameViewVo;

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.Optional;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

/**
 * 将「相似历史日统计出的固定班次」与考勤组 {@link ShiftPeriodVo}（{@code getPeriodList}）做匹配。
 *
 * <p>匹配顺序：
 * <ul>
 *   <li>若历史侧带有考勤班次 id（{@link jnpf.model.attendance.vo.ShiftPostStatVo#getShiftId()}／{@linkplain PostFixedShiftCandidate#getHistoricalAttendanceShiftId()}），
 *       且在 {@code ShiftPeriodVo} 列表中存在同 id，则直接使用该班次，<strong>不</strong>做下列时段阈值与择优；
 * </li>
 *   <li>否则：「精确」／阈值模糊匹配择优（见下）。
 * </li>
 * </ul>
 *
 * <p>规则摘要（时段路径）：
 * <ul>
 *   <li>先找「精确」：覆盖度与有效率均为 1（考勤总时长全部落在历史需求内、且历史需求被完全覆盖）。</li>
 *   <li>否则在覆盖度、有效率阈值下选最优：先比有效率，再比覆盖度；仍并列则随机。</li>
 *   <li>无满足项则该历史班次不参与后续固定班排班。</li>
 * </ul>
 */
public final class AttendanceGroupShiftPeriodMatcher {

    private static final double EPS = 1e-6;

    private AttendanceGroupShiftPeriodMatcher() {}

    /**
     * 历史班次 id 与考勤组班次 {@linkplain ShiftPeriodVo#getId()} 一致时直接使用，不进行覆盖度／有效率比对。
     *
     * @return 命中则精确结果；{@code periods} / id 不可用则 empty
     */
    public static Optional<AttendanceGroupShiftMatchResult> tryMatchConfiguredShiftById(
            String historicalAttendanceShiftId, List<ShiftPeriodVo> periods) {
        String hid = normalizedGroupShiftId(historicalAttendanceShiftId);
        if (hid.isEmpty() || periods == null || periods.isEmpty()) {
            return Optional.empty();
        }
        for (ShiftPeriodVo vo : periods) {
            if (vo == null || vo.getId() == null) {
                continue;
            }
            if (hid.equals(normalizedGroupShiftId(vo.getId()))) {
                String boundId = vo.getId().trim();
                return Optional.of(new AttendanceGroupShiftMatchResult(boundId, 1d, 1d, true));
            }
        }
        return Optional.empty();
    }

    private static String normalizedGroupShiftId(String s) {
        return s == null ? "" : s.trim();
    }

    /**
     * @param periods {@code null} 表示不做过滤（兼容旧调用）；空列表表示无考勤班次配置，将丢弃全部历史固定班候选。
     */
    public static Map<String, List<PostFixedShiftCandidate>> filterCandidatesByAttendancePeriods(
            Map<String, List<PostFixedShiftCandidate>> candidatesByPost,
            List<ShiftPeriodVo> periods,
            AttendanceGroupShiftMatchConfig cfg,
            Random random) {
        if (candidatesByPost == null || candidatesByPost.isEmpty()) {
            return candidatesByPost == null
                    ? Collections.emptyMap()
                    : Collections.unmodifiableMap(new LinkedHashMap<>());
        }
        if (periods == null) {
            return Collections.unmodifiableMap(new LinkedHashMap<>(candidatesByPost));
        }
        if (periods.isEmpty()) {
            Map<String, List<PostFixedShiftCandidate>> empty = new LinkedHashMap<>();
            for (String k : candidatesByPost.keySet()) {
                empty.put(k, Collections.emptyList());
            }
            return Collections.unmodifiableMap(empty);
        }
        AttendanceGroupShiftMatchConfig c = cfg == null ? AttendanceGroupShiftMatchConfig.defaults() : cfg;
        Random rnd = random == null ? ThreadLocalRandom.current() : random;
        Map<String, List<PostFixedShiftCandidate>> out = new LinkedHashMap<>();
        for (Map.Entry<String, List<PostFixedShiftCandidate>> e : candidatesByPost.entrySet()) {
            List<PostFixedShiftCandidate> kept = new ArrayList<>();
            for (PostFixedShiftCandidate cand : emptyIfNull(e.getValue())) {
                if (pickBestMatch(cand, periods, c, rnd).isPresent()) {
                    kept.add(cand);
                }
            }
            out.put(e.getKey(), Collections.unmodifiableList(kept));
        }
        return Collections.unmodifiableMap(out);
    }

    public static Optional<AttendanceGroupShiftMatchResult> pickBestMatch(
            PostFixedShiftCandidate historical,
            List<ShiftPeriodVo> periods,
            AttendanceGroupShiftMatchConfig cfg,
            Random random) {
        if (historical == null) {
            return Optional.empty();
        }
        Optional<AttendanceGroupShiftMatchResult> direct =
                tryMatchConfiguredShiftById(historical.getHistoricalAttendanceShiftId(), periods);
        if (direct.isPresent()) {
            return direct;
        }
        return pickBestMatchForTimeWindow(
                historical.getStartMinuteInclusive(),
                historical.getEndMinuteExclusive(),
                periods,
                cfg,
                random,
                null);
    }

    /**
     * 将计划时段与考勤组班次匹配：先可选按历史快照 id（{@link #tryMatchConfiguredShiftById}）；否则时段路径下先精确，
     * 否则在阈值下选最优。
     *
     * @param startMinuteInclusive 计划班次左闭起点（分钟）
     * @param endMinuteExclusive   计划班次右开终点（分钟）
     * @see #pickBestMatchForTimeWindow(int, int, List, AttendanceGroupShiftMatchConfig, Random, String)
     */
    public static Optional<AttendanceGroupShiftMatchResult> pickBestMatchForTimeWindow(
            int startMinuteInclusive,
            int endMinuteExclusive,
            List<ShiftPeriodVo> periods,
            AttendanceGroupShiftMatchConfig cfg,
            Random random) {
        return pickBestMatchForTimeWindow(
                startMinuteInclusive, endMinuteExclusive, periods, cfg, random, null);
    }

    /**
     * @param historicalAttendanceShiftId 若非空且在 {@code periods} 中存在相同 id，则不与计划时段比对，直接使用该班次
     */
    public static Optional<AttendanceGroupShiftMatchResult> pickBestMatchForTimeWindow(
            int startMinuteInclusive,
            int endMinuteExclusive,
            List<ShiftPeriodVo> periods,
            AttendanceGroupShiftMatchConfig cfg,
            Random random,
            String historicalAttendanceShiftId) {
        Optional<AttendanceGroupShiftMatchResult> direct =
                tryMatchConfiguredShiftById(historicalAttendanceShiftId, periods);
        if (direct.isPresent()) {
            return direct;
        }
        if (periods == null || periods.isEmpty()) {
            return Optional.empty();
        }
        AttendanceGroupShiftMatchConfig c = cfg == null ? AttendanceGroupShiftMatchConfig.defaults() : cfg;
        Random rnd = random == null ? ThreadLocalRandom.current() : random;
        int hS = startMinuteInclusive;
        int hE = endMinuteExclusive;
        int histLen = hE - hS;
        if (histLen <= 0) {
            return Optional.empty();
        }

        List<ScoredShift> exact = new ArrayList<>();
        List<ScoredShift> fuzzy = new ArrayList<>();
        for (ShiftPeriodVo vo : periods) {
            if (vo == null) {
                continue;
            }
            ScoredShift s = score(vo, hS, hE, histLen);
            if (s == null) {
                continue;
            }
            if (s.exact) {
                exact.add(s);
            } else if (s.coverage + EPS >= c.getMinHistoryCoverageRatio()
                    && s.efficiency + EPS >= c.getMinCandidateEfficiencyRatio()) {
                fuzzy.add(s);
            }
        }

        if (!exact.isEmpty()) {
            return Optional.of(toResult(pickRandom(exact, rnd)));
        }
        if (fuzzy.isEmpty()) {
            return Optional.empty();
        }
        fuzzy.sort(
                Comparator.comparingDouble((ScoredShift s) -> s.efficiency)
                        .thenComparingDouble(s -> s.coverage)
                        .thenComparing(s -> s.shiftId, Comparator.nullsFirst(String::compareTo))
                        .reversed());
        double bestEff = fuzzy.get(0).efficiency;
        double bestCov = fuzzy.get(0).coverage;
        List<ScoredShift> tier = new ArrayList<>();
        for (ScoredShift s : fuzzy) {
            if (Math.abs(s.efficiency - bestEff) <= EPS && Math.abs(s.coverage - bestCov) <= EPS) {
                tier.add(s);
            }
        }
        return Optional.of(toResult(pickRandom(tier, rnd)));
    }

    private static AttendanceGroupShiftMatchResult toResult(ScoredShift s) {
        return new AttendanceGroupShiftMatchResult(s.shiftId, s.coverage, s.efficiency, s.exact);
    }

    private static ScoredShift pickRandom(List<ScoredShift> list, Random rnd) {
        return list.get(rnd.nextInt(list.size()));
    }

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

    private static final class ScoredShift {
        final String shiftId;
        final double coverage;
        final double efficiency;
        final boolean exact;

        ScoredShift(String shiftId, double coverage, double efficiency, boolean exact) {
            this.shiftId = shiftId;
            this.coverage = coverage;
            this.efficiency = efficiency;
            this.exact = exact;
        }
    }

    /** @return null 若无法解析出有效时段 */
    private static ScoredShift score(ShiftPeriodVo vo, int hS, int hE, int histLen) {
        List<int[]> merged = mergedNormalizedSegments(vo);
        if (merged.isEmpty()) {
            return null;
        }
        long overlap = 0;
        for (int[] seg : merged) {
            overlap += overlapLen(seg[0], seg[1], hS, hE);
        }
        long totalCand = 0;
        for (int[] seg : merged) {
            totalCand += (long) seg[1] - seg[0];
        }
        if (totalCand <= 0) {
            return null;
        }
        double coverage = overlap / (double) histLen;
        double efficiency = overlap / (double) totalCand;
        boolean exact = coverage + EPS >= 1d && efficiency + EPS >= 1d;
        String id = vo.getId() == null ? "" : vo.getId();
        return new ScoredShift(id, coverage, efficiency, exact);
    }

    private static long overlapLen(int a, int b, int hS, int hE) {
        return Math.max(0, (long) Math.min(b, hE) - Math.max(a, hS));
    }

    private static List<int[]> mergedNormalizedSegments(ShiftPeriodVo vo) {
        ShiftNameViewVo sn = vo.getSchedulingPeriod();
        if (sn == null || sn.getPeriodNames() == null || sn.getPeriodNames().isEmpty()) {
            return Collections.emptyList();
        }
        List<int[]> raw = new ArrayList<>();
        for (PeriodNameVO p : sn.getPeriodNames()) {
            if (p == null) {
                continue;
            }
            for (int[] seg : splitToSameDayHalfHourSegments(p)) {
                if (seg[1] > seg[0]) {
                    raw.add(seg);
                }
            }
        }
        if (raw.isEmpty()) {
            return Collections.emptyList();
        }
        raw.sort(Comparator.comparingInt(a -> a[0]));
        List<int[]> merged = new ArrayList<>();
        int curS = raw.get(0)[0];
        int curE = raw.get(0)[1];
        for (int i = 1; i < raw.size(); i++) {
            int s = raw.get(i)[0];
            int e = raw.get(i)[1];
            if (s <= curE) {
                curE = Math.max(curE, e);
            } else {
                merged.add(new int[] {curS, curE});
                curS = s;
                curE = e;
            }
        }
        merged.add(new int[] {curS, curE});
        return merged;
    }

    /**
     * 将一条打卡时段拆到自然日 [0,1440) 上的半小时对齐区间；跨午夜拆两段（与历史排班片段对齐方式一致）。
     */
    private static List<int[]> splitToSameDayHalfHourSegments(PeriodNameVO p) {
        LocalTime inT = parseClockPoint(p.getInPoint());
        LocalTime outT = parseClockPoint(p.getOutPoint());
        if (inT == null || outT == null) {
            return Collections.emptyList();
        }
        int sm = HalfHourSlotLabel.localTimeToMinute(inT);
        int em = HalfHourSlotLabel.localTimeToMinute(outT);
        List<int[]> out = new ArrayList<>();
        if (outT.isAfter(inT)) {
            int[] norm = ShiftHalfHourNormalizer.normalizeMinuteRange(sm, em);
            if (norm != null && norm[1] > norm[0]) {
                out.add(norm);
            }
            return out;
        }
        int[] n1 = ShiftHalfHourNormalizer.normalizeMinuteRange(sm, 24 * 60);
        if (n1 != null && n1[1] > n1[0]) {
            out.add(n1);
        }
        int[] n2 = ShiftHalfHourNormalizer.normalizeMinuteRange(0, em);
        if (n2 != null && n2[1] > n2[0]) {
            out.add(n2);
        }
        return out;
    }

    static LocalTime parseClockPoint(String s) {
        if (s == null) {
            return null;
        }
        String t = s.trim();
        if (t.isEmpty()) {
            return null;
        }
        try {
            if (t.length() >= 5 && t.charAt(2) == ':') {
                return LocalTime.parse(t.substring(0, 5));
            }
            int sp = t.lastIndexOf(' ');
            if (sp >= 0 && sp + 6 <= t.length()) {
                String tail = t.substring(sp + 1, sp + 6);
                if (tail.charAt(2) == ':') {
                    return LocalTime.parse(tail);
                }
            }
            return LocalTime.parse(t);
        } catch (DateTimeParseException e) {
            return null;
        }
    }
}
