package org.springblade.common.notify.message;

import com.aliyun.openservices.ons.api.Message;
import com.aliyun.openservices.ons.api.OnExceptionContext;
import com.aliyun.openservices.ons.api.Producer;
import com.aliyun.openservices.ons.api.PropertyKeyConst;
import com.aliyun.openservices.ons.api.SendCallback;
import com.aliyun.openservices.ons.api.SendResult;
import org.springblade.common.notify.constant.NotifyErrorConstants;
import com.wn.common.validator.Validator;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;

/**
 * @author caiyihua
 */
@Slf4j
//@Component
@ConditionalOnProperty(name = "ali.mq.producer.support.flag", havingValue = "true", matchIfMissing = false)
public class AliMqProducer {
    @Value("${ali.mq.new.version:false}")
    @Setter
    private Boolean newVersion;
    @Value("${ali.mq.namesrv.addr:http://onsaddr.mq-internet-access.mq-internet.aliyuncs.com:80}")
    @Setter
    private String nameServer;
    @Value("${ali.mq.producer.id}")
    @Setter
    private String producerId;
    @Value("${ali.mq.access.key}")
    @Setter
    private String accessKey;
    @Value("${ali.mq.secret.key}")
    @Setter
    private String secretKey;
    @Value("${ali.mq.producer.pool.max.total:100}")
    @Setter
    private Integer maxTotal;
    @Value("${ali.mq.producer.pool.max.idle:10}")
    @Setter
    private Integer maxIdle;
    @Value("${ali.mq.producer.pool.min.idle:0}")
    @Setter
    private Integer minIdle;
    @Value("${ali.mq.producer.pool.max.wait.millis:5000}")
    @Setter
    private Integer maxWaitMillis;
    @Value("${ali.mq.producer.pool.min.evictable.idle.time.millis:600000}")
    @Setter
    private Integer minEvictableIdleTimeMillis;
    @Value("${ali.mq.producer.pool.time.between.eviction.runs.millis:3000}")
    @Setter
    private Integer timeBetweenEvictionRunsMillis;
    @Value("${ali.mq.producer.pool.num.tests.per.eviction.run:10}")
    @Setter
    private Integer numTestsPerEvictionRun;

    private Map<String, ProducerPool> producerMap;

    @PostConstruct
    private void init() {
        Validator.notEmpty(producerId, NotifyErrorConstants.MQ_MESSAGE_PARMETER_ERROR);
        producerMap = new ConcurrentHashMap<>();
        String[] producerIdList = producerId.split(",");
        for (String producerIdStr : producerIdList) {
            Properties properties = new Properties();
            // AccessKey 阿里云身份验证，在阿里云服务器管理控制台创建
            properties.put(PropertyKeyConst.AccessKey, accessKey);
            // SecretKey 阿里云身份验证，在阿里云服务器管理控制台创建
            properties.put(PropertyKeyConst.SecretKey, secretKey);
            if (newVersion) {
                // 您在控制台创建的 Group ID
                properties.put(PropertyKeyConst.GROUP_ID, producerIdStr);
                // 设置 TCP 接入域名（此处以公共云生产环境为例）
                properties.put(PropertyKeyConst.NAMESRV_ADDR, nameServer);
            } else {
                // 您在控制台创建的 Producer ID
                properties.put(PropertyKeyConst.ProducerId, producerIdStr);
                // 设置 TCP 接入域名（此处以公共云生产环境为例）
                properties.put(PropertyKeyConst.ONSAddr,
                        "http://onsaddr-internet.aliyun.com/rocketmq/nsaddr4client-internet");
            }

            GenericObjectPoolConfig config = new GenericObjectPoolConfig();
            // 整个池最大值
            config.setMaxTotal(maxTotal);
            // 最大空闲
            config.setMaxIdle(maxIdle);
            // 最小空闲
            config.setMinIdle(minIdle);
            // 最大等待时间，-1表示一直等
            config.setMaxWaitMillis(maxWaitMillis);
            // 当对象池没有空闲对象时，新的获取对象的请求是否阻塞。true阻塞。默认值是true
            config.setBlockWhenExhausted(true);
            // 在从对象池获取对象时是否检测对象有效，true是；默认值是false
            config.setTestOnBorrow(false);
            // 在向对象池中归还对象时是否检测对象有效，true是，默认值是false
            config.setTestOnReturn(false);
            // 在检测空闲对象线程检测到对象不需要移除时，是否检测对象的有效性。true是，默认值是false
            config.setTestWhileIdle(false);
            // 可发呆的时间, 10mins
            config.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
            // 发呆过长移除的时候是否test一下先
            config.setTestWhileIdle(true);
            // 回收资源线程的执行周期 3s
            config.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
            config.setNumTestsPerEvictionRun(numTestsPerEvictionRun);

            ProducerPool producerPool = new ProducerPool(properties, config);
            log.info("ALi Producer Pool for the producer ID {} started!", producerIdStr);
            producerMap.put(producerIdStr, producerPool);
        }
    }

