package com.ningdatech.pmapi.meeting.task;

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.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.helper.YxtCallOrSmsHelper;
import com.ningdatech.pmapi.meeting.manage.ExpertInviteManage;
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 com.ningdatech.pmapi.user.util.LoginUserUtil;
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.beans.factory.annotation.Qualifier;
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.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;
import java.util.stream.Collectors;


/**
 * <p>
 * ExpertInviteTask
 * </p>
 *
 * @author WendyYang
 * @since 21:25 2022/8/15
 */
@Slf4j
@Component
@AllArgsConstructor
public class ExpertInviteTask {

    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;
    @Qualifier("expertInviteScheduler")
    @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 YxtCallOrSmsHelper yxtCallOrSmsHelper;

    /**
     * 用来存入线程执行情况, 方便于停止定时任务时使用
     */
    protected static final ConcurrentMap<Long, ScheduledFuture<?>> INVITE_MAP = new ConcurrentHashMap<>();

    public ExpertInviteTask currProxy() {
        return (ExpertInviteTask) AopContext.currentProxy();
    }

    private CacheHashKey getCacheKey(Long meetingId) {
        return new CacheHashKey(MEETING_ID_INVITE_RANDOM, meetingId, EXPIRE_TIME);
    }

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

    /**
     * 项目重启之后重新初始化邀请任务
     */
    private void initInviteTaskAfterAppStarted() {
        Map<Long, InviteCacheDTO> caches = cachePlusOps.hGetAll(getCacheKey(null));
        if (MapUtils.isEmpty(caches)) {
            log.info("暂无需要初始化的抽取会议信息");
            return;
        }
        for (InviteCacheDTO cache : caches.values()) {
            addInviteExpertTask(cache.getMeetingId(), true, properties.getInviteDelay(), cache.getInvitedRefused());
        }
    }

    /**
     * 专家抽取校验
     *
     * @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())
                .in(MeetingExpert::getStatus, ExpertAttendStatusEnum.AGREED.getCode());
        List<MeetingExpert> experts = meetingExpertService.list(query);
        if (experts.isEmpty()) {
            return Boolean.FALSE;
        }
        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.FALSE;
            }
        }
        return Boolean.TRUE;
    }

    /**
     * 唤醒某个会议的抽取任务
     *
     * @param meetingId 会议ID
     * @author WendyYang
     **/
    public void notifyInviteTask(Long meetingId) {
        if (!INVITE_MAP.containsKey(meetingId)) {
            addInviteExpertTask(meetingId, false, properties.getInviteDelay(), true);
            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, true);
            cachePlusOps.hSet(getCacheKey(meetingId), cacheVal);
        }
    }

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

    /**
     * 创建会议时添加抽取任务
     *
     * @param meetingId      会议ID
     * @param delayedMinutes 延迟时间
     * @author WendyYang
     **/
    public void addInviteExpertTaskByMeetingCreate(Long meetingId, int delayedMinutes) {
        Assert.isTrue(properties.getEnable(), "随机邀请已关闭");
        addInviteExpertTask(meetingId, false, delayedMinutes, false);
        InviteCacheDTO cacheVal = InviteCacheDTO.of(meetingId, false);
        cachePlusOps.hSet(getCacheKey(meetingId), cacheVal);
    }

    @Transactional(rollbackFor = Exception.class)
    public void invite(Long meetingId, Boolean invitedRefused) {
        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.getAvoidInfoDto(meetingId);
        // 还需要抽取的规则数量
        AtomicInteger notIgnoreCnt = new AtomicInteger(ruleMap.size());
        AtomicInteger notSupportCnt = new AtomicInteger(0);
        ruleMap.forEach((ruleId, value) -> {
            List<MeetingExpert> tmpExperts = meetingExpertService.listExpertLastByMeetingId(meetingId);
            Map<Long, MeetingExpert> expertMap = CollUtils.listToMap(tmpExperts, MeetingExpert::getExpertId);
            // 统计通知中与同意参加专家数量
            Map<Long, ExpertCntBO> countMap = countByAgree(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, meeting.getStartTime(), meeting.getEndTime(), invitedRefused);

            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;
                });
                yxtCallOrSmsHelper.callByMeetingExperts(meeting, expertMeetings);
                log.info("会议：{} 后台抽取专家：{}名", meetingId, expertMeetings.size());
                meetingExpertService.saveBatch(expertMeetings);
            } else {
                // 抽取人数不够
                notSupportCnt.incrementAndGet();
            }
        });
        if (notIgnoreCnt.get() == 0 || notIgnoreCnt.get() == notSupportCnt.get()) {
            if (notSupportCnt.get() > 0) {
                // TODO
                /*UserInfo inviterBasic = userInfoService.getById(meeting.getCreateBy());
                UserInfo inviter = userInfoService.getById(inviterBasic.getId());
                SendSmsContext context = YxtSmsContextBuilder.smsByRandomInviteStop(inviter.getNickname(), meeting.getName(), inviterBasic.getPhoneNo());
                yxtCallOrSmsHelper.sendSms(context);*/
            }
            log.info("停止会议随机邀请：{} 未完成抽取规则数量 {} 无可抽取专家规则数量 {}", meetingId, notIgnoreCnt, notSupportCnt);
            currProxy().cancelByMeetingId(meetingId);
        }
    }

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

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

    private Map<Long, ExpertCntBO> countByAgree(Map<Long, MeetingExpert> expertMap) {
        return expertMap.entrySet().stream()
                .collect(Collectors.groupingBy(w -> w.getValue().getRuleId(),
                        Collectors.collectingAndThen(Collectors.mapping(Map.Entry::getValue, Collectors.toList()), w -> {
                            ExpertCntBO cnt = ExpertCntBO.zeroInit();
                            for (MeetingExpert expert : w) {
                                if (ExpertAttendStatusEnum.AGREED.eq(expert.getStatus())) {
                                    cnt.incrAgreeCnt();
                                } else if (ExpertAttendStatusEnum.NOTICING.eq(expert.getStatus())) {
                                    cnt.incrNoticeCnt();
                                }
                            }
                            return cnt;
                        })));
    }

    @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);
        }

    }

}
