package com.td.boss.game.complayerland.service;

import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateRange;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import com.td.boss.common.service.CommonServiceImpl;
import com.td.boss.game.commallseed.service.ComMallSeedService;
import com.td.boss.game.commallseed.vo.ComMallSeedVo;
import com.td.boss.game.complayerland.pojo.ComPlayerDisaster;
import com.td.boss.game.complayerland.pojo.ComPlayerDisasterLog;
import com.td.boss.game.complayerland.pojo.ComPlayerDisasterProtected;
import com.td.boss.game.complayerland.pojo.ComPlayerLand;
import com.td.boss.game.complayerland.repository.ComPlayerDisasterLogRepository;
import com.td.boss.game.complayerland.scheduled.ComPlayerDisasterDateUtil;
import com.td.boss.game.complayerland.vo.ComPlayerDisasterEnum;
import com.td.boss.game.complayerland.vo.ComPlayerDisasterLogVo;
import com.td.boss.game.complayerprofit.pojo.ComPlayerProfit;
import com.td.boss.game.complayerprofit.service.ComPlayerProfitService;
import com.td.boss.game.complayerprofit.vo.ComPlayerProfitVo;
import com.td.boss.util.CopyUtil;
import com.td.boss.util.DoubleUtil;
import com.td.boss.util.RedisData;
import com.td.boss.util.RedisLock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
@Transactional
public class ComPlayerDisasterLogServiceImpl extends CommonServiceImpl<ComPlayerDisasterLogVo, ComPlayerDisasterLog, String> implements ComPlayerDisasterLogService {
    @Autowired
    private ComPlayerDisasterLogRepository disasterLogRepository;

    @Autowired
    private ComPlayerDisasterProtectedService disasterProtectedService;

    @Autowired
    private ComPlayerDisasterService disasterService;

    @Autowired
    private ComPlayerLandService comPlayerLandService;

    @Autowired
    private ComPlayerProfitService comPlayerProfitService;

    @Autowired
    private ComMallSeedService comMallSeedService;

    @Autowired
    private RedisLock redisLock;

    /**
     * 伤害 会减产50%
     */
    private int damage = 40;

    /**
     * 防御 90%概率防御灾难
     */
    private double defend = 90;

    /**
     * 获取经历的灾难记录
     *
     * @param userId
     * @return
     */
    @Override
    public List<ComPlayerDisasterLog> getHistory(String userId) {
        //获取已发生未读的灾难集合
        List<ComPlayerDisasterLog> list = disasterLogRepository.getUnShowAndEnabled(userId).stream().sorted(Comparator.comparing(ComPlayerDisasterLog::getCreateTime)).collect(Collectors.toList());
        log.info("获取灾难记录:{}", JSONUtil.toJsonStr(list));
        //设置成已读
        list.forEach(a -> a.setShow(true));
        disasterLogRepository.saveAll(list);
        return list;
    }

    //region 灾难模式周任务：同步每个用户上周收入支出

