/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.activemq.artemis.protocol.amqp.broker;

import java.net.URI;
import java.security.Principal;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.activemq.artemis.api.core.ActiveMQBuffers;
import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.core.buffers.impl.ChannelBufferWrapper;
import org.apache.activemq.artemis.core.client.impl.TopologyMemberImpl;
import org.apache.activemq.artemis.core.remoting.CertificateUtil;
import org.apache.activemq.artemis.core.remoting.CloseListener;
import org.apache.activemq.artemis.core.remoting.FailureListener;
import org.apache.activemq.artemis.core.server.ActiveMQServer;
import org.apache.activemq.artemis.core.server.cluster.ClusterConnection;
import org.apache.activemq.artemis.core.server.cluster.ClusterManager;
import org.apache.activemq.artemis.core.transaction.Transaction;
import org.apache.activemq.artemis.core.transaction.impl.XidImpl;
import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPException;
import org.apache.activemq.artemis.protocol.amqp.logger.ActiveMQAMQPProtocolMessageBundle;
import org.apache.activemq.artemis.protocol.amqp.proton.AMQPConnectionContext;
import org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport;
import org.apache.activemq.artemis.protocol.amqp.proton.handler.ExtCapability;
import org.apache.activemq.artemis.protocol.amqp.proton.transaction.ProtonTransactionImpl;
import org.apache.activemq.artemis.protocol.amqp.sasl.AnonymousServerSASL;
import org.apache.activemq.artemis.protocol.amqp.sasl.ExternalServerSASL;
import org.apache.activemq.artemis.protocol.amqp.sasl.GSSAPIServerSASL;
import org.apache.activemq.artemis.protocol.amqp.sasl.PlainSASL;
import org.apache.activemq.artemis.protocol.amqp.sasl.SASLResult;
import org.apache.activemq.artemis.protocol.amqp.sasl.ServerSASL;
import org.apache.activemq.artemis.spi.core.remoting.Connection;
import org.apache.activemq.artemis.spi.core.remoting.ReadyListener;
import org.apache.activemq.artemis.utils.UUIDGenerator;
import org.apache.qpid.proton.amqp.Binary;
import org.apache.qpid.proton.amqp.Symbol;
import org.apache.qpid.proton.amqp.transport.AmqpError;
import org.jboss.logging.Logger;

import io.netty.buffer.ByteBuf;

public class AMQPConnectionCallback implements FailureListener, CloseListener {

   private static final Logger logger = Logger.getLogger(AMQPConnectionCallback.class);

   private ConcurrentMap<Binary, Transaction> transactions = new ConcurrentHashMap<>();

   private final ProtonProtocolManager manager;

   private final Connection connection;

   protected ActiveMQProtonRemotingConnection protonConnectionDelegate;

   protected AMQPConnectionContext amqpConnection;

   private final Executor closeExecutor;

   private String remoteContainerId;

   private AtomicBoolean registeredConnectionId = new AtomicBoolean(false);

   private ActiveMQServer server;

   private final String[] saslMechanisms;

   public AMQPConnectionCallback(ProtonProtocolManager manager,
                                 Connection connection,
                                 Executor closeExecutor,
                                 ActiveMQServer server) {
      this.manager = manager;
      this.connection = connection;
      this.closeExecutor = closeExecutor;
      this.server = server;
      saslMechanisms = manager.getSaslMechanisms();
   }

   public String[] getSaslMechanisms() {
      return saslMechanisms;
   }

   public ServerSASL getServerSASL(final String mechanism) {
      ServerSASL result = null;
      if (isPermittedMechanism(mechanism)) {
         switch (mechanism) {
            case PlainSASL.NAME:
               result = new PlainSASL(server.getSecurityStore());
               break;

            case AnonymousServerSASL.NAME:
               result = new AnonymousServerSASL();
               break;

            case GSSAPIServerSASL.NAME:
               GSSAPIServerSASL gssapiServerSASL = new GSSAPIServerSASL();
               gssapiServerSASL.setLoginConfigScope(manager.getSaslLoginConfigScope());
               result = gssapiServerSASL;
               break;

            case ExternalServerSASL.NAME:
               // validate ssl cert present
               Principal principal = CertificateUtil.getPeerPrincipalFromConnection(protonConnectionDelegate);
               if (principal != null) {
                  ExternalServerSASL externalServerSASL = new ExternalServerSASL();
                  externalServerSASL.setPrincipal(principal);
                  result = externalServerSASL;
               } else {
                  logger.debug("SASL EXTERNAL mechanism requires a TLS peer principal");
               }
               break;

            default:
               logger.debug("Mo matching mechanism found for: " + mechanism);
               break;
         }
      }
      return result;
   }

   private boolean isPermittedMechanism(String mechanism) {
      if (saslMechanisms == null || saslMechanisms.length == 0) {
         return AnonymousServerSASL.NAME.equals(mechanism);
      } else {
         for (String candidate : saslMechanisms) {
            if (candidate.equals(mechanism)) {
               return true;
            }
         }
      }
      return false;
   }

