package jnpf.attendance.schedule;

import lombok.Getter;

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

/**
 * 最终排班结果：按班次维度与按员工维度两种视图；另附不完整说明（如日级班段剔除、岗位未排满原因）。
 */
public final class ShiftPlanAssignmentResult implements Serializable {

    /** 当日班段均未匹配考勤组班次时段并已移除（AutoSchedule matcher 粒度：按日汇总剔除块数）。 */
    public static final String INCOMPLETE_ATTEND_SHIFT_BLOCKS_DROPPED = "ATTEND_SHIFT_BLOCKS_DROPPED";

    /**
     * 相似日第三步判定成功，但召回到的全部历史日在半小时展开后均无可用班次岗位数据（班次结构缺失或无法解析）。
     * 文案对齐 intelligent-schedule-product-constraints_reply §2.4。
     */
    public static final String INCOMPLETE_SIMILAR_HISTORY_NO_SHIFT_DATA = "SIMILAR_HISTORY_NO_SHIFT_DATA";

    /**
     * 「无营业额 / 仅用排班规律」相似日中，近窗有效排班不足（产品：先完成若干天手动排班后再智能排班），见
     * {@link SchedulePatternSimilarDaysAlgorithm}。
     */
    public static final String INCOMPLETE_PATTERN_HISTORY_INSUFFICIENT = "PATTERN_HISTORY_INSUFFICIENT";

    /** 过滤后当日该岗无专岗/通岗可排候选人。 */
    public static final String INCOMPLETE_NO_CANDIDATE = "NO_CANDIDATE";

    /** 候选人与工作站岗位绑定名单无交集。 */
    public static final String INCOMPLETE_WORKSTATION_POOL_EMPTY = "WORKSTATION_POOL_EMPTY";

    /** 历史需求中的岗位在主数据已无记录（产品与文档 intelligent-schedule §7.4），请手动处理。 */
    public static final String INCOMPLETE_HISTORICAL_POST_MISSING_MASTER = "HISTORICAL_POST_NOT_IN_MASTER";

    /** 固定班「必须」规则下候选无人可用（本班段该岗未排任一人员时常为此类）。 */
    public static final String INCOMPLETE_FIXED_MUST_BLOCK = "FIXED_MUST_BLOCK";

    /** 第九步硬约束、固定尽量项、同人块内占用、时段重叠等综合原因导致未凑满需求。 */
    public static final String INCOMPLETE_RULES_OR_CONFLICT = "RULES_OR_CONFLICT";

    private final List<AssignedShiftBlockView> byShift;
    private final List<AssignedEmployeeView> byEmployee;
    private final List<IncompleteScheduleReason> incompleteScheduleReasons;

    public ShiftPlanAssignmentResult(
            List<AssignedShiftBlockView> byShift, List<AssignedEmployeeView> byEmployee) {
        this(byShift, byEmployee, Collections.emptyList());
    }

    public ShiftPlanAssignmentResult(
            List<AssignedShiftBlockView> byShift,
            List<AssignedEmployeeView> byEmployee,
            List<IncompleteScheduleReason> incompleteScheduleReasons) {
        this.byShift =
                byShift == null
                        ? Collections.emptyList()
                        : Collections.unmodifiableList(byShift);
        this.byEmployee =
                byEmployee == null
                        ? Collections.emptyList()
                        : Collections.unmodifiableList(byEmployee);
        this.incompleteScheduleReasons =
                incompleteScheduleReasons == null
                        ? Collections.emptyList()
                        : Collections.unmodifiableList(new ArrayList<>(incompleteScheduleReasons));
    }

    /**
     * 将前置说明（如考勤班次匹配剔除）与用户分配阶段说明串联为新的结果视图（班次与人不变）。
     */
    public static ShiftPlanAssignmentResult prependIncompleteReasons(
            List<IncompleteScheduleReason> prefix, ShiftPlanAssignmentResult result) {
        Objects.requireNonNull(result, "result");
        List<IncompleteScheduleReason> p = prefix == null ? Collections.emptyList() : prefix;
        if (p.isEmpty()) {
            return result;
        }
        List<IncompleteScheduleReason> merged = new ArrayList<>(p.size() + result.incompleteScheduleReasons.size());
        merged.addAll(p);
        merged.addAll(result.incompleteScheduleReasons);
        return new ShiftPlanAssignmentResult(result.byShift, result.byEmployee, merged);
    }