    /**
     * 定时任务核心方法 同步每个用户上周收入支出
     * 灾难模式每周统计一次，如果这个期间新用户也需要这个功能。新注册的用户直接调用该方法。
     *
     * @param userId
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void asyncDisaster(String userId) {
        //获取“收入支出数据”，下一步计算灾难场数
        ComPlayerDisaster comPlayerDisaster = disasterService.getAndSave(userId);
        //如果从链端接口拉到数据了。那么就计算灾难发生明细
        if (comPlayerDisaster != null) {
            //本周内应该出现的灾难次数
            int disasterTimes = getDisasterTimes(comPlayerDisaster.getPayment(), comPlayerDisaster.getIncome());
            log.info("用户编号:{}本周发生灾难:{}次", userId, disasterTimes);

            //获得本周还剩下几天可以发生灾难，每周从周日到下周6
            Date now = new Date();
            DateRange dayRange = DateUtil.range(now, ComPlayerDisasterDateUtil.getEnd(), DateField.DAY_OF_WEEK);
            List<Date> hasDisasterDay = new ArrayList<>();
            while (dayRange.hasNext()) {
                hasDisasterDay.add(dayRange.next());
            }
            //发生灾难的时间点（本周具体哪天灾难），从可发生灾难【时间集合】中随机取【x】个时间点进行灾难
            List<Date> realDisasterDayList = RandomUtil.randomEleList(hasDisasterDay, disasterTimes);
            log.info("用户编号:{}本周发生灾难时间点:{}", userId, JSONUtil.toJsonStr(realDisasterDayList));

            //生成灾难明细记录
            List<ComPlayerDisasterLog> disasterLogList = getComPlayerDisasterLogVoList(userId, comPlayerDisaster.getId(), realDisasterDayList);
            log.info("用户编号:{}本周发生灾难明细:{}", userId, JSONUtil.toJsonStr(disasterLogList));

            //灾难明细持久化
            disasterLogRepository.saveAll(disasterLogList);
        }
    }

    /**
     * 获得本周灾难次数
     * a)当某个村长所属农场的支出小于等于收入30%时，按照正常频率，每周出现1次
     * b)当某个村长所属农场的支出大于收入30%时，每周出现2次
     * c)当某个村长所属农场的支出大于收入50%时，每周出现4次
     * d)当某个村长所属农场的支出大于收入80%时，每周出现6次
     * 支出对应pay_amount，收入对应swap_amount
     *
     * @param pay_amount  支出对应
     * @param swap_amount 收入对应
     */
    private int getDisasterTimes(double pay_amount, double swap_amount) {
        if (pay_amount > NumberUtil.mul(swap_amount, 0.8)) {
            return 6;
        } else if (pay_amount > NumberUtil.mul(swap_amount, 0.5)) {
            return 4;
        } else if (pay_amount > NumberUtil.mul(swap_amount, 0.3)) {
            return 2;
        } else {
            return 1;
        }
    }

    /**
     * 根据用户编号和灾难时间点 生成灾难表
     *
     * @param userId                   用户id
     * @param ComPlayerDisasterTableId 主表id
     * @param realDisasterDayList      发生灾难的时间点（本周具体哪天灾难）
     * @return
     */
    private List<ComPlayerDisasterLog> getComPlayerDisasterLogVoList(String userId, long ComPlayerDisasterTableId, List<Date> realDisasterDayList) {
        List<ComPlayerDisasterLog> list = new ArrayList<>();
        ComPlayerDisasterLog disasterLog;
        for (Date disasterDay : realDisasterDayList) {
            //随机获取一种灾难方式灾难对象
            List<String> disasterEnumFields = (List) EnumUtil.getFieldValues(ComPlayerDisasterEnum.class, "msg");
            ComPlayerDisasterEnum disasterEnum = EnumUtil.likeValueOf(ComPlayerDisasterEnum.class, RandomUtil.randomEle(disasterEnumFields));

            disasterLog = new ComPlayerDisasterLog();
            disasterLog.setDsasterId(ComPlayerDisasterTableId);
            disasterLog.setDsasterName(disasterEnum.getMsg());
            disasterLog.setDsasterType(disasterEnum.getCode());
            disasterLog.setUserId(userId);
            disasterLog.setDamage(damage);
            disasterLog.setDefend(defend);
            disasterLog.setCreateTime(new Date());
            disasterLog.setShow(false);
            disasterLog.setEnabled(false);
            disasterLog.setDisasterTime(disasterDay);
            list.add(disasterLog);
        }
        return list;
    }
    //endregion

    //region 灾难模式天任务：触发所有用户今日灾难和保护机制

