package com.ruoyi.web.domain;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.syzy.domain.BBusiWishUniversities;
import com.ruoyi.web.service.EnrollRateCalculator;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.compress.utils.Lists;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Triple;

import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class VoluntaryDto {

    @Data
    @ApiModel
    public static class VoluntaryRecord extends RenderRequest {
        Long id;
        String universityLogo;
        Long universityId;
        String universityName;
        Integer rank;
        List<VoluntaryMajorRecord> majors = Lists.newArrayList();
    }

    @Data
    @ApiModel
    public static class VoluntaryMajorRecord extends RenderRequest {
        Long majorId;
        String majorName;
        String majorAncestors;
        String majorGroup;
        Integer rank;
    }

    @Data
    @ApiModel
    public static class RenderRequest {
        @ApiModelProperty("0 render; 2. Skill")
        int renderType;
        @ApiModelProperty("院校专业")
        Long majorId;
        @ApiModelProperty("院校ID")
        Long universityId;
    }

    @Data
    @ApiModel
    @AllArgsConstructor
    @NoArgsConstructor
    public static class RenderRule {
        @ApiModelProperty("类型")
        String category;
        @ApiModelProperty("描述")
        String content;
        @ApiModelProperty("规则列表")
        List<AIRenderRule> details = Lists.newArrayList();
    }

    @Data
    @ApiModel
    public static class RenderMajorResult extends RenderRequest {
        @ApiModelProperty
        EnumPickType enumPickType;
        @ApiModelProperty
        Integer enrollRate; // 100进制，按原优志愿，为null表示无概率
        @ApiModelProperty
        String enrollRateText; // 概率描述，按原优志愿

        @ApiModelProperty
        List<RenderMajorHistory> histories = Lists.newArrayList();

        @ApiModelProperty
        RenderMajorSkill skill;
    }

    @Data
    @ApiModel
    public static class RenderMajorHistory {
        @ApiModelProperty
        Integer year; // 年份
        @ApiModelProperty
        String score; // 最低分
        @ApiModelProperty
        Integer plan; // 计划人数
        @ApiModelProperty
        Integer enroll; // 录取人数
        @ApiModelProperty
        Integer diff; // 负表示低于录取分；正数表示高于录取分
        @ApiModelProperty
        String ruleContent; // 完整的说明
        @ApiModelProperty
        String application; // 报名人数比值
        @ApiModelProperty
        String admission; // 计划人数比值
    }

    @Data
    @ApiModel
    public static class RenderMajorSkill {
        @ApiModelProperty
        Integer year; // 年份
        @ApiModelProperty
        String cultureScore; // 文化得分
        @ApiModelProperty
        String cultureRule; // 文化规则
        @ApiModelProperty
        String enrollScore; // 录取分
        @ApiModelProperty
        String skillScore; // 反向测技能分
        @ApiModelProperty
        String diff; // 负数表示低于skillScore，正数高于
    }


    // dynamic render rules
    // TODO: 部分规则不需要向前端返回：比如性别与考生类别，此类信息直接在当前用户中获取
    // 只需要向前端返回需要收集信息的部分
    @Data
    @ApiModel
    public static class AIRenderRule {
        // 类别；与AIResponse中的结论类型相对应；前会根据此类型分组展示表单，收集信息
        EnumRuleCategory enumRuleCategory;
        // 字段名，必填。前段会将收集的值以此名称返回
        // 请保持名称按一定的规则定义，如分数类型的由score开头等
        // 这样可以和原来定义的SingleRequest/MultipleRequest/AIRequest等吻合
        String fieldName;
        // 前端输入类型，见类型注释
        // 如果是分制，以filedName+`Total`返回分制选择结果
        EnumInputType enumInputType;
        // 校验规则
        // TODO: 先暂定前端自行生成，如果不行再考虑后台返回。
        // TODO: 前端会将录取规则设置为必填，其它规则设置为选填。如分制类输入，前端会将选中分制作为max校验。
        Boolean required;
        Integer min;
        Integer max;
        String regex;
        // 词典选项类选项 // 词典类选项的优先级最高
        String dictOptions;
        // 非词典选项 // 分制类规则，将多分制在此options中返回
        String[] options;
        // 对于多选时增加一个互斥选项
        String mutexOption;
        // 前端渲染字段
        String label; // 输入标题，必须填充
        String description; // 结尾描述文案，可选填充
        String placeholder; // 占位提示文案 // 选填，缺省前端会自行补充，后台返回的优先级高
        String tips; // 输入框下的特殊说明文案，可选填充
        // 特殊配置
        String defaultValue; // 缺省/初始化时的默认值
        Boolean dotDisable; // 数值输入时，是否带小数点，默认带
        String keyboardMode; // 当enumInputType==number时响应3种模式：number card car，默认number
        Boolean readonly;
    }

    @Data
    @ApiModel
    public static class AIRenderRequest {
        @ApiModelProperty("0 default; 1 AI。AI时职业技能输入得分率 2. 技能分计算")
        int renderType;
        @ApiModelProperty("一级专业大类")
        String majorCategory;
        @ApiModelProperty("二级专业细分集合")
        String[] majorTypes;
        @ApiModelProperty("三级专业集合")
        String[] majorCodes;
        @ApiModelProperty("专业招生代码")
        String[] majorEnrollCodes;
        @ApiModelProperty("学校")
        String universityCode;
        @ApiModelProperty("当前已经填写的表单信息")
        Map<String, String> form;
    }

    // 公共
    @Data
    @ApiModel
    public static class VoluntaryModel<I, O> {
        @ApiModelProperty("voluntaryType")
        EnumVoluntaryType voluntaryType;
        @ApiModelProperty("id")
        Long id;
        @ApiModelProperty("年份")
        Integer year;
        @ApiModelProperty("名称")
        String name;
        @ApiModelProperty("批次名")
        String batchName;
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
        Date createTime;
        @ApiModelProperty("条件")
        I request; // 当前的填写信息，AIRequest.filter可以不保存后台自行决定
        @ApiModelProperty("用户")
        User userSnapshot; // 用户相关快照信息，因为有的计算条件是从用户信息上取的（这个后台自己存，前端不传）
        // 考虑传输节流，AIResponse.university保留需要快照的属性
        // 如：code,enrollCode // TODO 按需罗列（最好是前端只传university.code，后端来决定快照哪些信息)
        // 删除AIResponse.majorDetails.recommends // 志愿表中不需要它
        // 前端主动请求此model时，后台自动填充缺省信息
        @ApiModelProperty("详情")
        List<O> details; // 填报的院校专业
    }
    @Data
    public static class User {
        String name;
        String sex;
        String examType;
        String provinceName;
    }
    @Data
    @ApiModel
    public static class VoluntaryConfig {
        // 目前只有填报数量的配置，院校2个，专业4个
        @ApiModelProperty
        int collegeLimit = 2;
        @ApiModelProperty
        int majorLimit = 4;
    }

    // Request
    @Data
    @ApiModel
    public static class SingleRequest {
        @ApiModelProperty("院校代码")
        String universityCode;
        @ApiModelProperty("专业代码")
        String majorCode;
        @ApiModelProperty("专业招生代码")
        String majorEnrollCode;
        // 因为单选学校，所以不需要显式提供scoreSkillLimit

        @ApiModelProperty
        Map<String, String> form = Maps.newHashMap();
    }
    @Data
    @ApiModel
    public static class MultipleRequest {
        // TODO: 院校查询也需要支持此参数，来查询包含此专业大类的院校
        // TODO: 先暂定限制大类，可能需要限制专业细分2级分类
        @ApiModelProperty
        String majorCategory; // 因为多选时想尽量让规则重合，所以限制专业大类
        @ApiModelProperty
        List<CollegeMajorDto> universities; // 多个学校，先定2个，会受分数类型限制
    }
    @Data
    @ApiModel
    public static class AIRequest extends MultipleRequest {
        @ApiModelProperty
        Integer pageNum;
        @ApiModelProperty
        Integer pageSize;

        // 推荐列表筛选条件 // 筛选条件因为可以不保存，所以单独提取出来
        @ApiModelProperty
        AIRequestFilter filter = new AIRequestFilter();
        // TODO: 会尽快统计出来一些需要额外补充的信息
        @ApiModelProperty
        Map<String, String> form = Maps.newHashMap();
    }
    @Data
    @ApiModel
    public static class CollegeMajorDto {
        @ApiModelProperty
        String code; // university code
        @ApiModelProperty
        List<String> majorCodes; // major codes in university
        @ApiModelProperty
        List<String> majorEnrollCodes; // major enroll codes in university
        @ApiModelProperty
        Map<String, String> form = Maps.newHashMap();
    }
    @Data
    @ApiModel
    public static class AIRequestFilter {
        @ApiModelProperty
        List<String> majorTypes; // majorCode 二级分类
        @ApiModelProperty
        EnumPickType enumPickType; // 默认All
        @ApiModelProperty
        EnumPickEmpty enumPickEmpty; // 默认null, EnumPickType与EnumPickEmpty会互斥，一个传了另一个就会不传
        // TODO: 后台确认一下，如果A学校有的10个专业，有5个不满足条件，有3个有概率，2个没有概率。
        //       则展示时冲稳保展示A学校+3个有概率专业；无概率查询显示A学校+2个无概率专业
        @ApiModelProperty
        Boolean hasClearing; // true: 仅看补录 false:不看补录 null:不限。默认null
        // 院校通用筛选
        @ApiModelProperty
        String keyword; // 院校名称
        @ApiModelProperty
        List<String> level; // 办学层次
        @ApiModelProperty
        List<String> type; // 院校类型
        @ApiModelProperty
        List<String> natureTypeCN; // 办学类型
        @ApiModelProperty
        List<String> location; // 院校省份
    }
    public enum EnumPickType {
        /** 有概率类型 **/
        // TODO    A 根据近3年有的录取分取平均值得到1个投档分
        //         B 胡总给出一组浮动范围 +N-M分 属于有机会录取的，概率值可以先线性分布，也可以配比例尺
        //         C B的配置需要做一套全局的，特定的学校可能会独立指定，因为头部学校和差学校的浮动范围肯定是不一样的
        All(""), // 全部
        Danger("冲"), // 冲
        Normal("稳"), // 稳
        Safety("保"); //

        String label;
        EnumPickType(String label) {
            this.label = label;
        }

        public String label() {
            return label;
        }
    }
    public enum EnumPickEmpty {
        /** 无概率类型 **/
        // 无概率：录取规则通过，特殊要求+专业要求不通过
        EnrollPass,
        // 新增/政策变动：无法计算录取规则
        // 如：没有往年录取数据
        New,
    }

    // Response
    @Data
    @ApiModel
    public static class SingleResponse {
        @ApiModelProperty
        String universityCode;
        @ApiModelProperty
        String majorCode;
        @ApiModelProperty
        String majorEnrollCode; // 专业招生代码
        @ApiModelProperty
        String majorGroup; // 专业组
        @ApiModelProperty
        String majorName; // 专业
        @ApiModelProperty
        String majorDirection;
        @ApiModelProperty
        EnumPickType enumPickType;
        @ApiModelProperty
        Integer enrollRate; // 100进制，按原优志愿，为null表示无概率
        @ApiModelProperty
        String enrollRateText; // 概率描述，按原优志愿
        @ApiModelProperty
        String tips; // 其它说明
        @ApiModelProperty
        List<MajorEnrollHistory> histories; // 历年数据
        @ApiModelProperty
        List<MajorClearingHistory> clearings; // 补录数据
        @ApiModelProperty
        List<MajorEnrollRule> rules; // 所有结论，前端可根据enumRuleType展示
        List<MajorEnrollRule> improves; // 各分项提升结论
        @JsonIgnore
        Double score;
        @JsonIgnore
        EnumPickEmpty enumPickEmpty;
    }

    // paging response
    @Data
    @ApiModel
    public static class AIResponse {
        @ApiModelProperty
        Integer enrollRate;
        @ApiModelProperty
        String enrollRateText;
        @ApiModelProperty
        EnumPickType enumPickType;
        @ApiModelProperty
        EnumPickEmpty enumPickEmpty;
        @ApiModelProperty
        String enrollCode; // 专业代码 TODO: 具体实现的时候看放在这里合不合适
        @ApiModelProperty
        BBusiWishUniversities university; // university base info.
        @ApiModelProperty
        List<SingleResponse> majorDetails; // major enroll rate details
    }
    @Data
    @ApiModel
    public static class MajorEnrollHistory {
        @ApiModelProperty
        Integer year; // 年份
        @ApiModelProperty
        String score; // Important: 返回差分 = myScore - historyScore; 返回'+N,-N'。以后可能返回真实分数
        @ApiModelProperty
        Integer plan; // 计划人数
        @ApiModelProperty
        Integer enroll; // 录取人数
    }

    @Data
    @ApiModel
    public static class MajorClearingHistory {
        @ApiModelProperty
        Integer year; // 年份
        @ApiModelProperty
        String score; // 同上，也暂时返回分差，返回'+N,-N'。
        @ApiModelProperty
        Integer realNum; // 补录人数
    }

    @Data
    @ApiModel
    public static class MajorEnrollRule {
        @ApiModelProperty
        EnumRuleCategory category; // 规则类别
        @ApiModelProperty
        EnumRuleType type; // 规则类型
        @ApiModelProperty
        String improveType; // 分类提升类型，随导入文件生成
        @ApiModelProperty
        String content; // 规则描述性文字
        @ApiModelProperty
        String description; // 有的规则会有额外的说明内容
        @ApiModelProperty
        Integer enrollRate; // 如果type=ScoreTotal,填充此字段
        @ApiModelProperty
        String enrollRateText; // 如果type=ScoreTotal,填充此字段
        @ApiModelProperty
        EnumPickType enumPickType;
        @ApiModelProperty
        Integer year;
        @ApiModelProperty
        String value; // 用户填写的值
        @ApiModelProperty
        Boolean valid; // true: 通过规则；false: 未通过规则； null: 未填写相关信息
        @ApiModelProperty
        String missingValue; // 差的数值: history.score-user.score。比如valid=false, 表示距离通过差的分值。
        @ApiModelProperty
        String failedMessage; // valid=false 或者 valid=null 时的文案说明
        @JsonIgnore
        EnrollRateCalculator.RateLevel rl;
    }

    public enum EnumInputType {
        Text, // 普通文本，一般情况下输入均使用此类。数值也可以使用此类型，结合校验规则使用
        Score, // AI的特殊输入类型，带分制的分数 // 此类输入之后可能涉及输入条件动态变化，NOTE：后面再考虑这种情况。
        Number, // 数值，会限制键盘。一般分数类、或者数值类的使用此类型。
        Radio, // radio单选。如果固定2个，用radio，否则用picker。一般超过3个会折行，影响美观
        Picker, // picker单选，一般使用Picker做单选。
        Eyesight,
        Checkbox // 多选
        // TODO：其它待发现
    }

    public enum EnumVoluntaryType {
        AI,
        Multiple
    }
    public enum EnumRuleCategory {
        None,
        Enroll, // 录取规则
        Special // 特殊要求
    }
    public enum EnumRuleType {
        None,
        // 1 总分，必须定义，有特殊展示
        // 2 学考与文化分，一般有特殊展示
        // 3 除学考与文化成绩外，录取规则涉及的分数，会有展示信息
        // 除以上3点外，均属于附加要求
        ScoreTotal, // 总分
        ScoreUnion, // 学考分，一般针对普高，为固定分，也有部分院校要求普高生也进行校考
        ScoreBase, // 文化分，校考，一般针对非普高
        ScoreSkill, // 技能分
        ScoreSingle, // 单科分
        Special, // 特殊要求, 包含判断的规则项
        Readonly, // 只读项不参与计算，仅在详情页展示
        other // 其它
    }

    public static class FormulaScoreStat {
        Set<String> validTotalSet = Sets.newHashSet("ScoreBase","ScoreSingle","ScoreSkill");
        Map<String, List<Triple<Double, Double, Double>>> groupItemListMap = Maps.newHashMap(); // 提分项目的 项目原始分 OriScore, 项目总分 Total，项目得分率 Rate
        Map<String, MutablePair<Double, Double>> groupTypeStatMap = Maps.newHashMap(); // 提分类型的 得分 Score，满分合计 Total
        Double groupAllTotal = 0.0; // 提分项满分合计

        Map<String, MutablePair<Double, Double>> typeKeyScoreMap = Maps.newHashMap(); // 类型项目的 得分 Score 及 原始分 OriScore
        Map<String, MutablePair<Double, Double>> typeScoreMap = Maps.newHashMap(); // 类型的 得分 Score 及 原始分 Total
        MutablePair<Double, Double> typeTotal = new MutablePair<>(0.0, 0.0); // 综合得分 及 原始分
        Integer typeAllTotal = 0; // 类型满分合计

        public boolean isValid() {
            return (typeScoreMap.containsKey("ScoreSingle") || typeScoreMap.containsKey("ScoreBase")) && typeScoreMap.containsKey("ScoreSkill");
        }

        public boolean isScoreValid() {
            return (typeScoreMap.containsKey("ScoreSingle") || typeScoreMap.containsKey("ScoreBase"));
        }

        public Integer getAllTotal() {
            return typeAllTotal;
        }
        /**
         * 按分类项统计
         * @param itemType ScoreSingle/ScoreBase, ScoreSkill
         * @param itemName Single对应科目
         * @param oriScore
         * @param rate
         * @return
         */
        public Double addType(String itemType, String itemName, Double oriScore, Double rate) {
            Double score = oriScore * rate;
            if (StringUtils.isBlank(itemType)) {
                return score;
            }
            merge(typeKeyScoreMap, itemType + "_" + itemName, score, oriScore); // 记录明细
            merge(typeScoreMap, itemType, score, oriScore); // 按类汇总
            if (validTotalSet.contains(itemType)) {
                typeTotal.setLeft(typeTotal.getLeft() + score); // 所有汇总
                typeTotal.setRight(typeTotal.getRight() + oriScore);
            }
            return score;
        }

        public void addTypeTotal(Integer total) {
            typeAllTotal += total;
        }

        public Double getTypeValue(String valueType, String itemName, Boolean isOri) {
            if ("StatSingle".equals(valueType)) { // 明细分
                String dataKey = "ScoreSingle_" + itemName;
                MutablePair<Double, Double> p = typeKeyScoreMap.get(dataKey);
                return null != p ? (isOri ? p.getRight() : p.getLeft()) : null;
            } else if ("StatSkill".equals(valueType)) { // 明细分
                String dataKey = "ScoreSkill_" + itemName;
                MutablePair<Double, Double> p = typeKeyScoreMap.get(dataKey);
                return null != p ? (isOri ? p.getRight() : p.getLeft()) : null;
            } else if ("StatCategoryScore".equals(valueType)) { // 分类分
                if(StringUtils.isBlank(itemName)) {
                    return isOri ? typeTotal.getRight() : typeTotal.getLeft();
                }
                MutablePair<Double, Double> p = typeScoreMap.get(itemName);
                if(null == p) {
                    p = typeScoreMap.get("ScoreBase".equals(itemName) ? "ScoreSingle" : "ScoreBase");
                }
                return null != p ? (isOri ? p.getRight() : p.getLeft()) : null;
            } else if ("StatRateScore".equals(valueType)) { // 综合分
                return isOri ? typeTotal.getRight() : typeTotal.getLeft();
            }
            return null;
        }

        /**
         * 按提分项统计  学考,综合测试
         * @param itemGroup
         * @param oriScore
         * @param total
         * @param rate
         * @return
         */
        public Double addGroup(String itemGroup, Double oriScore, Double total, Double rate) {
            Double rateScore = oriScore * rate;
            if (StringUtils.isBlank(itemGroup)) {
                return rateScore;
            }
            List<Triple<Double, Double, Double>> itemList = groupItemListMap.get(itemGroup);
            if (null == itemList) {
                itemList = Lists.newArrayList();
                groupItemListMap.put(itemGroup, itemList);
            }
            itemList.add(Triple.of(oriScore, total, rate));
            Double rateTotal = total * rate;
            merge(groupTypeStatMap, itemGroup, rateScore, rateTotal);
            groupAllTotal += rateTotal;
            return rateScore;
        }

        public List<MajorEnrollRule> getImproveScore(Double diffValue) {
            List<MajorEnrollRule> improvesList = Lists.newArrayList();
            if (groupTypeStatMap.containsKey("学考")) {
                MajorEnrollRule base = new MajorEnrollRule();
                base.setCategory(EnumRuleCategory.Enroll);
                base.setType(EnumRuleType.ScoreBase);
                base.setImproveType("学考");
                base.setMissingValue("0");
                improvesList.add(base);
            }
            for (String itemGroup : groupTypeStatMap.keySet()) {
                if ("学考".equals(itemGroup)) {
                    continue;
                }
                MajorEnrollRule enrollRule = new MajorEnrollRule();
                enrollRule.setCategory(EnumRuleCategory.Enroll);
                enrollRule.setType("技能测试".equals(itemGroup) ? EnumRuleType.ScoreSkill : EnumRuleType.other);
                enrollRule.setImproveType(itemGroup);
                MutablePair<Double, Double> scorePair = groupTypeStatMap.get(itemGroup);
                Double missRateScore = diffValue * scorePair.getRight() / groupAllTotal;
                enrollRule.setMissingValue(String.valueOf(Math.round(getOriScore(groupItemListMap.get(itemGroup), missRateScore)))); // 差分 * 本类组总分/所有组总分
                improvesList.add(enrollRule);
            }
            return improvesList;
        }

        public Double getImproveSkillScore(Double diffValue) {
            // TODO 暂不支持有系数的规则, 如果技能分有两个的还不好计算原始分？
            return diffValue;
        }

        // 原始分, 总分, 得分率
        private Double getOriScore(List<Triple<Double, Double, Double>> itemList, Double missRateScore) {
            Double statTotal = itemList.stream().mapToDouble(t -> t.getMiddle()).sum();
            Double oriTotal = 0.0;
            for (Triple<Double, Double, Double> t : itemList) {
                oriTotal += Math.min(t.getMiddle() - t.getLeft(), missRateScore * t.getMiddle() / statTotal / t.getRight());
            }
            return Math.round(oriTotal * 10) / 10.0;
        }
        private void merge(Map<String, MutablePair<Double, Double>> pairMap, String key, Double value, Double oriValue) {
            MutablePair<Double, Double> exist = pairMap.get(key);
            if (null == exist) {
                exist = new MutablePair<>(value, oriValue);
                pairMap.put(key, exist);
            } else {
                exist.setLeft(exist.getLeft() + value);
                exist.setRight(exist.getRight() + oriValue);
            }
        }

    }
}
