package cn.iocoder.yudao.module.bpm.service.oa;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.BetweenFormatter;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.iocoder.yudao.framework.common.Constants;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.UploadUserFile;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.controller.admin.oa.vo.overtime.BpmOAOvertimeCreateReqVO;
import cn.iocoder.yudao.module.bpm.convert.oa.BpmOAOvertimeConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.oa.BpmOAOvertimeDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.oa.BpmOAOvertimeItemDO;
import cn.iocoder.yudao.module.bpm.dal.mysql.oa.BpmOAOvertimeItemMapper;
import cn.iocoder.yudao.module.bpm.dal.mysql.oa.BpmOAOvertimeMapper;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceResultEnum;
import cn.iocoder.yudao.module.bpm.handler.WorkOvertimeHandler;
import cn.iocoder.yudao.module.bpm.handler.enums.WorkOvertimeEnum;
import cn.iocoder.yudao.module.bpm.service.task.BpmHistoryProcessInstanceService;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import cn.iocoder.yudao.module.bpm.service.workovertime.WorkOvertimeService;
import cn.iocoder.yudao.module.system.api.attendance.AttendanceApi;
import cn.iocoder.yudao.module.system.api.attendance.dto.AttendancePunchRecordDTO;
import cn.iocoder.yudao.module.system.api.attendance.dto.AttendanceTimeRangeInfoDTO;
import cn.iocoder.yudao.module.system.api.attendance.vo.AttendancePunchRecordVO;
import cn.iocoder.yudao.module.system.api.attendance.vo.AttendanceTimeRangeInfoVO;
import cn.iocoder.yudao.module.system.api.holiday.HolidayApi;
import cn.iocoder.yudao.module.system.api.holiday.dto.OvertimeIncreasesHolidayDTO;
import cn.iocoder.yudao.module.system.api.workovertime.WorkOvertimeApi;
import cn.iocoder.yudao.module.system.api.workovertime.vo.WorkOvertimeRuleApiVO;
import cn.iocoder.yudao.module.system.api.workovertime.vo.WorkOvertimeRuleItemApiVO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;

/**
 * OA 加班申请 Service 实现类
 *
 * @author 符溶馨
 */
@Service
@Validated
public class BpmOAOvertimeServiceImpl extends BpmOABaseService implements BpmOAOvertimeService {

    /**
     * OA 加班对应的流程定义 KEY
     */
    public static final String PROCESS_KEY = "oa_overtime_2";

