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.bean.applytask.CurrentApplyTaskVO;
import com.qmth.exam.reserve.bean.category.CategoryCacheBean;
import com.qmth.exam.reserve.bean.org.OrgInfo;
import com.qmth.exam.reserve.bean.timeperiod.TimePeriodExamSiteBean;
import com.qmth.exam.reserve.cache.CacheConstants;
import com.qmth.exam.reserve.cache.impl.ApplyTaskCacheService;
import com.qmth.exam.reserve.cache.impl.CategoryCacheService;
import com.qmth.exam.reserve.cache.impl.OrgCacheService;
import com.qmth.exam.reserve.dao.StudentApplyDao;
import com.qmth.exam.reserve.entity.*;
import com.qmth.exam.reserve.service.*;
import com.qmth.exam.reserve.util.DateUtil;
import com.qmth.exam.reserve.util.UnionUtil;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.redisson.api.RLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
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;

    @Autowired
    private TimePeriodService timePeriodService;

    @Autowired
    private CategoryCacheService categoryCacheService;

    @Autowired
    private StudentApplyService studentApplyService;

    @Autowired
    private OrgCacheService orgCacheService;


    @Override
    public String autoAssign(Long taskId, Long operateId) {
        StringJoiner stringJoiner = new StringJoiner("\n");
        log.warn("[autoAssign] 开始自动预约考试");

        RLock lock = (RLock) concurrentService.getLock(CacheConstants.LOCK_AUTO_APPLY);
        try {
            if (!lock.tryLock()) {
                log.warn("[autoAssign] 获取锁失败，不允许同时执行自动分配！lockKey:{}", CacheConstants.LOCK_AUTO_APPLY);
                throw new StatusException("其他老师正在执行自动分配，请不要重复执行！");
            }

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

            // 未完成预约的考生
            List<StudentEntity> studentList = studentService.listNoFinishStudent(taskId, Boolean.FALSE);
            //过滤掉已完成预约的考生
            studentList = studentList.stream()
                    .filter(item -> item.getApplyNumber() - cacheService.getStudentApplyFinishCount(item.getId()) > 0)
                    .collect(Collectors.toList());

            //按照教学点分组
            Map<Long, List<StudentEntity>> noFinishApplyMap = studentList.stream().collect(Collectors.groupingBy(StudentEntity::getCategoryId));
            // 所有预约时段
            List<TimePeriodExamSiteBean> timeList = timePeriodService.listTimePeriodByTask(taskId);

            stringJoiner.add(
                    MessageFormat.format("{0}-未完成预约的考生数:{1} 个", DateFormatUtils.format(new Date(), DateUtil.LongDateString), studentList.size()));

            // 考位是否充足
            checkTeachingCapacity(noFinishApplyMap, timeList, taskId);

            int successNum = 0;
            // 按照教学点安排考位。规则：不能和已预约的时间上有冲突
            for (Long teachingId : noFinishApplyMap.keySet()) {
                List<ExamSiteEntity> siteList = listExamSite(teachingId);
                if (siteList.isEmpty()) {
                    log.warn("[autoAssign] 教学点{}下没有考点数据,不参与自动分配", teachingId);
                    continue;
                }
                List<StudentEntity> teachingStudentList = noFinishApplyMap.get(teachingId);
                for (ExamSiteEntity site : siteList) {
                    //考点对应的可用时段
                    List<TimePeriodExamSiteBean> timePeriodExamSiteList = listAvailableTimePeriod(taskId, site, timeList);

                    for (TimePeriodExamSiteBean time : timePeriodExamSiteList) {
                        // 剩余的考位
                        int remainNum = cacheService.getApplyAvailableCount(site.getId(), time.getTimePeriodId());
                        if (remainNum > 0) {
                            int applyNum = assignStudentApply(operateId, site.getId(), time.getTimePeriodId(), teachingStudentList, remainNum);
                            successNum += applyNum;
                        }
                    }
                }
                // 判断是否还有剩余考生未完成预约，提醒考位不足
                if (!teachingStudentList.isEmpty()) {
                    CategoryCacheBean categoryBean = categoryCacheService.getCategoryById(teachingId);
                    throw new StatusException("【" + categoryBean.getName() + "】教学点考位不足,还需要【" + teachingStudentList.size() + "】个考位");
                }
            }
            stringJoiner.add(MessageFormat.format("{0}-自动预约成功的次数为:{1} 次", DateFormatUtils.format(new Date(), DateUtil.LongDateString), successNum));
        } 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());
            }
        }

        return stringJoiner.toString();
    }

    private void checkTeachingCapacity(Map<Long, List<StudentEntity>> noFinishApplyMap, List<TimePeriodExamSiteBean> timeList, Long taskId) {
        for (Long teachingId : noFinishApplyMap.keySet()) {
            List<ExamSiteEntity> siteList = listExamSite(teachingId);
            if (siteList.isEmpty()) {
                continue;
            }
            List<StudentEntity> teachingStudentList = noFinishApplyMap.get(teachingId);
            // 未预约的考位
            AtomicInteger toBeApplySum = new AtomicInteger();
            teachingStudentList.forEach(item -> {
                toBeApplySum.addAndGet(item.getApplyNumber() - cacheService.getStudentApplyFinishCount(item.getId()));
            });

            AtomicInteger remainSum = new AtomicInteger();
            for (ExamSiteEntity site : siteList) {
                //考点对应的可用时段
                List<TimePeriodExamSiteBean> timePeriodExamSiteList = listAvailableTimePeriod(taskId, site, timeList);
                //剩余可用考位数
                timePeriodExamSiteList.forEach(item -> {
                    remainSum.addAndGet(cacheService.getApplyAvailableCount(site.getId(), item.getTimePeriodId()));
                });
            }

            int difference = remainSum.get() - toBeApplySum.get();
            if (difference < 0) {
                CategoryCacheBean teachingBean = categoryCacheService.getCategoryById(teachingId);
                throw new StatusException("【" + teachingBean.getName() + "】教学点考位不足,还需要【" + (-difference) + "】个考位");
            }
        }
    }

    private List<TimePeriodExamSiteBean> listAvailableTimePeriod(Long taskId, ExamSiteEntity site, List<TimePeriodExamSiteBean> timeList) {
        List<TimePeriodExamSiteBean> timePeriodExamSiteList = timePeriodService.listTimePeriodByExamSiteId(taskId, site.getId());
        // 教学点未设置，则为所有的时段
        if (CollectionUtils.isEmpty(timePeriodExamSiteList)) {
            timePeriodExamSiteList = timeList;
        } else {
            // 解决教学点管理员在设置了预约日期后 学校管理员有新增预约时间段的场景
            timePeriodExamSiteList  = UnionUtil.unionByAttribute(timePeriodExamSiteList, timeList, TimePeriodExamSiteBean::getTimePeriodId);
        }
        // 只取可以预约的时段
        timePeriodExamSiteList = listNoCancelExamTimePeriod(timePeriodExamSiteList);
        timePeriodExamSiteList = timePeriodExamSiteList.stream()
                .filter(TimePeriodExamSiteBean::getEnable)
                .collect(Collectors.toList());
        return timePeriodExamSiteList;
    }

    private List<TimePeriodExamSiteBean> listNoCancelExamTimePeriod(List<TimePeriodExamSiteBean> timePeriodList) {
        OrgInfo org = orgCacheService.currentOrg();
        CurrentApplyTaskVO curApplyTask = cacheService.currentApplyTask(org.getOrgId());
        Long longToday = DateUtil.getLongTimeByDate(DateUtil.formatShortSplitDateString(new Date()) + " 00:00:00");
        Date today = new Date(longToday);
        Date otherDay = DateUtil.addValues(today, Calendar.DAY_OF_MONTH, curApplyTask.getAllowApplyCancelDays());
        Long longOtherDay = DateUtil.getLongTimeByDate(DateUtil.formatShortSplitDateString(otherDay) + " 23:59:59");
        return timePeriodList.stream().filter(time -> time.getStartTime() > longOtherDay).collect(Collectors.toList());
    }


    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 int assignStudentApply(Long operateId, Long siteId, Long timeId, List<StudentEntity> teachingStudentList, int remainNum) {
        int num = 0;
        String studentApplyLockKey;
        RLock studentApplyLock;
        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("[autoAssign] 获取锁失败，考生在同时操作预约！lockKey:{}", studentApplyLockKey);
                    iterator.remove();
                } else {
                    log.warn("[autoAssign] 获取锁成功！lockKey:{}", studentApplyLockKey);

                    // 考生已完成预约的数量
                    int haveApplyCount = cacheService.getStudentApplyFinishCount(student.getId());
                    // 还需要预约的数量
                    int toApplyNum = student.getApplyNumber() - haveApplyCount;

                    if (toApplyNum <= 0) {
                        log.warn("[autoAssign] 数据问题, 考生需要预约最大次数:{},已完成预约的数量:{} ", 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(operateId);
                        // 保存预约
                        boolean successFlag = studentApplyService.saveOrUpdateStudentApply(studentApply);
                        if(successFlag) {
                            ApplyRecordCacheBean bean = new ApplyRecordCacheBean();
                            bean.setStudentId(student.getId());
                            bean.setExamSiteId(siteId);
                            bean.setTimePeriodId(timeId);
                            bean.setCancel(Boolean.FALSE);
                            bean.setOperateId(operateId);
                            bean.setOperateTime(System.currentTimeMillis());
                            bean.setBizId(cacheService.increaseBizId());

                            // 某考点某时段的“剩余可约数量”（抢占1个数量）
                            boolean takeSuccess = cacheService.decreaseApplyAvailableCount(bean.getExamSiteId(), bean.getTimePeriodId());
                            if (!takeSuccess) {
                                log.warn("[autoAssign] 预约失败，当前预约时段已约满！examSiteId:{} timePeriodId:{} studentId:{}",
                                        bean.getExamSiteId(), bean.getTimePeriodId(), bean.getStudentId());
                                continue;
                            }
                            // 保存至预约缓存
                            cacheService.saveStudentApplyRecord(bean);
                        }

                        num++;

                        //完成所有的预约，移除考生
                        if (student.getApplyNumber() - (haveApplyCount + 1) == 0) {
                            iterator.remove();
                        }
                    }
                }
            } catch (Exception e) {
                throw new StatusException("自动安排预约失败，失败原因:" + e.getMessage());
            } finally {
                try {
                    // 解锁前检查当前线程是否持有该锁
                    if (studentApplyLock.isLocked() && studentApplyLock.isHeldByCurrentThread()) {
                        studentApplyLock.unlock();
                        log.info("解锁成功！lockKey:{}", studentApplyLockKey);
                    }
                } catch (Exception e) {
                    log.warn(e.getMessage());
                }
            }

        }
        return num;
    }

    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;
    }

}