package jnpf.attendance.schedule;

import java.io.Serializable;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

/**
 * 由半小时需求矩阵整理出的「完整班次」一段：起止时间、是否固定排班、各岗位需求人数。
 *
 * <p>时间语义与 {@link HalfHourSlotLabel} 一致：左闭右开 {@code [startMinuteInclusive, endMinuteExclusive)}，
 * 且须满足 {@link #isValidTimeWindow(int, int)}（自然日内 0～1440 分钟坐标，{@code endExclusive} 可至 1440 表示到当日 24:00）。
 */
public final class ShiftPlanBlock implements Serializable {

    /** 当日分钟上界：右端点可取 1440 表示 24:00（与 {@link HalfHourSlotLabel} 语义一致）。 */
    public static final int MAX_MINUTE_EXCLUSIVE = 24 * 60;

    private final LocalDate scheduleDay;
    private final int startMinuteInclusive;
    private final int endMinuteExclusive;
    /** 展示用起止，如 {@code 9:00-14:00}（与 {@link #getTimeRangeLabel()} 的语义一致，小时不补零）。 */
    private final String timeRangeText;
    private final boolean fixedScheduling;
    /** 考勤组 {@link jnpf.model.attendance.vo.ShiftPeriodVo#getId()}，经匹配后写入；未匹配前为 {@code null}。 */
    private final String shiftId;
    /**
     * 来自历史 {@link jnpf.model.attendance.vo.ShiftPostStatVo#getShiftId()}（仅草稿固定班链路携带）；若在配置中与某 {@link ShiftPeriodVo} 同 id，
     * 则 {@linkplain ShiftPlanAttendanceShiftIdResolver} 可先于时段模糊匹配绑定该班次。
     */
    private final String historicalAttendanceShiftId;
    private final List<ShiftPlanPostNeed> postNeeds;

    public ShiftPlanBlock(
            LocalDate scheduleDay,
            int startMinuteInclusive,
            int endMinuteExclusive,
            boolean fixedScheduling,
            List<ShiftPlanPostNeed> postNeeds) {
        this(scheduleDay, startMinuteInclusive, endMinuteExclusive, fixedScheduling, postNeeds, null, null);
    }

    public ShiftPlanBlock(
            LocalDate scheduleDay,
            int startMinuteInclusive,
            int endMinuteExclusive,
            boolean fixedScheduling,
            List<ShiftPlanPostNeed> postNeeds,
            String shiftId) {
        this(scheduleDay, startMinuteInclusive, endMinuteExclusive, fixedScheduling, postNeeds, shiftId, null);
    }

    public ShiftPlanBlock(
            LocalDate scheduleDay,
            int startMinuteInclusive,
            int endMinuteExclusive,
            boolean fixedScheduling,
            List<ShiftPlanPostNeed> postNeeds,
            String shiftId,
            String historicalAttendanceShiftId) {
        this.scheduleDay = Objects.requireNonNull(scheduleDay, "scheduleDay");
        validateTimeWindow(startMinuteInclusive, endMinuteExclusive);
        this.startMinuteInclusive = startMinuteInclusive;
        this.endMinuteExclusive = endMinuteExclusive;
        this.timeRangeText =
                HalfHourSlotLabel.formatMinuteRangeDisplay(startMinuteInclusive, endMinuteExclusive);
        this.fixedScheduling = fixedScheduling;
        this.shiftId = shiftId == null || shiftId.isEmpty() ? null : shiftId;
        this.historicalAttendanceShiftId =
                historicalAttendanceShiftId == null || historicalAttendanceShiftId.isBlank()
                        ? null
                        : historicalAttendanceShiftId.trim();
        this.postNeeds =
                postNeeds == null
                        ? Collections.emptyList()
                        : Collections.unmodifiableList(new ArrayList<>(postNeeds));
    }

    /** 写入考勤组班次 id，其余字段不变。 */
    public ShiftPlanBlock withShiftId(String shiftId) {
        return new ShiftPlanBlock(
                scheduleDay,
                startMinuteInclusive,
                endMinuteExclusive,
                fixedScheduling,
                postNeeds,
                shiftId,
                historicalAttendanceShiftId);
    }

    /**
     * 自然日分钟坐标下的合法班段（左闭右开）：{@code 0 ≤ start < end ≤ 1440}。
     *
     * <p>跨自然日的夜班须拆到两日或多段，不得用「end &lt; start」在同一 {@link LocalDate} 上编码。
     */
    public static boolean isValidTimeWindow(int startMinuteInclusive, int endMinuteExclusive) {
        return startMinuteInclusive >= 0
                && startMinuteInclusive < MAX_MINUTE_EXCLUSIVE
                && endMinuteExclusive > startMinuteInclusive
                && endMinuteExclusive <= MAX_MINUTE_EXCLUSIVE;
    }

    private static void validateTimeWindow(int startMinuteInclusive, int endMinuteExclusive) {
        if (!isValidTimeWindow(startMinuteInclusive, endMinuteExclusive)) {
            throw new IllegalArgumentException(
                    "invalid shift window [start,end)=["
                            + startMinuteInclusive
                            + ","
                            + endMinuteExclusive
                            + "); require 0 ≤ start < "
                            + MAX_MINUTE_EXCLUSIVE
                            + ", start < end ≤ "
                            + MAX_MINUTE_EXCLUSIVE);
        }
    }

    public LocalDate getScheduleDay() {
        return scheduleDay;
    }

    public int getStartMinuteInclusive() {
        return startMinuteInclusive;
    }

    public int getEndMinuteExclusive() {
        return endMinuteExclusive;
    }

    /**
     * 班次开始—结束的字符串，如 {@code 9:00-14:00}；与分钟字段同义，左闭右开区间的右端点为「结束时刻」展示值。
     */
    public String getTimeRangeText() {
        return timeRangeText;
    }

    public boolean isFixedScheduling() {
        return fixedScheduling;
    }

    public String getShiftId() {
        return shiftId;
    }

    public String getHistoricalAttendanceShiftId() {
        return historicalAttendanceShiftId;
    }

    public List<ShiftPlanPostNeed> getPostNeeds() {
        return postNeeds;
    }

    /** 与 {@link HalfHourSlotLabel#formatMinuteRange} 一致的可读起止。 */
    public String getTimeRangeLabel() {
        return HalfHourSlotLabel.formatMinuteRange(startMinuteInclusive, endMinuteExclusive);
    }

    @Override
    public String toString() {
        return "ShiftPlanBlock{"
                + "day=" + scheduleDay
                + ", time=" + timeRangeText
                + (shiftId != null ? ", shiftId=" + shiftId : "")
                + ", fixed=" + fixedScheduling
                + ", needs=" + postNeeds
                + '}';
    }
}
