package com.xxl.mq.admin.conf;

import com.xxl.mq.admin.core.model.XxlCommonRegistryData;
import com.xxl.mq.admin.core.model.XxlMqTopic;
import com.xxl.mq.admin.dao.IXxlMqMessageDao;
import com.xxl.mq.admin.dao.IXxlMqTopicDao;
import com.xxl.mq.admin.service.IXxlMqTopicService;
import com.xxl.mq.admin.service.impl.XxlCommonRegistryServiceImpl;
import com.xxl.mq.client.broker.IXxlMqBroker;
import com.xxl.mq.client.message.XxlMqMessage;
import com.xxl.mq.client.message.XxlMqMessageStatus;
import com.xxl.mq.client.util.LogHelper;
import com.xxl.rpc.remoting.net.NetEnum;
import com.xxl.rpc.remoting.provider.XxlRpcProviderFactory;
import com.xxl.rpc.serialize.Serializer;
import com.xxl.rpc.util.IpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.mail.internet.MimeMessage;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;

/**
 * Created by xuxueli on 16/8/28.
 */
@Component
public class XxlMqBrokerImpl implements IXxlMqBroker, InitializingBean, DisposableBean {
    private final static Logger logger = LoggerFactory.getLogger(XxlMqBrokerImpl.class);


    // ---------------------- param ----------------------


    @Value("${xxl-mq.rpc.remoting.ip}")
    private String ip;

    @Value("${xxl-mq.rpc.remoting.port}")
    private int port;

    @Value("${xxl.mq.log.logretentiondays}")
    private int logretentiondays;


    @Resource
    private IXxlMqMessageDao xxlMqMessageDao;
    @Resource
    private IXxlMqTopicService xxlMqTopicService;
    @Resource
    private IXxlMqTopicDao xxlMqTopicDao;

    @Resource
    private JavaMailSender mailSender;
    @Value("${spring.mail.username}")
    private String emailUserName;


    // ---------------------- broker server ----------------------

    @Override
    public void afterPropertiesSet() throws Exception {
        // init server
        initServer();

        // init thread
        initThead();
    }

    @Override
    public void destroy() throws Exception {

        // destory server
        destoryServer();

        // destory thread
        destroyThread();
    }

    // ---------------------- broker thread ----------------------

    private LinkedBlockingQueue<XxlMqMessage> newMessageQueue = new LinkedBlockingQueue<XxlMqMessage>();
    private LinkedBlockingQueue<XxlMqMessage> callbackMessageQueue = new LinkedBlockingQueue<XxlMqMessage>();
    private Map<String, Long> alarmMessageInfo = new ConcurrentHashMap<String, Long>();

    private ExecutorService executorService = Executors.newCachedThreadPool();
    private volatile boolean executorStoped = false;

