package jnpf.cultivate.v2.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import jnpf.authority.FtbAuthorityApi;
import jnpf.base.ActionResult;
import jnpf.base.service.SuperServiceImpl;
import jnpf.base.vo.PageListVO;
import jnpf.cultivate.mapper.*;
import jnpf.cultivate.service.*;
import jnpf.cultivate.utils.UserApiV2Util;
import jnpf.cultivate.utils.UserExamUtil;
import jnpf.cultivate.v2.service.CultivateExamDrawRuleService;
import jnpf.cultivate.v2.service.V2CultivateExamService;
import jnpf.cultivate.v2.util.CultivateMqSendUtil;
import jnpf.cultivate.v2.util.ExamChangeDetector;
import jnpf.cultivate.v2.util.ExamChangeResult;
import jnpf.culture.util.DateRangeUtil;
import jnpf.exception.HandleException;
import jnpf.model.cultivate.po.exam.FtbCultivateExam;
import jnpf.model.cultivate.po.exam.FtbCultivateExamFrequencyLog;
import jnpf.model.cultivate.po.exam.FtbCultivateExamUser;
import jnpf.model.cultivate.po.exam.FtbCultivateExamUserDetail;
import jnpf.model.cultivate.po.position.FtbCultivatePositionCourseExam;
import jnpf.model.cultivate.po.question.FtbCultivateQuestion;
import jnpf.model.cultivate.po.question.FtbCultivateQuestionBank;
import jnpf.model.cultivate.po.question.FtbCultivateQuestionOption;
import jnpf.model.cultivate.po.teaching.TeachingApprove;
import jnpf.model.cultivate.req.exam.SubExamQuestionReq;
import jnpf.model.cultivate.req.exam.WebRestartExamReq;
import jnpf.model.cultivate.resp.AppQuestionVo;
import jnpf.model.cultivate.resp.ExamPaperVo;
import jnpf.model.cultivate.resp.PaperDrawRuleVo;
import jnpf.model.cultivate.resp.SubExamVo;
import jnpf.model.cultivate.v2.enums.ExamStatusEnum;
import jnpf.model.cultivate.v2.enums.FrequencyEnum;
import jnpf.model.cultivate.v2.enums.MyExamStatusEnum;
import jnpf.model.cultivate.v2.enums.QuestionTypeEnum;
import jnpf.model.cultivate.v2.exam.po.CultivateExam;
import jnpf.model.cultivate.v2.exam.po.CultivateExamDrawRule;
import jnpf.model.cultivate.v2.exam.req.*;
import jnpf.model.cultivate.v2.exam.vo.*;
import jnpf.model.enums.CourseEnums;
import jnpf.model.enums.ExamUpdateStatus;
import jnpf.model.enums.SelfrowingEnum;
import jnpf.permission.V2UserApi;
import jnpf.permission.vo.v2.grades.GradeVO;
import jnpf.permission.vo.v2.position.PositionVO;
import jnpf.permission.vo.v2.user.UserBoundVO;
import jnpf.util.*;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.MutablePair;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 考试服务v2实现
 *
 * @author yanwenfu
 * @create 2026-02-27
 */
@Service
public class V2CultivateExamServiceImpl extends SuperServiceImpl<CultivateExamMapper, CultivateExam> implements V2CultivateExamService {

    @Resource
    private CultivateExamDrawRuleService examDrawRuleService;
    @Resource
    private FtbCultivatePositionCourseExamMapper positionCourseExamMapper;
    @Resource
    private UserApiV2Util userApiV2Util;
    @Resource
    private FtbCultivateExamService examService;
    @Resource
    private FtbCultivateExamUserService examUserService;
    @Resource
    private FtbCultivateExamUserDetailService examUserDetailService;
    @Resource
    private FtbCultivateExamUserMapper examUserMapper;
    @Resource
    private TeachingRecordService teachingRecordService;
    @Resource
    private TeachingApproveMapper teachingApproveMapper;
    @Resource
    private FtbCultivateQuestionMapper questionMapper;
    @Resource
    private FtbCultivateQuestionOptionMapper questionOptionMapper;
    @Resource
    private FtbCultivateQuestionBankMapper questionBankMapper;
    @Resource
    private ExamFrequencyLogService frequencyLogService;
    @Resource
    private UserExamUtil userExamUtil;
    @Resource
    private CultivateMqSendUtil cultivateMqSendUtil;
    @Autowired
    private V2UserApi v2UserApi;
    @Autowired
    private FtbAuthorityApi authorityApi;

    @Override
    public PageInfo<V2ExamListVo> getPage(V2QueryExamReq req) {

        return PageHelper.startPage(req.getCurrentPage(), req.getPageSize())
                .doSelectPageInfo(() -> baseMapper.selectListV2(
                        req.getExamName(),
                        req.getExamType(),
                        req.getNeedUpdate(),
                        req.getLimitedDate()
                ));
    }

    @Override
    public PageInfo<MyExamWebVo> getMyExamWebPage(MyExamWebQueryDto queryDto) {

        return PageHelper.startPage(queryDto.getCurrentPage(), queryDto.getPageSize())
                .doSelectPageInfo(() -> baseMapper.getMyExamWebList(
                        queryDto.getUserId()
                ));
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void addExam(V2SaveExamReq req) throws Exception {

        // req数据验证
        examReqCheck(req, null);
        CultivateExam exam = JsonUtil.getJsonToBean(req, CultivateExam.class);
        exam.setId(FtbUtil.getId());
        exam.setExamId(SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.EXAM));
        exam.setStatus(calExamStatus(req));
        exam.setEnabledMark(ConstantUtil.NUM_TRUE);
        exam.setNeedUpdate(ConstantUtil.UPDATE_FALSE);
        exam.setCreatorUserId(UserProvider.getLoginUserId());
        exam.setCreatorTime(new Date());
        exam.setLastModifyUserId(exam.getCreatorUserId());
        exam.setLastModifyTime(exam.getCreatorTime());
        // 抽题题库
        exam.setDrawDataBase(String.join(",", req.getDrawDataBaseList()));
        // 抽题规则处理
        List<CultivateExamDrawRule> ruleList = getExamDrawRuleList(req.getRandomDrawRule(), exam.getId(), req.getDrawMode(), exam.getCreatorUserId(), exam.getLastModifyUserId());
        List<ExamDrawRuleVo> ruleVoList = getRuleListChange(ruleList, exam);
        // 验证抽题是否可以满足
        List<ConnectDrawRuleVo> abnormalRuleList = getAbnormalRuleList(ruleVoList);
        if (!abnormalRuleList.isEmpty()) {
            List<Integer> questionTypeList = abnormalRuleList.stream().map(ConnectDrawRuleVo::getQuestionType).collect(Collectors.toList());
            throw new HandleException("抽题规则异常: " + getQuestionTypeStr(questionTypeList) + " 数量异常");
        }
        // 考试成员初始化exam_user数据
        if (exam.getExamType().equals(ConstantUtil.EXAM_TYPE_CUSTOM)) {
            insertExamUserBatch(new ArrayList<>(normalize(exam.getExamMemberId())), exam, ruleList);
        }
        this.save(exam);
        examDrawRuleService.saveBatch(ruleList);
    }

    private Integer calExamStatus(V2SaveExamReq req) {
        // 岗位考试始终为进行中状态
        if (req.getExamType().equals(ConstantUtil.EXAM_TYPE_POST)) {
            return ExamStatusEnum.IN_PROGRESS.getCode();
        }
        // 自定义考试
        Date now = new Date();
        // 无时间限制的自定义考试，默认为进行中
        if (req.getStartTime() == null && req.getEndTime() == null) {
            return ExamStatusEnum.IN_PROGRESS.getCode();
        }

        // 有开始时间和结束时间的考试，根据当前时间判断状态
        if (req.getStartTime() != null && req.getEndTime() != null) {
            // 开始时间在未来，未开始
            if (now.before(req.getStartTime())) {
                return ExamStatusEnum.NOT_STARTED.getCode();
            }
            // 在当前时间范围内，进行中
            if ((now.equals(req.getStartTime()) || now.after(req.getStartTime()))
                    && (now.equals(req.getEndTime()) || now.before(req.getEndTime()))) {
                return ExamStatusEnum.IN_PROGRESS.getCode();
            }
            // 结束时间已过，已结束
            if (now.after(req.getEndTime())) {
                return ExamStatusEnum.FINISHED.getCode();
            }
        }

        // 只有开始时间或只有结束时间的异常情况，默认未开始
        return ExamStatusEnum.NOT_STARTED.getCode();
    }

    private String getQuestionTypeStr(List<Integer> questionTypeList) {

        StringBuilder sb = new StringBuilder();
        questionTypeList.forEach(v -> {
            QuestionTypeEnum questionTypeEnum = QuestionTypeEnum.getByCode(v);
            if (null != questionTypeEnum) {
                sb.append(questionTypeEnum.getDescription()).append(",");
            }
        });
        return sb.length() != 0 ? sb.substring(0, sb.length() - 1) : "";
    }

