package com.bonus.mq.rabbit;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.bonus.mq.MQListener;
import com.bonus.mq.MQMessageConverter;
import com.bonus.mq.MQMessageListener;
import com.bonus.mq.MQProperties;
import com.bonus.mq.mqtt.MqttProperties;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.rocketmq.client.Validators;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListenerConfigurer;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerEndpoint;
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistrar;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.core.env.Environment;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.converter.SmartMessageConverter;
import org.springframework.messaging.support.MessageBuilder;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.*;

//@Configuration
public class RabbitListenerConfiguration  {
   //implements RabbitListenerConfigurer
   private static final Logger log = LoggerFactory.getLogger(RabbitListenerConfiguration.class);
   public static final String REDELIVERED_TIMES_HEADER_NAME = "__redelivered_times";
   public static final String ORG_DESTINATION = "__org_destination";
   private final MessageConverter messageConverter;
   private final RabbitPropertiesExt rabbitPropertiesExt;
   private final Set<SimpleRabbitListenerEndpoint> rabbitListenerEndpoints = Sets.newHashSet();
   private final ApplicationContext applicationContext;
   private final MQProperties mqProperties;
   private final AmqpAdmin amqpAdmin;
   private final Set<String> nowQueues = Sets.newHashSet();

   public RabbitListenerConfiguration(AmqpAdmin amqpAdmin, ApplicationContext applicationContext, Environment environment, MQMessageConverter mqMessageConverter, MQProperties mqProperties, RabbitPropertiesExt rabbitPropertiesExt, MqttProperties mqttProperties) throws MQClientException {
      this.applicationContext = applicationContext;
      this.messageConverter = mqMessageConverter.getMessageConverter();
      this.rabbitPropertiesExt = rabbitPropertiesExt;
      this.mqProperties = mqProperties;
      this.amqpAdmin = amqpAdmin;
      int counter = 0;
      Map<String, String> group_subscription = Maps.newHashMap();
      Map<String, Set<String>> queue_mqttTopic = mqttProperties.getQueueRefMqttTopic();
      Map<String, Object> beans = applicationContext.getBeansWithAnnotation(MQMessageListener.class);
      Iterator var12 = beans.values().iterator();

      while(var12.hasNext()) {
         Object bean = var12.next();
         Class<?> clazz = AopProxyUtils.ultimateTargetClass(bean);
         if (!MQListener.class.isAssignableFrom(bean.getClass())) {
            String var10002 = String.valueOf(clazz);
            throw new IllegalStateException(var10002 + " is not instance of " + MQListener.class.getName());
         }

         MQMessageListener ann = (MQMessageListener)clazz.getAnnotation(MQMessageListener.class);
         String resolvedGroup = environment.resolvePlaceholders(ann.group());
         Validators.checkGroup(resolvedGroup);
         String resolvedTopic = environment.resolvePlaceholders(ann.topic());
         Validators.checkTopic(resolvedTopic);
         mqProperties.checkTopicDefinition(resolvedTopic);
         String resolvedTag = environment.resolvePlaceholders(ann.tag());
         String actualTopic = resolvedTopic + ":" + resolvedTag;
         String subscription = (String)group_subscription.get(resolvedGroup);
         if (subscription != null && !subscription.equals(actualTopic)) {
            throw new IllegalArgumentException("Subscription consistency error: " + resolvedGroup + "-> [" + subscription + "," + actualTopic + "]");
         }

         String queueName = resolvedGroup + ":" + actualTopic;
         rabbitPropertiesExt.getQueue().add(new RabbitPropertiesExt.QueueProperties(queueName, mqProperties.getNamespace()));
         String routingKey = resolvedTopic + "." + (resolvedTag.equals("*") ? "#" : resolvedTag);
         rabbitPropertiesExt.getBinding().add(new RabbitPropertiesExt.BindingProperties(rabbitPropertiesExt.getDelayedExchange(), routingKey, queueName));
         rabbitPropertiesExt.getBinding().add(new RabbitPropertiesExt.BindingProperties(rabbitPropertiesExt.getNormalExchange(), routingKey, queueName));
         Set<String> mqttTopics = (Set)queue_mqttTopic.get(actualTopic);
         if (CollUtil.isNotEmpty(mqttTopics)) {
            Iterator var24 = mqttTopics.iterator();

            while(var24.hasNext()) {
               String mqttTopic = (String)var24.next();
               rabbitPropertiesExt.getBinding().add(new RabbitPropertiesExt.BindingProperties(rabbitPropertiesExt.getNormalExchange(), mqttTopic.replace("/", ".").replace("+", "*"), queueName));
            }
         }

         String beanName = String.format("%s_%s", SimpleRabbitListenerEndpoint.class.getName(), counter++);
         Type messageType = this.parseMessageType((MQListener)bean);
         MethodParameter methodParameter = this.parseMethodParameter((MQListener)bean);
         SimpleRabbitListenerEndpoint endpoint = new SimpleRabbitListenerEndpoint();
         endpoint.setId(beanName + ":" + queueName);
         endpoint.setQueueNames(new String[]{queueName});
         int var30 = ann.consumeThreadMin();
         endpoint.setConcurrency("" + var30 + "-" + ann.consumeThreadMax());
         endpoint.setMessageListener((ChannelAwareMessageListener)(message, channel) -> {
            assert channel != null;

            Object targetObj = null;
            boolean rejectMessage = false;
            String traceId = null;

            try {
               traceId = (String)message.getMessageProperties().getHeader("X-Trace-Id");
               traceId = traceId == null ? UUID.randomUUID().toString() : traceId;
               MDC.put("X-Trace-Id", traceId);
               message.getMessageProperties().setHeader("__SUB_TOPIC", message.getMessageProperties().getReceivedRoutingKey());
               targetObj = this.doConvertMessage(resolvedTopic, resolvedTag, message, messageType, methodParameter);
               ((MQListener)bean).onMessage(targetObj);
               return;
            } catch (Throwable var24) {
               rejectMessage = true;
               log.error("Consume failed", var24);
               Map<String, Object> headers = message.getMessageProperties().getHeaders();
               int redeliveredTimes = MapUtil.getInt(headers, "__redelivered_times", 0);
               if (redeliveredTimes < ann.maxReconsumeTimes()) {
                  ++redeliveredTimes;
                  Map<String, Object> newHeader = Maps.newHashMap();
                  newHeader.put("__redelivered_times", redeliveredTimes);
                  newHeader.put("X-Trace-Id", traceId);
                  newHeader.put("__org_destination", actualTopic);
                  newHeader.put("__SUB_TOPIC", message.getMessageProperties().getReceivedRoutingKey());
                  int delayTime = (int) ((int) Math.pow((double)redeliveredTimes, 3.0) * 1000L);
                  log.error("Redelivery after {}ms, message:{}", delayTime, targetObj);
                  ((RabbitTemplate)applicationContext.getBean(RabbitTemplate.class)).sendInternal(false, rabbitPropertiesExt.getRetryQueue(), message.getBody(), newHeader, message.getMessageProperties().getMessageId(), delayTime, false);
                  return;
               }
            } finally {
               if (rejectMessage) {
                  channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
               } else {
                  channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
               }

            }

         });
         this.rabbitListenerEndpoints.add(endpoint);
         group_subscription.put(resolvedGroup, actualTopic);
         this.nowQueues.add(queueName);
      }

   }

