/**
 * 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;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.URL;
import java.net.URLConnection;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.apache.activemq.artemis.logs.ActiveMQUtilLogger;
import org.apache.activemq.artemis.utils.ActiveMQThreadFactory;
import org.apache.activemq.artemis.utils.ConcurrentHashSet;
import org.jboss.logging.Logger;

/**
 * This will use {@link InetAddress#isReachable(int)} to determine if the network is alive.
 * It will have a set of addresses, and if any address is reached the network will be considered alive.
 */
public class NetworkHealthCheck extends ActiveMQScheduledComponent {

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

   private final Set<ActiveMQComponent> componentList = new ConcurrentHashSet<>();
   private final Set<InetAddress> addresses = new ConcurrentHashSet<>();
   private final Set<URL> urls = new ConcurrentHashSet<>();
   private NetworkInterface networkInterface;

   public static final String IPV6_DEFAULT_COMMAND = "ping6 -c 1 %2$s";

   public static final String IPV4_DEFAULT_COMMAND = "ping -c 1 -t %d %s";

   private String ipv4Command = IPV4_DEFAULT_COMMAND;

   private String ipv6Command = IPV6_DEFAULT_COMMAND;

   // To be used on tests. As we use the loopback as a valid address on tests.
   private boolean ignoreLoopback = false;

   /**
    * The timeout to be used on isReachable
    */
   private int networkTimeout;

   public NetworkHealthCheck() {
      this(null, 1000, 1000);
   }

   public NetworkHealthCheck(String nicName, long checkPeriod, int networkTimeout) {
      super(null, null, checkPeriod, TimeUnit.MILLISECONDS, false);
      this.networkTimeout = networkTimeout;
      this.setNICName(nicName);
   }

   public NetworkHealthCheck setNICName(String nicName) {
      NetworkInterface netToUse;
      try {
         if (nicName != null) {
            netToUse = NetworkInterface.getByName(nicName);
         } else {
            netToUse = null;
         }
      } catch (Exception e) {
         logger.warn(e.getMessage(), e);
         netToUse = null;
      }

      this.networkInterface = netToUse;
      return this;
   }

   public boolean isIgnoreLoopback() {
      return ignoreLoopback;
   }

   public NetworkHealthCheck setIgnoreLoopback(boolean ignoreLoopback) {
      this.ignoreLoopback = ignoreLoopback;
      return this;
   }

   public Set<InetAddress> getAddresses() {
      return addresses;
   }

   public Set<URL> getUrls() {
      return urls;
   }

   public String getNICName() {
      if (networkInterface != null) {
         return networkInterface.getName();
      } else {
         return null;
      }
   }

   public NetworkHealthCheck parseAddressList(String addressList) {
      if (addressList != null) {
         String[] addresses = addressList.split(",");

         for (String address : addresses) {
            if (!address.trim().isEmpty()) {
               try {
                  this.addAddress(InetAddress.getByName(address.trim()));
               } catch (Exception e) {
                  logger.warn(e.getMessage(), e);
               }
            }
         }
      }

      return this;
   }

   public NetworkHealthCheck parseURIList(String addressList) {
      if (addressList != null) {
         String[] addresses = addressList.split(",");

         for (String address : addresses) {
            if (!address.trim().isEmpty()) {
               try {
                  this.addURL(new URL(address.trim()));
               } catch (Exception e) {
                  logger.warn(e.getMessage(), e);
               }
            }
         }
      }

      return this;
   }

   @Override
   protected ActiveMQThreadFactory getThreadFactory() {
      return new ActiveMQThreadFactory("NetworkChecker", "Network-Checker-", false, getThisClassLoader());
   }


