package jnpf.attendance.service.impl;

import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jnpf.attendance.mapper.FtbScheduleGroupDrawingParamMapper;
import jnpf.attendance.mapper.FtbScheduleGroupFixedParamMapper;
import jnpf.attendance.service.ScheduleGroupRuleConfigService;
import jnpf.base.UserInfo;
import jnpf.base.service.SuperServiceImpl;
import jnpf.entity.attendance.FtbScheduleGroupDrawingParamEntity;
import jnpf.entity.attendance.FtbScheduleGroupFixedParamEntity;
import jnpf.exception.HandleException;
import jnpf.exception.QueryException;
import jnpf.model.attendance.dto.scheduling.FixedSchedulingRuleDto;
import jnpf.model.attendance.dto.scheduling.LineSchedulingRuleDto;
import jnpf.model.attendance.dto.scheduling.ScheduleGroupRuleConfigDto;
import jnpf.model.attendance.vo.scheduling.FixedSchedulingRuleVo;
import jnpf.model.attendance.vo.scheduling.LineSchedulingRuleVo;
import jnpf.model.attendance.vo.scheduling.ScheduleGroupRuleConfigVo;
import jnpf.util.RandomUtil;
import jnpf.util.StringUtil;
import jnpf.util.UserProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.Date;
import java.util.function.Consumer;

/**
 * 考勤组排班规则配置：固定排班核心参数 + 划线排班参数查询与保存。
 *
 * @author xiaofeng
 * @since 2026-05-13
 */
