package com.ruoyi.web.service;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.enums.PaperStatus;
import com.ruoyi.enums.PaperType;
import com.ruoyi.learn.domain.*;
import com.ruoyi.learn.mapper.*;
import lombok.Data;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 考卷服务
 */
@Service
public class PaperService {
    private final LearnPaperMapper paperMapper;
    private final LearnPaperQuestionMapper paperQuestionMapper;
    private final LearnQuestionsMapper questionsMapper;


    PaperService(LearnPaperMapper paperMapper, LearnPaperQuestionMapper paperQuestionMapper, LearnQuestionsMapper questionsMapper) {
        this.paperMapper = paperMapper;
        this.paperQuestionMapper = paperQuestionMapper;
        this.questionsMapper = questionsMapper;
    }

    public void test() {
//        PaperDef paperDef = new PaperDef();
//        paperDef.setFillExclude(true);
//        paperDef.setKnowIds(Lists.newArrayList(133614L,130166L,130187L));
//        List<TypeDef> typeDefList= Lists.newArrayList();
//        typeDefList.add(new TypeDef("单选题", "单选题", 80, 1));
//        typeDefList.add(new TypeDef("判断题", "判断题", 10, 2));
//        paperDef.setTypes(typeDefList);
//        getQuestions(1L, 100L, paperDef);
    }

    /**
     * 加载试卷
     * @param paperId
     * @return
     */
    public PaperVO loadPaper(Long paperId) {
        PaperVO result = new PaperVO();
        LearnPaper learnPaper = paperMapper.selectLearnPaperById(paperId);
        BeanUtils.copyProperties(learnPaper, result);
        List<LearnQuestions> questions = questionsMapper.selectQuestionByPaperId(paperId);
        result.setQuestions(questions.stream().map(t -> {
            PaperVO.QuestionSeq qs = new PaperVO.QuestionSeq();
            BeanUtils.copyProperties(t, qs);
            return qs;
        }).collect(Collectors.toList()));
        return result;
    }

    /**
     * 根据院校专业要求生成模拟试卷
     * @return
     */
    public Pair<LearnPaper, List<LearnPaperQuestion>> buildSimulatedPaper(LearnDirectedKnowledge directedKnowledge) {
        LearnPaper paper = new LearnPaper();
        paper.setPaperType(PaperType.Simulated.name());
        paper.setRelateId(directedKnowledge.getId()); // 定向ID
        paper.setYear(directedKnowledge.getYear());
        paper.setStatus(PaperStatus.Valid.getVal());
        paper.setDirectKey(directedKnowledge.getDirectKey());

        TestPaperVO.PaperDef paperDef = JSONObject.parseObject(directedKnowledge.getConditions(), TestPaperVO.PaperDef.class);
        paperDef.setKnowIds(Stream.of(directedKnowledge.getKnowledges().split(",")).map(Long::valueOf).collect(Collectors.toList()));
        paperDef.setTypes(JSONArray.parseArray(directedKnowledge.getQuestionTypes(), TestPaperVO.TypeDef.class));
        return buildPaper(null, paper, paperDef);
    }

    /**
     * 根据试卷定义生成试卷
     * @param studentId
     * @param paper
     * @param paperDef
     * @return
     */
    public Pair<LearnPaper, List<LearnPaperQuestion>> buildPaper(Long studentId, LearnPaper paper, TestPaperVO.PaperDef paperDef) {
        if(null == studentId){
            paperDef.setFillExclude(false);
        }
        List<LearnPaperQuestion> pqList = getQuestions(studentId, paperDef);
        return Pair.of(paper, pqList);
    }

    /**
     * 保存试卷
     * @param paper
     * @param pqList
     * @return
     */
    public LearnPaper savePaper(LearnPaper paper, List<LearnPaperQuestion> pqList) {
        paper.setNumber(pqList.size());
        paper.setFenshu(0);
        paperMapper.insertLearnPaper(paper);
        Long paperId = paper.getId();
        pqList.stream().forEach(t -> {
            t.setPaperId(paperId);
            paperQuestionMapper.insertLearnPaperQuestion(t);
        });
        return paper;
    }

