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

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.Constants;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.distance.GeoUtil;
import cn.iocoder.yudao.module.system.controller.admin.punchrecord.vo.AttendancePunchRecordSaveReqVO;
import cn.iocoder.yudao.module.system.controller.app.attendance.dto.AttendancePunchDTO;
import cn.iocoder.yudao.module.system.controller.app.attendance.dto.AttendancePunchPageDTO;
import cn.iocoder.yudao.module.system.controller.app.attendance.dto.AttendanceStatisticsDTO;
import cn.iocoder.yudao.module.system.controller.app.attendance.vo.AttendancePunchPageVO;
import cn.iocoder.yudao.module.system.controller.app.attendance.vo.AttendancePunchVO;
import cn.iocoder.yudao.module.system.controller.app.attendance.vo.AttendanceStatisticsVO;
import cn.iocoder.yudao.module.system.controller.app.attendance.vo.HolidayVO;
import cn.iocoder.yudao.module.system.dal.dataobject.attendance.group.AttendanceGroupDO;
import cn.iocoder.yudao.module.system.dal.dataobject.attendance.groupshift.AttendanceGroupShiftDO;
import cn.iocoder.yudao.module.system.dal.dataobject.attendance.groupshiftitem.AttendanceGroupShiftItemDO;
import cn.iocoder.yudao.module.system.handler.PunchHandler;
import cn.iocoder.yudao.module.system.service.attendance.group.AttendanceGroupService;
import cn.iocoder.yudao.module.system.service.attendance.groupshift.AttendanceGroupShiftService;
import cn.iocoder.yudao.module.system.service.attendance.groupshiftitem.AttendanceGroupShiftItemService;
import cn.iocoder.yudao.module.system.service.attendance.punch.PunchService;
import cn.iocoder.yudao.module.system.service.attendance.punch.dto.AttendanceOnTheDayDTO;
import cn.iocoder.yudao.module.system.service.attendance.punchrecord.AttendancePunchRecordService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
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.*;

/**
 * 考勤 实现类
 */
@Service
@Slf4j
public class AttendanceServiceImpl implements AttendanceService {
    @Resource
    private PunchHandler punchHandler;
    @Resource
    private AttendanceGroupService attendanceGroupService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private AttendanceGroupShiftService attendanceGroupShiftService;
    @Resource
    private AttendanceGroupShiftItemService attendanceGroupShiftItemService;

    @Resource
    private AttendancePunchRecordService attendancePunchRecordService;

    // 定义一些常量以提高代码的可读性和可维护性
    /**
     * 打卡类型(0上班卡 1下班卡 2迟到卡 3早退卡 4未到打卡时间)
     */
    private static final Integer PUNCH_TYPE_UP_WORK = 0;
    private static final Integer PUNCH_TYPE_DOWN_WORK = 1;
    private static final Integer PUNCH_TYPE_BE_LATE = 2;
    private static final Integer PUNCH_TYPE_LEAVE_EARLY = 3;
    private static final Integer PUNCH_TYPE_NOT_COME_YET = 4;

    /**
     * 上下班标识 0上班 1下班
     */
    private static final Integer UP_WORK = 0;
    private static final Integer DOWN_WORK = 1;