    public List<AssignedShiftBlockView> getByShift() {
        return byShift;
    }

    public List<AssignedEmployeeView> getByEmployee() {
        return byEmployee;
    }

    /**
     * 业务侧不完整说明：每项对应某日（及可选的具体班段/岗位）的人类可读描述与机器可读 {@link #getReasonCode()}。
     */
    public List<IncompleteScheduleReason> getIncompleteScheduleReasons() {
        return incompleteScheduleReasons;
    }

    /** 是否存在排班不完整类说明条目（考勤剔除、岗位未满等）；有则应对用户或运维展示解释文本。 */
    public boolean hasIncompleteScheduleExplanations() {
        return !incompleteScheduleReasons.isEmpty();
    }

    /** 已排班总人次（按岗位需求条目累计）。 */
    public int countAssignedStaff() {
        int n = 0;
        for (AssignedShiftBlockView shift : byShift) {
            for (AssignedPostStaffView post : shift.getPostAssignments()) {
                n += post.getStaff().size();
            }
        }
        return n;
    }

    /** 岗位需求总人数。 */
    public int countTotalNeed() {
        int n = 0;
        for (AssignedShiftBlockView shift : byShift) {
            for (AssignedPostStaffView post : shift.getPostAssignments()) {
                n += post.getNeedCount();
            }
        }
        return n;
    }

    /** 不完整说明可读文本（空列表返回固定提示）。 */
    public String formatIncompleteScheduleReasonsText() {
        if (incompleteScheduleReasons.isEmpty()) {
            return "【不完整说明】(无)";
        }
        StringBuilder sb = new StringBuilder(256);
        sb.append("【不完整说明】\n");
        for (IncompleteScheduleReason r : incompleteScheduleReasons) {
            sb.append(r.getScheduleDay())
                    .append(" [")
                    .append(r.getReasonCode())
                    .append("] ");
            if (r.getTimeRangeText() != null && !r.getTimeRangeText().isEmpty()) {
                sb.append(r.getTimeRangeText()).append(' ');
            }
            if (r.getPostId() != null && !r.getPostId().isEmpty()) {
                sb.append("岗位=").append(r.getPostId()).append(' ');
            }
            sb.append(r.getMessage()).append('\n');
        }
        return sb.toString();
    }

    /** 按班次维度的可读文本（a）。 */
    public String formatByShiftText() {
        if (byShift.isEmpty()) {
            return "(无班次排班结果)";
        }
        StringBuilder sb = new StringBuilder(512);
        sb.append("【按班次】\n");
        for (AssignedShiftBlockView shift : byShift) {
            String kind = shift.isFixedScheduling() ? "固定" : "划线";
            String shiftId = shift.getShiftId() == null || shift.getShiftId().isEmpty() ? "-" : shift.getShiftId();
            for (AssignedPostStaffView post : shift.getPostAssignments()) {
                sb.append(shift.getScheduleDay())
                        .append(' ')
                        .append(shift.getTimeRangeText())
                        .append(" [")
                        .append(kind)
                        .append(" shiftId=")
                        .append(shiftId)
                        .append("] 岗位=")
                        .append(post.getPostId());
                String pn = post.getPositionName();
                if (pn != null && !pn.isEmpty()) {
                    sb.append('(').append(pn).append(')');
                }
                sb.append(" 需求=")
                        .append(post.getNeedCount())
                        .append(" 已排=")
                        .append(post.getStaff().size())
                        .append(": ");
                sb.append(formatStaffList(post.getStaff()));
                sb.append('\n');
            }
        }
        return sb.toString();
    }

