package com.ruoyi.project.common.service.impl;

import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.project.common.domain.Message;
import com.ruoyi.project.common.domain.MessageUser;
import com.ruoyi.project.common.domain.MessageVo;
import com.ruoyi.project.common.dto.MessageRes;
import com.ruoyi.project.common.mapper.MessageMapper;
import com.ruoyi.project.common.mapper.MessageUserMapper;
import com.ruoyi.project.common.service.ISseEmitterService;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * @author lidl
 */
@Service
public class SseEmitterServiceImpl implements ISseEmitterService {
    private static final Logger log = LoggerFactory.getLogger(SseEmitterServiceImpl.class);
    /**
     * 在配置文件中设置连接超时时间
     */
    @Value("${sse.timeOut}")
    public Long sseTimeOut;

    @Resource
    private MessageMapper messageMapper;
    @Resource
    private MessageUserMapper messageUserMapper;


    /**
     * 容器，保存连接，用于输出返回 ;可使用其他方法实现
     */
    private static final Map<String, SseEmitter> sseCache = new ConcurrentHashMap<>();

    private static final Map<String, SseEmitter> sseChatCache = new ConcurrentHashMap<>();


    @Override
    public SseEmitter createConnect(String clientId, String lastEventId) {
        // 设置超时时间，0表示不过期。默认30秒，超过时间未完成会抛出异常：AsyncRequestTimeoutException
        SseEmitter sseEmitter = new SseEmitter(sseTimeOut);
        // 是否需要给客户端推送ID
        if (StringUtils.isBlank(clientId)) {
            clientId = IdUtils.simpleUUID();
        }
        // 注册回调
        // 长链接完成后回调接口(即关闭连接时调用)
        sseEmitter.onCompletion(completionCallBack(clientId));
        // 连接超时回调
        sseEmitter.onTimeout(timeoutCallBack(clientId));
        // 推送消息异常时，回调方法
        sseEmitter.onError(errorCallBack(clientId, false));
        // 将连接保存在容器中,后续发送消息从容器中获取连接发送
        sseCache.put(clientId, sseEmitter);
        log.info("创建新的sse连接，当前用户：{}    累计用户:{}", clientId, sseCache.size());
        // 注册成功,获取当前登录人所有的未读消息
        if (StringUtils.isBlank(lastEventId)) {
            // 处理需要发送的消息数据,boolean值用于前段消息展示是否弹出,true会弹出展示,false不会弹出展示
            sendMessageToUserId(SecurityUtils.getLoginUser().getUserId(), true);
        } else {
            sendMessageToUserId(SecurityUtils.getLoginUser().getUserId(), false);
        }
        return sseEmitter;
    }


    /**
     * @Author: zhaoby
     * @DATE: 2023/11/29 13:37:01
     * @Description: 根据业务类型推送未读消息数据
     */
    @Override
    @Async
    public void sendMessageToUserId(Long clientId, boolean popUpFlag) {
        try {
            long id = SecurityUtils.getUserId();
            // 入参的参数为空,默认不发送消息
            if (ObjectUtils.isEmpty(clientId)) {
                return;
            }
            //查询所有未读消息
            MessageUser param = new MessageUser();
            param.setUserId(clientId);
            param.setReadStatus("0");//未读
            List<MessageRes> totalNotice = messageUserMapper.selectMessageContantList(param);
            //查询未读聊天
            //todo
            if (ObjectUtils.isEmpty(totalNotice)) {
                //不存在未读消息 直接返回
                return;
            }
            // 用来整合三个模块的消息数据
            MessageVo messageVo = new MessageVo();

            Map<String, Object> messageMap = new HashMap<>();
            MessageRes messageRes = totalNotice.get(0);
            messageMap.put("type", messageRes.getMessageType());
            //messageMap.put("msg", totalNotice.get(0).getMessageContent());
            messageMap.put("msg", messageRes.getMessageContent());
            messageMap.put("msgRU", messageRes.getMessageContentRU());
            messageMap.put("msgUS", messageRes.getMessageContentUS());
            messageMap.put("msgES", messageRes.getMessageContentES());
            messageMap.put("time", DateUtils.CNTimeStr(messageRes.getCreateTime(), SecurityUtils.getTimeZone()));
            messageMap.put("msgTotal", totalNotice.size());
            messageVo.setData(messageMap);
            messageVo.setTotalQty((long) totalNotice.size());
            messageVo.setPopUp(popUpFlag);
            // 获取连接中所有user为当前id的连接
            // 前端再创建连接的时候所用的id是由  UserId+时间戳 的格式创建的,这里会根据userid获取所有的连接对所有的连接进行发送消息的操作
            // 可以根据实际情况进行修改,只要clientId和创建连接的时候保持一致即可发送消息
            List<String> clientIdList = sseCache.entrySet().stream()
                    .filter(entry -> entry.getKey().contains(String.valueOf(clientId + "_")))
                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)).keySet().stream().collect(Collectors.toList());
            Collections.sort(clientIdList, Collections.reverseOrder());
            for (String tempClientId : clientIdList) {
                messageVo.setClientId(tempClientId);
                sendMsgToClientByClientId(tempClientId, messageVo, sseCache.get(tempClientId));
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        // sendMessageToOneClient(clientId.toString(),messageVoList);
    }