   public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) {
      this.nowQueues.add(this.rabbitPropertiesExt.getRetryQueue());
      try {
         this.existQueues().forEach((queue, queueStatus) -> {
            if (!this.nowQueues.contains(queue) || !this.mqProperties.getNamespace().equals(queueStatus.getNamespace())) {
               if (queueStatus.messagesReady > 0L) {
                  throw new IllegalStateException("Queue be deleted while unconsumed messages exist. \"" + queue + "\"");
               }

               this.amqpAdmin.deleteQueue(queue);
            }

         });
      } catch (JsonProcessingException e) {
         throw new RuntimeException(e);
      }
      Iterator var2 = this.rabbitListenerEndpoints.iterator();

      while(var2.hasNext()) {
         SimpleRabbitListenerEndpoint rabbitListenerEndpoint = (SimpleRabbitListenerEndpoint)var2.next();
         registrar.registerEndpoint(rabbitListenerEndpoint);
      }

      SimpleRabbitListenerEndpoint retryEndpoint = new SimpleRabbitListenerEndpoint();
      retryEndpoint.setId(this.rabbitPropertiesExt.getRetryQueue());
      retryEndpoint.setQueueNames(new String[]{this.rabbitPropertiesExt.getRetryQueue()});
      retryEndpoint.setMessageListener((ChannelAwareMessageListener)(message, channel) -> {
         assert channel != null;

         try {
            Map<String, Object> headers = message.getMessageProperties().getHeaders();
            Map<String, Object> newHeader = Maps.newHashMap();
            newHeader.put("__redelivered_times", headers.get("__redelivered_times"));
            newHeader.put("__SUB_TOPIC", headers.get("__SUB_TOPIC"));
            newHeader.put("X-Trace-Id", headers.get("X-Trace-Id"));
            ((RabbitTemplate)this.applicationContext.getBean(RabbitTemplate.class)).sendInternal(true, MapUtil.getStr(headers, "__org_destination"), message.getBody(), newHeader, message.getMessageProperties().getMessageId(), null, false);
         } catch (Throwable var8) {
            log.error("Error occurred while retry message consuming: {}", var8.getMessage());
         } finally {
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
         }

      });
      registrar.registerEndpoint(retryEndpoint);
   }

   private Object doConvertMessage(String topic, String tag, Message message, Type messageType, MethodParameter methodParameter) {
      if (!Objects.equals(messageType, MessageExt.class) && !Objects.equals(messageType, org.apache.rocketmq.common.message.Message.class)) {
         String str = new String(message.getBody(), StandardCharsets.UTF_8);
         if (Objects.equals(messageType, String.class)) {
            return str;
         } else {
            try {
               return messageType instanceof Class ? this.messageConverter.fromMessage(MessageBuilder.withPayload(str).build(), (Class)messageType) : ((SmartMessageConverter)this.messageConverter).fromMessage(MessageBuilder.withPayload(str).build(), (Class)((ParameterizedType)messageType).getRawType(), methodParameter);
            } catch (Exception var8) {
               log.error("convert failed. from:{}, to:{}", str, messageType);
               throw new RuntimeException("cannot convert message to " + String.valueOf(messageType), var8);
            }
         }
      } else {
         MessageExt messageExt = new MessageExt();
         messageExt.setTopic(topic);
         messageExt.setTags(tag);
         String subTopic = (String)message.getMessageProperties().getHeader("__SUB_TOPIC");
         if (CharSequenceUtil.isNotBlank(subTopic)) {
            messageExt.putUserProperty("__SUB_TOPIC", subTopic.replace(".", "/"));
         }

         messageExt.setBody(message.getBody());
         return messageExt;
      }
   }

   private MethodParameter parseMethodParameter(MQListener<Object> mqListener) {
      Class<?> targetClass = AopProxyUtils.ultimateTargetClass(mqListener);
      Type messageType = this.parseMessageType(mqListener);
      Class clazz;
      if (messageType instanceof ParameterizedType && this.messageConverter instanceof SmartMessageConverter) {
         clazz = (Class)((ParameterizedType)messageType).getRawType();
      } else {
         if (!(messageType instanceof Class)) {
            throw new RuntimeException("parameterType:" + String.valueOf(messageType) + " of onMessage method is not supported");
         }

         clazz = (Class)messageType;
      }

      try {
         Method method = targetClass.getMethod("onMessage", clazz);
         return new MethodParameter(method, 0);
      } catch (NoSuchMethodException var6) {
         throw new RuntimeException("parameterType:" + String.valueOf(messageType) + " of onMessage method is not supported");
      }
   }

   private Type parseMessageType(MQListener<Object> mqListener) {
      Class<?> targetClass = AopProxyUtils.ultimateTargetClass(mqListener);

      Type matchedGenericInterface;
      Type[] actualTypeArguments;
      for(matchedGenericInterface = null; Objects.nonNull(targetClass); targetClass = targetClass.getSuperclass()) {
         actualTypeArguments = targetClass.getGenericInterfaces();
         Type[] var5 = actualTypeArguments;
         int var6 = actualTypeArguments.length;

         for(int var7 = 0; var7 < var6; ++var7) {
            Type type = var5[var7];
            if (type instanceof ParameterizedType && Objects.equals(((ParameterizedType)type).getRawType(), MQListener.class)) {
               matchedGenericInterface = type;
               break;
            }
         }
      }

      if (Objects.isNull(matchedGenericInterface)) {
         return Object.class;
      } else {
         actualTypeArguments = ((ParameterizedType)matchedGenericInterface).getActualTypeArguments();
         if (Objects.nonNull(actualTypeArguments) && actualTypeArguments.length > 0) {
            return actualTypeArguments[0];
         } else {
            return Object.class;
         }
      }
   }

   private Map<String, QueueStatus> existQueues() throws JsonProcessingException {
         String var10000 = this.rabbitPropertiesExt.determineHost();
         HttpResponse response = HttpRequest.get("http://" + var10000 + ":" + this.rabbitPropertiesExt.getManagerPort() + "/rabbitmq/api/queues").basicAuth(this.rabbitPropertiesExt.getUsername(), this.rabbitPropertiesExt.getPassword()).execute();
         Map<String, QueueStatus> queueRefUndelivered = Maps.newHashMap();
         MQMessageConverter.getObjectMapper().readTree(response.body()).forEach((jsonNode) -> {
            JsonNode arguments = jsonNode.get("arguments");
            if (arguments != null) {
               JsonNode namespaceJson = arguments.get("_namespace");
               JsonNode messagesReady = jsonNode.get("messages_ready");
               QueueStatus queueStatus = new QueueStatus(messagesReady == null ? 0L : messagesReady.longValue(), namespaceJson == null ? null : namespaceJson.textValue());
               String queueName = jsonNode.get("name").textValue();
               if (this.rabbitPropertiesExt.isCleanNoneNamespaceQueue() && CharSequenceUtil.isEmpty(queueStatus.getNamespace()) && !CharSequenceUtil.startWith(queueName, "mqtt-subscription-") || this.mqProperties.getNamespace().equals(queueStatus.getNamespace())) {
                  queueRefUndelivered.put(queueName, queueStatus);
               }

            }
         });
         return queueRefUndelivered;
   }

   private static class QueueStatus {
      private long messagesReady;
      private String namespace;

      public long getMessagesReady() {
         return this.messagesReady;
      }

      public String getNamespace() {
         return this.namespace;
      }

      public void setMessagesReady(final long messagesReady) {
         this.messagesReady = messagesReady;
      }

      public void setNamespace(final String namespace) {
         this.namespace = namespace;
      }

      public QueueStatus(final long messagesReady, final String namespace) {
         this.messagesReady = messagesReady;
         this.namespace = namespace;
      }
   }
}