    private List<ExamDrawRuleVo> getRuleListChange(List<CultivateExamDrawRule> ruleList, CultivateExam exam) {

        if (null == ruleList || ruleList.isEmpty()) {
            return List.of();
        }
        List<ExamDrawRuleVo> list = new ArrayList<>();
        ruleList.forEach(v -> {
            ExamDrawRuleVo bean = JsonUtil.getJsonToBean(v, ExamDrawRuleVo.class);
            bean.setDrawDataBase(exam.getDrawDataBase());
            list.add(bean);
        });
        return list;
    }

    private void examReqCheck(V2SaveExamReq req, String id) throws HandleException {
        // 考试名称不能重复
        long count = getRecordCountByName(req.getExamName(), id);
        if (count > 0) {
            throw new HandleException("考试名称已存在");
        }
        // 开始时间和结束时间不能只有一个为空
        if (ConstantUtil.EXAM_TYPE_POST.equals(req.getExamType())) {
            req.setStartTime(null);
            req.setEndTime(null);
        } else {
            // 自定义考试时
            if ((req.getStartTime() != null && req.getEndTime() == null) || (req.getStartTime() == null && req.getEndTime() != null)) {
                throw new HandleException("自定义考试开始时间或结束时间不能为空");
            }
            // 需要选择考试成员
            if (StringUtil.isEmpty(req.getExamMemberId())) {
                throw new HandleException("请选择参加自定义考试的成员");
            }
        }
        // 是否补考为是时, 需要设置补考频次
        if (req.getExamLimitation().equals(ConstantUtil.NUM_TRUE)) {
            if (req.getReExamFrequencyType() == null) {
                throw new HandleException("请选择补考频次");
            }
            if (!req.getReExamFrequencyType().equals(FrequencyEnum.UNLIMITED.getCode()) && null == req.getReExamFrequencyNum()) {
                throw new HandleException("请选择补考次数");
            }
        }
    }