    /** 按员工维度的可读文本（b）。 */
    public String formatByEmployeeText() {
        if (byEmployee.isEmpty()) {
            return "(无员工排班结果)";
        }
        StringBuilder sb = new StringBuilder(512);
        sb.append("【按员工】\n");
        for (AssignedEmployeeView emp : byEmployee) {
            sb.append(emp.getUserName())
                    .append('(')
                    .append(emp.getUserId())
                    .append("): ");
            if (emp.getShifts().isEmpty()) {
                sb.append("(无班次)\n");
                continue;
            }
            String shifts =
                    emp.getShifts().stream()
                            .map(
                                    s -> {
                                        String kind = s.isFixedScheduling() ? "固定" : "划线";
                                        String sid =
                                                s.getShiftId() == null || s.getShiftId().isEmpty()
                                                        ? "-"
                                                        : s.getShiftId();
                                        return s.getScheduleDay()
                                                + " "
                                                + s.getTimeRangeText()
                                                + " "
                                                + s.getPostId()
                                                + '/'
                                                + kind
                                                + "/shiftId="
                                                + sid;
                                    })
                            .collect(Collectors.joining("; "));
            sb.append(shifts).append('\n');
        }
        return sb.toString();
    }

    /** 完整排班结果（摘要 + 按班次 + 按员工）。 */
    public String formatPipelineResultText() {
        StringBuilder sb = new StringBuilder(1024);
        sb.append("========== 智能排班 Pipeline 结果 ==========\n");
        sb.append("班次块数=")
                .append(byShift.size())
                .append(" 员工数=")
                .append(byEmployee.size())
                .append(" 已排人次=")
                .append(countAssignedStaff())
                .append(" 需求人次=")
                .append(countTotalNeed())
                .append('\n');
        sb.append(formatIncompleteScheduleReasonsText()).append('\n');
        sb.append(formatByShiftText()).append('\n');
        sb.append(formatByEmployeeText());
        sb.append("========== 排班结果结束 ==========");
        return sb.toString();
    }

    private static String formatStaffList(List<AssignedStaffRef> staff) {
        if (staff == null || staff.isEmpty()) {
            return "(空)";
        }
        return staff.stream()
                .map(
                        s -> {
                            String tag = s.isExtra() ? "通岗" : "专岗";
                            String name =
                                    s.getUserName() == null || s.getUserName().isEmpty()
                                            ? s.getUserId()
                                            : s.getUserName();
                            return name + '(' + s.getUserId() + ")[" + tag + ']';
                        })
                .collect(Collectors.joining("; "));
    }

    /** 按班次块 + 岗位：班次信息及已排员工列表。 */
    public static final class AssignedShiftBlockView implements Serializable {
        private final LocalDate scheduleDay;
        private final String shiftId;
        private final String timeRangeText;
        private final boolean fixedScheduling;
        private final List<AssignedPostStaffView> postAssignments;

        public AssignedShiftBlockView(
                LocalDate scheduleDay,
                String shiftId,
                String timeRangeText,
                boolean fixedScheduling,
                List<AssignedPostStaffView> postAssignments) {
            this.scheduleDay = Objects.requireNonNull(scheduleDay, "scheduleDay");
            this.shiftId = shiftId;
            this.timeRangeText = timeRangeText == null ? "" : timeRangeText;
            this.fixedScheduling = fixedScheduling;
            this.postAssignments =
                    postAssignments == null
                            ? Collections.emptyList()
                            : Collections.unmodifiableList(postAssignments);
        }

        public LocalDate getScheduleDay() {
            return scheduleDay;
        }

        public String getShiftId() {
            return shiftId;
        }

        public String getTimeRangeText() {
            return timeRangeText;
        }

        public boolean isFixedScheduling() {
            return fixedScheduling;
        }

        public List<AssignedPostStaffView> getPostAssignments() {
            return postAssignments;
        }
    }

    public static final class AssignedPostStaffView implements Serializable {
        private final String postId;
        /** 与 {@link jnpf.model.attendance.vo.WorkstationWithUsersVo#getPositionName()} 对齐（按岗位首次匹配工作站）。 */
        private final String positionName;
        private final int needCount;
        private final List<AssignedStaffRef> staff;