    @Resource
    private BpmOAOvertimeMapper overtimeMapper;
    @Resource
    private BpmOAOvertimeItemMapper overtimeItemMapper;
    @Resource
    private BpmOAOvertimeItemService overtimeItemService;
    @Resource
    private BpmProcessInstanceApi processInstanceApi;
    @Resource
    private BpmProcessInstanceService processInstanceService;
    @Resource
    private BpmHistoryProcessInstanceService historyProcessInstanceService;
    @Resource
    private BpmOALeaveService leaveService;
    @Resource
    private WorkOvertimeApi workOvertimeApi;
    @Resource
    private AttendanceApi attendanceApi;
    @Resource
    private WorkOvertimeHandler workOvertimeHandler;
    @Resource
    private HolidayApi holidayApi;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createOvertime(Long userId, BpmOAOvertimeCreateReqVO vo) {
        // 判断是否已存在相同时间段的加班申请
        List<String> dataTimes = DateUtils.betweenDayList(vo.getStartTime(), vo.getEndTime());
        List<Integer> processingOrPassed = BpmProcessInstanceResultEnum.processingOrPassed();
        // -- 减去一分钟的时间 - 由于前端可能是一天中的最开始时间 - 这里要处理下
        LocalDateTime beginTime = LocalDateTimeUtil.beginOfDay(vo.getStartTime());
        LocalDateTime endTime = LocalDateTimeUtil.endOfDay(vo.getEndTime().plusMinutes(-1));
        List<BpmOAOvertimeDO> overtimeDOS = overtimeMapper.selectList(new LambdaQueryWrapper<BpmOAOvertimeDO>()
                .eq(BpmOAOvertimeDO::getUserId, userId)
                .le(BpmOAOvertimeDO::getStartTime, endTime)
                .gt(BpmOAOvertimeDO::getEndTime, beginTime)
                .in(BpmOAOvertimeDO::getResult, processingOrPassed)
        );
        LocalDateTime[] times = new LocalDateTime[]{beginTime, endTime};
        if (CollectionUtil.isNotEmpty(overtimeDOS)) {
            for (BpmOAOvertimeDO overtimeDO : overtimeDOS) {
                LocalDateTime[] itemTimes = new LocalDateTime[]{overtimeDO.getStartTime(), overtimeDO.getEndTime()};
                boolean intersects = LocalDateTimeUtils.intersects(times[0], times[1], itemTimes[0], itemTimes[1]);
                if (intersects) {
                    throw exception(OVERTIME_REQUESTS_ALREADY_EXIST_FOR_THE_SAME_TIME_PERIOD);
                }
            }
        }
        CommonResult<WorkOvertimeRuleApiVO> resultData = workOvertimeApi.getOvertimeRulesByUserId(userId);
        // -- 获取用户考勤信息
        if (!resultData.isSuccess()) {
            throw exception(EXCEPTION_OCCURRED_WHILE_OBTAINING_OVERTIME_SETTINGS);
        }
        // -- 不在考勤组内不需要加班
        WorkOvertimeRuleApiVO workOvertimeRuleApiVO = resultData.getCheckedData();
        if (Constants.ZERO.equals(workOvertimeRuleApiVO.getIsGroup())) {
            throw exception(NO_NEED_TO_APPLY_FOR_OVERTIME_IF_NOT_IN_THE_ATTENDANCE_GROUP);
        }
        if (Constants.ZERO.equals(workOvertimeRuleApiVO.getIsOvertime())) {
            throw exception(THE_CURRENT_USERS_ATTENDANCE_GROUP_HAS_NOT_SET_OVERTIME_RULES);
        }
        List<WorkOvertimeRuleItemApiVO> workOvertimeRuleItems = workOvertimeRuleApiVO.getWorkOvertimeRuleItems();
        Map<Integer, WorkOvertimeRuleItemApiVO> map = workOvertimeRuleItems.stream().collect(Collectors.toMap(WorkOvertimeRuleItemApiVO::getType, Function.identity()));
        // 根据时间计算是工作日还是节假日还是休息日
        Map<String, AttendanceTimeRangeInfoVO> attendanceInfoByTimeRange = leaveService.getAttendanceInfoByTimeRange(
                new AttendanceTimeRangeInfoDTO().setUserId(userId).setStartTime(vo.getStartTime()).setEndTime(vo.getEndTime())
        );

        for (String dataTime : dataTimes) {
            AttendanceTimeRangeInfoVO attendanceTimeRangeInfoVO = attendanceInfoByTimeRange.get(dataTime);
            WorkOvertimeRuleItemApiVO workOvertimeRuleItemApiVO = map.get(attendanceTimeRangeInfoVO.getDatType());
            if (workOvertimeRuleItemApiVO.getStatus() == 0) {
                String msg = "%s不允许加班";
                msg = String.format(msg, (dataTime + "号 " + (attendanceTimeRangeInfoVO.getDatType() == 1 ? "工作日" : attendanceTimeRangeInfoVO.getDatType() == 2 ? "休息日" : "节假日")));
                throw exception(new ErrorCode(1_009_001_017, msg));
            }
        }

        //插入OA 加班申请
        BpmOAOvertimeDO overtime = BpmOAOvertimeConvert.INSTANCE.convert(vo).setUserId(userId)
                .setResult(BpmProcessInstanceResultEnum.PROCESS.getResult());
        overtimeMapper.insert(overtime.setTimeLength(vo.getTotalTimeLength()));

        // -- 记录一个item表
        List<BpmOAOvertimeItemDO> itemDOS = BeanUtil.copyToList(vo.getOvertimeDateTimeVOS(), BpmOAOvertimeItemDO.class);
        itemDOS.forEach(overtimeItemDO -> overtimeItemDO.setOaOvertimeId(overtime.getId()).setUserId(userId)
                .setDateType(attendanceInfoByTimeRange.get(overtimeItemDO.getDateTimeStr()).getDatType()));
        overtimeItemMapper.insertBatch(itemDOS);

        // 发起 BPM 流程
        Map<String, Object> processInstanceVariables = new HashMap<>();
        String processInstanceId = processInstanceApi.createProcessInstance(userId,
                new BpmProcessInstanceCreateReqDTO().setProcessDefinitionKey(PROCESS_KEY)
                        .setVariables(processInstanceVariables).setBusinessKey(String.valueOf(overtime.getId()))).getCheckedData();

        // 将工作流的编号，更新到 OA 加班单中
        overtimeMapper.updateById(new BpmOAOvertimeDO().setId(overtime.getId()).setProcessInstanceId(processInstanceId));

        // 判断是否为重新发起的流程
        if (vo.getProcessInstanceId() != null && vo.getResult() == 3) {
            historyProcessInstanceService.createHistoryProcessInstance(processInstanceId, vo.getProcessInstanceId());
        }
        List<UploadUserFile> fileItems = vo.getFileItems();
        //这里的逻辑，如果fileItems不为空，且有数据，那么说明是上传了附件的，则需要更工作流文件表对应的实例Id
        if (fileItems != null && !fileItems.isEmpty()) {
            uploadBpmFileProcessInstanceId(processInstanceId, fileItems);
        }
        return overtime.getId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateOvertimeResult(String processInstanceId, Long id, Integer result) {
        validateLeaveExists(id);
        BpmOAOvertimeDO bpmOAOvertimeDO = overtimeMapper.selectById(id);
        overtimeMapper.updateById(bpmOAOvertimeDO.setResult(result));
        LocalDateTime now = LocalDateTime.now();
        //获取加班子表
        List<BpmOAOvertimeItemDO> items = overtimeItemService.getByOvertimeId(id);
        // -- 审核通过
        if (BpmProcessInstanceResultEnum.APPROVE.getResult().equals(result)) {
            ProcessInstance instance = processInstanceService.getProcessInstance(processInstanceId);
            // -- 如果是最终节点 并且子表有数据
            if (instance.isEnded() && !items.isEmpty()) {
                this.handlingOvertime(bpmOAOvertimeDO, items, now);
            }
        }
        overtimeMapper.updateById(new BpmOAOvertimeDO().setId(id).setResult(result));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void handlingOvertime(BpmOAOvertimeDO bpmOAOvertimeDO, List<BpmOAOvertimeItemDO> items, LocalDateTime now) {
        // 判断是否事后 - 事前的不需要处理 - 等定时任务处理即可
        if (bpmOAOvertimeDO.getEndTime().isBefore(now)) {
            // -- 获取到当前用户所在考勤组的加班规则 -
            Long userId = bpmOAOvertimeDO.getUserId();
            CommonResult<WorkOvertimeRuleApiVO> resultData = workOvertimeApi.getOvertimeRulesByUserId(userId);
            if (!resultData.isSuccess()) {
                throw exception(EXCEPTION_OCCURRED_WHILE_OBTAINING_OVERTIME_SETTINGS);
            }
            // -- 不在考勤组内不需要加班
            WorkOvertimeRuleApiVO workOvertimeRuleApiVO = resultData.getCheckedData();
            if (Constants.ZERO.equals(workOvertimeRuleApiVO.getIsGroup())) {
                throw exception(NO_NEED_TO_APPLY_FOR_OVERTIME_IF_NOT_IN_THE_ATTENDANCE_GROUP);
            }
            if (Constants.ZERO.equals(workOvertimeRuleApiVO.getIsOvertime())) {
                throw exception(THE_CURRENT_USERS_ATTENDANCE_GROUP_HAS_NOT_SET_OVERTIME_RULES);
            }
            List<WorkOvertimeRuleItemApiVO> workOvertimeRuleItems = workOvertimeRuleApiVO.getWorkOvertimeRuleItems();
            Map<Integer, WorkOvertimeRuleItemApiVO> map = workOvertimeRuleItems.stream().collect(Collectors.toMap(WorkOvertimeRuleItemApiVO::getType, Function.identity()));
            // ---  用户加班时间期间的打卡记录
            Map<String, List<AttendancePunchRecordVO>> recordMap = new HashMap<>();
            //已经发生的 - 获取该用户最近几天的考勤记录 - 并且对比时间
            CommonResult<Map<String, List<AttendancePunchRecordVO>>> attendancePunchRecordByTimeRange = attendanceApi.getAttendancePunchRecordByTimeRange(
                    new AttendancePunchRecordDTO().setUserId(bpmOAOvertimeDO.getUserId()).setStartTime(bpmOAOvertimeDO.getStartTime()).setEndTime(bpmOAOvertimeDO.getEndTime()));
            if (!attendancePunchRecordByTimeRange.isSuccess()) {
                throw exception(ABNORMAL_ACCESS_TO_ATTENDANCE_RECORDS);
            }
            recordMap = attendancePunchRecordByTimeRange.getCheckedData();
            List<BpmOAOvertimeItemDO> editItems = new ArrayList<>();
            Map<Long, BigDecimal> holidayMap = new HashMap<>();
            // - 这里不需要判断是否关闭加班 - 再申请的时候已经判断过了 - 这里只需要做计算加班时长的逻辑即可 !
            for (BpmOAOvertimeItemDO item : items) {
                WorkOvertimeRuleItemApiVO ruleItem = map.get(item.getDateType());
                WorkOvertimeService resource = workOvertimeHandler.getResource(WorkOvertimeEnum.getEnumServerName(ruleItem.getType()));
                List<AttendancePunchRecordVO> recordList = recordMap.get(item.getDateTimeStr());
                if (recordList == null) {
                    recordList = new ArrayList<>();
                }
                recordList.sort(Comparator.comparing(AttendancePunchRecordVO::getShouldPunchTime));
                // -- 计算加班时长
                resource.getOvertimeHours(item, recordList, workOvertimeRuleApiVO, ruleItem);
                editItems.add(item);
                // -- 如果是记为调休 - 并且假期id不为空
                if (ruleItem.getTransformationType() == 1 && ruleItem.getHolidayId() != null) {
                    // -- 计算调休
                    if (item.getActualTimeLength().compareTo(BigDecimal.ZERO) > 0) {
                        // -- 计算每个调休对象需要进行的调休时长 - 这里的单位是小时 - 还要获取假期的单位 来转换一下
                        holidayMap.put(ruleItem.getHolidayId(), holidayMap.getOrDefault(ruleItem.getHolidayId(), BigDecimal.ZERO)
                                .add(item.getActualTimeLength().multiply(ruleItem.getCompensatoryLeaveRatio())));
                    }
                } else {
                    // TODO: 2024/11/1  加班费不计算(后续有加班规则再计算)
                }
            }

            //更新加班主表
            overtimeMapper.updateById(bpmOAOvertimeDO.setSettlementFlag(1));
            //更新加班子表
            overtimeItemMapper.updateBatch(editItems);
            //加班奖励假期
            for (Map.Entry<Long, BigDecimal> entry : holidayMap.entrySet()) {
                holidayApi.overtimeIncreasesHoliday(new OvertimeIncreasesHolidayDTO()
                        .setUserId(userId)
                        .setDirection(0)
                        .setHolidayBalance(entry.getValue())
                        .setHolidaySettingId(entry.getKey())
                        .setUnit(workOvertimeRuleApiVO.getMoreRule().getUnit())
                        .setConversion(workOvertimeRuleApiVO.getMoreRule().getConversion()));
            }
        }
    }

    private void validateLeaveExists(Long id) {
        if (overtimeMapper.selectById(id) == null) {
            throw exception(OA_OVERTIME_NOT_EXISTS);
        }
    }

    @Override
    public BpmOAOvertimeDO getOvertime(Long id) {

        return overtimeMapper.selectById(id);
    }

    @Override
    public BpmOAOvertimeDO getByProcessInstanceId(String processInstanceId) {

        List<BpmOAOvertimeDO> bpmOAOvertimeDOS = overtimeMapper.selectList(BpmOAOvertimeDO::getProcessInstanceId, processInstanceId);
        if (!CollectionUtil.isEmpty(bpmOAOvertimeDOS)) {
            return bpmOAOvertimeDOS.get(0);
        }
        return null;
    }
}