    /**
     * 推送消息到客户端
     * 此处做了推送失败后，重试推送机制，可根据自己业务进行修改
     *
     * @param clientId  客户端ID
     * @param messageVo 推送信息，此处结合具体业务，定义自己的返回值即可
     **/
    @Async
    public void sendMsgToClientByClientId(String clientId, MessageVo messageVo, SseEmitter sseEmitter) {
        if (sseEmitter == null) {
            log.error("推送消息失败：客户端{}未创建长链接,失败消息:{}",
                    clientId, messageVo.toString());
            // 如果长连接不存在,由于SSE是由客户端向服务端发起请求创建链接的,所以,服务端并不知道链接是否存在,
            // 这里是如果链接不存在的话,在容器中将当前链接删除
            removeUser(clientId);
            return;
        }
        // 整合要发送消息内容数据,放到链接内
        SseEmitter.SseEventBuilder sendData = SseEmitter.event().id(clientId)
                .data(messageVo, MediaType.APPLICATION_JSON);
        try {
            // 最终发送消息的方法
            sseEmitter.send(sendData);
        } catch (Exception e) {
            /**
             * 这里通过捕捉发送消息一场的凡是判断是否发送成功,
             * 如果由异常产生,则视为发送失败,可做重试操作,或者其他发送失败的异常处理逻辑
             */
            // 推送消息失败，记录错误日志，进行重推
            log.error("推送消息失败：{},尝试进行重推", messageVo.toString());
            boolean isSuccess = true;
            // 推送消息失败后，每隔10s推送一次，推送5次
            for (int i = 0; i < 1; i++) {
                try {
                    // Thread.sleep(1000);
                    sseEmitter = sseCache.get(clientId);
                    if (sseEmitter == null) {
                        log.error("{}的第{}次消息重推失败，未创建长链接", clientId, i + 1);
                        removeUser(clientId);
                        return;
                    }
                    sseEmitter.send(sendData);
                } catch (Exception ex) {
                    log.error("{}的第{}次消息重推失败", clientId, i + 1, ex);
                    isSuccess = false;
                    continue;
                }
                log.info("{}的第{}次消息重推成功,{}", clientId, i + 1, messageVo.toString());
                isSuccess = true;
            }
            if (!isSuccess) {
                removeUser(clientId);
            }
        }
    }


    /**
     * 长链接完成后回调接口(即关闭连接时调用)
     *
     * @param clientId 客户端ID
     **/
    private Runnable completionCallBack(String clientId) {
        return () -> {
            log.info("结束连接：{}", clientId);
            // 由于SSE长连接只能由客户端发起连接 服务端不能主动与客户端建立连接所以在服务端内如果关闭连接做删除连接操作
            removeUser(clientId);
        };
    }

    /**
     * 连接超时时调用
     *
     * @param clientId 客户端ID
     **/
    private Runnable timeoutCallBack(String clientId) {
        return () -> {
            log.info("连接超时：{}", clientId);
            // 由于SSE长连接只能由客户端发起连接 服务端不能主动与客户端建立连接所以在服务端内如果连接超时仅做删除连接操作
            removeUser(clientId);
        };
    }