    public void initThead() throws Exception {

        /**
         * async save message, mult thread  (by event)
         */
        for (int i = 0; i < 3; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    while (!executorStoped) {
                        try {
                            XxlMqMessage message = newMessageQueue.take();
                            if (message != null) {
                                // load
                                List<XxlMqMessage> messageList = new ArrayList<>();
                                messageList.add(message);

                                List<XxlMqMessage> otherMessageList = new ArrayList<>();
                                int drainToNum = newMessageQueue.drainTo(otherMessageList, 100);
                                if (drainToNum > 0) {
                                    messageList.addAll(otherMessageList);
                                }

                                // save
                                xxlMqMessageDao.save(messageList);
                            }
                        } catch (Exception e) {
                            if (!executorStoped) {
                                logger.error(e.getMessage(), e);
                            }
                        }
                    }

                    // end save
                    List<XxlMqMessage> otherMessageList = new ArrayList<>();
                    int drainToNum = newMessageQueue.drainTo(otherMessageList);
                    if (drainToNum> 0) {
                        xxlMqMessageDao.save(otherMessageList);
                    }

                }
            });
        }

        /**
         * async callback message, mult thread  (by event)
         */
        for (int i = 0; i < 3; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    while (!executorStoped) {
                        try {
                            XxlMqMessage message = callbackMessageQueue.take();
                            if (message!=null) {
                                // load
                                List<XxlMqMessage> messageList = new ArrayList<>();
                                messageList.add(message);

                                List<XxlMqMessage> otherMessageList = new ArrayList<>();
                                int drainToNum = callbackMessageQueue.drainTo(otherMessageList, 100);
                                if (drainToNum > 0) {
                                    messageList.addAll(otherMessageList);
                                }

                                // save
                                xxlMqMessageDao.updateStatus(messageList);

                                // fill alarm info
                                for (XxlMqMessage alarmItem: messageList) {
                                    if (XxlMqMessageStatus.FAIL.name().equals(alarmItem.getStatus())) {
                                        Long failCount = alarmMessageInfo.get(alarmItem.getTopic());
                                        failCount = failCount!=null?failCount++:1;
                                        alarmMessageInfo.put(alarmItem.getTopic(), failCount);
                                    }
                                }

                            }

                        } catch (Exception e) {
                            if (!executorStoped) {
                                logger.error(e.getMessage(), e);
                            }
                        }
                    }

                    // end save
                    List<XxlMqMessage> otherMessageList = new ArrayList<>();
                    int drainToNum = callbackMessageQueue.drainTo(otherMessageList);
                    if (drainToNum > 0) {
                        xxlMqMessageDao.updateStatus(otherMessageList);
                    }

                }
            });
        }


        /**
         * auto retry message "retryCount-1 + status change"  (by cycle, 1/60s)
         *
         * auto reset block timeout message "check block + status change"  (by cycle, 1/60s)
         */
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                while (!executorStoped) {
                    try {
                        // mult retry message
                        String appendLog = LogHelper.makeLog("失败重试", "状态自动还原,剩余重试次数减一");
                        int count = xxlMqMessageDao.updateRetryCount(XxlMqMessageStatus.FAIL.name(), XxlMqMessageStatus.NEW.name(), appendLog);
                        if (count > 0) {
                            logger.info("xxl-mq, retry message, count:{}", count);
                        }
                    } catch (Exception e) {
                        if (!executorStoped) {
                            logger.error(e.getMessage(), e);
                        }
                    }
                    try {
                        // mult reset block message
                        String appendLog = LogHelper.makeLog("阻塞清理", "状态自动标记失败");
                        int count = xxlMqMessageDao.resetBlockTimeoutMessage(XxlMqMessageStatus.RUNNING.name(), XxlMqMessageStatus.FAIL.name(), appendLog);
                        if (count > 0) {
                            logger.info("xxl-mq, retry block message, count:{}", count);
                        }
                    } catch (Exception e) {
                        if (!executorStoped) {
                            logger.error(e.getMessage(), e);
                        }
                    }
                    try {
                        // sleep
                        TimeUnit.SECONDS.sleep(60);
                    } catch (Exception e) {
                        if (!executorStoped) {
                            logger.error(e.getMessage(), e);
                        }
                    }
                }
            }
        });

        /**
         * auto alarm "check topic fail count, send alarm"  (by cycle, 1/60s)
         */
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                while (!executorStoped) {
                    try {
                        // mult send alarm
                        if (alarmMessageInfo.size() > 0) {

                            // copy
                            Map<String, Long> alarmMessageInfoTmp = new HashMap<String, Long>();
                            alarmMessageInfoTmp.putAll(alarmMessageInfo);
                            alarmMessageInfo.clear();

                            // alarm
                            List<XxlMqTopic> topicList = xxlMqTopicDao.findAlarmByTopic(new ArrayList<String>(alarmMessageInfoTmp.keySet()));
                            if (topicList!=null && topicList.size()>0) {
                                for (XxlMqTopic mqTopic: topicList) {
                                    if (mqTopic.getAlarmEmails()!=null && mqTopic.getAlarmEmails().trim().length()>0) {
                                        Long failCount = alarmMessageInfoTmp.get(mqTopic.getTopic());

                                        String[] toEmailList = null;
                                        if (mqTopic.getAlarmEmails().contains(",")) {
                                            toEmailList = mqTopic.getAlarmEmails().split(",");
                                        } else {
                                            toEmailList = new String[]{mqTopic.getAlarmEmails()};
                                        }
                                        String emailContent = MessageFormat.format("告警类型:消息失败;<br>Topic:{0};<br>备注:{1}",
                                                mqTopic.getTopic(), "1min内失败消息数量=" + failCount);

                                        // make mail
                                        try {
                                            MimeMessage mimeMessage = mailSender.createMimeMessage();

                                            MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
                                            helper.setFrom(emailUserName, "分布式消息队列XXL-MQ");
                                            helper.setTo(toEmailList);
                                            helper.setSubject("消息队列中心监控报警");
                                            helper.setText(emailContent, true);

                                            mailSender.send(mimeMessage);
                                        } catch (Exception e) {
                                            logger.error(">>>>>>>>>>> message monitor alarm email send error, topic:{}, failCount:{}", mqTopic.getTopic(), failCount);
                                        }


                                        // TODO, custom alarm strategy, such as sms


                                    }
                                }
                            }
                        }
                    } catch (Exception e) {
                        if (!executorStoped) {
                            logger.error(e.getMessage(), e);
                        }
                    }
                    try {
                        // sleep
                        TimeUnit.MINUTES.sleep(1);
                    } catch (Exception e) {
                        if (!executorStoped) {
                            logger.error(e.getMessage(), e);
                        }
                    }
                }
            }
        });

        /**
         * auto clean success message  (by cycle, 1/>=3day)
         */
        if (logretentiondays >= 3) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    while (!executorStoped) {
                        try {
                            int count = xxlMqMessageDao.cleanSuccessMessage(XxlMqMessageStatus.SUCCESS.name(), logretentiondays);
                            logger.info("xxl-mq, clean success message, count:{}", count);
                        } catch (Exception e) {
                            if (!executorStoped) {
                                logger.error(e.getMessage(), e);
                            }
                        }
                        try {
                            TimeUnit.DAYS.sleep(logretentiondays);
                        } catch (Exception e) {
                            if (!executorStoped) {
                                logger.error(e.getMessage(), e);
                            }
                        }
                    }
                }
            });
        }

        /**
         * auto find new topic from message  (by cycle, 1+N/1min)
         */
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                while (!executorStoped) {
                    try {
                        // find new topic, set messageInfo
                        List<String> topicList = xxlMqMessageDao.findNewTopicList();
                        if (topicList!=null && topicList.size()>0) {
                            for (String topic:topicList) {
                                XxlMqTopic newTopic = new XxlMqTopic();
                                newTopic.setTopic(topic);
                                xxlMqTopicService.add(newTopic);
                            }
                        }

                    } catch (Exception e) {
                        if (!executorStoped) {
                            logger.error(e.getMessage(), e);
                        }
                    }
                    try {
                        TimeUnit.MINUTES.sleep(1);
                    } catch (Exception e) {
                        if (!executorStoped) {
                            logger.error(e.getMessage(), e);
                        }
                    }
                }
            }
        });

    }
    public void destroyThread(){
        executorStoped = true;
        executorService.shutdownNow();
    }


    // ---------------------- broker server ----------------------

    private XxlRpcProviderFactory providerFactory;

    public void initServer() throws Exception {

        // address, static registry
        ip = (ip!=null&&ip.trim().length()>0)?ip:IpUtil.getIp();
        String address = IpUtil.getIpPort(ip, port);

        XxlCommonRegistryData xxlCommonRegistryData = new XxlCommonRegistryData();
        xxlCommonRegistryData.setKey(IXxlMqBroker.class.getName());
        xxlCommonRegistryData.setValue(address);
        XxlCommonRegistryServiceImpl.staticRegistryData = xxlCommonRegistryData;


        // init server
        providerFactory = new XxlRpcProviderFactory();
        providerFactory.initConfig(NetEnum.NETTY, Serializer.SerializeEnum.HESSIAN.getSerializer(), ip, port, null, null, null);

        // add server
        providerFactory.addService(IXxlMqBroker.class.getName(), null, this);

        // start server
        providerFactory.start();
    }

    public void destoryServer() throws Exception {
        // stop server
        if (providerFactory != null) {
            providerFactory.stop();
        }
    }


    // ---------------------- broker api ----------------------

    @Override
    public int addMessages(List<XxlMqMessage> messages) {
        newMessageQueue.addAll(messages);
        return messages.size();
    }

    @Override
    public List<XxlMqMessage> pullNewMessage(String topic, String group, int consumerRank, int consumerTotal, int pagesize) {
        List<XxlMqMessage> list = xxlMqMessageDao.pullNewMessage(XxlMqMessageStatus.NEW.name(), topic, group, consumerRank, consumerTotal, pagesize);
        return list;
    }

    @Override
    public int lockMessage(long id, String appendLog) {
        return xxlMqMessageDao.lockMessage(id, appendLog, XxlMqMessageStatus.NEW.name(), XxlMqMessageStatus.RUNNING.name());
    }

    @Override
    public int callbackMessages(List<XxlMqMessage> messages) {
        callbackMessageQueue.addAll(messages);
        return messages.size();
    }

}