        /**
         * 兼容旧调用：不传岗位名称时 {@link #getPositionName()} 为空串。
         */
        public AssignedPostStaffView(String postId, int needCount, List<AssignedStaffRef> staff) {
            this(postId, "", needCount, staff);
        }

        public AssignedPostStaffView(
                String postId, String positionName, int needCount, List<AssignedStaffRef> staff) {
            this.postId = postId == null ? "" : postId;
            this.positionName = positionName == null ? "" : positionName.trim();
            this.needCount = Math.max(0, needCount);
            this.staff = staff == null ? Collections.emptyList() : Collections.unmodifiableList(staff);
        }

        public String getPostId() {
            return postId;
        }

        public String getPositionName() {
            return positionName;
        }

        public int getNeedCount() {
            return needCount;
        }

        public List<AssignedStaffRef> getStaff() {
            return staff;
        }
    }

    public static final class AssignedStaffRef implements Serializable {
        private final String userId;
        private final String userName;
        private final boolean extra;

        public AssignedStaffRef(String userId, String userName, boolean extra) {
            this.userId = userId == null ? "" : userId;
            this.userName = userName == null ? "" : userName;
            this.extra = extra;
        }

        public String getUserId() {
            return userId;
        }

        public String getUserName() {
            return userName;
        }

        public boolean isExtra() {
            return extra;
        }
    }

    /** 按员工：员工信息及其班次列表。 */
    @Getter
    public static final class AssignedEmployeeView implements Serializable {
        private final String userId;
        private final String userName;
        private final List<AssignedShiftRef> shifts;

        public AssignedEmployeeView(String userId, String userName, List<AssignedShiftRef> shifts) {
            this.userId = userId == null ? "" : userId;
            this.userName = userName == null ? "" : userName;
            this.shifts = shifts == null ? Collections.emptyList() : Collections.unmodifiableList(shifts);
        }

    }

    @Getter
    public static final class AssignedShiftRef implements Serializable {
        private final LocalDate scheduleDay;
        private final String shiftId;
        private final String timeRangeText;
        private final boolean fixedScheduling;
        private final String postId;

        public AssignedShiftRef(
                LocalDate scheduleDay,
                String shiftId,
                String timeRangeText,
                boolean fixedScheduling,
                String postId) {
            this.scheduleDay = Objects.requireNonNull(scheduleDay, "scheduleDay");
            this.shiftId = shiftId;
            this.timeRangeText = timeRangeText == null ? "" : timeRangeText;
            this.fixedScheduling = fixedScheduling;
            this.postId = postId == null ? "" : postId;
        }

    }

    /**
     * 单日或单班段维度的排班不完整说明；{@link #getTimeRangeText()}、{@link #getPostId()} 为空串时表示日级汇总。
     */
    @Getter
    public static final class IncompleteScheduleReason implements Serializable {
        private final LocalDate scheduleDay;
        /**
         * -- GETTER --
         * 班次块时段文案；日级条目可为空串。
         */
        private final String timeRangeText;
        /**
         * -- GETTER --
         * 岗位 id；日级或非岗位类说明可为空串。
         */
        private final String postId;
        /**
         * -- GETTER --
         * 与类上
         *  常量对齐。
         */
        private final String reasonCode;
        /**
         * -- GETTER --
         * 简短中文叙述，可直接展示给用户。
         */
        private final String message;

        public IncompleteScheduleReason(
                LocalDate scheduleDay,
                String timeRangeText,
                String postId,
                String reasonCode,
                String message) {
            this.scheduleDay = Objects.requireNonNull(scheduleDay, "scheduleDay");
            this.timeRangeText = timeRangeText == null ? "" : timeRangeText;
            this.postId = postId == null ? "" : postId.trim();
            this.reasonCode = reasonCode == null ? "" : reasonCode.trim();
            this.message = message == null ? "" : message.trim();
        }

    }
}
