package top.arkstack.shine.mq;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.DirectMessageListenerContainer;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import top.arkstack.shine.mq.bean.EventMessage;
import top.arkstack.shine.mq.bean.SendTypeEnum;
import top.arkstack.shine.mq.constant.MqConstant;
import top.arkstack.shine.mq.coordinator.Coordinator;
import top.arkstack.shine.mq.processor.Processor;
import top.arkstack.shine.mq.template.RabbitmqTemplate;
import top.arkstack.shine.mq.template.Template;

import java.util.*;

/**
 * rabbitmq工厂
 * 提供rabbitmq的初始化,以及exchange和queue的添加
 *
 * @author 7le
 * @version 1.0.0
 */
@Data
@Slf4j
public class RabbitmqFactory implements Factory {

    @Autowired
    ApplicationContext applicationContext;

    @Autowired
    MessageAdapterHandler msgAdapterHandler;

    private static RabbitmqFactory rabbitmqFactory;

    private MqProperties config;

    private MqProperties.Rabbit rabbit;

    private static CachingConnectionFactory rabbitConnectionFactory;

    private RabbitAdmin rabbitAdmin;

    protected RabbitTemplate rabbitTemplate;

    private Template template;

    private DirectMessageListenerContainer listenerContainer;

    private Map<String, Queue> queues = new HashMap<>();

    private Set<String> bind = new HashSet<>();

    private Map<String, Exchange> exchanges = new HashMap<>();

    /**
     * 缺省序列化方式 Jackson2JsonMessageConverter
     */
    private MessageConverter serializerMessageConverter = new Jackson2JsonMessageConverter();


    private RabbitmqFactory(MqProperties config) {
        Objects.requireNonNull(config, "The RabbitmqProperties is empty.");
        this.config = config;
        this.rabbit = config.getRabbit();
        rabbitAdmin = new RabbitAdmin(rabbitConnectionFactory);
        rabbitTemplate = new RabbitTemplate(rabbitConnectionFactory);
        rabbitTemplate.setMessageConverter(serializerMessageConverter);
        if (config.getDistributed().isTransaction()) {
            setRabbitTemplateForDis(config);
        }
        template = new RabbitmqTemplate(this, rabbitTemplate, serializerMessageConverter);
    }

    public synchronized static RabbitmqFactory getInstance(MqProperties config, CachingConnectionFactory factory) {
        rabbitConnectionFactory = factory;
        //设置生成者确认机制
        rabbitConnectionFactory.setPublisherConfirms(true);
        if (config.getRabbit().getChannelCacheSize() != null) {
            rabbitConnectionFactory.setChannelCacheSize(config.getRabbit().getChannelCacheSize());
        }
        if (rabbitmqFactory == null) {
            rabbitmqFactory = new RabbitmqFactory(config);
        }
        return rabbitmqFactory;
    }

    /**
     * 初始化消息监听器容器
     */
    private void initMsgListenerAdapter() {
        listenerContainer = new DirectMessageListenerContainer();
        listenerContainer.setConnectionFactory(rabbitConnectionFactory);
        if (rabbit.getAcknowledgeMode() == 1) {
            listenerContainer.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        } else {
            listenerContainer.setAcknowledgeMode(
                    rabbit.getAcknowledgeMode() == 2 ? AcknowledgeMode.NONE : AcknowledgeMode.AUTO);
        }
        listenerContainer.setMessageListener(msgAdapterHandler);
        listenerContainer.setErrorHandler(new MessageErrorHandler());
        if (rabbit.getPrefetchCount() != null) {
            listenerContainer.setPrefetchCount(rabbit.getPrefetchCount());
        }
        if (rabbit.getConsumersPerQueue() != null) {
            listenerContainer.setConsumersPerQueue(rabbit.getConsumersPerQueue());
        }
        listenerContainer.setQueues(queues.values().toArray(new Queue[queues.size()]));
        listenerContainer.start();
    }

