package cn.iocoder.yudao.module.system.service.attendance.punchrecord;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.Constants;
import cn.iocoder.yudao.framework.common.pojo.BpmOALeaveDTO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.system.api.attendance.dto.AttendancePunchRecordDTO;
import cn.iocoder.yudao.module.system.controller.admin.punchrecord.vo.AttendancePunchRecordPageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.punchrecord.vo.AttendancePunchRecordSaveReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.attendance.group.AttendanceGroupDO;
import cn.iocoder.yudao.module.system.dal.dataobject.attendance.groupshiftitem.AttendanceGroupShiftItemDO;
import cn.iocoder.yudao.module.system.dal.dataobject.attendance.groupuser.AttendanceGroupUserDO;
import cn.iocoder.yudao.module.system.dal.dataobject.attendance.punchrecord.AttendancePunchRecordDO;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import cn.iocoder.yudao.module.system.dal.mysql.attendance.group.AttendanceGroupMapper;
import cn.iocoder.yudao.module.system.dal.mysql.attendance.groupuser.AttendanceGroupUserMapper;
import cn.iocoder.yudao.module.system.dal.mysql.attendance.punchrecord.AttendancePunchRecordMapper;
import cn.iocoder.yudao.module.system.service.attendance.AttendanceService;
import cn.iocoder.yudao.module.system.service.attendance.fixed.AttendanceFixedService;
import cn.iocoder.yudao.module.system.service.attendance.groupshiftitem.AttendanceGroupShiftItemService;
import cn.iocoder.yudao.module.system.service.attendance.punch.dto.AttendanceOnTheDayDTO;
import cn.iocoder.yudao.module.system.service.attendance.scheduling.AttendanceSchedulingService;
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.PUNCH_RECORD_NOT_EXISTS;

/**
 * 用户打卡记录 Service 实现类
 *
 * @author 艾楷
 */
@Service
@Validated
public class AttendancePunchRecordServiceImpl implements AttendancePunchRecordService {

    @Resource
    private AttendancePunchRecordMapper punchRecordMapper;
    @Resource
    @Lazy // 避免依赖循环
    private AttendanceService attendanceService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private AttendanceFixedService attendanceFixedService;
    @Resource
    private AttendanceSchedulingService attendanceSchedulingService;
    @Resource
    private AttendanceGroupShiftItemService attendanceGroupShiftItemService;
    @Resource
    private AttendanceGroupUserMapper attendanceGroupUserMapper;
    @Resource
    private AttendanceGroupMapper attendanceGroupMapper;
    @Resource
    private AdminUserService adminUserService;
//    @Resource
//    private BpmOALeaveApi bpmOALeaveApi;

    @Override
    public Long createPunchRecord(AttendancePunchRecordSaveReqVO createReqVO) {
        // 插入
        AttendancePunchRecordDO punchRecord = BeanUtils.toBean(createReqVO, AttendancePunchRecordDO.class);
        punchRecordMapper.insert(punchRecord);
        // 返回
        return punchRecord.getId();
    }

    @Override
    public void updatePunchRecord(AttendancePunchRecordSaveReqVO updateReqVO) {
        // 校验存在
        validatePunchRecordExists(updateReqVO.getId());
        // 更新
        AttendancePunchRecordDO updateObj = BeanUtils.toBean(updateReqVO, AttendancePunchRecordDO.class);
        punchRecordMapper.updateById(updateObj);
    }

    @Override
    public void deletePunchRecord(Long id) {
        // 校验存在
        validatePunchRecordExists(id);
        // 删除
        punchRecordMapper.deleteById(id);
    }

    private void validatePunchRecordExists(Long id) {
        if (punchRecordMapper.selectById(id) == null) {
            throw exception(PUNCH_RECORD_NOT_EXISTS);
        }
    }

    @Override
    public AttendancePunchRecordDO getPunchRecord(Long id) {
        return punchRecordMapper.selectById(id);
    }

