package com.ningdatech.pmapi.meeting.task;

import cn.hutool.core.util.ArrayUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ningdatech.basic.util.CollUtils;
import com.ningdatech.basic.util.SpringUtils;
import com.ningdatech.cache.model.cache.CacheHashKey;
import com.ningdatech.cache.repository.CachePlusOps;
import com.ningdatech.pmapi.common.util.SpringContextHolder;
import com.ningdatech.pmapi.meeting.builder.ExpertInviteBuilder;
import com.ningdatech.pmapi.meeting.entity.domain.Meeting;
import com.ningdatech.pmapi.meeting.entity.domain.MeetingExpert;
import com.ningdatech.pmapi.meeting.entity.dto.AvoidRuleDTO;
import com.ningdatech.pmapi.meeting.entity.dto.ExpertChooseDTO;
import com.ningdatech.pmapi.meeting.entity.dto.InviteCacheDTO;
import com.ningdatech.pmapi.meeting.entity.dto.RandomInviteRuleDTO;
import com.ningdatech.pmapi.meeting.entity.enumeration.ExpertAttendStatusEnum;
import com.ningdatech.pmapi.meeting.entity.req.ConfirmedRosterReq;
import com.ningdatech.pmapi.meeting.helper.MeetingCallOrMsgHelper;
import com.ningdatech.pmapi.meeting.manage.ExpertInviteManage;
import com.ningdatech.pmapi.meeting.manage.MeetingManage;
import com.ningdatech.pmapi.meeting.service.IExpertInviteAvoidRuleService;
import com.ningdatech.pmapi.meeting.service.IExpertInviteRuleService;
import com.ningdatech.pmapi.meeting.service.IMeetingExpertService;
import com.ningdatech.pmapi.meeting.service.IMeetingService;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import org.springframework.aop.framework.AopContext;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicInteger;


/**
 * <p>
 * 专家随机邀请任务
 * </p>
 *
 * @author WendyYang
 * @since 21:25 2022/8/15
 */
@Slf4j
@Component
@AllArgsConstructor
public class ExpertRandomInviteTask {

    private final RandomInviteProperties properties;
    private static final String MEETING_ID_INVITE_RANDOM = "MEETING_ID_INVITE_RANDOM";

    private static final Duration EXPIRE_TIME = Duration.ofDays(60);

    private final CachePlusOps cachePlusOps;
    @Resource(name = "expertInviteScheduler")
    private ThreadPoolTaskScheduler scheduler;
    private final IMeetingExpertService meetingExpertService;
    private final IExpertInviteRuleService inviteRuleService;
    private final IMeetingService meetingService;
    private final ExpertInviteManage expertInviteManage;
    private final IExpertInviteAvoidRuleService inviteAvoidRuleService;
    private final MeetingCallOrMsgHelper meetingCallOrMsgHelper;

    /**
     * 用来存入线程执行句柄, 停止定时任务时使用
     */
    private static final ConcurrentMap<Long, ScheduledFuture<?>> INVITE_TASK_MAP = new ConcurrentHashMap<>();

    private ExpertRandomInviteTask currProxy() {
        return (ExpertRandomInviteTask) AopContext.currentProxy();
    }

    private CacheHashKey getCacheKey(Long meetingId) {
        String field = meetingId == null ? null : meetingId.toString();
        return new CacheHashKey(MEETING_ID_INVITE_RANDOM, field, EXPIRE_TIME);
    }

    @PostConstruct
    public void initTask() {
        if (!properties.getEnable()) {
            log.warn("随机邀请已关闭……");
            return;
        }
        initInviteTaskAfterStarted();
    }

    /**
     * 项目重启之后重新初始化邀请任务
     */
    private void initInviteTaskAfterStarted() {
        Map<Long, InviteCacheDTO> caches = cachePlusOps.hGetAll(getCacheKey(null));
        if (MapUtils.isEmpty(caches)) {
            log.info("暂无需要初始化的抽取会议信息");
            return;
        }
        Integer inviteDelay = properties.getInviteDelay();
        for (InviteCacheDTO cache : caches.values()) {
            Boolean reInvite = cache.getReInvite();
            LocalDateTime tsTime = cache.getTaskStartTime();
            boolean added = addInviteTask(cache.getMeetingId(), true, inviteDelay, reInvite, tsTime);
            if (!added) {
                cachePlusOps.hDel(getCacheKey(cache.getMeetingId()));
            }
        }
    }