    private void setRabbitTemplateForDis(MqProperties config) {
        if (config.getRabbit().getAcknowledgeMode() != 1) {
            throw new ShineMqException("Distributed transactions must use MANUAL(AcknowledgeMode=1) mode!");
        }
        //消息发送到RabbitMQ交换器后接收ack回调
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            if (correlationData != null) {
                log.info("ConfirmCallback ack: {} correlationData: {} cause: {}", ack, correlationData, cause);
                String msgId = correlationData.getId();
                CorrelationDataExt ext = (CorrelationDataExt) correlationData;
                if (ext.getMessage() != null ) {
                    if (SendTypeEnum.DISTRIBUTED.toString().equals(ext.getMessage().getSendTypeEnum())) {
                        Coordinator coordinator = (Coordinator) applicationContext.getBean(ext.getCoordinator());
                        coordinator.confirmCallback(correlationData, ack);
                        // 如果发送到交换器成功,但是没有匹配的队列(比如说取消了绑定),ack返回值为还是true(这里是一个坑,需要注意)
                        if (ack && !coordinator.getReturnCallback(msgId)) {
                            log.info("The message has been successfully delivered to the queue, correlationData:{}", correlationData);
                            coordinator.delReady(msgId);
                        } else {
                            //失败了判断重试次数,重试次数大于0则继续发送
                            if (ext.getMaxRetries() > 0) {
                                try {
                                    rabbitmqFactory.setCorrelationData(msgId, ext.getCoordinator(), ext.getMessage(),
                                            ext.getMaxRetries() - 1);
                                    rabbitmqFactory.getTemplate().send(ext.getMessage(), 0, 0, SendTypeEnum.DISTRIBUTED);
                                } catch (Exception e) {
                                    log.error("Message retry failed to send, message:{} exception: ", ext.getMessage(), e);
                                }
                            } else {
                                log.error("Message delivery failed, msgId: {}, cause: {}", msgId, cause);
                            }
                        }
                        coordinator.delReturnCallback(msgId);
                    }
                    if(SendTypeEnum.ROLLBACK.toString().equals(ext.getMessage().getSendTypeEnum())){
                        Coordinator coordinator = (Coordinator) applicationContext.getBean(ext.getCoordinator());
                        coordinator.confirmCallback(correlationData, ack);
                        // 如果发送到交换器成功,但是没有匹配的队列(比如说取消了绑定),ack返回值为还是true(这里是一个坑,需要注意)
                        if (ack && !coordinator.getReturnCallback(msgId)) {
                            log.info("The rollback message has been successfully delivered to the queue, correlationData:{}", correlationData);
                            coordinator.delRollback(msgId);
                        }
                    }
                }
            }
        });
        //使用return-callback时必须设置mandatory为true
        rabbitTemplate.setMandatory(true);
        //消息发送到RabbitMQ交换器,但无对应queue时的回调
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            String messageId = message.getMessageProperties().getMessageId();
            String coordinatorName = messageId.split(MqConstant.SPLIT)[0];
            Coordinator coordinator = (Coordinator) applicationContext.getBean(coordinatorName);
            coordinator.setReturnCallback(messageId);
            log.error("ReturnCallback exception, no matching queue found. message id: {}, replyCode: {}, replyText: {},"
                    + "exchange: {}, routingKey: {}", messageId, replyCode, replyText, exchange, routingKey);
        });
    }

    @Override
    public Factory add(String queueName, String exchangeName, String routingKey) {
        return add(queueName, exchangeName, routingKey, null, SendTypeEnum.DIRECT, serializerMessageConverter);
    }

    @Override
    public Factory add(String queueName, String exchangeName, String routingKey, SendTypeEnum type) {
        return add(queueName, exchangeName, routingKey, null, type, serializerMessageConverter);
    }

    @Override
    public Factory add(String queueName, String exchangeName, String routingKey, Processor processor) {
        return add(queueName, exchangeName, routingKey, processor, SendTypeEnum.DIRECT, serializerMessageConverter);
    }

    @Override
    public Factory add(String queueName, String exchangeName, String routingKey, Processor processor, SendTypeEnum type) {
        return add(queueName, exchangeName, routingKey, processor, type, serializerMessageConverter);
    }

    @Override
    public Factory add(String queueName, String exchangeName, String routingKey, Processor processor, SendTypeEnum type,
                       MessageConverter messageConverter) {
        if (processor != null) {
            msgAdapterHandler.add(exchangeName, routingKey, processor, type, messageConverter);
            if (rabbit.isListenerEnable()) {
                declareBinding(queueName, exchangeName, routingKey, true,
                        type == null ? SendTypeEnum.DIRECT.toString() : type.toString(), false);
                if (listenerContainer == null) {
                    initMsgListenerAdapter();
                } else {
                    listenerContainer.addQueueNames(queueName);
                }
            } else {
                declareBinding(queueName, exchangeName, routingKey, false,
                        type == null ? SendTypeEnum.DIRECT.toString() : type.toString(), false);
            }
            return this;
        } else {
            declareBinding(queueName, exchangeName, routingKey, false,
                    type == null ? SendTypeEnum.DIRECT.toString() : type.toString(), false);
            return this;
        }
    }

    @Override
    public Factory addDLX(String queueName, String exchangeName, String routingKey, Processor processor, SendTypeEnum type) {
        return addDLX(queueName, exchangeName, routingKey, processor, type, serializerMessageConverter);
    }

    @Override
    public Factory addDLX(String queueName, String exchangeName, String routingKey, Processor processor, SendTypeEnum type,
                          MessageConverter messageConverter) {
        if (processor != null) {
            msgAdapterHandler.add(exchangeName, routingKey, processor, type, messageConverter);
            if (rabbit.isListenerEnable()) {
                declareBinding(queueName, exchangeName, routingKey, true,
                        type == null ? SendTypeEnum.DIRECT.toString() : type.toString(), true);
                if (listenerContainer == null) {
                    initMsgListenerAdapter();
                } else {
                    listenerContainer.addQueueNames(queueName);
                }
            } else {
                declareBinding(queueName, exchangeName, routingKey, false,
                        type == null ? SendTypeEnum.DIRECT.toString() : type.toString(), true);
            }
            return this;
        } else {
            declareBinding(queueName, exchangeName, routingKey, false,
                    type == null ? SendTypeEnum.DIRECT.toString() : type.toString(), true);
            return this;
        }
    }

    @Override
    public void delete(String queueName, String exchangeName, String routingKey, SendTypeEnum type) {
        queues.remove(queueName);
        msgAdapterHandler.remove(exchangeName, routingKey, type);
        listenerContainer.removeQueueNames(queueName);
        rabbitAdmin.deleteQueue(queueName);
    }

    private synchronized void declareBinding(String queueName, String exchangeName, String routingKey,
                                             boolean isPutQueue, String type, boolean isDlx) {
        String bindRelation = queueName + "_" + exchangeName + "_" + routingKey + "_" + type;
        if (bind.contains(bindRelation)) {
            return;
        }
        boolean needBinding = false;
        Exchange exchange = exchanges.get(exchangeName);
        if (exchange == null) {
            if (SendTypeEnum.TOPIC.toString().equals(type)) {
                exchange = new TopicExchange(exchangeName, rabbit.isDurable(), rabbit.isAutoDelete(), null);
            } else {
                exchange = new DirectExchange(exchangeName, rabbit.isDurable(), rabbit.isAutoDelete(), null);
            }
            exchanges.put(exchangeName, exchange);
            rabbitAdmin.declareExchange(exchange);
            needBinding = true;
        }
        Queue queue = queues.get(queueName);
        if (queue == null) {
            if (isDlx) {
                Map<String, Object> args = new HashMap<>(2);
                args.put("x-dead-letter-exchange", MqConstant.DEAD_LETTER_EXCHANGE);
                args.put("x-dead-letter-routing-key", MqConstant.DEAD_LETTER_ROUTEKEY);
                queue = new Queue(queueName, rabbit.isDurable(), rabbit.isExclusive(), rabbit.isAutoDelete(), args);
            } else {
                queue = new Queue(queueName, rabbit.isDurable(), rabbit.isExclusive(), rabbit.isAutoDelete());
            }
            if (isPutQueue) {
                queues.put(queueName, queue);
            }
            rabbitAdmin.declareQueue(queue);
            needBinding = true;
        }
        if (needBinding) {
            Binding binding;
            if (SendTypeEnum.TOPIC.toString().equals(type)) {
                binding = BindingBuilder.bind(queue).to((TopicExchange) exchange).with(routingKey);
            } else {
                binding = BindingBuilder.bind(queue).to((DirectExchange) exchange).with(routingKey);
            }
            rabbitAdmin.declareBinding(binding);
            bind.add(bindRelation);
        }
    }

    /**
     * 扩展消息的CorrelationData,方便在回调中应用
     */
    public void setCorrelationData(String id, String coordinator, EventMessage msg, Integer retry) {
        rabbitTemplate.setCorrelationDataPostProcessor(((message, correlationData) ->
                new CorrelationDataExt(id, coordinator,
                        retry == null ? config.getDistributed().getCommitMaxRetries() : retry, msg)));
    }
}