package cn.com.duiba.icbc;

import cn.com.duiba.projectx.sdk.UserRequestContext;
import cn.com.duiba.projectx.sdk.annotation.CustomRequestAction;
import cn.com.duiba.projectx.sdk.annotation.EnableRiskControl;
import cn.com.duiba.projectx.sdk.annotation.UserConcurrentLock;
import cn.com.duiba.projectx.sdk.data.ValidateData;
import cn.com.duiba.projectx.sdk.data.ValidateTypeEnum;
import cn.com.duiba.projectx.sdk.dto.ActionRecordDto;
import cn.com.duiba.projectx.sdk.playway.base.Ranking;
import cn.com.duiba.projectx.sdk.playway.scoring.ScoringPlaywayInstance;
import cn.com.duiba.projectx.sdk.playway.scoring.ScoringUserRequestApi;
import cn.com.duiba.projectx.sdk.playway.scoring.StartRecord;
import cn.com.duiba.projectx.sdk.riskmddata.MarkTypeEnum;
import cn.com.duiba.projectx.sdk.riskmddata.SendPrizeDecisionEnum;
import cn.com.duiba.projectx.sdk.utils.Conditions;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Maps;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.annotation.Transactional;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjusters;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * Created by HePeng on 2019/10/28 16:13.
 */
public class GameScoringPlayway extends ScoringPlaywayInstance {
    /******************************** 常量 ****************************/
    private static final Logger LOGGER = LoggerFactory.getLogger(GameScoringPlayway.class);
    // 增加的游戏次数对应的键
    private static final String ADD_GAME_TIMES_KEY = "add_game_times";
    // 游戏中途统计数据对应的键的前缀
    private static final String PUSH_STAT_DATA_KEY_PREFIX = "push_stat_data_";
    // 游戏复活数据对应的键的前缀
    private static final String REVIVE_DATA_KEY_PREFIX = "revive_data_";
    // 通用分隔符
    private static final char NORMAL_SPLIT = ',';
    // 加分卡
    private static final Map<String, Integer> ADD_SCORE_CARD_MAP = Maps.newLinkedHashMap();
    static {
        ADD_SCORE_CARD_MAP.put("sp_2", 100);
        ADD_SCORE_CARD_MAP.put("sp_3", 200);
        ADD_SCORE_CARD_MAP.put("sp_4", 300);
        ADD_SCORE_CARD_MAP.put("sp_5", 600);
        ADD_SCORE_CARD_MAP.put("sp_6", 800);
    }
    // 复活卡片id
    private static final String REVIVE_CARD_SP_ID = "sp_1";
    // 所有卡片id
    private static final List<String> ALL_CARD_SP_ID = Arrays.asList("sp_1", "sp_2", "sp_3", "sp_4", "sp_5", "sp_6");

    // 日期格式化类
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("MMddHHmmss");
    // 加密的后缀
    private static final String ENCRYPT_SUFFIX = "dui88";
    // seq平均间隔
    private static final int SEQ_GAP = 5;
    // 最多允许丢失的推送次数
    private static final int TOLERANT_LOST_PUSH_TIMES = 3;
    // 单次最大推送分数
    private static final int MAX_ONCE_PUSH_SCORE = 100;
    // 最多允许超过的分数
    private static final int TOLERANT_EXCEEDING_SCORE = 300;

    // 变量名
    private static final String START_ID = "startId";
    private static final String SEQ = "seq";
    private static final String SCORE = "score";
    private static final String USE_SP_ID = "useSpId";
    private static final String TOKEN = "token";
    private static final String VALIDATE = "validate";

    // 运营配置项变量名
    private static final String DAILY_FREE_TIMES = "dailyFreeTimes";
    private static final String DAILY_MAX_SHARE_TIMES = "dailyMaxShareTimes";
    private static final String MAX_USE_SP_CNT = "maxUseSpCnt";
    private static final String MIN_ENABLE_CAPTCHA_SCORE = "minEnableCaptchaScore";
    private static final String ONCE_MAX_SCORE = "onceMaxScore";