    /**
     * 专家抽取校验（抽取数量是否不足）
     *
     * @param meetingId 会议ID
     * @return boolean
     * @author WendyYang
     **/
    private boolean inviteCountCheck(Long meetingId) {
        Map<Long, RandomInviteRuleDTO> ruleMap = inviteRuleService.randomRuleByMeetingId(meetingId);
        if (ruleMap.isEmpty()) {
            return Boolean.FALSE;
        }
        LambdaQueryWrapper<MeetingExpert> query = Wrappers.lambdaQuery(MeetingExpert.class)
                .select(MeetingExpert::getRuleId, MeetingExpert::getStatus)
                .in(MeetingExpert::getRuleId, ruleMap.keySet())
                .eq(MeetingExpert::getStatus, ExpertAttendStatusEnum.AGREED.getCode());
        List<MeetingExpert> experts = meetingExpertService.list(query);
        if (experts.isEmpty()) {
            return Boolean.TRUE;
        }
        Map<Long, Long> cntMap = CollUtils.groupCount(experts, MeetingExpert::getRuleId);
        for (Map.Entry<Long, RandomInviteRuleDTO> entry : ruleMap.entrySet()) {
            Long agreeCnt = cntMap.getOrDefault(entry.getKey(), 0L);
            if (agreeCnt < entry.getValue().getCount()) {
                return Boolean.TRUE;
            }
        }
        return Boolean.FALSE;
    }

    /**
     * 唤醒某个会议的抽取任务
     *
     * @param meetingId 会议ID
     * @param reInvite  是否可邀请已拒绝的专家
     * @author WendyYang
     **/
    public void notifyInviteTask(Long meetingId, boolean... reInvite) {
        boolean tmpReInvite = ArrayUtil.isEmpty(reInvite) || reInvite[0];
        if (!INVITE_TASK_MAP.containsKey(meetingId)) {
            if (addInviteTask(meetingId, false, properties.getInviteDelay(), tmpReInvite, LocalDateTime.now())) {
                log.info("重置会议的随机抽取状态：{}", meetingId);
                LambdaUpdateWrapper<Meeting> update = Wrappers.lambdaUpdate(Meeting.class);
                update.set(Meeting::getInviteStatus, false);
                update.eq(Meeting::getId, meetingId);
                meetingService.update(update);
                InviteCacheDTO cacheVal = InviteCacheDTO.of(meetingId, tmpReInvite, LocalDateTime.now());
                cachePlusOps.hSet(getCacheKey(meetingId), cacheVal);
            }
        }
    }

    /**
     * 添加专家抽取校验任务
     *
     * @param meetingId 会议ID
     * @param checked   是否前置校验
     * @param delayTime 延迟执行时间
     * @param reInvite  是否可以邀请被拒绝的专家
     * @param tsTime    任务启动时间
     * @return 是否添加任务成功
     * @author WendyYang
     **/
    private boolean addInviteTask(Long meetingId, boolean checked, int delayTime, boolean reInvite, LocalDateTime tsTime) {
        if (checked && !inviteCountCheck(meetingId)) {
            // 如果抽取数量满足直接返回
            return Boolean.FALSE;
        }
        Instant startTime = LocalDateTime.now().plusMinutes(delayTime).atZone(ZoneId.systemDefault()).toInstant();
        ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(() -> {
            ExpertRandomInviteTask bean = SpringContextHolder.getBean(ExpertRandomInviteTask.class);
            try {
                bean.invite(meetingId, reInvite, tsTime);
            } catch (Exception e) {
                log.error("执行专家邀请任务异常：{}", meetingId, e);
            }
        }, startTime, Duration.ofMinutes(properties.getInviteFixedRate()));
        INVITE_TASK_MAP.putIfAbsent(meetingId, future);
        log.info("添加专家抽取后台任务：{}", meetingId);
        return Boolean.TRUE;
    }

    /**
     * 创建会议时添加抽取任务
     *
     * @param meetingId 会议ID
     * @param tsTime    开始时间
     * @author WendyYang
     **/
    public void addInviteTaskByMeetingCreate(Long meetingId, LocalDateTime tsTime) {
        Assert.isTrue(properties.getEnable(), "随机邀请已关闭");
        addInviteTask(meetingId, false, properties.getInviteDelay(), false, tsTime);
        InviteCacheDTO cacheVal = InviteCacheDTO.of(meetingId, false, tsTime);
        cachePlusOps.hSet(getCacheKey(meetingId), cacheVal);
    }