    /**
     * 推送消息异常时，回调方法
     *
     * @param clientId 客户端ID
     **/
    private Consumer<Throwable> errorCallBack(String clientId, boolean removeFlag) {
        return throwable -> {
            log.error("SseEmitterServiceImpl[errorCallBack]：连接异常,客户端ID:{}", clientId);

            // 推送消息失败后，这里可以根据业务需求设置重发逻辑,重发前提是客户端与服务端长连接存在,如果不存在可添加删除连接逻辑
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(5000);
                    SseEmitter sseEmitter = sseCache.get(clientId);
                    if (sseEmitter == null) {
                        log.error("SseEmitterServiceImpl[errorCallBack]：第{}次消息重推失败,未获取到 {} 对应的长链接", i + 1, clientId);
                        continue;
                    }
                    sseEmitter.send("失败后重新推送");
                } catch (Exception e) {
                    if (i == 4) {
                        if (removeFlag) {
                            log.error("SseEmitterServiceImpl[errorCallBack]：第{}次聊天室重新创建失败,{} 对应的长链接已删除", i + 1, clientId);
                            removeUser(clientId);
                        }
                    }

                    e.printStackTrace();
                }
            }
        };
    }

    /**
     * 移除用户连接
     *
     * @param clientId 客户端ID
     **/
    private void removeUser(String clientId) {
        sseCache.remove(clientId);
        log.info("SseEmitterServiceImpl[removeUser]:移除用户：{}", clientId);
    }

    private void removeChatUser(String clientId) {
        sseChatCache.remove(clientId);
        log.info("SseEmitterServiceImpl[removeChatUser]:移除用户：{}", clientId);
    }


    /**
     * 关闭连接
     *
     * @param clientId 客户端ID
     */
    @Override
    public void closeConnect(String clientId) {
        SseEmitter sseEmitter = sseCache.get(clientId);
        if (sseEmitter != null) {
            sseEmitter.complete();
            removeUser(clientId);
        }
    }

    @Override
    public void sendMessageToUserIdList(List<Long> clientIdList, boolean popUpFlag) {
        for (Long clientId : clientIdList) {
            sendMessageToUserId(clientId, popUpFlag);
        }
    }

    /**
     * 给指定客户端发送消息
     *
     * @param clientId 客户端ID
     * @param msg      消息内容
     */
    @Override
    public void sendMessageToOneClient(String clientId, Object msg) {
        MessageVo messageVo = new MessageVo();
        messageVo.setClientId(clientId);
        messageVo.setData(msg);
        sendMsgToClientByClientId(clientId, messageVo, sseCache.get(clientId));
    }


    /**
     * 发送消息给所有客户端
     *
     * @param msg 消息内容
     */
    @Override
    public void sendMessageToAllClient(String msg) {
        if (ObjectUtils.isEmpty(sseCache)) {
            return;
        }
        // 判断发送的消息是否为空
        for (Map.Entry<String, SseEmitter> entry : sseCache.entrySet()) {
            MessageVo messageVo = new MessageVo();
            messageVo.setClientId(entry.getKey());
            messageVo.setData(msg);
            sendMsgToClientByClientId(entry.getKey(), messageVo, entry.getValue());
        }

    }

    /**
     * 根据客户端id获取SseEmitter对象
     *
     * @param clientId 客户端ID
     */
    @Override
    public SseEmitter getSseEmitterByClientId(String clientId) {
        return sseCache.get(clientId);
    }


    /**
     * @Author: xiewd
     * @DATE: 2023/11/29 13:37:01
     * @Description: 根据userId，新增消息实体并推送消息到客户端
     */
    @Override
    @Async
    public String sendSaveMessage(List<Long> userIds, Message message) {
        boolean popUpFlag = true;
        String res = "";
        try {
            //long id = SecurityUtils.getUserId();
            // 入参的参数为空,默认不发送消息
            if (ObjectUtils.isEmpty(userIds)) {
                return res;
            }
            //首先新增消息表
            message.setPushTime(new Date());
            message.setPushStatus("1");
            if (ObjectUtils.isEmpty(message.getCreateBy())) {
                message.setCreateBy(SecurityUtils.getUserId() + "");
            }
            message.setCreateTime(new Date());
            messageMapper.insertMessage(message);
            //在增加消息关系表

            List<MessageUser> userMsg = new ArrayList<>();
            for (Long userId : userIds) {
                MessageUser user = new MessageUser();
                user.setMessageId(message.getId());
                user.setUserId(userId);
                if (ObjectUtils.isEmpty(message.getCreateBy())) {
                    user.setCreateBy(SecurityUtils.getUserId() + "");
                }
                user.setCreateTime(new Date());
                user.setRemark("消息推送");
                userMsg.add(user);
            }
            messageUserMapper.InsertBySQL(userMsg);
            // 用来整合消息数据 推送
            MessageVo messageVo = new MessageVo();
            Map<String, Object> resFormMap = new HashMap<>();
            resFormMap.put("type", message.getMessageType());
            resFormMap.put("msg", message.getMessageContent());
            resFormMap.put("msgRU", message.getMessageContentRU());
            resFormMap.put("msgUS", message.getMessageContentUS());
            resFormMap.put("msgES", message.getMessageContentES());

            resFormMap.put("time", DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", message.getCreateTime()));

            // 获取连接中所有user为当前id的连接
            // 前端再创建连接的时候所用的id是由  UserId+时间戳 的格式创建的,这里会根据userid获取所有的连接对所有的连接进行发送消息的操作
            // 可以根据实际情况进行修改,只要clientId和创建连接的时候保持一致即可发送消息
            for (Long clientId : userIds) {
                //查询每个人所有未读消息
                Long total = messageUserMapper.selectUnReadById(clientId);
                resFormMap.put("msgTotal", total);
                messageVo.setData(resFormMap);
                messageVo.setTotalQty(total);
                messageVo.setPopUp(popUpFlag);

                List<String> clientIdList = sseCache.entrySet().stream()
                        .filter(entry -> entry.getKey().contains(String.valueOf(clientId + "_")))
                        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)).keySet().stream().collect(Collectors.toList());
                Collections.sort(clientIdList, Collections.reverseOrder());
                for (String tempClientId : clientIdList) {
                    messageVo.setClientId(tempClientId);
                    sendMsgToClientByClientId(tempClientId, messageVo, sseCache.get(tempClientId));
                }
            }

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return res;
        // sendMessageToOneClient(clientId.toString(),messageVoList);
    }
}