    /******************************** 接口 ****************************/

    @Override
    public void config(ScoringConfig config){
        config.setUserlockEnable(true);
    }

    @CustomRequestAction(id="visit", name="游戏首页")
    public Object visit(UserRequestContext context, ScoringUserRequestApi api) {
        JSONObject result = new JSONObject();
        // 用户校验
        checkUser(context);
        // 获取剩余游戏次数
        result.put("leftTimes", getLeftTimes(api));
        // 获取剩余分享次数
        result.put("leftShareTimes", getLeftShareTimes(api));
        // 分数大于等于多少启动滑块验证
        result.put(MIN_ENABLE_CAPTCHA_SCORE, api.getIntVariable(MIN_ENABLE_CAPTCHA_SCORE));
        return result;
    }

    @Override
    public Object start(UserRequestContext context, ScoringUserRequestApi api) {
        JSONObject result = new JSONObject();
        // 用户校验
        checkUser(context);
        // 校验游戏剩余次数
        Conditions.expectTrue(getLeftTimes(api) > 0, 500021, "游戏次数已用完");
        // 扣除游戏次数，生成游戏订单
        Long startId = api.getCurrentRecordId();
        // 返回游戏单号
        result.put(START_ID, startId);
        // 获取用户本期得分
        String currentPeriod = getCurrentWeekFirstDay().format(DATE_FORMATTER);
        Ranking myRankInfo = api.getRankingApi().getMyRankInfo(currentPeriod);
        result.put("maxScore", myRankInfo != null ? myRankInfo.getMaxScore() : null);
        return result;
    }

    @CustomRequestAction(id="push", name="中途提交分数")
    @UserConcurrentLock
    public Object push(UserRequestContext context, ScoringUserRequestApi api) {
        // 用户校验
        checkUser(context);
        // 参数基本校验
        Long startId = getParameter(context, START_ID, Long.class);
        Integer seq = getParameter(context, SEQ, Integer.class);
        Integer score = getParameter(context, SCORE, Integer.class);
        String token = getParameter(context, TOKEN, String.class);
        Conditions.expectFalse(startId == null || startId <= 0 || seq == null || seq <= 0 || score == null || score < 0 || StringUtils.isBlank(token), "参数有误");
        // seq校验
        PushStatData pushStatData = getPushStatData(api, startId);
        if(seq <= pushStatData.getLastSeq()) {
            String pushStatDataStr = JSONObject.toJSONString(pushStatData);
            LOGGER.info("工行打星球-中途提交分数，seq异常，小于等于之前的seq，seq=[{}], pushStatData=[{}]", seq, pushStatDataStr);
            return null;
        }
        // 提交次数校验
        if(Math.abs(pushStatData.getSubmitTimes() + 1 - (seq + SEQ_GAP - 1) / SEQ_GAP) > TOLERANT_LOST_PUSH_TIMES) {
            String pushStatDataStr = JSONObject.toJSONString(pushStatData);
            LOGGER.info("工行打星球-中途提交分数，提交次数异常，seq=[{}], pushStatData=[{}]", seq, pushStatDataStr);
            return null;
        }
        // token校验
        String realToken = DigestUtils.md5Hex(startId + "" + seq + "" + score + ENCRYPT_SUFFIX);
        if(!realToken.equalsIgnoreCase(token)) {
            LOGGER.info("工行打星球-中途提交分数，token校验失败，startId=[{}], seq=[{}], score=[{}], realToken=[{}], token=[{}]",
                    startId, seq, score, realToken, token);
            return null;
        }
        // 游戏订单查询校验
        ActionRecordDto actionRecordDto = api.findById(startId);
        if(!checkRecordDto(actionRecordDto, context)) {
            String dtoStr = JSONObject.toJSONString(actionRecordDto);
            LOGGER.info("工行打星球-中途提交分数，游戏订单异常，actionRecordDto=[{}]", dtoStr);
            return null;
        }
        // 分数校验
        if(score > MAX_ONCE_PUSH_SCORE) {
            LOGGER.info("工行打星球-中途提交分数，分数异常，score=[{}], MAX_ONCE_PUSH_SCORE=[{}]", score, MAX_ONCE_PUSH_SCORE);
            return null;
        }
        // 记录
        pushStatData.setLastSeq(seq);
        pushStatData.setScore(pushStatData.getScore() + score);
        pushStatData.setSubmitTimes(pushStatData.getSubmitTimes() + 1);
        persistPushStatData(api, startId, pushStatData);
        return null;
    }