    /**
     * 动态组卷查题
     * @param studentId
     * @param paperDef
     * @return
     */
    public List<LearnPaperQuestion> getQuestions(Long studentId, TestPaperVO.PaperDef paperDef) {
        // 题型分布定义, 知识点列表, 分值定义
        // 统计知识点+类型的有效数量 TODO 总量可以缓存
        Map<String, KnowTypeAssign> knowTypeAssignMap = Maps.newHashMap();
        List<String> typeSet = paperDef.getTypes().stream().map(TestPaperVO.TypeDef::getType).collect(Collectors.toList());
        Map cond = Maps.newHashMap();
        cond.put("studentId", studentId);
        cond.put("knowIds", paperDef.getKnowIds());
        cond.put("types", typeSet);
        setValue(knowTypeAssignMap, cond, 1); // 填充排除总量
        if (paperDef.getFillExclude()) {
            cond.remove("studentId");
            setValue(knowTypeAssignMap, cond, 2); // 按需填充总量
        }

        // 循环补充未做+已做，如果知识点总数不够时才填充其他知识点的
        Long lackTotal = paperDef.getTotal();
        AtomicLong assignCount = new AtomicLong(0);
        Map<Long, Integer> knowTypesMap = Maps.newHashMap();
        int typeCount = paperDef.getTypes().size();
        for (Long knowId : paperDef.getKnowIds()) {
            knowTypesMap.put(knowId, typeCount);
        }
        Set<Long> knowSet = Sets.newHashSet(paperDef.getKnowIds());
        Long minKnowTypeCount = Long.MAX_VALUE;
        do {
            Integer knowCount = knowSet.size();
            knowSet.clear();
            for (Long knowId : paperDef.getKnowIds()) {
                typeCount = knowTypesMap.get(knowId);
                if (0 == typeCount) {
                    continue;
                }
                Long avgKnowTypeCount = Math.min(minKnowTypeCount, lackTotal / knowCount / typeCount);
                if(avgKnowTypeCount == 0 && lackTotal > 0) {
                    avgKnowTypeCount = 1L;
                }
                typeCount = 0;
                for (TestPaperVO.TypeDef typeDef : paperDef.getTypes()) {
                    Long tmpMinKnowTypeCount = assignKnownCount(knowId, typeDef.getType(), knowTypeAssignMap, avgKnowTypeCount, paperDef.getFillExclude(), assignCount);
                    if (tmpMinKnowTypeCount > 0) {
                        minKnowTypeCount = Math.min(minKnowTypeCount, tmpMinKnowTypeCount);
                        knowSet.add(knowId);
                        typeCount++;
                    }
                }
                knowTypesMap.put(knowId, typeCount);
            }
            lackTotal = paperDef.getTotal() - assignCount.get();
            if (lackTotal <= 0 || knowSet.isEmpty()) {
                break;
            }
        } while (true);

        // 知识点已经分配，准备题型分配
        LearnQuestions qCond = new LearnQuestions();
        Random random = new Random();
        List<LearnPaperQuestion> pqList = Lists.newArrayList();
        Set<Long> existQuestionIdSet = Sets.newHashSet();
        for (TestPaperVO.TypeDef typeDef : paperDef.getTypes()) {
            for (Long knowId : paperDef.getKnowIds()) {
                String key = knowId + "_" + typeDef.getType();
                KnowTypeAssign ktc = knowTypeAssignMap.get(key);
                qCond.setKnowledgeId(ktc.getKnowId());
                qCond.setQtpye(ktc.getType());
                if(ktc.exclAssign > 0){
                    qCond.setId(studentId);
                    qCond.setNumber(ktc.assign > 500 ? (long) random.nextInt(ktc.exclAssign.intValue() - 500) :  0L);
                    List<LearnQuestions> questions = questionsMapper.selectQuestionsForPaper(qCond);
                    addRandomList(questions, random, paperDef.getTotal(), ktc.exclAssign, typeDef.getScore(), existQuestionIdSet, pqList);
                }
                if(ktc.assign > 0L) {
                    qCond.setId(null);
                    qCond.setNumber(ktc.assign > 500 ? (long) random.nextInt(ktc.assign.intValue() - 500) :  0L);
                    List<LearnQuestions> questions = questionsMapper.selectQuestionsForPaper(qCond);
                    addRandomList(questions, random, paperDef.getTotal(), ktc.exclAssign, typeDef.getScore(), existQuestionIdSet, pqList);
                }

            }
        }
        return pqList;
    }