    /**
     * 灾难模式天任务：触发所有用户今日灾难和保护机制
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void ComPlayerDisasterDayTask(ComPlayerDisasterLog disasterLog) {
        Date now = new Date();
        //查询当前人员购买防护历史记录
        List<ComPlayerDisasterProtected> protectList = disasterProtectedService.getComPlayerDisasterProtectedByUserIdOrderByProtectTimeDesc(disasterLog.getUserId());
        log.info("用户编号：{},防护历史记录：{}", disasterLog.getUserId(), JSONUtil.toJsonStr(protectList));
        //获得对应灾难类型的防护记录    并得到这个 防护到期时间
        Date protectedDate = protectList.stream()
                .filter(a -> disasterLog.getDsasterType().equals(a.getDsasterType()))
                .sorted(Comparator.comparing(ComPlayerDisasterProtected::getProtectTime).reversed())
                .map(ComPlayerDisasterProtected::getProtectTime)
                .findFirst().orElse(DateUtil.parse(null));

        log.info("用户编号：{},灾难：{},防护到期时间：{}", disasterLog.getUserId(), JSONUtil.toJsonStr(disasterLog), protectedDate);

        //存储防护到期时间，可能是null代表没有购买过防护
        disasterLog.setProtectTime(protectedDate);
        //设置本次灾难已经发生
        disasterLog.setEnabled(true);
        //灾难具体触发时间，代表每天的周期任务时间
        disasterLog.setUpdateTime(now);
        //默认是没有防护的
        disasterLog.setProtect(false);

        //如果没有防护或者防护到期。则直接发生灾难进行减产
        if (protectedDate != null && now.before(protectedDate)) {
            int randomProtect = RandomUtil.randomInt(0, 101);
            log.info("灾难几率：{}", randomProtect);
            //todo 先注释这个防护，这里触发的灾难默认是不防护的
            //if (randomProtect > (100 - defend)) {
            //    disasterLog.setProtect(true);
            //}
        }

        log.info("灾难日志：{}", JSONUtil.toJsonStr(disasterLog));
        disasterLogRepository.save(disasterLog);
        //todo 减产接口 减产逻辑
        if (!disasterLog.isProtect()) {
            log.info("灾难结果：减产");
            //todo 插入受灾减产数据
            long _disasterTime = System.currentTimeMillis() + RedisData.getPlayerDisastersFirstTimeout();

            //查找用户的全部土地
            List<ComPlayerLand> comPlayerLands = comPlayerLandService.findAllByUserIdAndPlantAndMature(disasterLog.getUserId());
            for (int i = 0; i < comPlayerLands.size(); i++) {
                ComPlayerLand _comPlayerLand = comPlayerLands.get(i);
                ComMallSeedVo comMallSeedVo = comMallSeedService.findById(_comPlayerLand.getPlantId());

                //这里处理一次判断是否发生过第一次灾难
                //todo 添加一个灾难损失，灾难损失的时间点是种植开始到成熟阶段的一个随机时间
                // 加个锁判断，是否在其他地方触发了这次灾难计算（自解锁）
                String _disasterKey = RedisData.getPlayerDisastersFirstKey() + _comPlayerLand.getConfigLandId() + _comPlayerLand.getUserId();
                try {
                    if (!redisLock.lock(_disasterKey, String.valueOf(_disasterTime))) {
                        log.info("锁住key："+_disasterKey);
                       continue;
                    }

                    //灾难发生时间
                    Date endDate = com.td.boss.util.DateUtil.getOldDateAddDay(_comPlayerLand.getPlantStart(), _comPlayerLand.getPlantMature());
                    long randomDate = _comPlayerLand.getPlantStart().compareTo(endDate) == 0 ?
                            endDate.getTime() :
                            RandomUtil.randomLong(_comPlayerLand.getPlantStart().getTime(), endDate.getTime());
                    Date disasterDate = new Date(randomDate);
                    //获取损失的数据
                    List<ComPlayerProfit> comPlayerProfits = comPlayerProfitService.findByUserIdAndPlantFlagAndLandId(_comPlayerLand.getUserId(), _comPlayerLand.getPlantFlag(), _comPlayerLand.getConfigLandId());
                    //todo 总共受损的数量
                    Double _allLossProfit = 0d;
                    boolean isNaturalProfit = false, isBeastProfit = false;
                    Integer _disasterCount = 0;
                    for (int j = 0; j < comPlayerProfits.size(); j++) {
                        ComPlayerProfit _profit = comPlayerProfits.get(j);
                        _allLossProfit = DoubleUtil.add(_allLossProfit, _profit.getStolen());
                        if (_profit.getLossType().equals(2)) {
                            if(DoubleUtil.compare(-1D, _profit.getDsasterId().doubleValue()).equals(0)){
                                isNaturalProfit = true;
                            }
                            //出现灾难的次数
                            _disasterCount++;
                        } else if (_profit.getLossType().equals(3)) {
                            if(DoubleUtil.compare(-1D, _profit.getDsasterId().doubleValue()).equals(0)){
                                isBeastProfit = true;
                            }
                            //出现灾难的次数
                            _disasterCount++;
                        }
                    }
                    log.info("用户编号：{},防护历史记录：{}", _comPlayerLand.getUserId(), JSONUtil.toJsonStr(protectList));
                    //获得对应灾难类型的防护记录    并得到这个 防护到期时间. 自然灾害类型
                    //如果没有防护或者防护到期。则直接发生灾难进行减产
                    if (!isNaturalProfit && (protectedDate == null || disasterDate.after(protectedDate))) {
                        //灾难的随机时间比防护包时间后，说明防护包到期
                        //成熟的时候触发第一次自然灾难
                        ComPlayerProfitVo naturalProfitVo = comPlayerProfitService.addFirstDisasterAndLoss(CopyUtil.copy(_comPlayerLand, ComPlayerLand.class),
                                comMallSeedVo, 2, _allLossProfit,-1);
                        //多次执行需要加上当前操作的利润
                        _allLossProfit += naturalProfitVo.getStolen();

                        //添加自然灾害灾难记录
                        addDisasterEnable(_comPlayerLand.getUserId(), -1L, 2, protectedDate);

                    }
                    if (!isBeastProfit && (protectedDate == null || disasterDate.after(protectedDate))) {
                        //灾难的随机时间比防护包时间后，说明防护包到期
                        //成熟的时候触发第一次自然灾难
                        ComPlayerProfitVo beastProfitVo = comPlayerProfitService.addFirstDisasterAndLoss(CopyUtil.copy(_comPlayerLand, ComPlayerLand.class),
                                comMallSeedVo, 3, _allLossProfit,-1);
                        //多次执行需要加上当前操作的利润
                        _allLossProfit += beastProfitVo.getStolen();

                        //添加野兽灾害灾难记录
                        addDisasterEnable(_comPlayerLand.getUserId(), -1L, 3, protectedDate);
                    }

                    //todo 添加一条单独减产的数据 ，对应灾难
                    comPlayerProfitService.addOtherDisasterAndLoss(CopyUtil.copy(_comPlayerLand, ComPlayerLand.class),
                            comMallSeedVo, disasterLog.getDsasterType(), _allLossProfit, _disasterCount,disasterLog.getDsasterId());

                    redisLock.unlock(_disasterKey, String.valueOf(_disasterTime));
                } catch (Exception e) {
                    redisLock.unlock(_disasterKey, String.valueOf(_disasterTime));
                    log.error("额外灾难加锁异常：" + e.getMessage());
                }
            }
        } else {
            log.info("灾难结果：已防御");
        }
    }

    /**
     * 获取今天的灾难，并且没有触发过的灾难
     *
     * @return
     */
    @Override
    public List<ComPlayerDisasterLog> getTodayDisasterUnEnabledList() {
        Date now = new Date();
        DateTime begin = DateUtil.beginOfDay(now);
        DateTime end = DateUtil.endOfDay(now);
        Specification specification = new Specification<ComPlayerDisasterLog>() {
            @Override
            public Predicate toPredicate(Root<ComPlayerDisasterLog> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
                List<Predicate> predicates = new ArrayList<>();
                predicates.add(criteriaBuilder.between(root.get("disasterTime"), begin, end));
                predicates.add(criteriaBuilder.equal(root.get("isEnabled"), 0));
                return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
            }
        };
        return disasterLogRepository.findAll(specification);
    }
    //endregion


    @Override
    public ComPlayerDisasterLog addDisasterEnable(String userId, long disasterTableId, Integer disasterType, Date disasterDay) {

        ComPlayerDisasterEnum disasterEnum = EnumUtil.likeValueOf(ComPlayerDisasterEnum.class, disasterType);
        ComPlayerDisasterLog disasterLog = new ComPlayerDisasterLog();
        disasterLog.setDsasterId(disasterTableId);
        disasterLog.setDsasterName(disasterEnum.getMsg());
        disasterLog.setDsasterType(disasterEnum.getCode());
        disasterLog.setUserId(userId);
        disasterLog.setDamage(damage);
        disasterLog.setDefend(defend);
        disasterLog.setCreateTime(new Date());
        disasterLog.setShow(false);
        disasterLog.setEnabled(true);
        disasterLog.setDisasterTime(disasterDay);

        save(CopyUtil.copy(disasterLog, ComPlayerDisasterLogVo.class));
        return disasterLog;
    }
}