    @CustomRequestAction(id="spData", name="游戏结束获取卡片信息")
    public Object spData(UserRequestContext context, ScoringUserRequestApi api) {
        JSONObject result = new JSONObject();
        // 所有道具
        Map<String, Long> retMap = api.getMyUserContext().getAllStageProperty();
        Map<String, Integer> spMap = ALL_CARD_SP_ID.stream().collect(Collectors.toMap(t -> t,t -> Optional.ofNullable(retMap.get(t)).orElse(0L).intValue()));
        Map<String, Integer> testMap=new HashMap<String, Integer>();
        for (int i =1; i<=3;i++){
            testMap.put("sp_1",10);
        }
        result.put("spMap", testMap);
        // 本局游戏剩余可使用道具数量
        int revivedTimes = getRevivedTimes(getReviveData(api, getParameter(context, START_ID, Long.class)));
        result.put("leftUseSpCnt", Math.max(api.getIntVariable(MAX_USE_SP_CNT) - revivedTimes, 0));
        return result;
    }

    @CustomRequestAction(id="revive", name="复活")
    @Transactional
    @UserConcurrentLock
    public Object revive(UserRequestContext context, ScoringUserRequestApi api) {
        // 用户校验
        checkUser(context);
        // 参数基本校验
        Long startId = getParameter(context, START_ID, Long.class);
        Integer seq = getParameter(context, SEQ, Integer.class);
        String token = context.getHttpRequest().getParameter(TOKEN);
        Conditions.expectFalse(startId == null || startId <= 0 || seq == null || seq <= 0 || StringUtils.isBlank(token), "参数有误");
        // seq校验
        String reviveData = getReviveData(api, startId);
        int revivedTimes = getRevivedTimes(reviveData);
        Conditions.expectTrue((seq + SEQ_GAP - 1) / SEQ_GAP == revivedTimes + 1, 500051, "seq参数有误");
        // token校验
        String realToken = DigestUtils.md5Hex(startId + "" + seq + ENCRYPT_SUFFIX);
        Conditions.expectTrue(realToken.equalsIgnoreCase(token), 500052, "token校验失败");
        // 游戏订单查询校验
        Conditions.expectTrue(checkRecordDto(api.findById(startId), context), 500053, "本局游戏无法复活");
        // 复活卡剩余数量校验
        int reviveCardCnt = Optional.ofNullable(api.getMyUserContext().getStageProperty(REVIVE_CARD_SP_ID)).orElse(0L).intValue();
        Conditions.expectTrue(reviveCardCnt > 0, 500054, "复活卡已用完");
        // 单局游戏卡片使用上限校验
        int maxUseSpCnt = api.getIntVariable(MAX_USE_SP_CNT);
        Conditions.expectTrue(revivedTimes < maxUseSpCnt, 500055, "每局游戏只能使用" + maxUseSpCnt + "次道具");
        // 扣除复活卡
        Conditions.expectTrue(api.consumeStageProperty(REVIVE_CARD_SP_ID, 1), 500056, "使用复活卡失败，请稍后再试");
        // 增加复活记录
        persistReviveData(api, startId, reviveData + NORMAL_SPLIT + LocalDateTime.now().format(DATE_TIME_FORMATTER));
        return null;
    }