    private void addRandomList(List<LearnQuestions> questions, Random random, Long totalCount, Long count, Integer score, Set<Long> existQuestionIdSet, List<LearnPaperQuestion> pqList) {
        while(count > 0 && questions.size() > 0) {
            LearnQuestions q = questions.size() > 1 ? questions.remove(random.nextInt(questions.size() - 1)) : questions.remove(0);
            if(existQuestionIdSet.add(q.getId())) {
                LearnPaperQuestion pq = new LearnPaperQuestion();
                pq.setSeq(pqList.size() + 1);
                pq.setScore(score);
                pq.setQuestionId(q.getId());
                pqList.add(pq);
                count--;
                if(pqList.size() == totalCount) {
                    break;
                }
            }
        }
    }

    /**
     * 给指定类型分配
     * @param knowId
     * @param qtype
     * @param knowTypeAssignMap
     * @param knowTypeCount
     * @param fillExclude
     * @param assignCount
     */
    private Long assignKnownCount(Long knowId, String qtype, Map<String, KnowTypeAssign> knowTypeAssignMap, Long knowTypeCount, Boolean fillExclude,
                                  AtomicLong assignCount) {
        String key = knowId + "_" + qtype;
        KnowTypeAssign knowTypeAssign = knowTypeAssignMap.get(key);
        long lackCount;
        if (knowTypeAssign.exclCount > 0) {
            lackCount = knowTypeCount - knowTypeAssign.exclCount;
            if (lackCount <= 0) { // 足量
                assignCount.getAndAdd(knowTypeCount);
                knowTypeAssign.exclCount -= knowTypeCount;
                knowTypeAssign.exclAssign += knowTypeCount;
            } else { // 不足时且还有时全转
                assignCount.getAndAdd(knowTypeAssign.exclCount);
                knowTypeAssign.exclAssign += knowTypeAssign.exclCount;
                knowTypeAssign.exclCount = 0L;
            }
        } else {
            lackCount = knowTypeCount;
        }
        long lack = 0;
        if (lackCount > 0) { // 差额优先补充已做过的
            if(fillExclude && knowTypeAssign.total > 0) {
                lack = lackCount - knowTypeAssign.total;
                if (lack <= 0) { // 足量
                    assignCount.getAndAdd(lackCount);
                    knowTypeAssign.total -= lackCount;
                    knowTypeAssign.assign += lackCount;
                } else { // 不足时全转
                    assignCount.getAndAdd(knowTypeAssign.total);
                    knowTypeAssign.assign += knowTypeAssign.total;
                    knowTypeAssign.total = 0L;
                }
            } else {
                lack = lackCount;
            }
        }
        if(lackCount < 0) {
            return -lackCount;
        } else if(lack < 0) {
            return -lack;
        }
        return 0L;
    }

    /**
     * 合并 排除总量和总量
     * @param knowTypeAssignMap
     * @param cond
     * @param index 1 free 2 total
     */
    private void setValue(Map<String, KnowTypeAssign> knowTypeAssignMap, Map cond, Integer index) {
        for (LearnQuestions q : questionsMapper.statByKnowledgeType(cond)) {
            String key = q.getKnowledgeId() + "_" + q.getQtpye();
            KnowTypeAssign knowTypeAssign = knowTypeAssignMap.get(key);
            if (null == knowTypeAssign) {
                knowTypeAssign = new KnowTypeAssign();
                knowTypeAssign.setKnowId(q.getKnowledgeId());
                knowTypeAssign.setType(q.getQtpye());
                knowTypeAssignMap.put(key, knowTypeAssign);
            }
            if (1 == index) {
                knowTypeAssign.total = 0L;
                knowTypeAssign.exclAssign = 0L;
                knowTypeAssign.assign = 0L;
                knowTypeAssign.exclCount = q.getNumber();
            } else {
                knowTypeAssign.total = q.getNumber() - knowTypeAssign.exclCount;
            }
        }
    }

    @Data
    public static class KnowTypeAssign {
        Long knowId;
        String type;
        Long exclAssign; // 分配个数
        Long assign; // 全量分配个数
        Long exclCount; // 排除总数
        Long total; // 全量总数
    }
}