    @Override
    public AttendancePunchPageVO getPunchPage(AttendancePunchPageDTO dto) {
        AttendancePunchPageVO vo = new AttendancePunchPageVO();
        //获取当前登录用户所在群组
        AttendanceGroupDO activationGroup = attendanceGroupService.getByUserId(dto.getUserId());
        //不在考勤组 - 返回回去
        if (activationGroup == null) {
            vo.setInGroup(AttendanceGroupDO.FALSE);
            return vo;
        }
        // 策略模式 - 将不同考勤类型的 分散开逻辑 - 后续好拓展
        PunchService punchService = punchHandler.getResource(AttendanceGroupDO.getCodeByType(activationGroup.getType()));
        return punchService.getPunchPage(dto.setActivationGroup(activationGroup));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public AttendancePunchVO punch(AttendancePunchDTO dto) {
        AttendancePunchPageVO pageVO = this.getPunchPage(new AttendancePunchPageDTO().setLongitude(dto.getLongitude())
                .setLatitude(dto.getLatitude()).setFlag(true).setLocalDateTime(dto.getLocalDateTime()).setUserId(dto.getUserId()));
        // -- 获取当天的考勤记录
        if (AttendanceGroupDO.FALSE.equals(pageVO.getInGroup())) {
            throw exception(NOT_IN_THE_ATTENDANCE_GROUP);
        }
        if (AttendanceGroupDO.FALSE.equals(pageVO.getTodayNeedAttendance())) {
            throw exception(NO_ATTENDANCE_REQUIRED_ON_THE_DAY);
        }
        if (AttendanceGroupDO.FALSE.equals(pageVO.getFieldworkFlag()) && AttendanceGroupDO.FALSE.equals(pageVO.getPunchPoint())) {
            throw exception(DID_NOT_ENTER_THE_ATTENDANCE_RANGE);
        }
        if (PUNCH_TYPE_NOT_COME_YET.equals(pageVO.getPunchType())) {
            throw exception(ATTENDANCE_TIME_NOT_ARRIVED);
        }
        AttendancePunchVO vo = new AttendancePunchVO();
        // -- 插入数据到数据库中 - 如果插入成功 更新redis
        List<AttendanceOnTheDayDTO> attendanceOnTheDayDTOS = pageVO.getAttendanceOnTheDayDTOS();
        AttendanceOnTheDayDTO dayDTO = attendanceOnTheDayDTOS.get(pageVO.getIndex());
        Integer status = AttendancePunchRecordSaveReqVO.convertStatus(pageVO.getPunchType());

        AttendancePunchRecordSaveReqVO attendancePunchRecordSaveReqVO = new AttendancePunchRecordSaveReqVO()
                .setUserId(dto.getUserId())
                .setAttendanceGroupId(pageVO.getActivationGroup().getId())
                .setAttendanceGroupShiftId(pageVO.getAttendanceGroupShiftDO().getId())
                .setAttendanceGroupShiftItemId(pageVO.getAttendanceGroupShiftItemId())
                .setType(pageVO.getActivationGroup().getType())
                .setPunchType(AttendanceGroupDO.PUNCH_TYPE_MI_NI_APP)
                .setWorkType(pageVO.getType())
                .setLevel(dayDTO.getLevel())
                .setStatus(status)
                .setFieldServiceFlag(
                        (AttendanceGroupDO.TRUE.equals(pageVO.getFieldworkFlag()) && AttendanceGroupDO.FALSE.equals(pageVO.getPunchPoint()))
                                ? AttendanceGroupDO.TRUE : AttendanceGroupDO.FALSE)
                .setDayTime(pageVO.getTargetDayStr())
                .setShouldPunchTime(pageVO.getShouldPunchTime())
                .setPunchTime(dto.getLocalDateTime())
                //取绝对值 如果status是负数则变为正数
                .setLateTime(Constants.ONE.equals(status) ? Math.abs(LocalDateTimeUtil.between(dto.getLocalDateTime(), pageVO.getShouldPunchTime(), ChronoUnit.MILLIS)) : 0L)
                .setLeaveEarlyTime(Constants.TWO.equals(status) ? Math.abs(LocalDateTimeUtil.between(dto.getLocalDateTime(), pageVO.getShouldPunchTime(), ChronoUnit.MILLIS)) : 0L)
                .setRemark(dto.getRemark())
                .setImage(dto.getImage())
                .setPunchAddress(dto.getPunchAddress());
        Long id = attendancePunchRecordService.createPunchRecord(attendancePunchRecordSaveReqVO);
        // -- 新增成功后更新redis
        if (id != null) {
            dayDTO.setPunchTime(dto.getLocalDateTime().format(Constants.REPO_DATE_FORMAT));
            dayDTO.setPunchStatus(attendancePunchRecordSaveReqVO.getStatus());
            dayDTO.setFieldServiceFlag(attendancePunchRecordSaveReqVO.getFieldServiceFlag());
            dayDTO.setPunchAddress(dto.getPunchAddress());
            dayDTO.setRemark(dto.getRemark());
            dayDTO.setImage(dto.getImage());
            dayDTO.setShouldPunchTime(pageVO.getShouldPunchTime());
            dayDTO.setPunchLocalDateTime(dto.getLocalDateTime());
            dayDTO.setLateTime(attendancePunchRecordSaveReqVO.getLateTime());
            dayDTO.setLeaveEarlyTime(attendancePunchRecordSaveReqVO.getLeaveEarlyTime());

            String mapKey = dto.getUserId().toString();
            stringRedisTemplate.opsForHash().put(pageVO.getRedisKey(), mapKey, JSONUtil.toJsonStr(attendanceOnTheDayDTOS));
        }
        vo.setActivationGroup(pageVO.getActivationGroup());
        vo.setAttendanceGroupShiftDO(pageVO.getAttendanceGroupShiftDO());
        vo.setAttendanceOnTheDayDTOS(attendanceOnTheDayDTOS);
        return vo;
    }

    @Override
    public AttendanceStatisticsVO statistics(AttendanceStatisticsDTO dto) {
        if (AttendanceStatisticsDTO.TYPE_DAY.equals(dto.getType())) {
            // 按天
            return null;
        }
        return null;
    }


    @Override
    public Boolean isHoliday(LocalDateTime localDateTime) {
        String dateStr = localDateTime.format(Constants.REPO_DATE_FORMAT);
        String year = dateStr.substring(0, 4);
        String key = "holiday_" + year;
        Boolean flag = stringRedisTemplate.hasKey(key);
        // 缓存不存在
        if (Boolean.FALSE.equals(flag)) {
            String url = "https://cdn.jsdelivr.net/gh/NateScarlet/holiday-cn@master/" + year + ".json";
            String json = HttpUtil.get(url, 0);
            JSONObject jsonObject = JSONUtil.parseObj(json);
            Map<String, String> map = jsonObject.getBeanList("days", HolidayVO.class).stream().collect(Collectors.toMap(HolidayVO::getDate, HolidayVO::getIsOffDay));
            stringRedisTemplate.opsForHash().putAll(key, map);
            // -- 删除去年的
            String lastKey = "holiday_" + (Integer.parseInt(year) - 1);
            stringRedisTemplate.delete(lastKey);
        }
        return (Boolean) stringRedisTemplate.opsForHash().get(key, dateStr);
    }


    @Override
    public void calculatePunch(AttendancePunchPageDTO dto, AttendancePunchPageVO vo) {
        LocalDateTime localDateTime = dto.getLocalDateTime();
        AttendanceGroupDO activationGroup = dto.getActivationGroup();
        vo.setFieldworkFlag(activationGroup.getFieldworkFlag());
        // - 根据经纬度判断是否在对应班组的打卡点上
        vo.setPunchPoint(GeoUtil.distance(Double.parseDouble(dto.getLatitude()), Double.parseDouble(dto.getLongitude())
                , Double.parseDouble(activationGroup.getLatitude()), Double.parseDouble(activationGroup.getLongitude()), activationGroup.getScope()));

        // -- 获取班次
        AttendanceGroupShiftDO attendanceGroupShiftDO = attendanceGroupShiftService.getGroupShift(vo.getAttendanceGroupShiftId());
        // -- 获取班次item
        List<AttendanceGroupShiftItemDO> attendanceGroupShiftItemDOList = attendanceGroupShiftItemService.getGroupShiftItemListByShiftId(vo.getAttendanceGroupShiftId());

        String key = Constants.ATTENDANCE + Constants.UNDERLINE + activationGroup.getId() + Constants.UNDERLINE; // + 时间
        String mapKey = dto.getUserId().toString();


        String yesterdayStr = LocalDateTimeUtil.offset(localDateTime, -1, ChronoUnit.DAYS).format(Constants.REPO_DATE_FORMAT);
        String toDayStr = localDateTime.format(Constants.REPO_DATE_FORMAT);
        String targetDayStr = null;
        if (attendanceGroupShiftDO.getNextDayFlag() == null || AttendanceGroupDO.FALSE.equals(attendanceGroupShiftDO.getNextDayFlag())) {
            targetDayStr = toDayStr;
        } else {
            // 如果跨天的话 所以不能够直接通过判断是否跨天来取redis 数据 - 而是应该获取最晚打下班卡时间判断当前时间是否在最晚打卡下班时间之后 - 如果是的话 插入今天的数据
            targetDayStr = yesterdayStr;
            // -- 获取最后一个元素
            AttendanceGroupShiftItemDO attendanceOnTheDayDTO = CollUtil.getLast(attendanceGroupShiftItemDOList);
            // -- 获取最晚下班打卡时间
            LocalDateTime endTime = LocalDateTime.ofInstant(DateUtils.buildHHmmTime(attendanceOnTheDayDTO.getEndTime()).toInstant(), ZoneId.systemDefault()).plusMinutes(attendanceOnTheDayDTO.getAfterPunchTimeDownWork());
            if (localDateTime.isAfter(endTime)) {
                targetDayStr = toDayStr;
            }
        }
        List<AttendanceOnTheDayDTO> attendanceOnTheDayDTOS = this.getAttendanceOnTheDay(key, mapKey, targetDayStr, attendanceGroupShiftItemDOList);
        // -- 这里没有的情况 只可能是redis 数据库崩了/或者班次子表没数据 - 暂时先返回不需要考勤
        if (attendanceOnTheDayDTOS.isEmpty()) {
            vo.setTodayNeedAttendance(AttendanceGroupDO.FALSE);
            return;
        }

        // 定义是否需要更新redis - 默认不需要更新
        boolean updateRedis = false;
        int index = 0;
        for (AttendanceOnTheDayDTO attendanceOnTheDayDTO : attendanceOnTheDayDTOS) {
            if (AttendanceOnTheDayDTO.PUNCH_STATUS_UN_PUNCH.equals(attendanceOnTheDayDTO.getPunchStatus())) {
                LocalDateTime time = LocalDateTime.ofInstant(DateUtils.buildHHmmTime(attendanceOnTheDayDTO.getTime()).toInstant(), ZoneId.systemDefault());
                LocalDateTime lastTime = time.minusMinutes(attendanceOnTheDayDTO.getBeforePunchTime());
                LocalDateTime endTime = time.plusMinutes(attendanceOnTheDayDTO.getAfterPunchTime());
                // 确保beforePunchTime/AfterPunchTime是正数
                if (attendanceOnTheDayDTO.getBeforePunchTime() <= Constants.ZERO || attendanceOnTheDayDTO.getAfterPunchTime() <= Constants.ZERO) {
                    continue; // 跳过当前循环，避免错误 - 如果所有的都错了话 那么返回默认的 未到打卡时间
                }
                if (LocalDateTimeUtil.isIn(localDateTime, lastTime, endTime)) {
                    int punchType = this.determinePunchType(localDateTime, time, UP_WORK.equals(attendanceOnTheDayDTO.getType()));
                    vo.setPunchType(punchType);
                    if (dto.getFlag()) {
                        vo.setType(attendanceOnTheDayDTO.getType());
                        vo.setShouldPunchTime(time);
                        vo.setAttendanceGroupShiftItemId(attendanceOnTheDayDTO.getId());
                        vo.setIndex(index);
                    }
                    break;
                } else {
                    // 如果是未打卡 - 判断当前时间是否在打卡区间内 - 如果不在的话直接set缺卡
                    // - 定时任务还没来得及刷新redis 数据 - 这里需要手动刷新
                    updateRedis = true;
                    attendanceOnTheDayDTO.setPunchStatus(AttendanceOnTheDayDTO.PUNCH_STATUS_MISS);
                }
            }
            index++;
        }
        vo.setTargetDayStr(targetDayStr);
        vo.setAttendanceOnTheDayDTOS(attendanceOnTheDayDTOS);
        if (dto.getFlag()) {
            vo.setRedisKey(key + targetDayStr);
            vo.setAttendanceGroupShiftDO(attendanceGroupShiftDO);
        }
        // 更新redis
        if (updateRedis) {
            stringRedisTemplate.opsForHash().put(key + targetDayStr, mapKey, JSONUtil.toJsonStr(attendanceOnTheDayDTOS));
        }
    }


    /**
     * 确定打卡类型 - 上班打卡 忽略秒 比如 8点上班 8点0分59秒打卡不算迟到
     *
     * @param localDateTime
     * @param time
     * @param isUpWork
     * @return
     */
    private int determinePunchType(LocalDateTime localDateTime, LocalDateTime time, boolean isUpWork) {
        if (localDateTime.isAfter(time.plusMinutes(Constants.ONE)) && isUpWork) {
            return PUNCH_TYPE_BE_LATE;
        } else if (time.plusMinutes(1).isAfter(localDateTime) && isUpWork) {
            return PUNCH_TYPE_UP_WORK;
        } else if (localDateTime.isAfter(time) && !isUpWork) {
            return PUNCH_TYPE_DOWN_WORK;
        } else {
            return PUNCH_TYPE_LEAVE_EARLY;
        }
    }

    private List<AttendanceOnTheDayDTO> getAttendanceOnTheDay(String key, String mapKey, String targetDayStr, List<AttendanceGroupShiftItemDO> attendanceGroupShiftItemDOList) {
        List<AttendanceOnTheDayDTO> attendanceOnTheDayDTOS = new ArrayList<>();
        // 尝试从Redis获取数据
        try {
            attendanceOnTheDayDTOS = this.loadFromRedis(key, targetDayStr, mapKey);
        } catch (Exception e) {
            // Redis操作异常处理逻辑
            log.error("Load from Redis failed.", e);
        }

        // 如果Redis中没有，则从数据库加载并存入Redis
        if (attendanceOnTheDayDTOS.isEmpty()) {
            attendanceOnTheDayDTOS = this.buildAttendanceOnTheDay(attendanceGroupShiftItemDOList);
            this.saveToRedis(key, targetDayStr, mapKey, attendanceOnTheDayDTOS);
        }
        return attendanceOnTheDayDTOS;
    }

    private List<AttendanceOnTheDayDTO> loadFromRedis(String key, String targetDayStr, String mapKey) {
        Object o = stringRedisTemplate.opsForHash().get(key + targetDayStr, mapKey);
        return o != null ? JSONUtil.toList(o.toString(), AttendanceOnTheDayDTO.class) : new ArrayList<>();
    }

    /**
     * 考勤数据设置两天过期
     *
     * @param key
     * @param targetDayStr
     * @param mapKey
     * @param attendanceOnTheDayDTOS
     */
    private void saveToRedis(String key, String targetDayStr, String mapKey, List<AttendanceOnTheDayDTO> attendanceOnTheDayDTOS) {
        stringRedisTemplate.opsForHash().put(key + targetDayStr, mapKey, JSONUtil.toJsonStr(attendanceOnTheDayDTOS));
        //设置缓存 2天
        stringRedisTemplate.expire(key + targetDayStr, 2, TimeUnit.DAYS);
    }

    private List<AttendanceOnTheDayDTO> buildAttendanceOnTheDay(List<AttendanceGroupShiftItemDO> attendanceGroupShiftItemDOList) {
        List<AttendanceOnTheDayDTO> attendanceOnTheDayDTOS = new ArrayList<>();
        //上下班时间
        for (AttendanceGroupShiftItemDO attendanceGroupShiftItemDO : attendanceGroupShiftItemDOList) {
            AttendanceOnTheDayDTO dto = new AttendanceOnTheDayDTO();
            dto.setId(attendanceGroupShiftItemDO.getId());
            dto.setTime(attendanceGroupShiftItemDO.getBeginTime());
            dto.setPunchTime(StringUtils.EMPTY);
            dto.setType(UP_WORK);
            dto.setLevel(attendanceGroupShiftItemDO.getLevel());
            dto.setBeforePunchTime(attendanceGroupShiftItemDO.getBeforePunchTimeUpWork());
            dto.setAfterPunchTime(attendanceGroupShiftItemDO.getAfterPunchTimeUpWork());
            dto.setPunchStatus(AttendanceOnTheDayDTO.PUNCH_STATUS_UN_PUNCH);
            dto.setFieldServiceFlag(AttendanceGroupDO.FALSE);
            attendanceOnTheDayDTOS.add(dto);

            dto = new AttendanceOnTheDayDTO();
            dto.setId(attendanceGroupShiftItemDO.getId());
            dto.setTime(attendanceGroupShiftItemDO.getEndTime());
            dto.setPunchTime(StringUtils.EMPTY);
            dto.setType(DOWN_WORK);
            dto.setLevel(attendanceGroupShiftItemDO.getLevel());
            dto.setBeforePunchTime(attendanceGroupShiftItemDO.getBeforePunchTimeDownWork());
            dto.setAfterPunchTime(attendanceGroupShiftItemDO.getAfterPunchTimeDownWork());
            dto.setPunchStatus(AttendanceOnTheDayDTO.PUNCH_STATUS_UN_PUNCH);
            dto.setFieldServiceFlag(AttendanceGroupDO.FALSE);
            attendanceOnTheDayDTOS.add(dto);
        }
        return attendanceOnTheDayDTOS;
    }
}
