/*
 * 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.core.server.impl;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;

import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.api.core.Pair;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.api.core.TransportConfiguration;
import org.apache.activemq.artemis.api.core.client.ActiveMQClient;
import org.apache.activemq.artemis.core.client.impl.ClientSessionFactoryInternal;
import org.apache.activemq.artemis.core.client.impl.ServerLocatorInternal;
import org.apache.activemq.artemis.core.postoffice.DuplicateIDCache;
import org.apache.activemq.artemis.core.postoffice.impl.PostOfficeImpl;
import org.apache.activemq.artemis.core.remoting.server.RemotingService;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
import org.apache.activemq.artemis.core.server.LiveNodeLocator;
import org.apache.activemq.artemis.core.server.cluster.ActiveMQServerSideProtocolManagerFactory;
import org.apache.activemq.artemis.core.server.cluster.ha.LiveOnlyPolicy;
import org.apache.activemq.artemis.core.server.cluster.ha.ScaleDownPolicy;
import org.jboss.logging.Logger;

public class LiveOnlyActivation extends Activation {

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

   //this is how we act when we initially start as live
   private LiveOnlyPolicy liveOnlyPolicy;

   private final ActiveMQServerImpl activeMQServer;

   private ServerLocatorInternal scaleDownServerLocator;

   private ClientSessionFactoryInternal scaleDownClientSessionFactory;

   public LiveOnlyActivation(ActiveMQServerImpl server, LiveOnlyPolicy liveOnlyPolicy) {
      this.activeMQServer = server;
      this.liveOnlyPolicy = liveOnlyPolicy;
   }

   @Override
   public void run() {
      try {
         activeMQServer.initialisePart1(false);

         activeMQServer.initialisePart2(false);

         activeMQServer.completeActivation();

         if (activeMQServer.getIdentity() != null) {
            ActiveMQServerLogger.LOGGER.serverIsLive(activeMQServer.getIdentity());
         } else {
            ActiveMQServerLogger.LOGGER.serverIsLive();
         }
      } catch (Exception e) {
         ActiveMQServerLogger.LOGGER.initializationError(e);
         activeMQServer.callActivationFailureListeners(e);
      }
   }

   @Override
   public void close(boolean permanently, boolean restarting) throws Exception {
      if (scaleDownServerLocator != null) {
         scaleDownServerLocator.close();
         scaleDownServerLocator = null;
      }
   }

   @Override
   public void freezeConnections(RemotingService remotingService) {
      // connect to the scale-down target first so that when we freeze/disconnect the clients we can tell them where
      // we're sending the messages
      if (liveOnlyPolicy.getScaleDownPolicy() != null && liveOnlyPolicy.getScaleDownPolicy().isEnabled()) {
         connectToScaleDownTarget(liveOnlyPolicy.getScaleDownPolicy());
      }

      TransportConfiguration tc = scaleDownClientSessionFactory == null ? null : scaleDownClientSessionFactory.getConnectorConfiguration();
      String nodeID = tc == null ? null : scaleDownClientSessionFactory.getServerLocator().getTopology().getMember(tc).getNodeId();
      if (remotingService != null) {
         remotingService.freeze(nodeID, null);
      }
   }

   @Override
   public void postConnectionFreeze() {
      if (liveOnlyPolicy.getScaleDownPolicy() != null && liveOnlyPolicy.getScaleDownPolicy().isEnabled() && scaleDownClientSessionFactory != null) {
         try {
            scaleDown();
         } catch (Exception e) {
            ActiveMQServerLogger.LOGGER.failedToScaleDown(e);
         } finally {
            scaleDownClientSessionFactory.close();
            scaleDownServerLocator.close();
         }
      }
   }

   public void connectToScaleDownTarget(ScaleDownPolicy scaleDownPolicy) {
      try {
         scaleDownServerLocator = ScaleDownPolicy.getScaleDownConnector(scaleDownPolicy, activeMQServer);
         //use a Node Locator to connect to the cluster
         scaleDownServerLocator.setProtocolManagerFactory(ActiveMQServerSideProtocolManagerFactory.getInstance(scaleDownServerLocator));
         LiveNodeLocator nodeLocator = scaleDownPolicy.getGroupName() == null ? new AnyLiveNodeLocatorForScaleDown(activeMQServer) : new NamedLiveNodeLocatorForScaleDown(scaleDownPolicy.getGroupName(), activeMQServer);
         scaleDownServerLocator.addClusterTopologyListener(nodeLocator);

         nodeLocator.connectToCluster(scaleDownServerLocator);
         // a timeout is necessary here in case we use a NamedLiveNodeLocatorForScaleDown and there's no matching node in the cluster
         // should the timeout be configurable?
         nodeLocator.locateNode(ActiveMQClient.DEFAULT_DISCOVERY_INITIAL_WAIT_TIMEOUT);
         ClientSessionFactoryInternal clientSessionFactory = null;
         while (clientSessionFactory == null) {
            Pair<TransportConfiguration, TransportConfiguration> possibleLive = null;
            try {
               possibleLive = nodeLocator.getLiveConfiguration();
               if (possibleLive == null)  // we've tried every connector
                  break;
               clientSessionFactory = (ClientSessionFactoryInternal) scaleDownServerLocator.createSessionFactory(possibleLive.getA(), 0, false);
            } catch (Exception e) {
               logger.trace("Failed to connect to " + possibleLive.getA());
               nodeLocator.notifyRegistrationFailed(false);
               if (clientSessionFactory != null) {
                  clientSessionFactory.close();
               }
               clientSessionFactory = null;
               // should I try the backup (i.e. getB()) from possibleLive?
            }
         }
         if (clientSessionFactory != null) {
            scaleDownClientSessionFactory = clientSessionFactory;
         } else {
            throw new ActiveMQException("Unable to connect to server for scale-down");
         }
      } catch (Exception e) {
         ActiveMQServerLogger.LOGGER.failedToScaleDown(e);
      }
   }

   public long scaleDown() throws Exception {
      ScaleDownHandler scaleDownHandler = new ScaleDownHandler(activeMQServer.getPagingManager(), activeMQServer.getPostOffice(), activeMQServer.getNodeManager(), activeMQServer.getClusterManager().getClusterController(), activeMQServer.getStorageManager());
      ConcurrentMap<SimpleString, DuplicateIDCache> duplicateIDCaches = ((PostOfficeImpl) activeMQServer.getPostOffice()).getDuplicateIDCaches();
      Map<SimpleString, List<Pair<byte[], Long>>> duplicateIDMap = new HashMap<>();
      for (SimpleString address : duplicateIDCaches.keySet()) {
         DuplicateIDCache duplicateIDCache = activeMQServer.getPostOffice().getDuplicateIDCache(address);
         duplicateIDMap.put(address, duplicateIDCache.getMap());
      }
      return scaleDownHandler.scaleDown(scaleDownClientSessionFactory, activeMQServer.getResourceManager(), duplicateIDMap, activeMQServer.getManagementService().getManagementAddress(), null);
   }
}