    @Override
    @Transactional
    @EnableRiskControl(value = MarkTypeEnum.ACTIVITY, isSPtoRisk = SendPrizeDecisionEnum.SENDPRIZENOGORISK)
    public Object submit(StartRecord record, int score, UserRequestContext context, ScoringUserRequestApi api) {
        JSONObject result = new JSONObject();
        // 用户校验
        checkUser(context);
        // 参数基本校验
        String useSpId = getParameter(context, USE_SP_ID, String.class);
        String token = getParameter(context, TOKEN, String.class);
        String validate = getParameter(context, VALIDATE, String.class);
        Conditions.expectFalse(StringUtils.isBlank(token), "参数有误");
        // token校验
        String realToken = DigestUtils.md5Hex(record.getStartId() + "" + score + ENCRYPT_SUFFIX);
        Conditions.expectTrue(realToken.equalsIgnoreCase(token), 500061, "token校验失败");
        // 分数校验
        PushStatData pushStatData = getPushStatData(api, record.getStartId());
        Conditions.expectTrue(score <= pushStatData.getScore() + TOLERANT_EXCEEDING_SCORE && score <= api.getIntVariable(ONCE_MAX_SCORE), 500063, "分数存在异常");
        // 滑块校验
        if(score >= api.getIntVariable(MIN_ENABLE_CAPTCHA_SCORE)) {
            Conditions.expectTrue(StringUtils.isNotBlank(validate), 500064, "滑块校验失败");
            ValidateData validateData = api.getDuibaApi().getValidateApi().isCaptchaPass(validate, ValidateTypeEnum.SLIDE);
            Conditions.expectTrue(validateData != null && validateData.isResult(), 500064, "滑块校验失败");
        }
        // 卡片校验
        int finalScore = score;
        if(useSpId != null) {
            // 卡片合法性校验
            Conditions.expectTrue(ADD_SCORE_CARD_MAP.containsKey(useSpId), "参数有误");
            // 单局游戏卡片使用上限校验
            int revivedTimes = getRevivedTimes(getReviveData(api, record.getStartId()));
            int maxUseSpCnt = api.getIntVariable(MAX_USE_SP_CNT);
            Conditions.expectTrue(revivedTimes < maxUseSpCnt, 500065, "每局游戏只能使用" + maxUseSpCnt + "次道具");
            // 剩余数量校验，校验通过扣除卡片
            int useSpCnt = 1;
            int leftSpCnt = Optional.ofNullable(api.getMyUserContext().getStageProperty(useSpId)).orElse(0L).intValue();
            Conditions.expectTrue(useSpCnt <= leftSpCnt && api.consumeStageProperty(useSpId, useSpCnt), 500066, "加分卡不足");
            // 计算最终得分
            finalScore += ADD_SCORE_CARD_MAP.get(useSpId) * useSpCnt;
        }
        // 提交分数
        api.submitScore(record.getStartId(), finalScore);
        // 创建或更新排行榜配置
        String currentPeriod = getCurrentWeekFirstDay().format(DATE_FORMATTER);
        api.getRankingApi().insertRankingConfig(currentPeriod, currentPeriod + "期", false);
        // 更新排行榜数据
        api.getRankingApi().updateRankingScore(currentPeriod, finalScore, true);
        result.put("finalScore", finalScore);
        result.put("maxScore", api.getRankingApi().getMyRankInfo(currentPeriod).getMaxScore());
        return result;
    }

    /******************************** 工具方法 ****************************/

    // 校验用户
    private void checkUser(UserRequestContext context) {
        Conditions.expectFalse(isNotLoginUser(context), 100001, "请先登录");
    }

    // 是否是未登录用户
    private boolean isNotLoginUser(UserRequestContext context) {
        String partnerUserId = context.getPartnerUserId();
        return partnerUserId == null
                || "not_login".equals(partnerUserId)
                || partnerUserId.startsWith("gen_")
                || partnerUserId.startsWith("obs_");
    }

    // 获取请求参数
    @SuppressWarnings("unchecked")
    private <T> T getParameter(UserRequestContext context, String name, Class<T> clazz) {
        String value = context.getHttpRequest().getParameter(name);
        if(StringUtils.isBlank(value)) {
            return null;
        }
        try {
            if(clazz.equals(String.class)) {
                return (T) value;
            } else if(clazz.equals(Long.class)) {
                return (T) Long.valueOf(value);
            } else if(clazz.equals(Integer.class)) {
                return (T) Integer.valueOf(value);
            } else {
                return null;
            }
        } catch (Exception e) {
            return null;
        }
    }