    /**
     * 根据考试名称查询记录数
     *
     * @param examName 考试名称
     * @param id       考试id
     * @return long
     */
    private long getRecordCountByName(String examName, String id) {

        return this.count(new LambdaQueryWrapper<CultivateExam>()
                .eq(CultivateExam::getExamName, examName)
                .eq(CultivateExam::getEnabledMark, ConstantUtil.NUM_TRUE)
                .ne(StringUtil.isNotEmpty(id), CultivateExam::getId, id));
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateExam(String id, V2SaveExamReq req) throws Exception {

        CultivateExam exam = this.getById(id);
        if (null == exam) {
            throw new HandleException("未找到该考试");
        }
        if (!CourseEnums.ExamType.POSITION.getCode().equals(exam.getExamType())
                && null != exam.getStatus() && !exam.getStatus().equals(ExamStatusEnum.NOT_STARTED.getCode())) {
            throw new HandleException("您只能编辑尚未开始的考试");
        }
        // req数据验证
        examReqCheck(req, id);
        List<CultivateExamDrawRule> oldRuleList = examDrawRuleService.list(new LambdaQueryWrapper<CultivateExamDrawRule>()
                .eq(CultivateExamDrawRule::getExamId, exam.getId())
                .eq(CultivateExamDrawRule::getDeleteMark, ConstantUtil.NUM_FALSE)
        );
        List<CultivateExamDrawRule> ruleList = getExamDrawRuleList(req.getRandomDrawRule(), exam.getId(), req.getDrawMode(), exam.getCreatorUserId(), UserProvider.getLoginUserId());
        ExamChangeResult detect = ExamChangeDetector.detect(exam, req, oldRuleList, ruleList);
        if (detect.isChanged()) {
            exam.setVersionBatch(FtbUtil.getId());
        }
        // 成员变更
        String oldMembers = exam.getExamMemberId();
        BeanUtils.copyProperties(req, exam);
        // 设置其他字段的值
        exam.setLastModifyUserId(UserProvider.getLoginUserId());
        exam.setLastModifyTime(new Date());
        // 抽题题库
        exam.setDrawDataBase(String.join(",", req.getDrawDataBaseList()));
        // 抽题规则处理
        List<ExamDrawRuleVo> ruleVoList = getRuleListChange(ruleList, exam);
        // 验证抽题是否可以满足
        List<ConnectDrawRuleVo> abnormalRuleList = getAbnormalRuleList(ruleVoList);
        if (!abnormalRuleList.isEmpty()) {
            List<Integer> questionTypeList = abnormalRuleList.stream().map(ConnectDrawRuleVo::getQuestionType).collect(Collectors.toList());
            throw new HandleException("抽题规则异常: " + getQuestionTypeStr(questionTypeList) + " 数量异常");
        } else {
            if (exam.getNeedUpdate().equals(ConstantUtil.UPDATE_TRUE)) {
                exam.setNeedUpdate(ConstantUtil.UPDATE_FALSE);
            }
        }
        Set<String> oldSet = normalize(oldMembers);
        Set<String> newSet = normalize(req.getExamMemberId());
        // 删除的成员：old - new
        Set<String> removed = new HashSet<>(oldSet);
        removed.removeAll(newSet);
        // 待考试的考卷, 删除后生成新的版本
        Set<String> stayed = new HashSet<>(oldSet);
        stayed.removeAll(removed);
        // 查询保持不变的中待考试的考卷
        List<FtbCultivateExamUser> stayList;
        if (stayed.isEmpty()) {
            stayList = new ArrayList<>();
        } else {
            stayList = examUserService.list(new LambdaQueryWrapper<FtbCultivateExamUser>()
                    .eq(FtbCultivateExamUser::getExamId, exam.getId())
                    .in(FtbCultivateExamUser::getUserId, stayed)
                    .eq(FtbCultivateExamUser::getStatus, CourseEnums.ExamStatus.WAIT.getCode())
                    .eq(FtbCultivateExamUser::getEnabledMark, ConstantUtil.NUM_TRUE));
        }
        Set<String> update = stayList.stream().map(FtbCultivateExamUser::getUserId).collect(Collectors.toSet());
        // 变更的考试成员初始化exam_user数据, 删除后重新添加
        if (exam.getExamType().equals(ConstantUtil.EXAM_TYPE_CUSTOM)) {
            // 新增的成员：new - old
            Set<String> added = new HashSet<>(newSet);
            added.removeAll(oldSet);
            // 岗位考试编辑考试信息时不会生成新的exam_user数据
            removed.addAll(update);
            if (!removed.isEmpty()) {
                removeExamUser(exam.getId(), removed);
            }
            added.addAll(update);
            if (!added.isEmpty()) {
                insertExamUserBatch(new ArrayList<>(added), exam, ruleList);
            }
        } else {
            if (!update.isEmpty()) {
                removeExamUser(exam.getId(), update);
                insertExamUserBatch(new ArrayList<>(update), exam, ruleList);
            }
        }
        exam.setStatus(calExamStatus(req));
        this.updateById(exam);
        if (detect.isRuleChanged()) {
            examDrawRuleService.remove(new LambdaQueryWrapper<CultivateExamDrawRule>().eq(CultivateExamDrawRule::getExamId, id));
            examDrawRuleService.saveBatch(ruleList);
        }
    }

    private void removeExamUser(String id, Set<String> removed) {

        examUserService.update(new LambdaUpdateWrapper<FtbCultivateExamUser>()
                .eq(FtbCultivateExamUser::getExamId, id)
                .in(FtbCultivateExamUser::getUserId, removed)
                .set(FtbCultivateExamUser::getEnabledMark, ConstantUtil.NUM_FALSE)
                .set(FtbCultivateExamUser::getDeleteUserId, UserProvider.getLoginUserId())
                .set(FtbCultivateExamUser::getDeleteTime, new Date())
        );
    }

    private static Set<String> normalize(String str) {
        if (str == null || str.trim().isEmpty()) {
            return Collections.emptySet();
        }

        return Arrays.stream(str.split(","))
                .map(String::trim)
                .filter(s -> !s.isEmpty())
                .collect(Collectors.toSet());
    }

    private void insertExamUserBatch(List<String> userIds, CultivateExam exam, List<CultivateExamDrawRule> ruleList) {

        List<UserBoundVO> list = v2UserApi.userListAndCopy(userIds, null, UserProvider.getUser().getTenantId());
        if (!list.isEmpty()) {
            List<FtbCultivateExamUser> examUserList = new ArrayList<>();
            int questionNum = 0;
            int totalScore = 0;
            for (CultivateExamDrawRule drawRule : ruleList) {
                questionNum += drawRule.getPickCount();
                totalScore += drawRule.getScore() * drawRule.getPickCount();
            }
            // List<FtbCultivateExamFrequencyLog> logList = new ArrayList<>();
            for (UserBoundVO user : list) {
                FtbCultivateExamUser examUser = new FtbCultivateExamUser(exam.getId(), exam.getExamTime(),
                        calculateScore(totalScore, exam.getPassType(), exam.getPassMark()), calculateScore(totalScore, exam.getExcellentType(), exam.getExcellentMark()),
                        user.getId(), exam.getStartTime(), exam.getEndTime(),
                        user.getOrganizeId(), totalScore, questionNum, JsonUtil.getObjectToString(ruleList));
                examUser.setVersionBatch(exam.getVersionBatch());
                examUserList.add(examUser);
                // FtbCultivateExamFrequencyLog log = new FtbCultivateExamFrequencyLog(FtbUtil.getId(), exam.getId(), user.getId(), UserProvider.getLoginUserId());
                // logList.add(log);
            }
            examUserService.saveBatch(examUserList);
            // frequencyLogService.saveBatch(logList);
        }
    }

    private BigDecimal calculateScore(int totalScore, Integer type, Integer mark) {

        if (type.equals(1)) {
            return new BigDecimal(mark).setScale(2, RoundingMode.HALF_UP);
        }
        return BigDecimal.valueOf(totalScore)
                .multiply(BigDecimal.valueOf(mark))
                .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
    }

    private List<CultivateExamDrawRule> getExamDrawRuleList(List<V2RandomDrawRuleReq> randomDrawRule, String examId,
                                                            Integer drawMode, String createUser, String updateUser) throws Exception {

        List<CultivateExamDrawRule> ruleList = JsonUtil.getJsonToList(randomDrawRule, CultivateExamDrawRule.class);
        for (CultivateExamDrawRule v : ruleList) {
            v.setId(FtbUtil.getId());
            v.setExamId(examId);
            if (drawMode.equals(ConstantUtil.DRAW_MODE_RANDOM)) {
                if (null == v.getEasyCount()) {
                    v.setEasyCount(0);
                }
                if (null == v.getMediumCount()) {
                    v.setMediumCount(0);
                }
                if (null == v.getHardCount()) {
                    v.setHardCount(0);
                }
                int totalCount = v.getEasyCount() + v.getMediumCount() + v.getHardCount();
                if (totalCount != v.getPickCount()) {
                    throw new HandleException("抽题数量异常");
                }
            }
            v.setCreatorUserId(createUser);
            v.setLastModifyUserId(updateUser);
        }
        return ruleList;
    }

    @Override
    public V2ExamVo getDetail(String id, Integer copy) {

        CultivateExam exam = this.getById(id);
        V2ExamVo vo = JsonUtil.getJsonToBean(exam, V2ExamVo.class);
        String[] split = exam.getDrawDataBase().split(",");
        vo.setDrawDataBaseList(Arrays.asList(split));
        // 抽题规则查询
        List<CultivateExamDrawRule> list = examDrawRuleService.list(new LambdaQueryWrapper<CultivateExamDrawRule>().eq(CultivateExamDrawRule::getExamId, vo.getId()));
        List<V2RandomDrawRuleReq> ruleList = JsonUtil.getJsonToList(list, V2RandomDrawRuleReq.class);
        if (!ruleList.isEmpty() && StringUtil.isNotEmpty(ruleList.get(0).getQuestionId())) {
            List<String> questionIds = ruleList.stream().map(V2RandomDrawRuleReq::getQuestionId).collect(Collectors.toList());
            List<FtbCultivateQuestion> questionList = questionMapper.selectList(new LambdaQueryWrapper<FtbCultivateQuestion>().in(FtbCultivateQuestion::getId, questionIds));
            Map<String, FtbCultivateQuestion> questionMap = questionList.stream().collect(Collectors.toMap(FtbCultivateQuestion::getId, Function.identity()));
            ruleList.forEach(v -> {
                FtbCultivateQuestion question = questionMap.get(v.getQuestionId());
                if (null != question) {
                    v.setQuestionName(question.getContent());
                }
            });
        }
        vo.setRandomDrawRule(ruleList);
        if (ConstantUtil.NUM_TRUE == copy) {
            vo.setId(null);
            vo.setExamId(null);
            vo.setExamName(null);
        }
        return vo;
    }

    @Override
    public V2ExamDetailVo getMarkDetail(String id) {

        CultivateExam exam = this.getById(id);
        V2ExamDetailVo vo = JsonUtil.getJsonToBean(exam, V2ExamDetailVo.class);
        // 相关岗位查询 postNames
        vo.setPostNames(getPostInfoList(id));
        // 查询抽题规则 获取总题目数、总分数
        List<CultivateExamDrawRule> list = examDrawRuleService.list(new LambdaQueryWrapper<CultivateExamDrawRule>().eq(CultivateExamDrawRule::getExamId, vo.getId()));
        if (!list.isEmpty()) {
            int totalQuestionNum = 0;
            int totalScore = 0;
            for (CultivateExamDrawRule rule : list) {
                totalQuestionNum += rule.getPickCount();
                totalScore += rule.getScore() * rule.getPickCount();
            }
            vo.setTotalQuestionNum(totalQuestionNum);
            vo.setTotalScore(totalScore);
        }
        return vo;
    }

    private List<String> getPostInfoList(String id) {

        List<FtbCultivatePositionCourseExam> postList = positionCourseExamMapper.selectList(new LambdaQueryWrapper<FtbCultivatePositionCourseExam>()
                .eq(FtbCultivatePositionCourseExam::getEnabledMark, ConstantUtil.NUM_TRUE)
                .eq(FtbCultivatePositionCourseExam::getExamId, id));
        if (postList.isEmpty()) {
            return List.of();
        }
        Set<String> postSet = new HashSet<>();
        Set<String> gradeSet = new HashSet<>();
        postList.forEach(v -> {
            if (StringUtil.isNotEmpty(v.getPostRankId())) {
                postSet.add(v.getPostRankId());
            }
            if (StringUtil.isNotEmpty(v.getGradeId())) {
                gradeSet.add(v.getGradeId());
            }
        });
        String tenantId = UserProvider.getUser().getTenantId();
        Map<String, PositionVO> postMap = userApiV2Util.listPositionDetailInfoByIdsReturnMap(new ArrayList<>(postSet), tenantId);
        Map<String, GradeVO> gradeMap = userApiV2Util.listGradeByIdsReturnMap(new ArrayList<>(gradeSet), tenantId);
        List<String> postInfoList = new ArrayList<>();
        postList.forEach(v -> {
            PositionVO post = postMap.get(v.getPostRankId());
            String postInfo = post.getFullName();
            if (!StringUtil.isEmpty(v.getGradeId())) {
                GradeVO grade = gradeMap.get(v.getGradeId());
                if (null != grade) {
                    postInfo += "（" + grade.getFullName() + "）";
                }
            }
            postInfoList.add(postInfo);
        });
        return postInfoList;
    }

    @Override
    public PageInfo<V2TestPaperVo> getMarkDetailPage(MarkDetailQueryReq req) {
        List<String> userIds = new ArrayList<>();
        if (StringUtil.isNotEmpty(req.getKeyword())) {
            List<UserBoundVO> list = v2UserApi.userListAndCopyLikeName(req.getKeyword(), null, UserProvider.getUser().getTenantId());
            if (null == list || list.isEmpty()) {
                return new PageInfo<>();
            }
            userIds = list.stream().map(UserBoundVO::getId).collect(Collectors.toList());
        }
        // 获取用户权限
        List<String> authUserList = getAuthUserList();
        // 查询考试版本号
        FtbCultivateExam exam = examService.getById(req.getId());
        PageHelper.startPage(req.getCurrentPage(), req.getPageSize());
        PageInfo<V2TestPaperVo> page = new PageInfo<>(examUserMapper.getExamUserList(req.getId(), exam.getVersionBatch(), userIds, req.getScoreSort()));
        if (CollUtil.isNotEmpty(page.getList())) {
            userIds = page.getList().stream().map(V2TestPaperVo::getUserId).collect(Collectors.toList());
            authUserList.retainAll(new HashSet<>(userIds));
            Map<String, UserBoundVO> userMap = userApiV2Util.getUserPrimaryBoundBatch(userIds, null);
            page.getList().forEach(v -> {
                UserBoundVO user = userMap.get(v.getUserId());
                if (authUserList.contains(v.getUserId())) {
                    v.setHasMarkAuth(ConstantUtil.NUM_TRUE);
                } else {
                    v.setHasMarkAuth(ConstantUtil.NUM_FALSE);
                }
                if (null != user) {
                    v.setUserName(user.getUserName());
                    v.setPostId(user.getPositionId());
                    v.setPostName(user.getPositionName());
                    v.setOrganizeId(user.getOrganizeId());
                    v.setOrganizeName(user.getOrganizeName());
                    v.setGradeId(user.getGradeId());
                    v.setGradeName(user.getGradeName());
                }
            });
        }
        return page;
    }

    private List<String> getAuthUserList() {

        List<String> authUserList;
        ActionResult<List<String>> authList = authorityApi.authGetPermissionScopeUserIds();
        if (null == authList || CollUtil.isEmpty(authList.getData())) {
            authUserList = new ArrayList<>();
        } else {
            authUserList = authList.getData();
        }
        return authUserList;
    }

    @Override
    public void delExam(String id) throws Exception {

        CultivateExam exam = this.getById(id);
        if (null == exam) {
            throw new HandleException("未找到该考试");
        }
        if (!exam.getExamType().equals(CourseEnums.ExamType.POSITION.getCode()) && null != exam.getStatus() && !exam.getStatus().equals(ExamStatusEnum.NOT_STARTED.getCode())
                && !exam.getStatus().equals(ExamStatusEnum.REVOKE.getCode())) {
            throw new HandleException("当前考试状态无法被删除");
        }
        this.update(new LambdaUpdateWrapper<CultivateExam>()
                .eq(CultivateExam::getId, id)
                .set(CultivateExam::getEnabledMark, ConstantUtil.NUM_FALSE)
                .set(CultivateExam::getDeleteTime, new Date())
                .set(CultivateExam::getDeleteUserId, UserProvider.getLoginUserId()));
        // 删除还未考试, 或已逾期的exam_user
        examUserService.update(new LambdaUpdateWrapper<FtbCultivateExamUser>()
                .eq(FtbCultivateExamUser::getExamId, id)
                .in(FtbCultivateExamUser::getStatus, CourseEnums.ExamStatus.WAIT.getCode(), CourseEnums.ExamStatus.OVERDUE.getCode())
                .set(FtbCultivateExamUser::getEnabledMark, ConstantUtil.NUM_FALSE)
                .set(FtbCultivateExamUser::getDeleteTime, new Date())
                .set(FtbCultivateExamUser::getDeleteUserId, UserProvider.getLoginUserId()));
    }

    @Override
    public PageInfo<MyExamVo> myExamList(V2AppQueryExamListReq req) {

        return PageHelper.startPage(req.getCurrentPage(), req.getPageSize())
                .doSelectPageInfo(() -> examUserMapper.getMyExamList(
                        req.getStatus(),
                        UserProvider.getLoginUserId()
                ));
    }

    @Override
    public MyExamTabCountVo getMyExamTabCount() {

        Integer waitExamNum = examUserMapper.getStatusCount(CourseEnums.ExamStatus.WAIT.getCode(), UserProvider.getLoginUserId());
        Integer passExamNum = examUserMapper.getStatusCount(CourseEnums.ExamStatus.PASS.getCode(), UserProvider.getLoginUserId());
        Integer failExamNum = examUserMapper.getStatusCount(CourseEnums.ExamStatus.NO_PASS.getCode(), UserProvider.getLoginUserId());
        return new MyExamTabCountVo(null == waitExamNum ? 0 : waitExamNum,
                null == passExamNum ? 0 : passExamNum,
                null == failExamNum ? 0 : failExamNum
        );
    }

    @Override
    public MyExamDetailVo myExamDetail(String userExamId) {

        // 考试信息
        MyExamDetailVo detail = examUserMapper.getMyExamDetail(userExamId);
        if (detail == null) {
            return null;
        }
        // 考试结果
        MyExamStatusEnum myStatus = MyExamStatusEnum.getByCode(detail.getMyExamStatus());
        if (null != myStatus) {
            if (myStatus.equals(MyExamStatusEnum.QUALIFIED) || myStatus.equals(MyExamStatusEnum.UNQUALIFIED) || myStatus.equals(MyExamStatusEnum.EXCELLENT)) {
                ExamResultVo examResult = examUserMapper.getExamResult(userExamId);
                // 计算正确占比
                if (null != examResult.getCorrectCount()) {
                    BigDecimal correctRate;
                    if (examResult.getCorrectCount().equals(0)) {
                        correctRate = BigDecimal.ZERO;
                    } else {
                        correctRate = BigDecimal.valueOf(examResult.getCorrectCount())
                                .divide(BigDecimal.valueOf(examResult.getQuestionNum()), 2, RoundingMode.HALF_UP)
                                .multiply(new BigDecimal(100));
                    }
                    // 计算错误占比
                    BigDecimal errorRate = BigDecimal.valueOf(100).subtract(correctRate);
                    examResult.setCorrectRate(correctRate);
                    examResult.setErrorRate(errorRate);
                }
                // 查询用户信息
                List<UserBoundVO> list = v2UserApi.userListAndCopy(List.of(examResult.getUserId()), null, UserProvider.getUser().getTenantId());
                if (null != list && !list.isEmpty()) {
                    examResult.setUserName(list.get(0).getUserName());
                }
                detail.setExamResult(examResult);
            }
            // 是否可以考试
            int couldExam = ConstantUtil.NUM_FALSE;
            if (myStatus.equals(MyExamStatusEnum.WAIT)) {
                if (null == detail.getStartTime() && null == detail.getEndTime()) {
                    couldExam = ConstantUtil.NUM_TRUE;
                } else {
                    if (DateDetail.checkTimeBetween(new Date(), detail.getStartTime(), detail.getEndTime())) {
                        couldExam = ConstantUtil.NUM_TRUE;
                    }
                }
            }
            detail.setCouldExam(couldExam);
        }
        // 抽题规则
        if (StringUtil.isNotEmpty(detail.getPickQuestionRule())) {
            List<ExamDrawRuleVo> ruleList = JsonUtil.getJsonToList(detail.getPickQuestionRule(), ExamDrawRuleVo.class);
            List<ExamDrawRuleVo> result = ruleList.stream()
                    .collect(Collectors.groupingBy(ExamDrawRuleVo::getQuestionType))
                    .values()
                    .stream()
                    .map(list -> {
                        ExamDrawRuleVo vo = new ExamDrawRuleVo();
                        vo.setQuestionType(list.get(0).getQuestionType());
                        int totalCount = list.stream()
                                .mapToInt(ExamDrawRuleVo::getPickCount)
                                .sum();
                        vo.setPickCount(totalCount);
                        return vo;
                    })
                    .collect(Collectors.toList());
            detail.setDrawRuleList(result);
            detail.setPickQuestionRule(null);
        }
        return detail;
    }

    @Override
    public List<V2AppQuestionVo> getExamQuestionList(String userExamId) throws Exception {

        MyExamDetailVo detail = examUserMapper.getMyExamDetail(userExamId);
        if (detail == null) {
            return List.of();
        }
        CultivateExam exam = this.getById(detail.getExamId());
        if (null == exam) {
            return List.of();
        }
        if (exam.getNeedUpdate().equals(ConstantUtil.UPDATE_TRUE)) {
            throw new HandleException("考试异常");
        }
        // 抽题规则
        List<CultivateExamDrawRule> ruleList = examDrawRuleService.list(new LambdaQueryWrapper<CultivateExamDrawRule>().eq(CultivateExamDrawRule::getExamId, exam.getId()));
        if (ruleList.isEmpty()) {
            return List.of();
        }
        List<V2AppQuestionVo> questionList;
        if (exam.getDrawMode().equals(ConstantUtil.DRAW_MODE_RANDOM)) {
            // 随机抽题
            questionList = questionMapper.getRandQuestionByRule(ruleList, exam.getDrawDataBase());
            if (questionList.isEmpty()) {
                throw new HandleException("抽题异常");
            }
            List<String> questionIds = questionList.stream().map(V2AppQuestionVo::getQuestionId).collect(Collectors.toList());
            // ruleList为空, 因为sql里已经设置了score的值
            setOptionList(questionList, questionIds, null);
        } else {
            // 固定抽题
            List<String> questionIds = ruleList.stream().map(CultivateExamDrawRule::getQuestionId).collect(Collectors.toList());
            questionList = questionMapper.selectListById(questionIds);
            if (questionList.isEmpty() || questionList.size() != questionIds.size()) {
                throw new HandleException("抽题异常");
            }
            setOptionList(questionList, questionIds, ruleList);
        }
        return questionList;
    }

    private void setOptionList(List<V2AppQuestionVo> questionList, List<String> questionIds, List<CultivateExamDrawRule> ruleList) {

        Map<String, CultivateExamDrawRule> ruleMap;
        if (null != ruleList) {
            ruleMap = ruleList.stream().collect(Collectors.toMap(CultivateExamDrawRule::getQuestionId, Function.identity()));
        } else {
            ruleMap = new HashMap<>();
        }
        // 查询选项
        List<FtbCultivateQuestionOption> optionList = questionOptionMapper.selectList(new LambdaQueryWrapper<FtbCultivateQuestionOption>()
                .in(FtbCultivateQuestionOption::getQuestionId, questionIds)
                .eq(FtbCultivateQuestionOption::getEnabledMark, ConstantUtil.NUM_TRUE)
                .orderByDesc(FtbCultivateQuestionOption::getQuestionId)
                .orderByAsc(FtbCultivateQuestionOption::getSortCode));
        Map<String, List<FtbCultivateQuestionOption>> optionMap = optionList.stream().collect(Collectors.groupingBy(FtbCultivateQuestionOption::getQuestionId));
        questionList.forEach(question -> {
            List<FtbCultivateQuestionOption> oList = optionMap.get(question.getQuestionId());
            if (null != oList && !oList.isEmpty()) {
                question.setQuestionOptionVoList(JsonUtil.getJsonToList(oList, V2QuestionOptionVo.class));
            }
            CultivateExamDrawRule rule = ruleMap.get(question.getQuestionId());
            if (null != rule) {
                question.setQuestionScore(rule.getScore());
            }
        });
    }

    @Override
    public PageInfo<ReadOverExamVo> readOverExamList(QueryReadOverExamListReq req) {

        // 获取用户权限
        List<String> authUserList = getAuthUserList();
        // 组织过滤
        MutablePair<List<String>, Map<String, UserBoundVO>> pair = getOrgQueryFilter(req.getOrganizeList());
        if (null == pair) {
            return new PageInfo<>();
        }
        authUserList.retainAll(pair.getLeft());
        if (authUserList.isEmpty()) {
            return new PageInfo<>();
        }
        pair.setLeft(authUserList);
        PageInfo<ReadOverExamVo> page = PageHelper.startPage(req.getCurrentPage(), req.getPageSize())
                .doSelectPageInfo(() -> examUserMapper.readOverExamList(
                        req.getKeyword(),
                        req.getStatus(),
                        pair.getLeft()
                ));
        if (!page.getList().isEmpty()) {
            List<String> userIdList = page.getList().stream().map(ReadOverExamVo::getUserId).collect(Collectors.toList());
            updateUserMap(pair, userIdList);
            for (ReadOverExamVo vo : page.getList()) {
                UserBoundVO user = pair.getRight().get(vo.getUserId());
                if (null != user) {
                    vo.setUserName(user.getUserName());
                }
            }
        }
        return page;
    }

    private void updateUserMap(MutablePair<List<String>, Map<String, UserBoundVO>> pair, List<String> userIds) {

        if (null == pair.getRight()) {
            List<UserBoundVO> list = v2UserApi.userListAndCopy(userIds, null, UserProvider.getUser().getTenantId());
            if (null == list || list.isEmpty()) {
                pair.setRight(new HashMap<>());
            } else {
                pair.setRight(
                        list.stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity()))
                );
            }
        }
    }

    private MutablePair<List<String>, Map<String, UserBoundVO>> getOrgQueryFilter(List<String> organizeList) {

        List<String> userIds;
        Map<String, UserBoundVO> userMap;
        if (null != organizeList && !organizeList.isEmpty()) {
            ActionResult<List<UserBoundVO>> result = v2UserApi.listTargetOrganizesOrHaveChild(organizeList, false, null, UserProvider.getUser().getTenantId());
            if (null == result || null == result.getData() || result.getData().isEmpty()) {
                return null;
            }
            userIds = result.getData().stream().map(UserBoundVO::getId).collect(Collectors.toList());
            userMap = result.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity()));
        } else {
            userIds = new ArrayList<>();
            userMap = null;
        }
        return MutablePair.of(userIds, userMap);
    }

    @Override
    public ReadOverTabCountVo getReadOverTabCount(List<String> organizeList) {

        // 获取用户权限
        List<String> authUserList = getAuthUserList();
        MutablePair<List<String>, Map<String, UserBoundVO>> pair = getOrgQueryFilter(organizeList);
        if (null == pair) {
            return new ReadOverTabCountVo(0);
        }
        authUserList.retainAll(pair.getLeft());
        if (authUserList.isEmpty()) {
            return new ReadOverTabCountVo(0);
        }
        pair.setLeft(authUserList);
        Integer waitCheckExamNum = examUserMapper.getWaitCheckCount(CourseEnums.ExamStatus.WAIT_CHECK.getCode(), pair.getLeft());
        return new ReadOverTabCountVo(waitCheckExamNum);
    }

    @Override
    public UserExamRankVo getExamRankList(ExamRankReq req) {

        // 组织过滤
        MutablePair<List<String>, Map<String, UserBoundVO>> pair = getOrgQueryFilter(req.getOrganizeList());
        if (null == pair) {
            return null;
        }
        UserExamRankVo rankVo = new UserExamRankVo();
        List<V2AppExamRankingVo> allList = examUserMapper.getUserExamRankList(req.getExamId(), pair.getLeft());
        // 手动分页
        int pageNum = req.getCurrentPage();
        int pageSize = req.getPageSize();
        int start = (pageNum - 1) * pageSize;
        int end = Math.min(start + pageSize, allList.size());
        List<V2AppExamRankingVo> pageList = start >= allList.size() ? Collections.emptyList() : allList.subList(start, end);
        // 构建 PageInfo
        PageInfo<V2AppExamRankingVo> page = new PageInfo<>();
        page.setList(pageList);
        page.setTotal(allList.size());
        page.setPageNum(pageNum);
        page.setPageSize(pageSize);
        if (!page.getList().isEmpty()) {
            // 填写用户信息
            List<String> userIdList = page.getList().stream().map(V2AppExamRankingVo::getUserId).collect(Collectors.toList());
            updateUserMap(pair, userIdList);
            for (V2AppExamRankingVo vo : page.getList()) {
                UserBoundVO user = pair.getRight().get(vo.getUserId());
                setUserInfo(user, vo);
            }
        }
        PageListVO<V2AppExamRankingVo> returnPage = new PageListVO<>();
        returnPage.setList(page.getList());
        returnPage.setPagination(FtbUtil.getPagination(page));
        rankVo.setPage(returnPage);
        V2AppExamRankingVo myRank = allList.stream().filter(v -> v.getUserId().equals(UserProvider.getLoginUserId())).findFirst().orElse(null);
        if (null != myRank) {
            List<UserBoundVO> list = v2UserApi.userListAndCopy(List.of(myRank.getUserId()), null, UserProvider.getUser().getTenantId());
            if (null != list && !list.isEmpty()) {
                setUserInfo(list.get(0), myRank);
            }
            rankVo.setMyRank(myRank);
        }
        return rankVo;
    }

    private void setUserInfo(UserBoundVO user, V2AppExamRankingVo vo) {

        if (null != user) {
            vo.setUserName(user.getUserName());
            vo.setHeadLogo(UploaderUtil.uploaderImg(StringUtil.isBlank(user.getHeadIcon()) ? "001.png" : user.getHeadIcon()));
            V2PostAndPosition position = V2PostAndPosition.builder()
                    .organizeId(user.getOrganizeId())
                    .organizeName(user.getOrganizeName())
                    .postId(user.getPositionId())
                    .postName(user.getPositionName())
                    .gradeId(user.getGradeId())
                    .gradeName(user.getGradeName())
                    .build();
            vo.setPostAndPosition(position);
        }
    }

    @Override
    public BankAnalysisVo getBankAnalysis(List<String> bankIdList) {

        List<FtbCultivateQuestionBank> bankList = questionBankMapper.selectList(new LambdaQueryWrapper<FtbCultivateQuestionBank>()
                .in(FtbCultivateQuestionBank::getId, bankIdList)
                .eq(FtbCultivateQuestionBank::getEnabledMark, ConstantUtil.NUM_TRUE));
        if (bankList.isEmpty()) {
            return null;
        }
        List<String> nameList = new ArrayList<>();
        List<String> idList = new ArrayList<>();
        bankList.forEach(item -> {
            nameList.add(item.getBankContent());
            idList.add(item.getId());
        });
        // 题型统计
        List<QuestionTypeAnalysisVo> questionTypeAnalysisList = questionMapper.getTypeAnalysis(idList);
        return new BankAnalysisVo(nameList, questionTypeAnalysisList);
    }

    @Override
    public PageInfo<QuestionVo> getQuestionPage(QuestionReq req) {

        List<String> bankIds = StringUtil.isBlank(req.getBankIds()) ? List.of() : List.of(req.getBankIds().split(","));
        return PageHelper.startPage(req.getCurrentPage(), req.getPageSize())
                .doSelectPageInfo(() -> questionMapper.getQuestionList(
                        bankIds,
                        req.getKeyword()
                ));
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void questionChangeDeal(List<String> questionIds) {

        // 自定义考试, 查询题目在哪些考试中
        List<ConnectDrawRuleVo> connectDrawRuleList = examDrawRuleService.getConnectQuestionRule(questionIds);
        dealAbnormalData(connectDrawRuleList);
        // 岗位考试, 查询删除的题目分别属于哪个题库, 哪个题型, 查询受影响的考试
        List<FtbCultivateQuestion> questionList = questionMapper.selectList(new LambdaQueryWrapper<FtbCultivateQuestion>().in(FtbCultivateQuestion::getId, questionIds));
        if (!questionList.isEmpty()) {
            // 目前批量删除题目只会删除同一个题库的, 判定受影响的题库和题型
            String bankId = questionList.get(0).getClassifyId();
            List<Integer> typeList = questionList.stream().map(FtbCultivateQuestion::getType).distinct().collect(Collectors.toList());
            List<ExamDrawRuleVo> ruleList = baseMapper.getAffectedExamRule(bankId, typeList);
            if (!ruleList.isEmpty()) {
                List<ConnectDrawRuleVo> abnormalRuleList = getAbnormalRuleList(ruleList);
                // 标记为异常
                dealAbnormalData(abnormalRuleList);
            }
        }
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public String restartExam(String userExamId) throws Exception {

        MutablePair<CultivateExam, FtbCultivateExamUser> pair = baseValid(userExamId);
        return restartGenerate(pair.getRight());
    }

    private MutablePair<CultivateExam, FtbCultivateExamUser> baseValid(String userExamId) throws Exception {

        // 不带重考次数验证, 可直接发起重考
        FtbCultivateExamUser examUser = examUserMapper.selectById(userExamId);
        if (null == examUser) {
            throw new HandleException("未找到考试信息");
        }
        // 判定考试状态是否正常
        CultivateExam exam = this.getById(examUser.getExamId());
        if (exam.getNeedUpdate().equals(ConstantUtil.UPDATE_TRUE)) {
            throw new HandleException("考试异常，无法重新考试");
        }
        return MutablePair.of(exam, examUser);
    }

    /**
     * 用户考试重新考试 ID不变
     *
     * @param examUser 用户考试
     * @return 重新考试ID
     */
    private String restartGenerate(FtbCultivateExamUser examUser) {

        // 更新用户考试详情为 删除
        LambdaUpdateWrapper<FtbCultivateExamUserDetail> updateDetailWrapper = new LambdaUpdateWrapper<FtbCultivateExamUserDetail>()
                .eq(FtbCultivateExamUserDetail::getUserExamId, examUser.getId())
                .set(FtbCultivateExamUserDetail::getEnabledMark, ConstantUtil.NUM_FALSE)
                .set(FtbCultivateExamUserDetail::getLastModifyUserId, UserProvider.getLoginUserId())
                .set(FtbCultivateExamUserDetail::getLastModifyTime, new Date());
        examUserDetailService.update(updateDetailWrapper);
        // 用户考试复制
        FtbCultivateExamUser newExamUser = new FtbCultivateExamUser(examUser.getExamId(), examUser.getExamTime(), examUser.getPassScore(), examUser.getExcellentScore(),
                examUser.getUserId(), examUser.getStartTime(), examUser.getEndTime(),
                examUser.getUserOrgList(), examUser.getTotalScore(), examUser.getQuestionNumber(), examUser.getPickQuestionRule());
        newExamUser.setId(examUser.getId());
        newExamUser.setUserExamCount(examUser.getUserExamCount() + 1);
        newExamUser.setLastModifyTime(new Date());
        newExamUser.setLastModifyUserId(UserProvider.getLoginUserId());
        newExamUser.setStatus(CourseEnums.ExamStatus.WAIT.getCode());
        newExamUser.setScore(0);
        newExamUser.setAutoScore(0);
        newExamUser.setReadOverScore(0);
        newExamUser.setRemark(null);
        examUserService.updateById(newExamUser);
        // 新增补考记录
        FtbCultivateExamFrequencyLog log = new FtbCultivateExamFrequencyLog(FtbUtil.getId(), examUser.getExamId(), examUser.getUserId(), UserProvider.getLoginUserId());
        frequencyLogService.save(log);
        return newExamUser.getId();
    }

    @Override
    public String restartExamWithValid(String userExamId) throws Exception {

        // 查询重新考试配置
        MutablePair<CultivateExam, FtbCultivateExamUser> pair = baseValid(userExamId);
        CultivateExam exam = pair.getLeft();
        FtbCultivateExamUser examUser = pair.getRight();
        hasTimesValid(exam, examUser);
        return restartGenerate(examUser);
    }

    private void hasTimesValid(CultivateExam exam, FtbCultivateExamUser examUser) throws Exception {

        if (exam.getExamLimitation().equals(ConstantUtil.NUM_FALSE)) {
            throw new HandleException("暂不允许补考");
        }
        FrequencyEnum frequencyEnum = FrequencyEnum.getByCode(exam.getReExamFrequencyType());
        assert frequencyEnum != null;
        switch (frequencyEnum) {
            case EVERY_DAY:
            case EVERY_WEEK:
            case EVERY_MONTH:
                MutablePair<String, String> range = DateRangeUtil.getRange(LocalDate.now().toString(), frequencyEnum.getDescription());
                // 查询范围内的考试次数
                Integer num = frequencyLogService.queryExamNumByDateStr(exam.getId(), examUser.getUserId(), range.getLeft(), range.getRight());
                if (num >= exam.getReExamFrequencyNum()) {
                    throw new HandleException("暂无补考次数");
                }
                break;
            case TOTAL:
                Integer totalNum = frequencyLogService.queryExamNumByDateStr(exam.getId(), examUser.getUserId(), null, null);
                totalNum -= 1;
                if (totalNum >= exam.getReExamFrequencyNum()) {
                    throw new HandleException("暂无补考次数");
                }
                break;
            default:
                break;
        }
    }

    @Override
    public Boolean checkRestartExam(String userExamId) throws Exception {

        MutablePair<CultivateExam, FtbCultivateExamUser> pair = baseValid(userExamId);
        hasTimesValid(pair.getLeft(), pair.getRight());
        return Boolean.TRUE;
    }

    /**
     * 检查并更新过期用户的考试状态为逾期
     * 筛选条件：
     * 1. 结束时间小于当前时间
     * 2. 当前状态为待考
     * 3. 考试类型为基础考试
     * 4. 启用标记为有效
     */
    @Override
    public void checkAndUpdateExpireUserExamStatus() {
        // 获取当前时间，确保后续时间判断的一致性
        Date currentTime = new Date();

        // 构建更新条件，将符合条件的用户考试记录状态更新为逾期
        LambdaUpdateWrapper<FtbCultivateExamUser> wrapper = new LambdaUpdateWrapper<FtbCultivateExamUser>()
                .set(FtbCultivateExamUser::getStatus, CourseEnums.ExamStatus.OVERDUE.getCode())
                .set(FtbCultivateExamUser::getLastModifyTime, currentTime)
                .lt(FtbCultivateExamUser::getEndTime, currentTime)
                .eq(FtbCultivateExamUser::getStatus, CourseEnums.ExamStatus.WAIT.getCode())
                .eq(FtbCultivateExamUser::getExamType, CourseEnums.ExamType.BASE.getCode())
                .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode());

        // 执行批量更新操作
        examUserMapper.update(null, wrapper);
    }

    /**
     * 检查并更新考试状态
     * 根据当前时间与考试的开始/结束时间对比，批量更新考试状态：
     * 1. 未开始：开始时间大于等于当前时间
     * 2. 进行中：开始时间小于等于当前时间且结束时间大于等于当前时间
     * 3. 已结束：结束时间小于等于当前时间
     * 仅处理基础考试类型且有效的考试记录
     */
    @Override
    public void checkAndUpdateExamStatus() {
        // 获取当前时间，确保时间判断的一致性
        Date currentTime = new Date();

        // 更新状态为未开始：开始时间在未来且当前状态不是未开始
        baseMapper.update(null, new LambdaUpdateWrapper<CultivateExam>()
                .set(CultivateExam::getStatus, CourseEnums.ExamTimeStatus.NOT_STARTED.getCode())
                .ge(CultivateExam::getStartTime, currentTime)
                .eq(CultivateExam::getExamType, CourseEnums.ExamType.BASE.getCode())
                .ne(CultivateExam::getStatus, CourseEnums.ExamTimeStatus.NOT_STARTED.getCode())
                .eq(CultivateExam::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()));

        // 更新状态为进行中：在当前时间范围内且当前状态为未开始
        baseMapper.update(null, new LambdaUpdateWrapper<CultivateExam>()
                .set(CultivateExam::getStatus, CourseEnums.ExamTimeStatus.IN_PROGRESS.getCode())
                .le(CultivateExam::getStartTime, currentTime)
                .ge(CultivateExam::getEndTime, currentTime)
                .eq(CultivateExam::getStatus, CourseEnums.ExamTimeStatus.NOT_STARTED.getCode())
                .eq(CultivateExam::getExamType, CourseEnums.ExamType.BASE.getCode())
                .eq(CultivateExam::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()));

        // 更新状态为已结束：结束时间已过且当前状态为进行中，同时标记无需更新
        baseMapper.update(null, new LambdaUpdateWrapper<CultivateExam>()
                .set(CultivateExam::getNeedUpdate, ExamUpdateStatus.NO_UPDATE.getCode())
                .set(CultivateExam::getStatus, CourseEnums.ExamTimeStatus.DONE.getCode())
                .le(CultivateExam::getEndTime, currentTime)
                .eq(CultivateExam::getStatus, CourseEnums.ExamTimeStatus.IN_PROGRESS.getCode())
                .eq(CultivateExam::getExamType, CourseEnums.ExamType.BASE.getCode())
                .eq(CultivateExam::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()));
    }


    @Override
    @Transactional
    public void webRestartExam(WebRestartExamReq req) {
        CultivateExam exam = this.getById(req.getExamId());
        checkWebRestartExam(exam, req);
        LambdaQueryWrapper<FtbCultivateExamUser> wrapper = new LambdaQueryWrapper<FtbCultivateExamUser>()
                .eq(FtbCultivateExamUser::getExamId, exam.getId())
                .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode())
                .in(FtbCultivateExamUser::getId, req.getUserExamIdList())
                .orderByDesc(FtbCultivateExamUser::getCreatorTime);
        List<FtbCultivateExamUser> userExamList = examUserMapper.selectList(wrapper);

        if (CollectionUtil.isEmpty(userExamList)) {
            return;
        }
        for (FtbCultivateExamUser oldExamUser : userExamList) {
            oldExamUser.setStartTime(req.getStartTime());
            oldExamUser.setEndTime(req.getEndTime());
            restartGenerate(oldExamUser);
            //删除考试次数日志
            /*frequencyLogService.remove(new LambdaQueryWrapper<FtbCultivateExamFrequencyLog>()
                    .eq(FtbCultivateExamFrequencyLog::getUserId, oldExamUser.getUserId())
                    .eq(FtbCultivateExamFrequencyLog::getExamId, exam.getId())
            );*/
        }

    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public SubExamVo subUserExam(String userExamId, SubExamQuestionReq req) {

        SubExamVo vo = new SubExamVo();
        if (CollectionUtil.isEmpty(req.getAnswerDetail())) {
            throw new RuntimeException("提交的答案信息为空");
        }
        if (null == req.getDuration()) {
            throw new RuntimeException("考试时长为空");
        }
        //获取当前用户信息
        String loginUserId = UserProvider.getLoginUserId();
        //查看是否有记录
        LambdaQueryWrapper<FtbCultivateExamUser> wrapper = new LambdaQueryWrapper<FtbCultivateExamUser>()
                .eq(FtbCultivateExamUser::getUserId, loginUserId)
                .eq(FtbCultivateExamUser::getId, userExamId)
                .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode());
        FtbCultivateExamUser examUser = examUserMapper.selectOne(wrapper);
        if (null == examUser) {
            throw new RuntimeException("用户考试信息不存在");
        }
        if (CourseEnums.EnabledMarkType.INVALID.getCode().equals(examUser.getEnabledMark())) {
            throw new RuntimeException("参加考试的人员考试取消");
        }
        //查看考试信息
        FtbCultivateExam exam = examService.queryExamInfo(examUser.getExamId());
        if (exam.getStatus().equals(3)) {
            userExamUtil.updateUserExamNullify(userExamId);
            return vo;
        }
        //检测是否可以提交考试
        checkSubmitExam(examUser, exam);
        //写入用户考试的题目和答案信息
        List<FtbCultivateExamUserDetail> userDetailList = writeUserQuestion(req, examUser, exam);
        int autoScore = 0;//试卷自动阅卷得分
        int correctCount = 0;//正确数
        boolean isNeedReview = false;//是否需要批阅试卷 true:需要批阅 false:不需要批阅
        for (FtbCultivateExamUserDetail userDetail : userDetailList) {
            CourseEnums.QuestionType questionType = CourseEnums.QuestionType.fromCode(userDetail.getType());
            assert questionType != null;
            switch (questionType) {
                case SINGLE://单选
                case JUDGE://判断
                case MULTI://多选
                case ONE_OR_MULTI://不定项
                    if (Objects.equals(userDetail.getIsRight(), CourseEnums.IsRightAnswer.YES.getCode())) {
                        autoScore += userDetail.getQuestionScore();
                        correctCount++;
                    }
                    break;
                case FILL://填空题
                case INPUT://问答题
                    isNeedReview = true;
                    break;
            }
        }
        //修改考试题目信息
        Date now = new Date();
        FtbCultivateExamUser updateExamUser = new FtbCultivateExamUser();
        updateExamUser.setId(examUser.getId());
        updateExamUser.setUserId(examUser.getUserId());
        updateExamUser.setCorrectCount(correctCount);
        updateExamUser.setExamId(examUser.getExamId());
        updateExamUser.setAutoScore(autoScore);
        updateExamUser.setScore(autoScore);
        updateExamUser.setFinishTime(now);
        updateExamUser.setUserStartTime(new Date(now.getTime() - req.getDuration() * 1000));
        updateExamUser.setDuration(req.getDuration());
        updateExamUser.setStatus(CourseEnums.ExamStatus.WAIT_CHECK.getCode());
        updateExamUser.setTotalScore(examUser.getTotalScore());
        updateExamUser.setQuestionNumber(examUser.getQuestionNumber());
        // 修改用户考试
        if (!isNeedReview) {
            //根据试卷ID查询试卷信息
            updateExamUser.setStatus(QuestionAnalysisUtil.calculateUserExamStatus(exam, updateExamUser.getTotalScore(), updateExamUser.getScore()));
            BigDecimal score = BigDecimal.valueOf(updateExamUser.getScore());
            BigDecimal totalScore = BigDecimal.valueOf(updateExamUser.getTotalScore());
            BigDecimal divide = score.divide(totalScore, 2, RoundingMode.HALF_UP);
            updateExamUser.setScoreRatio(divide);
        }
        examUserMapper.updateById(updateExamUser);
        cultivateMqSendUtil.sendExamMessage(updateExamUser);
        return vo;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public Boolean dealExamOldData() {

        // 查询考试列表
        List<ExamPaperVo> examList = this.baseMapper.getOldExamList();
        if (examList.isEmpty()) {
            return Boolean.TRUE;
        }
        // ftb_cultivate_exam 中 F_DrawDataBase, F_DrawMode, F_NeedUpdate 数据填写
        this.baseMapper.updateOldExamBatch(examList);
        // 以前试卷的抽题规则需要维护到ftb_cultivate_exam_draw_rule表中 随机抽题生成规则方式待确认
        List<String> paperIds = examList.stream().map(ExamPaperVo::getPaperId).distinct().collect(Collectors.toList());
        List<PaperDrawRuleVo> paperDrawRuleList = questionMapper.getPaperDrawRuleList(paperIds);
        Map<String, List<PaperDrawRuleVo>> paperRuleMap = paperDrawRuleList.stream().collect(Collectors.groupingBy(PaperDrawRuleVo::getPaperId));
        List<CultivateExamDrawRule> allList = new ArrayList<>();
        examList.forEach(exam -> {
            List<PaperDrawRuleVo> paperRuleList = paperRuleMap.get(exam.getPaperId());
            if (null != paperRuleList && !paperRuleList.isEmpty()) {
                List<CultivateExamDrawRule> ruleList = getRuleList(exam.getExamId(), exam.getDrawMode(), paperRuleList);
                exam.setPickQuestionRule(JsonUtil.getObjectToString(ruleList));
                allList.addAll(ruleList);
            }
        });
        examDrawRuleService.saveBatch(allList);
        // ftb_cultivate_exam_user表 F_ExamTime, F_PickQuestionRule, F_PassScore, F_ExcellentScore 数据填写
        examUserMapper.updateOldExamUserBatch(examList);
        // 以前的带教/练习中绑定的视频文件 需要绑定到表 ftb_cultivate_file中
        teachingRecordService.removeAllOldFile();
        // 带教审批生成
        teachingApproveMapper.generateOldApproveData();
        return Boolean.TRUE;
    }

    @Override
    public void delUserExam(String userExamId) throws Exception {

        FtbCultivateExamUser examUser = examUserMapper.selectById(userExamId);
        if (null == examUser || examUser.getEnabledMark().equals(ConstantUtil.NUM_FALSE)) {
            throw new Exception("未找到用户考试记录");
        }
        LambdaUpdateWrapper<FtbCultivateExamUser> updateWrapper = new LambdaUpdateWrapper<FtbCultivateExamUser>()
                .set(FtbCultivateExamUser::getEnabledMark, ConstantUtil.NUM_FALSE)
                .set(FtbCultivateExamUser::getDeleteTime, new Date())
                .set(FtbCultivateExamUser::getDeleteUserId, UserProvider.getLoginUserId())
                .eq(FtbCultivateExamUser::getId, userExamId);
        examUserMapper.update(null, updateWrapper);
    }

    @Override
    public PaperDetailVo getMarkPaperDetail(String userExamId) throws Exception {

        FtbCultivateExamUser examUser = examUserMapper.selectById(userExamId);
        if (examUser == null) {
            throw new Exception("未找到记录");
        }
        PaperDetailVo vo = JsonUtil.getJsonToBean(examUser, PaperDetailVo.class);
        vo.setUserExamId(examUser.getId());
        FtbCultivateExam exam = examService.getById(vo.getExamId());
        vo.setExamName(null == exam ? "--" : exam.getExamName());
        // 查询用户信息
        List<UserBoundVO> list = v2UserApi.userListAndCopy(List.of(vo.getUserId()), null, UserProvider.getUser().getTenantId());
        if (CollUtil.isNotEmpty(list)) {
            UserBoundVO user = list.get(0);
            vo.setUserName(user.getUserName());
            vo.setOrganizeName(user.getOrganizeName());
            vo.setPostName(user.getPositionName());
            vo.setGradeName(user.getGradeName());
        }
        return vo;
    }

    private List<CultivateExamDrawRule> getRuleList(String examId, Integer drawMode, List<PaperDrawRuleVo> paperRuleList) {

        List<CultivateExamDrawRule> ruleList;
        if (drawMode.equals(ConstantUtil.NUM_TRUE)) {
            // 固定抽题
            ruleList = JsonUtil.getJsonToList(paperRuleList, CultivateExamDrawRule.class);
            ruleList.forEach(v -> {
                v.setId(FtbUtil.getId());
                v.setExamId(examId);
            });
        } else {
            // 随机抽题
            ruleList = paperRuleList.stream()
                    // 按题型分组
                    .collect(Collectors.groupingBy(PaperDrawRuleVo::getQuestionType))
                    .entrySet()
                    .stream()
                    .map(entry -> {
                        Integer questionType = entry.getKey();
                        List<PaperDrawRuleVo> list = entry.getValue();
                        // 题目数量
                        Map<Integer, List<PaperDrawRuleVo>> difficultyMap = list.stream().collect(Collectors.groupingBy(PaperDrawRuleVo::getDifficulty));
                        int pickCount = list.size();
                        List<PaperDrawRuleVo> easyList = difficultyMap.getOrDefault(ConstantUtil.EASY, List.of());
                        List<PaperDrawRuleVo> mediumList = difficultyMap.getOrDefault(ConstantUtil.MEDIUM, List.of());
                        List<PaperDrawRuleVo> hardList = difficultyMap.getOrDefault(ConstantUtil.HARD, List.of());
                        // 总分
                        int totalScore = list.stream()
                                .filter(v -> v.getScore() != null)
                                .mapToInt(PaperDrawRuleVo::getScore)
                                .sum();
                        // 平均分（四舍五入）
                        int avgScore = (int) Math.round((double) totalScore / pickCount);
                        // 构建新规则
                        CultivateExamDrawRule rule = new CultivateExamDrawRule();
                        rule.setId(FtbUtil.getId());
                        rule.setExamId(examId);
                        rule.setQuestionType(questionType);
                        rule.setPickCount(pickCount);
                        rule.setEasyCount(easyList.size());
                        rule.setMediumCount(mediumList.size());
                        rule.setHardCount(hardList.size());
                        rule.setScore(avgScore);
                        rule.setQuestionId(null);
                        rule.setNormal(ConstantUtil.NUM_FALSE);
                        return rule;
                    })
                    .collect(Collectors.toList());
        }
        return ruleList;
    }

    /**
     * 检测是否可以提交考试
     */
    private void checkSubmitExam(FtbCultivateExamUser examUser, FtbCultivateExam exam) {
        CourseEnums.ExamType examType = CourseEnums.ExamType.fromCode(exam.getExamType());
        assert examType != null;
        switch (examType) {
            case BASE://常规考试
                if (StringUtils.isEmpty(examUser.getRelationTaskId()) && examUser.getStartTime() != null && examUser.getEndTime() != null) {
                    //判断当前时间是否已经过了考试结束时间
                    long time = new Date().getTime();//当前时间毫秒数
                    //判断当前时间是否已经过了考试开始时间
                    if (examUser.getStartTime().getTime() > time) {
                        throw new RuntimeException("考试未开始");
                    }
                }
                break;
            case POSITION://岗位学习考试
                break;
        }
        if (!CourseEnums.ExamStatus.WAIT.getCode().equals(examUser.getStatus()) && !CourseEnums.ExamStatus.OVERDUE.getCode().equals(examUser.getStatus())) {
            throw new RuntimeException("考试已经提交,请不要重复提交");
        }
    }

    private List<FtbCultivateExamUserDetail> writeUserQuestion(SubExamQuestionReq req, FtbCultivateExamUser examUser, FtbCultivateExam exam) {

        //获取当前用户信息
        String loginUserId = UserProvider.getLoginUserId();
        List<AppQuestionVo> answerDetailList = req.getAnswerDetail();
        if (CollectionUtil.isEmpty(answerDetailList)) {
            CollectionUtil.newArrayList();
        }
        List<FtbCultivateExamUserDetail> addList = new ArrayList<>();
        for (AppQuestionVo answerDetail : answerDetailList) {
            String questionId = answerDetail.getQuestionId();
            String userAnswer = answerDetail.getUserAnswer();
            //查询题目分数
            Integer questionScore = answerDetail.getQuestionScore();
            FtbCultivateExamUserDetail user = FtbCultivateExamUserDetail.builder()
                    .enabledMark(CourseEnums.EnabledMarkType.VALID.getCode())
                    .userExamId(examUser.getId())
                    .examId(exam.getId())
                    .questionId(questionId)
                    .questionOption(JSONUtil.toJsonStr(answerDetail.getQuestionOptionVoList()))
                    .classifyId(answerDetail.getClassifyId())
                    .type(answerDetail.getType())
                    .difficulty(answerDetail.getDifficulty())
                    .userAnswer(userAnswer)
                    .content(answerDetail.getContent())
                    .analysis(answerDetail.getAnalysis())
                    .paperId(exam.getPaperId())
                    .userId(loginUserId)
                    .answer(answerDetail.getAnswer())
                    .questionScore(questionScore)
                    .batch(examUser.getBatch()).build();

            CourseEnums.QuestionType questionType = CourseEnums.QuestionType.fromCode(answerDetail.getType());
            assert questionType != null;
            switch (questionType) {
                case SINGLE://单选
                case JUDGE://判断
                    if (answerDetail.getAnswer().equals(userAnswer)) {
                        user.setUserScore(user.getQuestionScore());
                        user.setIsRight(CourseEnums.IsRightAnswer.YES.getCode());
                    } else {
                        user.setUserScore(0);
                        user.setIsRight(CourseEnums.IsRightAnswer.NO.getCode());
                    }
                    break;
                case MULTI://多选
                case ONE_OR_MULTI://不定项选择题
                    if (QuestionAnalysisUtil.checkMultiRight(user.getAnswer(), user.getUserAnswer())) {
                        user.setUserScore(user.getQuestionScore());
                        user.setIsRight(CourseEnums.IsRightAnswer.YES.getCode());
                    } else {
                        user.setUserScore(0);
                        user.setIsRight(CourseEnums.IsRightAnswer.NO.getCode());
                    }
                    break;
                case FILL://填空题
                case INPUT://问答题
                    user.setUserScore(0);//给一个默认分数
                    break;

            }
            addList.add(user);
        }
        examUserDetailService.saveBatch(addList);
        return addList;
    }

    private void checkWebRestartExam(CultivateExam exam, WebRestartExamReq req) {
        if (CourseEnums.ExamType.BASE.getCode().equals(exam.getExamType())) {
            if (null == req.getStartTime()) {
                throw new RuntimeException("开始考试时间不能为空");
            } else if (null == req.getEndTime()) {
                throw new RuntimeException("结束考试时间不能为空");
            }
            if (req.getStartTime().after(req.getEndTime())) {
                throw new RuntimeException("考试开始时间不能大于结束时间");
            }
        }
        if (CollectionUtil.isEmpty(req.getUserExamIdList())) {
            throw new RuntimeException("请选择需要重考的用户考试Id");
        }
    }

    private List<ConnectDrawRuleVo> getAbnormalRuleList(List<ExamDrawRuleVo> ruleList) {

        // 查询受影响的考试还够不够抽题
        Set<String> set = new HashSet<>();
        List<BankPickVo> bankPickList = ruleList.stream()
                .flatMap(rule -> Arrays.stream(rule.getDrawDataBase().split(","))
                        .map(b -> new BankPickVo(b, rule.getQuestionType())))
                .filter(v -> set.add(v.getBankId() + "_" + v.getQuestionType()))
                .collect(Collectors.toList());
        // 查询bankPickList每个库每个题型有多少题目
        List<BankPickVo> countList = questionMapper.getBankQuestionTypeCount(bankPickList);
        Map<String, BankPickVo> questionCountMap = countList.stream()
                .collect(Collectors.toMap(
                        v -> v.getBankId() + "_" + v.getQuestionType(),
                        Function.identity()
                ));
        List<ConnectDrawRuleVo> abnormalRules = new ArrayList<>();
        // 4. 校验每条规则
        for (ExamDrawRuleVo rule : ruleList) {
            int needEasy = Optional.ofNullable(rule.getEasyCount()).orElse(0);
            int needMedium = Optional.ofNullable(rule.getMediumCount()).orElse(0);
            int needHard = Optional.ofNullable(rule.getHardCount()).orElse(0);
            int easyTotal = 0;
            int mediumTotal = 0;
            int hardTotal = 0;
            for (String bId : rule.getDrawDataBase().split(",")) {
                String key = bId + "_" + rule.getQuestionType();
                BankPickVo vo = questionCountMap.get(key);
                if (vo != null) {
                    easyTotal += Optional.ofNullable(vo.getEasyCount()).orElse(0);
                    mediumTotal += Optional.ofNullable(vo.getMediumCount()).orElse(0);
                    hardTotal += Optional.ofNullable(vo.getHardCount()).orElse(0);
                }
            }
            // 5. 只要任意一个难度不满足，就算异常
            if (easyTotal < needEasy
                    || mediumTotal < needMedium
                    || hardTotal < needHard) {
                abnormalRules.add(new ConnectDrawRuleVo(
                        rule.getId(),
                        rule.getExamId(),
                        rule.getQuestionType()
                ));
            }
        }
        return abnormalRules;
    }

    private void dealAbnormalData(List<ConnectDrawRuleVo> connectDrawRuleList) {
        if (null != connectDrawRuleList && !connectDrawRuleList.isEmpty()) {
            // 异常考试
            List<String> examIds = connectDrawRuleList.stream().map(ConnectDrawRuleVo::getExamId).distinct().collect(Collectors.toList());
            // 异常固定题目
            List<String> ruleIds = connectDrawRuleList.stream().map(ConnectDrawRuleVo::getRuleId).collect(Collectors.toList());
            this.update(new LambdaUpdateWrapper<CultivateExam>()
                    .in(CultivateExam::getId, examIds)
                    .set(CultivateExam::getNeedUpdate, ConstantUtil.UPDATE_TRUE)
                    .set(CultivateExam::getLastModifyTime, new Date())
                    .set(CultivateExam::getLastModifyUserId, UserProvider.getLoginUserId()));
            examDrawRuleService.update(new LambdaUpdateWrapper<CultivateExamDrawRule>()
                    .in(CultivateExamDrawRule::getId, ruleIds)
                    .set(CultivateExamDrawRule::getNormal, ConstantUtil.NUM_FALSE)
                    .set(CultivateExamDrawRule::getLastModifyTime, new Date())
                    .set(CultivateExamDrawRule::getLastModifyUserId, UserProvider.getLoginUserId()));
        }
    }
}