   public boolean isSupportsAnonymous() {
      boolean supportsAnonymous = false;
      try {
         server.getSecurityStore().authenticate(null, null, null);
         supportsAnonymous = true;
      } catch (Exception e) {
         // authentication failed so no anonymous support
      }
      return supportsAnonymous;
   }

   public void close() {
      try {
         if (registeredConnectionId.getAndSet(false)) {
            server.removeClientConnection(remoteContainerId);
         }
         connection.close();
         amqpConnection.close(null);
      } finally {
         for (Transaction tx : transactions.values()) {
            try {
               tx.rollback();
            } catch (Exception e) {
               logger.warn(e.getMessage(), e);
            }
         }
      }
   }

   public Executor getExeuctor() {
      if (protonConnectionDelegate != null) {
         return protonConnectionDelegate.getExecutor();
      } else {
         return null;
      }
   }

   public void setConnection(AMQPConnectionContext connection) {
      this.amqpConnection = connection;
   }

   public AMQPConnectionContext getConnection() {
      return amqpConnection;
   }

   public ActiveMQProtonRemotingConnection getProtonConnectionDelegate() {
      return protonConnectionDelegate;
   }

   public void setProtonConnectionDelegate(ActiveMQProtonRemotingConnection protonConnectionDelegate) {

      this.protonConnectionDelegate = protonConnectionDelegate;
   }

   public void onTransport(ByteBuf byteBuf, AMQPConnectionContext amqpConnection) {
      connection.write(new ChannelBufferWrapper(byteBuf, true));
   }

   public boolean isWritable(ReadyListener readyListener) {
      return connection.isWritable(readyListener);
   }


   public AMQPSessionCallback createSessionCallback(AMQPConnectionContext connection) {
      return new AMQPSessionCallback(this, manager, connection, this.connection, closeExecutor, server.newOperationContext());
   }

   public void sendSASLSupported() {
      connection.write(ActiveMQBuffers.wrappedBuffer(new byte[]{'A', 'M', 'Q', 'P', 3, 1, 0, 0}));
   }

   public boolean validateConnection(org.apache.qpid.proton.engine.Connection connection, SASLResult saslResult) {
      remoteContainerId = connection.getRemoteContainer();
      boolean idOK = server.addClientConnection(remoteContainerId, ExtCapability.needUniqueConnection(connection));
      if (!idOK) {
         //https://issues.apache.org/jira/browse/ARTEMIS-728
         Map<Symbol, Object> connProp = new HashMap<>();
         connProp.put(AmqpSupport.CONNECTION_OPEN_FAILED, "true");
         connection.setProperties(connProp);
         connection.getCondition().setCondition(AmqpError.INVALID_FIELD);
         Map<Symbol, Symbol> info = new HashMap<>();
         info.put(AmqpSupport.INVALID_FIELD, AmqpSupport.CONTAINER_ID);
         connection.getCondition().setInfo(info);
         return false;
      }
      registeredConnectionId.set(true);
      return true;
   }

   @Override
   public void connectionClosed() {
      close();
   }

   @Override
   public void connectionFailed(ActiveMQException exception, boolean failedOver) {
      close();
   }

   @Override
   public void connectionFailed(ActiveMQException exception, boolean failedOver, String scaleDownTargetNodeID) {
      close();
   }

   public Binary newTransaction() {
      XidImpl xid = newXID();
      Binary binary = new Binary(xid.getGlobalTransactionId());
      Transaction transaction = new ProtonTransactionImpl(xid, server.getStorageManager(), -1);
      transactions.put(binary, transaction);
      return binary;
   }

   public Transaction getTransaction(Binary txid, boolean remove) throws ActiveMQAMQPException {
      Transaction tx;

      if (remove) {
         tx = transactions.remove(txid);
      } else {
         tx = transactions.get(txid);
      }

      if (tx == null) {
         logger.warn("Couldn't find txid = " + txid);
         throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.txNotFound(txid.toString());
      }

      return tx;
   }

   protected XidImpl newXID() {
      return newXID(UUIDGenerator.getInstance().generateStringUUID().getBytes());
   }

   protected XidImpl newXID(byte[] bytes) {
      return new XidImpl("amqp".getBytes(), 1, bytes);
   }

   public URI getFailoverList() {
      ClusterManager clusterManager = server.getClusterManager();
      ClusterConnection clusterConnection = clusterManager.getDefaultConnection(null);
      if (clusterConnection != null) {
         TopologyMemberImpl member = clusterConnection.getTopology().getMember(server.getNodeID().toString());
         if (member != null) {
            return member.toBackupURI();
         }
      }
      return null;
   }

   public void invokeIncomingInterceptors(AMQPMessage message, ActiveMQProtonRemotingConnection connection) {
      manager.invokeIncoming(message, connection);
   }

   public void invokeOutgoingInterceptors(AMQPMessage message, ActiveMQProtonRemotingConnection connection) {
      manager.invokeOutgoing(message, connection);
   }
}