    // 获取剩余游戏次数
    private int getLeftTimes(ScoringUserRequestApi api) {
        int dailyFreeTimes = api.getIntVariable(DAILY_FREE_TIMES);
        int addTimes = Optional.ofNullable(api.getMyUserContext().getUserDataLong(ADD_GAME_TIMES_KEY)).orElse(0L).intValue();
        int useTimes = Optional.ofNullable(api.queryMyTodayCount()).orElse(0L).intValue();
        return Math.max(dailyFreeTimes + addTimes - useTimes, 0);
    }

    // 获取剩余分享次数
    private int getLeftShareTimes(ScoringUserRequestApi api) {
        int dailyMaxShareTimes = api.getIntVariable(DAILY_MAX_SHARE_TIMES);
        int addTimes = Optional.ofNullable(api.getMyUserContext().getUserDataLong(ADD_GAME_TIMES_KEY)).orElse(0L).intValue();
        return Math.max(dailyMaxShareTimes - addTimes, 0);
    }

    // 获取某局游戏中途推送统计数据
    private PushStatData getPushStatData(ScoringUserRequestApi api, Long startId) {
        String key = PUSH_STAT_DATA_KEY_PREFIX + startId;
        String userData = api.getMyUserContext().getUserData(key);
        if(StringUtils.isBlank(userData)) {
            return new PushStatData();
        }
        return JSONObject.parseObject(userData, PushStatData.class);
    }

    // 保存某局游戏中途推送统计数据
    private void persistPushStatData(ScoringUserRequestApi api, Long startId, PushStatData data) {
        String key = PUSH_STAT_DATA_KEY_PREFIX + startId;
        api.getMyUserContext().putUserData(key, JSONObject.toJSONString(data));
    }

    // 校验游戏订单
    private boolean checkRecordDto(ActionRecordDto recordDto, UserRequestContext context) {
        return recordDto != null
                && Objects.equals(recordDto.getUserId(), context.getUserId());
    }

    // 获取某局游戏复活数据
    private String getReviveData(ScoringUserRequestApi api, Long startId) {
        if(startId == null) {
            return "";
        }
        String key = REVIVE_DATA_KEY_PREFIX + startId;
        return StringUtils.defaultIfBlank(api.getMyUserContext().getUserData(key), "");
    }

    // 根据复活数据计算复活次数
    private int getRevivedTimes(String reviveData) {
        if(StringUtils.isBlank(reviveData)) {
            return 0;
        }
        return (int) reviveData.chars().filter(t -> t == (int) NORMAL_SPLIT).count();
    }

    // 存储某局游戏复活数据
    private void persistReviveData(ScoringUserRequestApi api, Long startId, String reviveData) {
        String key = REVIVE_DATA_KEY_PREFIX + startId;
        api.getMyUserContext().putUserData(key, reviveData);
    }

    // 获取本周周一
    private LocalDate getCurrentWeekFirstDay() {
        return LocalDate.now().with(TemporalAdjusters.ofDateAdjuster(
                localDate -> localDate.minusDays((long) localDate.getDayOfWeek().getValue() - DayOfWeek.MONDAY.getValue())
        ));
    }

    /******************************** VO ****************************/

    // 中途提交数据累计结果
    public static class PushStatData {
        private int lastSeq; // 最后一次提交的seq
        private int submitTimes; // 累计提交次数
        private int score; // 累计得分

        public int getLastSeq() {
            return lastSeq;
        }

        public void setLastSeq(int lastSeq) {
            this.lastSeq = lastSeq;
        }

        public int getSubmitTimes() {
            return submitTimes;
        }

        public void setSubmitTimes(int submitTimes) {
            this.submitTimes = submitTimes;
        }

        public int getScore() {
            return score;
        }

        public void setScore(int score) {
            this.score = score;
        }
    }
}

