package com.qmth.exam.reserve.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.qmth.boot.core.concurrent.service.ConcurrentService;
import com.qmth.boot.core.exception.StatusException;
import com.qmth.exam.reserve.bean.apply.ApplyRecordCacheBean;
import com.qmth.exam.reserve.cache.CacheConstants;
import com.qmth.exam.reserve.cache.impl.ApplyTaskCacheService;
import com.qmth.exam.reserve.dao.StudentApplyDao;
import com.qmth.exam.reserve.entity.*;
import com.qmth.exam.reserve.service.*;
import org.redisson.api.RLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

@Service
public class StudentAutoAssignServiceImpl extends ServiceImpl<StudentApplyDao, StudentApplyEntity> implements StudentAutoAssignService {

    private final Logger log = LoggerFactory.getLogger(StudentAutoAssignServiceImpl.class);

    @Autowired
    private ConcurrentService concurrentService;

    @Autowired
    private StudentService studentService;

    @Autowired
    private ApplyTaskCacheService cacheService;

    @Autowired
    private CategoryService categoryService;

    @Autowired
    private ExamSiteService examSiteService;

    @Async
    @Override
    public void autoAssign(Long taskId, Long userId, List<TimePeriodEntity> timeList) {
        log.warn("开始自动预约考试");
        long start = System.currentTimeMillis();
        RLock lock = (RLock) concurrentService.getLock(CacheConstants.LOCK_AUTO_APPLY);
        try {
            if (!lock.tryLock()) {
                log.warn("获取锁失败，不允许同时执行自动分配！lockKey:{}", CacheConstants.LOCK_AUTO_APPLY);
                throw new StatusException("其他老师正在执行自动分配，请不要重复执行！");
            }

            log.warn("获取锁成功！lockKey:{}", CacheConstants.LOCK_AUTO_APPLY);

            // 1、未完成预约的考生
            List<StudentEntity> studentList = studentService.listNoFinishStudent(taskId, Boolean.FALSE);
            Map<Long, List<StudentEntity>> noFinishApplyMap = studentList.stream().collect(Collectors.groupingBy(StudentEntity::getCategoryId));
            // 2、考位是否充足
            //List<TimePeriodEntity> timeList = listTimePeriod(taskId);
            //timeList = listNoCancelExamTimePeriod(timeList, taskId);
            //checkTeachingCapacity(noFinishApplyMap, timeList, taskId);
            // 3、按照教学点安排考位。规则：不能和已预约的时间上有冲突
            for (Long key : noFinishApplyMap.keySet()) {
                List<ExamSiteEntity> siteList = listExamSite(key);
                if(siteList.isEmpty()) {
                    log.warn("教学点{}下没有考点数据,不参与自动分配", key);
                    continue;
                }
                List<StudentEntity> teachingStudentList = noFinishApplyMap.get(key);
                for (ExamSiteEntity site : siteList) {
                    for (TimePeriodEntity time : timeList) {
                        // 该时段已预约的考生
                        Integer haveApplyNum = cacheService.getApplyFinishCount(site.getId(), time.getId());
                        if (haveApplyNum == 0) {
                            haveApplyNum = getHaveApplyNum(site.getId(), time.getId());
                        }
                        // 剩余的考位
                        int remainNum = site.getCapacity() - haveApplyNum;
                        if (remainNum < 0) {
                            log.warn("已预约的考生数超出考点的最大容量,redis中有脏数据。siteId:{} - timeId:{}", site.getId(), time.getId());
                            throw new StatusException("数据异常");
                        }

                        assignStudentApply(userId, site.getId(), time.getId(), teachingStudentList, remainNum);
                    }
                }
                // 4、判断是否还有剩余考生未完成预约，提醒考位不够
                if (!teachingStudentList.isEmpty())
                    throw new StatusException("【" + categoryService.getById(key).getName() + "】教学点考位不足");
            }
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new StatusException(e.getMessage());
        } finally {
            try {
                // 解锁前检查当前线程是否持有该锁
                if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                    lock.unlock();
                    log.info("解锁成功！lockKey:{}", CacheConstants.LOCK_AUTO_APPLY);
                }
            } catch (Exception e) {
                log.warn(e.getMessage());
            }
        }