    @Override
    public PageResult<AttendancePunchRecordDO> getPunchRecordPage(AttendancePunchRecordPageReqVO pageReqVO) {
        return punchRecordMapper.selectPage(pageReqVO);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveBatch(List<AttendancePunchRecordDO> attendancePunchRecordDOList) {
        punchRecordMapper.insertBatch(attendancePunchRecordDOList);
    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    public void defaultPersistence(Long groupId, List<Long> userIds, LocalDateTime localDateTime) {
        if (groupId == null || CollectionUtil.isEmpty(userIds)) {
            return;
        }
        AttendanceGroupDO attendanceGroupDO = attendanceGroupMapper.selectById(groupId);
        List<AttendanceGroupDO> attendanceGroupDOS = new ArrayList<>();
        attendanceGroupDOS.add(attendanceGroupDO);

        List<AttendanceGroupUserDO> attendanceGroupUserDOS = attendanceGroupUserMapper.selectList(new LambdaQueryWrapper<AttendanceGroupUserDO>()
                .eq(AttendanceGroupUserDO::getAttendanceGroupId, groupId)
                .in(AttendanceGroupUserDO::getUserId, userIds));
        this.defaultPersistence(attendanceGroupDOS, attendanceGroupUserDOS, localDateTime);
    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    public void defaultPersistence(List<Long> groupIds, LocalDateTime localDateTime) {
        List<AttendanceGroupDO> attendanceGroupDOS;
        List<AttendanceGroupUserDO> attendanceGroupUserDOS;
        if (CollectionUtil.isNotEmpty(groupIds)) {
            attendanceGroupDOS = attendanceGroupMapper.selectBatchIds(groupIds);
            attendanceGroupUserDOS = attendanceGroupUserMapper.selectList(new LambdaQueryWrapper<AttendanceGroupUserDO>()
                    .in(AttendanceGroupUserDO::getAttendanceGroupId, groupIds));
        } else {
            attendanceGroupDOS = attendanceGroupMapper.selectList();
            attendanceGroupUserDOS = attendanceGroupUserMapper.selectList();
        }
        this.defaultPersistence(attendanceGroupDOS, attendanceGroupUserDOS, localDateTime);
    }

    @Override
    public void defaultPersistence(List<AttendanceGroupDO> attendanceGroupDOS, List<AttendanceGroupUserDO> attendanceGroupUserDOS, LocalDateTime localDateTime) {
        // 获取所有考勤组
        // -- 根据考勤组ids 获取所有人员
        //将attendanceGroupUserDOS对象中的type相同的userId合并成一个数组 type作为key 用户id列表List<Long>作为value
        Map<Long, List<Long>> groupUserMap = attendanceGroupUserDOS.stream().collect(Collectors.groupingBy(AttendanceGroupUserDO::getAttendanceGroupId,
                Collectors.mapping(AttendanceGroupUserDO::getUserId, Collectors.toList())));
        //获取用户部门
        //合并groupUserMap的values
        List<Long> userIds = groupUserMap.values().stream().flatMap(Collection::stream).collect(Collectors.toList());
        List<AdminUserDO> userList = adminUserService.getUserList(userIds);
        Map<Long, AdminUserDO> userMap = userList.stream().collect(Collectors.toMap(AdminUserDO::getId, a -> a));
        // -- 获取考勤组下考勤规则 - 将将考勤组分组 - 按类型
        String time = localDateTime.format(Constants.REPO_DATE_FORMAT);
        // 获取到考勤组 - 班次 key/value 格式
        Map<Long, Long> map = this.getAttendanceGroupShiftIdGroupByGroup(attendanceGroupDOS, localDateTime);
        Map<Long, AttendanceGroupDO> groupMap = attendanceGroupDOS.stream().collect(Collectors.toMap(AttendanceGroupDO::getId, v -> v));
        // -- 获取班次子表列表
        if (MapUtil.isNotEmpty(map)) {
            List<AttendanceGroupShiftItemDO> attendanceGroupShiftItemDOList = attendanceGroupShiftItemService.getGroupShiftItemListByShiftIds(new ArrayList<>(map.values()));
            Map<Long, List<AttendanceGroupShiftItemDO>> itemMaps = attendanceGroupShiftItemDOList.stream().collect(Collectors.groupingBy(AttendanceGroupShiftItemDO::getKqAttendanceGroupShiftId));
            this.defaultPersistence(map, groupMap, groupUserMap, userMap, itemMaps, time, localDateTime);
        }
    }


    /**
     * @param map           获取到考勤组 - 班次 key/value 格式
     * @param groupMap      考勤组id分map
     * @param groupUserMap  考勤组id分组用户列表
     * @param itemMaps      考勤班次子表根据考勤班次分组
     * @param time          时间格式 yyyy-MM-dd
     * @param localDateTime 需要预设的时间
     */
    @Override
    public void defaultPersistence(Map<Long, Long> map, Map<Long, AttendanceGroupDO> groupMap, Map<Long, List<Long>> groupUserMap,
                                   Map<Long, AdminUserDO> userMap, Map<Long, List<AttendanceGroupShiftItemDO>> itemMaps, String time, LocalDateTime localDateTime) {
        List<AttendancePunchRecordDO> attendancePunchRecordDOList = new ArrayList<>();
        LocalDateTime nextDayLocalDateTime = LocalDateTimeUtil.offset(localDateTime, 1, ChronoUnit.DAYS);

        // -- 查看这天是否有通过的请假单子 / --------------事前请假流程
//        List<BpmOALeaveRpcVO> leaveListByTime = bpmOALeaveApi.getLeaveListByTime(time).getCheckedData();
//        // 如果有的话怎么处理？
//        Map<Long, List<BpmOALeaveRpcVO>> leaveMap = leaveListByTime.stream().collect(Collectors.groupingBy(BpmOALeaveRpcVO::getUserId));
//        List<BpmOALeaveRpcVO> bpmOALeaveRpcVOS = leaveMap.get(1L);
//        for (BpmOALeaveRpcVO bpmOALeaveRpcVO : bpmOALeaveRpcVOS) {
//            if (bpmOALeaveRpcVO.getStartTime().isBefore(localDateTime) && bpmOALeaveRpcVO.getEndTime().isAfter(localDateTime)) {
//
//            }
//        }
        Map<String, String[]> delLeaveMap = new HashMap<>();
        for (Map.Entry<Long, Long> entry : map.entrySet()) {
            String key = Constants.ATTENDANCE + Constants.UNDERLINE + entry.getKey() + Constants.UNDERLINE; // + 时间
            AttendanceGroupDO attendanceGroupDO = groupMap.get(entry.getKey());
            List<Long> userIds = groupUserMap.get(entry.getKey());
            if (CollectionUtil.isEmpty(userIds)) {
                continue;
            }
            List<AttendanceGroupShiftItemDO> attendanceGroupShiftItemDOS = itemMaps.get(entry.getValue());
            if (CollectionUtil.isEmpty(attendanceGroupShiftItemDOS)) {
                continue;
            }
            List<AttendanceOnTheDayDTO> attendanceOnTheDayDTOS = attendanceService.buildAttendanceOnTheDay(attendanceGroupShiftItemDOS);
            for (Long userId : userIds) {
                AdminUserDO adminUserDO = userMap.get(userId);
                Map<Object, Object> leaveRedisMap = this.getAttendanceLeaveRedisMap(userId);
                Long deptId = adminUserDO == null ? null : adminUserDO.getDeptId();
                List<String> leaveIds = new ArrayList<>();
                for (AttendanceOnTheDayDTO attendanceOnTheDayDTO : attendanceOnTheDayDTOS) {
                    AttendancePunchRecordDO attendancePunchRecordDO = new AttendancePunchRecordDO();
                    attendancePunchRecordDO.setUserId(userId);
                    attendancePunchRecordDO.setDeptId(deptId);
                    attendancePunchRecordDO.setAttendanceGroupId(entry.getKey());
                    attendancePunchRecordDO.setAttendanceGroupName(attendanceGroupDO.getGroupName());
                    attendancePunchRecordDO.setAttendanceGroupShiftId(attendanceOnTheDayDTO.getKqAttendanceGroupShiftId());
                    attendancePunchRecordDO.setAttendanceGroupShiftName(attendanceOnTheDayDTO.getKqAttendanceGroupShiftName());
                    attendancePunchRecordDO.setAttendanceGroupShiftItemId(attendanceOnTheDayDTO.getId());
                    attendancePunchRecordDO.setType(attendanceGroupDO.getType());
                    attendancePunchRecordDO.setPunchType(attendanceGroupDO.getPunchType());
                    attendancePunchRecordDO.setWorkType(attendanceOnTheDayDTO.getType());
                    attendancePunchRecordDO.setLevel(attendanceOnTheDayDTO.getLevel());
                    attendancePunchRecordDO.setStatus(AttendanceOnTheDayDTO.PUNCH_STATUS_UN_PUNCH);
                    attendancePunchRecordDO.setFieldServiceFlag(Constants.FALSE);
                    attendancePunchRecordDO.setNextDayFlag(Constants.TRUE);
                    attendancePunchRecordDO.setDayTime(time);

                    LocalDateTime shouldPunchTime = LocalDateTime.ofInstant(DateUtils.buildHHmmTime(attendanceOnTheDayDTO.getTime(),
                            (attendanceOnTheDayDTO.getNextDayFlag() == 0 ? localDateTime : nextDayLocalDateTime)).toInstant(), ZoneId.systemDefault());
                    // -- 请假插入预设
                    for (Map.Entry<Object, Object> leaveEntry : leaveRedisMap.entrySet()) {
                        BpmOALeaveDTO dto = JSONUtil.toBean(leaveEntry.getValue().toString(), BpmOALeaveDTO.class);
                        // - 如果在这个区间之内 - 那么就是需要设为请假的
                        if ((dto.getStartTime().isBefore(shouldPunchTime) || dto.getStartTime().equals(shouldPunchTime)) &&
                                (dto.getEndTime().isAfter(shouldPunchTime) || dto.getEndTime().equals(shouldPunchTime))) {
                            attendancePunchRecordDO.setStatus(AttendanceOnTheDayDTO.ASK_FOR_LEAVE);
                            attendancePunchRecordDO.setLeaveId(dto.getId());
                            attendancePunchRecordDO.setLeaveType(dto.getType());
                        } else if (dto.getEndTime().isBefore(shouldPunchTime)) {
                            leaveIds.add(dto.getId().toString());
                        }
                    }

                    String actualDayTime = shouldPunchTime.format(Constants.REPO_DATE_FORMAT);
                    attendancePunchRecordDO.setActualDayTime(actualDayTime);
                    attendancePunchRecordDO.setShouldPunchTime(shouldPunchTime);
                    attendancePunchRecordDO.setLatestPunchTime(shouldPunchTime.plusMinutes(attendanceOnTheDayDTO.getAfterPunchTime()));
                    attendancePunchRecordDOList.add(attendancePunchRecordDO);
                }
                stringRedisTemplate.opsForHash().put(key + time, userId.toString(), JSONUtil.toJsonStr(attendanceOnTheDayDTOS));
                if (!leaveIds.isEmpty()) {
                    delLeaveMap.put("leave" + "_" + userId, leaveIds.toArray(new String[0]));
                }
            }
            //设置缓存 2天
            stringRedisTemplate.expire(key + time, 2, TimeUnit.DAYS);
        }
        // -- 删除redis中的请假数据
        this.delLeave(delLeaveMap);
        // -- 批量
        this.saveBatch(attendancePunchRecordDOList);
    }

    /**
     * 删除redis中的请假数据
     *
     * @param delLeaveMap
     */
    private void delLeave(Map<String, String[]> delLeaveMap) {
        if (MapUtil.isNotEmpty(delLeaveMap)) {
            for (Map.Entry<String, String[]> entry : delLeaveMap.entrySet()) {
                stringRedisTemplate.opsForHash().delete(entry.getKey(), entry.getValue());
            }
        }
    }

    /**
     * 获取用户请假redisMap
     *
     * @param userId
     * @return
     */
    private Map<Object, Object> getAttendanceLeaveRedisMap(Long userId) {
        HashOperations<String, Object, Object> hashOps = stringRedisTemplate.opsForHash();
        String key = "leave" + "_" + userId.toString();
        return hashOps.entries(key);
    }

    @Override
    public List<AttendancePunchRecordDO> getNotReminded(LocalDateTime localDateTime) {
        String targetDayStr = localDateTime.format(Constants.REPO_DATE_FORMAT);
        return punchRecordMapper.selectList(new LambdaQueryWrapper<AttendancePunchRecordDO>()
                .eq(AttendancePunchRecordDO::getDayTime, targetDayStr)
                .eq(AttendancePunchRecordDO::getStatus, AttendanceOnTheDayDTO.PUNCH_STATUS_UN_PUNCH)
                .eq(AttendancePunchRecordDO::getRemindFlag, Constants.FALSE));
    }

    @Override
    public void batchUpdate(List<AttendancePunchRecordDO> editList) {
        punchRecordMapper.updateBatch(editList);
    }


    @Override
    public void askingForLeaveAfterwardsToModifyAttendance(AttendancePunchRecordDTO dto) {
        punchRecordMapper.update(new AttendancePunchRecordDO()
                        .setStatus(AttendanceOnTheDayDTO.ASK_FOR_LEAVE)
                        .setLeaveId(dto.getLeaveId())
                        .setLeaveType(dto.getLeaveType()),
                new LambdaUpdateWrapper<AttendancePunchRecordDO>().eq(AttendancePunchRecordDO::getUserId, dto.getUserId())
                        .ge(AttendancePunchRecordDO::getShouldPunchTime, dto.getStartTime())
                        .le(AttendancePunchRecordDO::getShouldPunchTime, dto.getEndTime()));
    }

    private Map<Long, Long> getAttendanceGroupShiftIdGroupByGroup(List<AttendanceGroupDO> attendanceGroupDOS, LocalDateTime localDateTime) {
        Map<Long, Long> map = new HashMap<>();
        List<AttendanceGroupDO> fixedList = attendanceGroupDOS.stream().filter(a -> a.getType().equals(1)).collect(Collectors.toList());
        List<AttendanceGroupDO> schedulingList = attendanceGroupDOS.stream().filter(a -> a.getType().equals(2)).collect(Collectors.toList());
        if (!fixedList.isEmpty()) {
            Map<Long, Long> attendanceFixedMap = attendanceFixedService.getGroupToShiftIdMap(fixedList, localDateTime);
            if (ObjectUtil.isNotEmpty(attendanceFixedMap)) {
                map.putAll(attendanceFixedMap);
            }
        }
        if (!schedulingList.isEmpty()) {
            Map<Long, Long> attendanceSchedulingMap = attendanceSchedulingService.getGroupToShiftIdMap(schedulingList, localDateTime);
            if (ObjectUtil.isNotEmpty(attendanceSchedulingMap)) {
                map.putAll(attendanceSchedulingMap);
            }
        }
        return map;
    }

}