@Slf4j
@Service
public class ScheduleGroupRuleConfigServiceImpl
        extends SuperServiceImpl<FtbScheduleGroupFixedParamMapper, FtbScheduleGroupFixedParamEntity>
        implements ScheduleGroupRuleConfigService {

    /** 约束级别：0=优先满足（软约束） */
    private static final byte PRIORITY_PREFER = 0;
    /** 约束级别：1=必须满足（硬约束） */
    private static final byte PRIORITY_MUST = 1;

    @Resource
    private FtbScheduleGroupFixedParamMapper fixedParamMapper;

    @Resource
    private FtbScheduleGroupDrawingParamMapper drawingParamMapper;

    /**
     * 按考勤组查询固定排班与划线排班规则；库中无记录时对子块填充产品默认结构后返回 VO。
     */
    @Override
    public ScheduleGroupRuleConfigVo getRuleConfig(String groupId) throws HandleException, QueryException {
        long start = System.currentTimeMillis();
        assertGroupIdNotBlank(groupId);

        ScheduleGroupRuleConfigVo result = new ScheduleGroupRuleConfigVo();
        result.setGroupId(groupId);
        result.setFixedScheduling(
                toFixedVo(normalizeFixedRuleForGet(fixedParamMapper.selectFixedRuleDtoByGroupId(groupId))));
        result.setLineScheduling(
                toLineVo(normalizeLineRuleForGet(drawingParamMapper.selectLineRuleDtoByGroupId(groupId))));

        log.info("获取排班规则配置，groupId=>{}，耗时=>{} 毫秒", groupId, System.currentTimeMillis() - start);
        return result;
    }

    /**
     * 校验两块规则后，在同一事务内分别 upsert 固定参数表与划线参数表，并回查最新配置。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ScheduleGroupRuleConfigVo saveRuleConfig(String groupId, ScheduleGroupRuleConfigDto dto)
            throws HandleException, QueryException {
        long start = System.currentTimeMillis();

        // 1、参数校验
        if (dto == null) {
            throw new HandleException("请求体不能为空");
        }
        String effectiveGroupId = StringUtil.isNotBlank(groupId) ? groupId : dto.getGroupId();
        assertGroupIdNotBlank(effectiveGroupId);
        if (StringUtil.isNotBlank(dto.getGroupId()) && !effectiveGroupId.equals(dto.getGroupId())) {
            throw new HandleException("请求体 groupId 与入参不一致");
        }

        if (dto.getFixedScheduling() == null || dto.getLineScheduling() == null) {
            throw new HandleException("fixedScheduling、lineScheduling 不能为空");
        }

        validateFixedRule(dto.getFixedScheduling());
        validateLineRule(dto.getLineScheduling());

        Date operateTime = new Date();
        String operatorId = currentUserId();

        upsertFixedRule(effectiveGroupId, dto.getFixedScheduling(), operateTime, operatorId);
        upsertLineRule(effectiveGroupId, dto.getLineScheduling(), operateTime, operatorId);

        log.info("保存排班规则配置，groupId=>{}，耗时=>{} 毫秒", effectiveGroupId, System.currentTimeMillis() - start);
        return getRuleConfig(effectiveGroupId);
    }

    private static void assertGroupIdNotBlank(String groupId) throws HandleException {
        if (StringUtil.isBlank(groupId)) {
            throw new HandleException("考勤组ID不能为空");
        }
    }

    private static String currentUserId() {
        UserInfo userInfo = UserProvider.getUser();
        return userInfo == null ? null : userInfo.getUserId();
    }

    /**
     * 校验固定排班五项约束：启用时数值非空且非负，优先级仅允许 0/1。
     */
    private static void validateFixedRule(FixedSchedulingRuleDto rule) throws HandleException {
        assertValueWhenEnabled("单日工作小时数", rule.getDailyWorkHoursEnabled(), rule.getDailyWorkHoursValue());
        assertPriorityRange("单日工作小时数", rule.getDailyWorkHoursPriority());
        assertValueWhenEnabled("连续工作天数", rule.getConsecutiveWorkDaysEnabled(), rule.getConsecutiveWorkDaysValue());
        assertPriorityRange("连续工作天数", rule.getConsecutiveWorkDaysPriority());
        assertValueWhenEnabled("每周工作天数", rule.getWeeklyWorkDaysEnabled(), rule.getWeeklyWorkDaysValue());
        assertPriorityRange("每周工作天数", rule.getWeeklyWorkDaysPriority());
        assertValueWhenEnabled("每周工作小时数", rule.getWeeklyWorkHoursEnabled(), rule.getWeeklyWorkHoursValue());
        assertPriorityRange("每周工作小时数", rule.getWeeklyWorkHoursPriority());
        assertValueWhenEnabled("班次间最小休息间隔", rule.getMinRestBetweenShiftsEnabled(), rule.getMinRestBetweenShiftsValue());
        assertPriorityRange("班次间最小休息间隔", rule.getMinRestBetweenShiftsPriority());
    }

    /**
     * 校验划线排班三项约束：启用时数值非空且非负，优先级仅允许 0/1。
     */
    private static void validateLineRule(LineSchedulingRuleDto rule) throws HandleException {
        assertValueWhenEnabled("单日班段个数", rule.getMaxDailySegmentsEnabled(), rule.getMaxDailySegmentsValue());
        assertPriorityRange("单日班段个数", rule.getMaxDailySegmentsPriority());
        assertValueWhenEnabled("单段最长", rule.getMaxSingleSegmentHoursEnabled(), rule.getMaxSingleSegmentHoursValue());
        assertPriorityRange("单段最长", rule.getMaxSingleSegmentHoursPriority());
        assertDecimalValueWhenEnabled("单段最短", rule.getMinSingleSegmentHoursEnabled(), rule.getMinSingleSegmentHoursValue());
        assertPriorityRange("单段最短", rule.getMinSingleSegmentHoursPriority());
    }

    private static void assertDecimalValueWhenEnabled(String label, Boolean enabled, BigDecimal value) throws HandleException {
        if (Boolean.TRUE.equals(enabled) && value == null) {
            throw new HandleException(label + "启用时数值不能为空");
        }
        if (Boolean.TRUE.equals(enabled) && value != null && value.compareTo(BigDecimal.ZERO) < 0) {
            throw new HandleException(label + "数值不能为负数");
        }
    }

    private static void assertValueWhenEnabled(String label, Boolean enabled, Integer value) throws HandleException {
        if (Boolean.TRUE.equals(enabled) && value == null) {
            throw new HandleException(label + "启用时数值不能为空");
        }
        if (Boolean.TRUE.equals(enabled) && value != null && value < 0) {
            throw new HandleException(label + "数值不能为负数");
        }
    }

    /**
     * 约束级别取值校验：{@link #PRIORITY_PREFER} 或 {@link #PRIORITY_MUST}，null 表示未设置由落库默认补齐。
     */
    private static void assertPriorityRange(String label, Byte priority) throws HandleException {
        if (priority != null && priority != PRIORITY_PREFER && priority != PRIORITY_MUST) {
            throw new HandleException(label + "约束级别取值须为 0 或 1");
        }
    }

    private FtbScheduleGroupFixedParamEntity selectFixedParamByGroupId(String groupId) {
        return lambdaQuery()
                .eq(FtbScheduleGroupFixedParamEntity::getGroupId, groupId)
                .one();
    }

    private FtbScheduleGroupDrawingParamEntity selectDrawingParamByGroupId(String groupId) {
        return drawingParamMapper.selectOne(Wrappers.<FtbScheduleGroupDrawingParamEntity>lambdaQuery()
                .eq(FtbScheduleGroupDrawingParamEntity::getGroupId, groupId));
    }

    private static FixedSchedulingRuleVo toFixedVo(FixedSchedulingRuleDto dto) {
        return BeanUtil.toBean(dto, FixedSchedulingRuleVo.class);
    }

    private static LineSchedulingRuleVo toLineVo(LineSchedulingRuleDto dto) {
        return BeanUtil.toBean(dto, LineSchedulingRuleVo.class);
    }

    /**
     * 查询结果归一化：无库记录时返回与表 DEFAULT 一致的全量默认 DTO。
     */
    private static FixedSchedulingRuleDto normalizeFixedRuleForGet(FixedSchedulingRuleDto fromDb) {
        if (fromDb == null) {
            return applyFixedDefaultsToDto();
        }
        return fromDb;
    }

    /**
     * 查询结果归一化：无库记录时返回与表 DEFAULT 一致的全量默认 DTO。
     */
    private static LineSchedulingRuleDto normalizeLineRuleForGet(LineSchedulingRuleDto fromDb) {
        if (fromDb == null) {
            return applyLineDefaultsToDto();
        }
        return fromDb;
    }

    /** 固定排班无记录时的接口层默认结构（与产品约定及表 DEFAULT 对齐）。 */
    private static FixedSchedulingRuleDto applyFixedDefaultsToDto() {
        FixedSchedulingRuleDto d = new FixedSchedulingRuleDto();
        d.setDailyWorkHoursEnabled(Boolean.TRUE);
        d.setDailyWorkHoursValue(8);
        d.setDailyWorkHoursPriority(PRIORITY_MUST);
        d.setConsecutiveWorkDaysEnabled(Boolean.TRUE);
        d.setConsecutiveWorkDaysValue(6);
        d.setConsecutiveWorkDaysPriority(PRIORITY_MUST);
        d.setWeeklyWorkDaysEnabled(Boolean.TRUE);
        d.setWeeklyWorkDaysValue(6);
        d.setWeeklyWorkDaysPriority(PRIORITY_MUST);
        d.setWeeklyWorkHoursEnabled(Boolean.TRUE);
        d.setWeeklyWorkHoursValue(40);
        d.setWeeklyWorkHoursPriority(PRIORITY_MUST);
        d.setMinRestBetweenShiftsEnabled(Boolean.TRUE);
        d.setMinRestBetweenShiftsValue(10);
        d.setMinRestBetweenShiftsPriority(PRIORITY_MUST);
        return d;
    }

    /** 划线排班无记录时的接口层默认结构（与产品约定及表 DEFAULT 对齐）。 */
    private static LineSchedulingRuleDto applyLineDefaultsToDto() {
        LineSchedulingRuleDto d = new LineSchedulingRuleDto();
        d.setMaxDailySegmentsEnabled(Boolean.TRUE);
        d.setMaxDailySegmentsValue(2);
        d.setMaxDailySegmentsPriority(PRIORITY_MUST);
        d.setMaxSingleSegmentHoursEnabled(Boolean.TRUE);
        d.setMaxSingleSegmentHoursValue(4);
        d.setMaxSingleSegmentHoursPriority(PRIORITY_MUST);
        d.setMinSingleSegmentHoursEnabled(Boolean.TRUE);
        d.setMinSingleSegmentHoursValue(new BigDecimal("2"));
        d.setMinSingleSegmentHoursPriority(PRIORITY_MUST);
        return d;
    }

    /**
     * 落库前补齐固定排班实体中 null 的开关与优先级字段，避免写入不完整行。
     */
    private static void applyFixedDefaults(FtbScheduleGroupFixedParamEntity entity) {
        if (entity.getDailyWorkHoursEnabled() == null) {
            entity.setDailyWorkHoursEnabled(Boolean.TRUE);
        }
        if (entity.getDailyWorkHoursPriority() == null) {
            entity.setDailyWorkHoursPriority(PRIORITY_MUST);
        }
        if (entity.getConsecutiveWorkDaysEnabled() == null) {
            entity.setConsecutiveWorkDaysEnabled(Boolean.TRUE);
        }
        if (entity.getConsecutiveWorkDaysPriority() == null) {
            entity.setConsecutiveWorkDaysPriority(PRIORITY_MUST);
        }
        if (entity.getWeeklyWorkDaysEnabled() == null) {
            entity.setWeeklyWorkDaysEnabled(Boolean.TRUE);
        }
        if (entity.getWeeklyWorkDaysPriority() == null) {
            entity.setWeeklyWorkDaysPriority(PRIORITY_MUST);
        }
        if (entity.getWeeklyWorkHoursEnabled() == null) {
            entity.setWeeklyWorkHoursEnabled(Boolean.TRUE);
        }
        if (entity.getWeeklyWorkHoursPriority() == null) {
            entity.setWeeklyWorkHoursPriority(PRIORITY_MUST);
        }
        if (entity.getMinRestBetweenShiftsEnabled() == null) {
            entity.setMinRestBetweenShiftsEnabled(Boolean.TRUE);
        }
        if (entity.getMinRestBetweenShiftsPriority() == null) {
            entity.setMinRestBetweenShiftsPriority(PRIORITY_MUST);
        }
    }

    /**
     * 落库前补齐划线排班实体中 null 的开关、数值与优先级；单段最短默认关闭但保留默认时长。
     */
    private static void applyLineDefaults(FtbScheduleGroupDrawingParamEntity entity) {
        if (entity.getMaxDailySegmentsEnabled() == null) {
            entity.setMaxDailySegmentsEnabled(Boolean.TRUE);
        }
        if (entity.getMaxDailySegmentsPriority() == null) {
            entity.setMaxDailySegmentsPriority(PRIORITY_MUST);
        }
        if (entity.getMaxSingleSegmentHoursEnabled() == null) {
            entity.setMaxSingleSegmentHoursEnabled(Boolean.TRUE);
        }
        if (entity.getMaxSingleSegmentHoursPriority() == null) {
            entity.setMaxSingleSegmentHoursPriority(PRIORITY_MUST);
        }
        if (entity.getMinSingleSegmentHoursEnabled() == null) {
            entity.setMinSingleSegmentHoursEnabled(Boolean.FALSE);
        }
        if (entity.getMinSingleSegmentHoursValue() == null) {
            entity.setMinSingleSegmentHoursValue(new BigDecimal("2"));
        }
        if (entity.getMinSingleSegmentHoursPriority() == null) {
            entity.setMinSingleSegmentHoursPriority(PRIORITY_MUST);
        }
    }

    /**
     * 按 groupId 对固定排班参数表 insert 或 update，保留原创建人/创建时间。
     */
    private void upsertFixedRule(String groupId, FixedSchedulingRuleDto dto, Date operateTime, String operatorId) {
        FtbScheduleGroupFixedParamEntity existing = selectFixedParamByGroupId(groupId);
        FtbScheduleGroupFixedParamEntity row = BeanUtil.toBean(dto, FtbScheduleGroupFixedParamEntity.class);
        row.setGroupId(groupId);
        applyFixedDefaults(row);
        if (existing == null) {
            row.setId(StringUtil.isNotBlank(dto.getId()) ? dto.getId() : RandomUtil.uuId());
            fillAuditOnInsert(row::setCreateTime, row::setUpdateTime, row::setCreator, row::setUpdater, operateTime, operatorId);
            save(row);
        } else {
            row.setId(existing.getId());
            row.setCreateTime(existing.getCreateTime());
            row.setCreator(existing.getCreator());
            row.setUpdateTime(operateTime);
            row.setUpdater(operatorId);
            updateById(row);
        }
    }

    /**
     * 按 groupId 对划线排班参数表 insert 或 update，保留原创建人/创建时间。
     */
    private void upsertLineRule(String groupId, LineSchedulingRuleDto dto, Date operateTime, String operatorId) {
        FtbScheduleGroupDrawingParamEntity existing = selectDrawingParamByGroupId(groupId);
        FtbScheduleGroupDrawingParamEntity row = BeanUtil.toBean(dto, FtbScheduleGroupDrawingParamEntity.class);
        row.setGroupId(groupId);
        applyLineDefaults(row);
        if (existing == null) {
            row.setId(StringUtil.isNotBlank(dto.getId()) ? dto.getId() : RandomUtil.uuId());
            fillAuditOnInsert(row::setCreateTime, row::setUpdateTime, row::setCreator, row::setUpdater, operateTime, operatorId);
            drawingParamMapper.insert(row);
        } else {
            row.setId(existing.getId());
            row.setCreateTime(existing.getCreateTime());
            row.setCreator(existing.getCreator());
            row.setUpdateTime(operateTime);
            row.setUpdater(operatorId);
            drawingParamMapper.updateById(row);
        }
    }

    private static void fillAuditOnInsert(Consumer<Date> setCreateTime,
                                          Consumer<Date> setUpdateTime,
                                          Consumer<String> setCreator,
                                          Consumer<String> setUpdater,
                                          Date operateTime,
                                          String operatorId) {
        setCreateTime.accept(operateTime);
        setUpdateTime.accept(operateTime);
        setCreator.accept(operatorId);
        setUpdater.accept(operatorId);
    }
}