    /**
     * 同步发送消息
     *
     * @param producerId
     * @param topic            请注意topic应该和producerId配置一致
     * @param tag
     * @param messageBody
     * @param userProperties
     * @param startDeliverTime 延迟发送时间，是时间戳
     * @return
     */
    @Retryable(value = { Exception.class }, maxAttempts = 10, backoff = @Backoff(delay = 10000L, multiplier = 2))
    public SendResult sendMessage(String producerId, String topic, String tag, String messageBody,
            Properties userProperties, Long startDeliverTime) {
        Validator.notEmpty(topic, NotifyErrorConstants.MQ_MESSAGE_PARMETER_ERROR);
        Validator.notEmpty(tag, NotifyErrorConstants.MQ_MESSAGE_PARMETER_ERROR);

        Message msg = new Message(
                // 您在控制台创建的 Topic
                topic,
                // Message Tag, 可理解为 Gmail 中的标签，对消息进行再归类，方便 Consumer 指定过滤条件在 MQ 服务器过滤
                tag,
                // Message Body 可以是任何二进制形式的数据， MQ 不做任何干预，需要 Producer 与 Consumer 协商好一致的序列化和反序列化方式
                messageBody.getBytes(StandardCharsets.UTF_8));
        // 设置代表消息的业务关键属性，请尽可能全局唯一。
        // 以方便您在无法正常收到消息情况下，可通过 MQ 控制台查询消息并补发。
        // 注意：不设置也不会影响消息正常收发
        msg.setKey(topic + "_" + tag + "_" + String.valueOf(System.currentTimeMillis() / 1000));
        if (userProperties != null) {
            msg.setUserProperties(userProperties);
        }
        // 设置消息需要被投递的时间
        if (startDeliverTime != null) {
            msg.setStartDeliverTime(startDeliverTime);
        }

        ProducerPool producerPool = producerMap.get(producerId);
        Producer producer = producerPool.getProducer();
        if (producer == null) {
            log.warn("Can not find the producer for the ID {}", producerId);
            return null;
        }
        SendResult sendResult = producer.send(msg);
        // 同步发送消息，只要不抛异常就是成功
        if (sendResult != null) {
            log.info("Send mq message success. Consumer ID is: {}, Topic is: {}, tag is: {}, msgId is: {}. ",
                    producerId, topic, tag, sendResult.getMessageId());
            producerPool.returnProducer(producer);
            return sendResult;
        }

        log.error("Send mq message failed. Topic is: {}, tag is: {}, message body is: {} ", topic, tag, messageBody);
        return null;
    }

    /**
     * 异步发送消息
     *
     * @param producerId
     * @param topic
     * @param tag
     * @param messageBody
     * @param userProperties
     * @return
     */
    public SendResult sendAsyncMessage(String producerId, String topic, String tag, String messageBody,
            Properties userProperties, Long startDeliverTime) {
        Validator.notEmpty(topic, NotifyErrorConstants.MQ_MESSAGE_PARMETER_ERROR);
        Validator.notEmpty(tag, NotifyErrorConstants.MQ_MESSAGE_PARMETER_ERROR);

        Message msg = new Message(
                // 您在控制台创建的 Topic
                topic,
                // Message Tag, 可理解为 Gmail 中的标签，对消息进行再归类，方便 Consumer 指定过滤条件在 MQ 服务器过滤
                tag,
                // Message Body 可以是任何二进制形式的数据， MQ 不做任何干预，需要 Producer 与 Consumer 协商好一致的序列化和反序列化方式
                messageBody.getBytes(StandardCharsets.UTF_8));
        // 设置代表消息的业务关键属性，请尽可能全局唯一。
        // 以方便您在无法正常收到消息情况下，可通过 MQ 控制台查询消息并补发。
        // 注意：不设置也不会影响消息正常收发
        msg.setKey(topic + "_" + tag + "_" + String.valueOf(System.currentTimeMillis() / 1000));
        if (userProperties != null) {
            msg.setUserProperties(userProperties);
        }
        // 设置消息需要被投递的时间
        if (startDeliverTime != null) {
            msg.setStartDeliverTime(startDeliverTime);
        }

        ProducerPool producerPool = producerMap.get(producerId);
        Producer producer = producerPool.getProducer();
        if (producer == null) {
            log.warn("Can not find the producer for the ID {}", producerId);
            return null;
        }
        // 异步发送消息, 发送结果通过 callback 返回给客户端。
        producer.sendAsync(msg, new SendCallback() {
            @Override
            public void onSuccess(final SendResult sendResult) {
                // 消息发送成功
                log.info("Send mq message success. Consumer ID is: {}, Topic is: {}, tag is: {}, msgId is: {}. ",
                        producerId, topic, tag, sendResult.getMessageId());
            }

            @Override
            public void onException(OnExceptionContext context) {
                // 消息发送失败，需要进行重试处理，可重新发送这条消息或持久化这条数据进行补偿处理
                log.warn("send message failed. topic=" + context.getTopic() + ", msgId=" + context.getMessageId());
                sendMessage(producerId, topic, messageBody, tag, userProperties, null);
            }
        });

        producerPool.returnProducer(producer);
        // 在 callback 返回之前即可取得 msgId
        log.info("send message async. topic=" + msg.getTopic() + ", msgId=" + msg.getMsgID());
        return null;
    }

    @PreDestroy
    private void destroy() {
        // 在应用退出前，销毁 Producer 对象<br>
        // 注意：如果不销毁也没有问题
        for (ProducerPool producerPool : producerMap.values()) {
            producerPool.close();
        }
        producerMap.clear();
    }
}