   private ClassLoader getThisClassLoader() {
      return AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
         @Override
         public ClassLoader run() {
            return NetworkHealthCheck.this.getClass().getClassLoader();
         }
      });

   }

   public int getNetworkTimeout() {
      return networkTimeout;
   }

   @Override
   public NetworkHealthCheck setPeriod(long period) {
      super.setPeriod(period);
      return this;
   }

   @Override
   public NetworkHealthCheck setTimeUnit(TimeUnit timeUnit) {
      super.setTimeUnit(timeUnit);
      return this;
   }

   public NetworkHealthCheck setNetworkTimeout(int networkTimeout) {
      this.networkTimeout = networkTimeout;
      return this;
   }

   public NetworkHealthCheck addComponent(ActiveMQComponent component) {
      componentList.add(component);
      checkStart();
      return this;
   }

   public NetworkHealthCheck clearComponents() {
      componentList.clear();
      return this;
   }

   public NetworkHealthCheck addAddress(InetAddress address) {
      if (!check(address)) {
         logger.warn("Ping Address " + address + " wasn't reacheable");
      }

      if (!ignoreLoopback && address.isLoopbackAddress()) {
         ActiveMQUtilLogger.LOGGER.addressloopback(address.toString());
      } else {
         addresses.add(address);
         checkStart();
      }
      return this;
   }

   public NetworkHealthCheck removeAddress(InetAddress address) {
      addresses.remove(address);
      return this;
   }

   public NetworkHealthCheck clearAddresses() {
      addresses.clear();
      return this;
   }

   public NetworkHealthCheck addURL(URL url) {
      if (!check(url)) {
         logger.warn("Ping url " + url + " wasn't reacheable");
      }
      urls.add(url);
      checkStart();
      return this;
   }

   public NetworkHealthCheck removeURL(URL url) {
      urls.remove(url);
      return this;
   }

   public NetworkHealthCheck clearURL() {
      urls.clear();
      return this;
   }

   public String getIpv4Command() {
      return ipv4Command;
   }

   public NetworkHealthCheck setIpv4Command(String ipv4Command) {
      this.ipv4Command = ipv4Command;
      return this;
   }

   public String getIpv6Command() {
      return ipv6Command;
   }

   public NetworkHealthCheck setIpv6Command(String ipv6Command) {
      this.ipv6Command = ipv6Command;
      return this;
   }

   private void checkStart() {
      if (!isStarted() && (!addresses.isEmpty() || !urls.isEmpty()) && !componentList.isEmpty()) {
         start();
      }
   }

   @Override
   public void run() {
      boolean healthy = check();

      if (healthy) {
         for (ActiveMQComponent component : componentList) {
            if (!component.isStarted()) {
               try {
                  logger.info("Network is healthy, starting service " + component);
                  component.start();
               } catch (Exception e) {
                  logger.warn("Error starting component " + component, e);
               }
            }
         }
      } else {
         for (ActiveMQComponent component : componentList) {
            if (component.isStarted()) {
               try {
                  logger.info("Network is unhealthy, stopping service " + component);
                  component.stop();
               } catch (Exception e) {
                  logger.warn("Error stopping component " + component, e);
               }
            }
         }
      }

   }

   public boolean check() {
      boolean isEmpty = true;
      for (InetAddress address : addresses) {
         isEmpty = false;
         if (check(address)) {
            return true;
         }
      }

      for (URL url : urls) {
         isEmpty = false;
         if (check(url)) {
            return true;
         }
      }

      // This should return true if no checks were done, on this case it's empty
      // This is tested by {@link NetworkHe
      return isEmpty;
   }

   public boolean check(InetAddress address) {
      try {
         if (address.isReachable(networkInterface, 0, networkTimeout)) {
            if (logger.isTraceEnabled()) {
               logger.tracef(address + " OK");
            }
            return true;
         } else {
            return purePing(address);
         }
      } catch (Exception e) {
         logger.warn(e.getMessage(), e);
         return false;
      }
   }

   public boolean purePing(InetAddress address) throws IOException, InterruptedException {
      long timeout = Math.max(1, TimeUnit.MILLISECONDS.toSeconds(networkTimeout));
      // it did not work with a simple isReachable, it could be because there's no root access, so we will try ping executable

      if (logger.isTraceEnabled()) {
         logger.trace("purePing on canonical address " + address.getCanonicalHostName());
      }
      ProcessBuilder processBuilder;
      if (address instanceof Inet6Address) {
         processBuilder = buildProcess(ipv6Command, timeout, address.getCanonicalHostName());
      } else {
         processBuilder = buildProcess(ipv4Command, timeout, address.getCanonicalHostName());
      }

      Process pingProcess = processBuilder.start();

      readStream(pingProcess.getInputStream(), false);
      readStream(pingProcess.getErrorStream(), true);

      return pingProcess.waitFor() == 0;
   }


   private ProcessBuilder buildProcess(String expressionCommand, long timeout, String host) {
      String command = String.format(expressionCommand, timeout, host);

      if (logger.isDebugEnabled()) {
         logger.debug("executing ping:: " + command);
      }

      ProcessBuilder builder = new ProcessBuilder(command.split(" "));

      return builder;
   }

   private void readStream(InputStream stream, boolean error) throws IOException {
      BufferedReader reader = new BufferedReader(new InputStreamReader(stream));

      String inputLine;
      while ((inputLine = reader.readLine()) != null) {
         if (error) {
            logger.warn(inputLine);
         } else {
            logger.trace(inputLine);
         }
      }

      reader.close();
   }

   public boolean check(URL url) {
      try {
         URLConnection connection = url.openConnection();
         connection.setReadTimeout(networkTimeout);
         InputStream is = connection.getInputStream();
         is.close();
         return true;
      } catch (Exception e) {
         logger.warn(e.getMessage(), e);
         return false;
      }
   }
}
