package jnpf.attendance.schedule;

import jnpf.model.attendance.vo.attendance.UserWorkSituationVo;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

/**
 * 排班区间内<strong>动态累计</strong>员工工时与工作状态，供第十步规则与
 * {@link ShiftPlanFinalStaffAssigner#buildContext} 使用。
 * <p>
 * <b>生命周期</b>
 * <ol>
 *   <li>{@link #from(Map)}：用考勤组查询的初始 {@link UserWorkSituationVo} 快照初始化；</li>
 *   <li>每次 {@link ShiftPlanFinalStaffAssigner#assign} 成功选中一人后调用 {@link #recordAssignment}；</li>
 *   <li>{@link #asSituationMap()} 返回的 map 与内部状态<strong>同一引用</strong>，随排班推进持续更新。</li>
 * </ol>
 * <p>
 * 与 {@link StaffAssignmentDayLedger} 分工：Tracker 管工时/周累计/休息间隔；Ledger 管同日固定 vs 划线互斥。
 */
public final class SchedulePeriodWorkTracker {

    /** 员工工作状态（连续天数、周工作天数、周工时），选人后递增 */
    private final Map<String, UserWorkSituationVo> situationByUserId;
    /** 员工 → 排班日 → 当日已排工时（小时） */
    private final Map<String, Map<LocalDate, Double>> hoursByUserThenDay = new HashMap<>();
    /** 员工 → 排班日 → 当日最后一段班的结束分钟（0～1440，用于同日/跨日休息间隔） */
    private final Map<String, Map<LocalDate, Integer>> lastEndMinuteByUserThenDay = new HashMap<>();
    /** 员工 → 最近一次被分配的自然日（用于连续工作天数） */
    private final Map<String, LocalDate> lastAssignedDayByUser = new HashMap<>();

    private SchedulePeriodWorkTracker(Map<String, UserWorkSituationVo> situationByUserId) {
        this.situationByUserId = situationByUserId;
    }

    /**
     * 由排班开始前的人事统计快照构建 Tracker。
     *
     * @param base userId → 初始 {@link UserWorkSituationVo}（如 {@code attendanceDayStatisticsService.queryUserWorkSituation}）
     */
    public static SchedulePeriodWorkTracker from(Map<String, UserWorkSituationVo> base) {
        Map<String, UserWorkSituationVo> copy = new LinkedHashMap<>();
        if (base != null) {
            for (Map.Entry<String, UserWorkSituationVo> e : base.entrySet()) {
                if (e.getKey() == null || e.getKey().trim().isEmpty()) {
                    continue;
                }
                UserWorkSituationVo src = e.getValue();
                copy.put(
                        e.getKey().trim(),
                        UserWorkSituationVo.builder()
                                .continuousDays(src == null ? null : src.getContinuousDays())
                                .weekWorkDays(src == null ? null : src.getWeekWorkDays())
                                .weekWorkHours(
                                        src == null || src.getWeekWorkHours() == null
                                                ? BigDecimal.ZERO
                                                : src.getWeekWorkHours())
                                .build());
            }
        }
        return new SchedulePeriodWorkTracker(copy);
    }

    /**
     * 供 {@link StaffRuleEvaluationPortUsingScheduleRulesAndWorkSituation} 读取的最新工作状态。
     * <p>
     * 返回内部 map 的不可变视图包装；排班过程中 {@link #recordAssignment} 会修改其中 VO 字段。
     */
    public Map<String, UserWorkSituationVo> asSituationMap() {
        return Collections.unmodifiableMap(situationByUserId);
    }

    /**
     * 该员工在指定排班日<strong>已分配</strong>的工时合计（小时）。
     * <p>
     * 用于构造 {@link StaffAssignmentContext#getExistingSameDayScheduledWorkHours()}。
     */
    public double getSameDayScheduledHours(String employeeId, LocalDate day) {
        if (employeeId == null || day == null) {
            return 0d;
        }
        Map<LocalDate, Double> byDay = hoursByUserThenDay.get(employeeId.trim());
        if (byDay == null) {
            return 0d;
        }
        return byDay.getOrDefault(day, 0d);
    }

    /**
     * 当前拟排班段开始时刻，与<strong>上一段已排班结束</strong>时刻的间隔（分钟）。
     * <ul>
     *   <li>同日：{@code startMinuteInclusive - sameDayEnd}（仅当 start ≥ 上一段结束）；</li>
     *   <li>跨日：前一日最后结束到当日 0 点 + 当日开始分钟；</li>
     *   <li>无上一段：{@code null}（最小休息规则不判违反）。</li>
     * </ul>
     */
    public Integer minutesRestSincePriorShiftEnd(
            String employeeId, LocalDate scheduleDay, int startMinuteInclusive) {
        if (employeeId == null || scheduleDay == null) {
            return null;
        }
        String id = employeeId.trim();
        Map<LocalDate, Integer> ends = lastEndMinuteByUserThenDay.get(id);
        if (ends != null) {
            Integer sameDayEnd = ends.get(scheduleDay);
            if (sameDayEnd != null && startMinuteInclusive >= sameDayEnd) {
                return startMinuteInclusive - sameDayEnd;
            }
        }
        LocalDate prev = scheduleDay.minusDays(1);
        if (ends != null) {
            Integer prevEnd = ends.get(prev);
            if (prevEnd != null) {
                return (24 * 60 - prevEnd) + startMinuteInclusive;
            }
        }
        return null;
    }

    /**
     * 记录一次成功排班分配，更新日工时、段结束时刻、连续天数、周工作天数、周工时。
     * <p>
     * 应在 {@link ShiftPlanFinalStaffAssigner} 将员工写入结果集<strong>之后</strong>调用，
     * 以便后续班次能读到本段的累计效应。
     *
     * @param startMinuteInclusive 班段开始（当日 0 点起算分钟）
     * @param endMinuteExclusive   班段结束（开区间）
     */
    public void recordAssignment(
            String employeeId, LocalDate scheduleDay, int startMinuteInclusive, int endMinuteExclusive) {
        Objects.requireNonNull(scheduleDay, "scheduleDay");
        if (employeeId == null || employeeId.trim().isEmpty()) {
            return;
        }
        String id = employeeId.trim();
        double hours = Math.max(0d, (endMinuteExclusive - startMinuteInclusive) / 60.0);
        hoursByUserThenDay
                .computeIfAbsent(id, k -> new HashMap<>())
                .merge(scheduleDay, hours, Double::sum);
        lastEndMinuteByUserThenDay
                .computeIfAbsent(id, k -> new HashMap<>())
                .merge(scheduleDay, endMinuteExclusive, Math::max);

        UserWorkSituationVo vo =
                situationByUserId.computeIfAbsent(id, k -> UserWorkSituationVo.builder().build());
        LocalDate last = lastAssignedDayByUser.get(id);
        boolean firstOnDay = last == null || !last.equals(scheduleDay);
        if (firstOnDay) {
            int prevConsecutive = nz(vo.getContinuousDays());
            if (last == null || last.equals(scheduleDay.minusDays(1))) {
                vo.setContinuousDays(prevConsecutive + 1);
            } else {
                vo.setContinuousDays(1);
            }
            vo.setWeekWorkDays(nz(vo.getWeekWorkDays()) + 1);
        }
        BigDecimal wh = vo.getWeekWorkHours() == null ? BigDecimal.ZERO : vo.getWeekWorkHours();
        vo.setWeekWorkHours(wh.add(BigDecimal.valueOf(hours)));
        lastAssignedDayByUser.put(id, scheduleDay);
    }

    private static int nz(Integer x) {
        return x == null ? 0 : x;
    }
}