        long end = System.currentTimeMillis();
        log.warn("完成自动预约，耗时：{}s", (end - start) / 1000);
    }


    private List<ExamSiteEntity> listExamSite(Long categoryId) {
        LambdaQueryWrapper<ExamSiteEntity> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ExamSiteEntity::getCategoryId, categoryId);
        wrapper.eq(ExamSiteEntity::getEnable, Boolean.TRUE);
        return examSiteService.list(wrapper);
    }

    private Integer getHaveApplyNum(Long siteId, Long timeId) {
        LambdaQueryWrapper<StudentApplyEntity> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(StudentApplyEntity::getExamSiteId, siteId);
        wrapper.eq(StudentApplyEntity::getTimePeriodId, timeId);
        wrapper.eq(StudentApplyEntity::getCancel, Boolean.FALSE);
        return baseMapper.selectCount(wrapper);
    }

    private void assignStudentApply(Long userId, Long siteId, Long timeId, List<StudentEntity> teachingStudentList, Integer remainNum) {
        int num = 0;
        String studentApplyLockKey = null;
        RLock studentApplyLock = null;
        for (Iterator<StudentEntity> iterator = teachingStudentList.iterator(); iterator.hasNext(); ) {
            StudentEntity student = iterator.next();
            if (num >= remainNum) break;
            studentApplyLockKey = String.format(CacheConstants.LOCK_STUDENT_APPLY, student.getId());
            studentApplyLock = (RLock) concurrentService.getLock(studentApplyLockKey);
            try {
                if (!studentApplyLock.tryLock()) {
                    log.warn("获取锁失败，考生在同时操作预约！lockKey:{}", studentApplyLockKey);
                    iterator.remove();
                } else {
                    log.warn("获取锁成功！lockKey:{}", studentApplyLockKey);

                    //List<StudentApplyEntity> studentApplyList = listStudentApply(student.getId());
                    int haveApplyCount = cacheService.getStudentApplyFinishCount(student.getId());
                    int toApplyNum = student.getApplyNumber() - haveApplyCount;
                    if (toApplyNum > 0 && !haveApplySameTimePeriod(siteId, timeId, student.getId())) {
                        StudentApplyEntity studentApply = new StudentApplyEntity();
                        studentApply.setStudentId(student.getId());
                        studentApply.setExamSiteId(siteId);
                        studentApply.setCancel(Boolean.FALSE);
                        studentApply.setTimePeriodId(timeId);
                        studentApply.setOperateId(userId);

                        StudentApplyEntity existStudentApply = findStudentApply(studentApply);
                        if (existStudentApply != null) {
                            existStudentApply.setCancel(Boolean.FALSE);
                            baseMapper.updateById(existStudentApply);
                        } else {
                            baseMapper.insert(studentApply);
                        }

                        num++;
                        if (student.getApplyNumber() - (haveApplyCount + 1) == 0) {
                            iterator.remove();
                        }

                        ApplyRecordCacheBean bean = new ApplyRecordCacheBean();
                        bean.setStudentId(studentApply.getStudentId());
                        bean.setExamSiteId(studentApply.getExamSiteId());
                        bean.setTimePeriodId(studentApply.getTimePeriodId());
                        bean.setCancel(Boolean.FALSE);
                        bean.setOperateId(userId);
                        bean.setOperateTime(System.currentTimeMillis());

                        // 先推送至预约队列
                        cacheService.pushStudentApplyRecordQueue(bean);
                        cacheService.saveStudentApplyRecord(bean);
                        cacheService.increaseApplyFinishCount(studentApply.getExamSiteId(), studentApply.getTimePeriodId());
                    }
                }
            } catch (Exception e) {
                log.error("自动安排预约失败,错误信息：{}", e.getMessage());
                throw new StatusException("自动安排预约失败，请稍后再试！");
            } finally {
                try {
                    // 解锁前检查当前线程是否持有该锁
                    if (studentApplyLock.isLocked() && studentApplyLock.isHeldByCurrentThread()) {
                        studentApplyLock.unlock();
                        log.info("解锁成功！lockKey:{}", studentApplyLockKey);
                    }
                } catch (Exception e) {
                    log.warn(e.getMessage());
                }
            }

        }
    }

    private List<StudentApplyEntity> listStudentApply(Long stdId) {
        LambdaQueryWrapper<StudentApplyEntity> lm = new LambdaQueryWrapper<>();
        lm.eq(StudentApplyEntity::getStudentId, stdId);
        lm.eq(StudentApplyEntity::getCancel, Boolean.FALSE);
        return baseMapper.selectList(lm);
    }

    private StudentApplyEntity findStudentApply(StudentApplyEntity studentApply) {
        LambdaQueryWrapper<StudentApplyEntity> lm = new LambdaQueryWrapper<>();
        lm.eq(StudentApplyEntity::getExamSiteId, studentApply.getExamSiteId());
        lm.eq(StudentApplyEntity::getTimePeriodId, studentApply.getTimePeriodId());
        lm.eq(StudentApplyEntity::getStudentId, studentApply.getStudentId());
        return baseMapper.selectOne(lm);
    }

    private boolean haveApplySameTimePeriod(Long siteId, Long timeId, Long studentId) {
        LambdaQueryWrapper<StudentApplyEntity> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(StudentApplyEntity::getExamSiteId, siteId);
        wrapper.eq(StudentApplyEntity::getTimePeriodId, timeId);
        wrapper.eq(StudentApplyEntity::getStudentId, studentId);
        wrapper.eq(StudentApplyEntity::getCancel, Boolean.FALSE);
        StudentApplyEntity studentApply = baseMapper.selectOne(wrapper);
        return studentApply != null;
    }
}