    /**
     * 抽取过程
     *
     * @param meetingId 会议ID
     * @param reInvite  是否可以邀请已拒绝的专家
     * @param tsTime    任务开启时间
     */
    @Transactional(rollbackFor = Exception.class)
    public void invite(Long meetingId, Boolean reInvite, LocalDateTime tsTime) {
        log.info("开始进行专家后台抽取：{}", meetingId);
        Meeting meeting = meetingService.getById(meetingId);
        if (meeting.getStartTime().isBefore(LocalDateTime.now())) {
            log.info("会议已开始停止抽取：{}", meeting);
            cancelByMeetingId(meetingId);
            return;
        }
        // 随机邀请规则
        Map<Long, RandomInviteRuleDTO> ruleMap = inviteRuleService.randomRuleByMeetingId(meetingId);
        // 回避规则
        AvoidRuleDTO avoidRule = inviteAvoidRuleService.getAvoidInfo(meetingId);
        // 还需要抽取的规则数量
        AtomicInteger notIgnoreCnt = new AtomicInteger(ruleMap.size());
        AtomicInteger notSupportCnt = new AtomicInteger(0);
        LocalDateTime msTime = meeting.getStartTime();
        LocalDateTime meTime = meeting.getStartTime();
        ruleMap.forEach((ruleId, value) -> {
            List<MeetingExpert> tmpExperts = meetingExpertService.listExpertLastByMeetingId(meetingId);
            Map<Long, MeetingExpert> expertMap = CollUtils.listToMap(tmpExperts, MeetingExpert::getExpertId);
            // 统计通知中与同意参加专家数量
            Map<Long, ExpertCntBO> countMap = countByAttendStatus(expertMap);
            ExpertCntBO cnt = countMap.getOrDefault(ruleId, ExpertCntBO.zeroInit());
            int wouldAttendCnt = cnt.getAgreeCnt() + cnt.getNoticeCnt();
            if (wouldAttendCnt == value.getCount()) {
                if (cnt.getAgreeCnt().equals(value.getCount())) {
                    notIgnoreCnt.decrementAndGet();
                }
                return;
            }
            int needInviteCnt = value.getCount() - wouldAttendCnt;
            ExpertChooseDTO expertChoose = expertInviteManage.expertReplaceByRandomRule(avoidRule, value,
                    tmpExperts, needInviteCnt, msTime, meTime, tsTime, reInvite);

            if (expertChoose.getTotal() > 0) {
                List<MeetingExpert> expertMeetings = CollUtils.convert(expertChoose.getExperts(), w -> {
                    MeetingExpert expert = ExpertInviteBuilder.getExpertByRandom(meetingId, w, ruleId);
                    expert.setStatus(ExpertAttendStatusEnum.NOTICING.getCode());
                    return expert;
                });
                meetingCallOrMsgHelper.callExpertByMeeting(meeting, expertMeetings);
                log.info("会议：{} 后台抽取专家：{}名", meetingId, expertMeetings.size());
                meetingExpertService.saveBatch(expertMeetings);
            } else {
                // 抽取人数不够
                notSupportCnt.incrementAndGet();
            }
        });
        if (notIgnoreCnt.get() == 0 || notIgnoreCnt.get() == notSupportCnt.get()) {
            log.info("停止会议随机邀请：{} 未完成抽取规则数量 {} 无可抽取专家规则数量 {}", meetingId, notIgnoreCnt, notSupportCnt);
            currProxy().cancelByMeetingId(meetingId);
            meetingCallOrMsgHelper.sendInviteStopMsg(meeting.getCreateBy(), meetingId, meeting.getName());
        }
        if (notIgnoreCnt.get() == 0 && notSupportCnt.get() == 0) {
            MeetingManage meetingManage = SpringUtils.getBean(MeetingManage.class);
            ConfirmedRosterReq req = new ConfirmedRosterReq();
            req.setReconfirmed(false);
            req.setMeetingId(meetingId);
            meetingManage.confirmedRoster(req);
        }
    }

    @Transactional(rollbackFor = Exception.class)
    public void cancelByMeetingId(Long meetingId) {
        log.info("终止专家抽取：{}", meetingId);
        meetingService.stopRandomInvite(meetingId);
        cachePlusOps.hDel(getCacheKey(meetingId));
        ScheduledFuture<?> future = INVITE_TASK_MAP.get(meetingId);
        if (future != null) {
            INVITE_TASK_MAP.remove(meetingId);
            if (!future.isCancelled()) {
                future.cancel(true);
            }
        }
    }

    //==================================================================================================================

    private Map<Long, ExpertCntBO> countByAttendStatus(Map<Long, MeetingExpert> expertMap) {
        Map<Long, ExpertCntBO> cntMap = new HashMap<>(8);
        expertMap.values().forEach(w -> {
            Long ruleId = w.getRuleId();
            ExpertCntBO cnt = cntMap.computeIfAbsent(ruleId, k -> ExpertCntBO.zeroInit());
            if (ExpertAttendStatusEnum.AGREED.eq(w.getStatus())) {
                cnt.incrAgreeCnt();
            } else if (ExpertAttendStatusEnum.NOTICING.eq(w.getStatus())) {
                cnt.incrNoticeCnt();
            }
        });
        return cntMap;
    }

    @Data
    public static class ExpertCntBO {

        private Integer agreeCnt;

        private Integer noticeCnt;

        public ExpertCntBO(Integer agreeCnt, Integer noticeCnt) {
            this.agreeCnt = agreeCnt;
            this.noticeCnt = noticeCnt;
        }

        public void incrAgreeCnt() {
            agreeCnt++;
        }

        public void incrNoticeCnt() {
            noticeCnt++;
        }

        public static ExpertCntBO zeroInit() {
            return new ExpertCntBO(0, 0);
        }

    }

}
