/*
 * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * WSO2 Inc. 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 io.entgra.device.mgt.plugins.virtualfirealarm.agent.transport;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import io.entgra.device.mgt.plugins.virtualfirealarm.agent.core.AgentConstants;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramSocket;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.SocketException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

public class TransportUtils {
	private static final Log log = LogFactory.getLog(TransportUtils.class);

	public static final int MIN_PORT_NUMBER = 9000;
	public static final int MAX_PORT_NUMBER = 11000;

	/**
	 * Given a server endpoint as a String, this method splits it into Protocol, Host and Port
	 *
	 * @param ipString a network endpoint in the format - '<PROTOCOL>://<HOST>:<PORT>'
	 * @return a map with keys "Protocol", "Host" & "Port" for the related values from the ipString
	 * @throws TransportHandlerException
	 */
	public static Map<String, String> getHostAndPort(String ipString)
			throws TransportHandlerException {
		Map<String, String> ipPortMap = new HashMap<String, String>();
		String[] ipPortArray = ipString.split(":");

		if (ipPortArray.length != 3) {
			String errorMsg =
					"The IP String - '" + ipString +
							"' is invalid. It needs to be in format '<PROTOCOL>://<HOST>:<PORT>'.";
			log.info(errorMsg);
			throw new TransportHandlerException(errorMsg);
		}

		ipPortMap.put(AgentConstants.PROTOCOL_PROPERTY, ipPortArray[0]);
		ipPortMap.put(AgentConstants.HOST_PROPERTY, ipPortArray[1].replace("/", ""));
		ipPortMap.put(AgentConstants.PORT_PROPERTY, ipPortArray[2]);
		return ipPortMap;
	}

	/**
	 * This method validates whether a specific IP Address is of IPv4 type
	 *
	 * @param ipAddress the IP Address which needs to be validated
	 * @return true if it is of IPv4 type and false otherwise
	 */
	public static boolean validateIPv4(String ipAddress) {
		try {
			if (ipAddress == null || ipAddress.isEmpty()) {
				return false;
			}

			String[] parts = ipAddress.split("\\.");
			if (parts.length != 4) {
				return false;
			}

			for (String s : parts) {
				int i = Integer.parseInt(s);
				if ((i < 0) || (i > 255)) {
					return false;
				}
			}
			return !ipAddress.endsWith(".");

		} catch (NumberFormatException nfe) {
			log.warn("The IP Address: " + ipAddress + " could not be validated against IPv4-style");
			return false;
		}
	}


	public static Map<String, String> getInterfaceIPMap() throws TransportHandlerException {

		Map<String, String> interfaceToIPMap = new HashMap<String, String>();
		Enumeration<NetworkInterface> networkInterfaces;
		String networkInterfaceName = "";
		String ipAddress;

		try {
			networkInterfaces = NetworkInterface.getNetworkInterfaces();
		} catch (SocketException exception) {
			String errorMsg =
					"Error encountered whilst trying to get the list of network-interfaces";
			log.error(errorMsg);
			throw new TransportHandlerException(errorMsg, exception);
		}

		try {
			for (; networkInterfaces.hasMoreElements(); ) {
				networkInterfaceName = networkInterfaces.nextElement().getName();

				if (log.isDebugEnabled()) {
					log.debug("Network Interface: " + networkInterfaceName);
					log.debug("------------------------------------------");
				}

				Enumeration<InetAddress> interfaceIPAddresses = NetworkInterface.getByName(
						networkInterfaceName).getInetAddresses();

				for (; interfaceIPAddresses.hasMoreElements(); ) {
					ipAddress = interfaceIPAddresses.nextElement().getHostAddress();

					if (log.isDebugEnabled()) {
						log.debug("IP Address: " + ipAddress);
					}

					if (TransportUtils.validateIPv4(ipAddress)) {
						interfaceToIPMap.put(networkInterfaceName, ipAddress);
					}
				}

				if (log.isDebugEnabled()) {
					log.debug("------------------------------------------");
				}
			}
		} catch (SocketException exception) {
			String errorMsg =
					"Error encountered whilst trying to get the IP Addresses of the network " +
							"interface: " + networkInterfaceName;
			log.error(errorMsg);
			throw new TransportHandlerException(errorMsg, exception);
		}

		return interfaceToIPMap;
	}


	/**
	 * Attempts to find a free port between the MIN_PORT_NUMBER(9000) and MAX_PORT_NUMBER(11000).
	 * Tries 'RANDOMLY picked' port numbers between this range up-until "randomAttempts" number of
	 * times. If still fails, then tries each port in descending order from the MAX_PORT_NUMBER
	 * whilst skipping already attempted ones via random selection.
	 *
	 * @param randomAttempts no of times to TEST port numbers picked randomly over the given range
	 * @return an available/free port
	 */
	public static synchronized int getAvailablePort(int randomAttempts) {
		ArrayList<Integer> failedPorts = new ArrayList<Integer>(randomAttempts);
		try {
			SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
			int randomPort = MAX_PORT_NUMBER;
			while (randomAttempts > 0) {
				randomPort = secureRandom.nextInt(MAX_PORT_NUMBER - MIN_PORT_NUMBER) + MIN_PORT_NUMBER;
				if (checkIfPortAvailable(randomPort)) {
					return randomPort;
				}
				failedPorts.add(randomPort);
				randomAttempts--;
			}
			randomPort = MAX_PORT_NUMBER;
			while (true) {
				if (!failedPorts.contains(randomPort) && checkIfPortAvailable(randomPort)) {
					return randomPort;
				}
				randomPort--;
			}
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException("SHA1PRNG algorithm could not be found.");
		}
	}


	private static boolean checkIfPortAvailable(int port) {
		ServerSocket tcpSocket = null;
		DatagramSocket udpSocket = null;

		try {
			tcpSocket = new ServerSocket(port);
			tcpSocket.setReuseAddress(true);

			udpSocket = new DatagramSocket(port);
			udpSocket.setReuseAddress(true);
			return true;
		} catch (IOException ex) {
			// denotes the port is in use
		} finally {
			if (tcpSocket != null) {
				try {
					tcpSocket.close();
				} catch (IOException e) {
	                    /* not to be thrown */
				}
			}

			if (udpSocket != null) {
				udpSocket.close();
			}
		}

		return false;
	}


	/**
	 * This is a utility method that creates and returns a HTTP connection object.
	 *
	 * @param urlString the URL pattern to which the connection needs to be created
	 * @return an HTTPConnection object which cn be used to send HTTP requests
	 * @throws TransportHandlerException if errors occur when creating the HTTP connection with
	 *                                       the given URL string
	 */
	public static HttpURLConnection getHttpConnection(String urlString) throws
	                                                                    TransportHandlerException {
		URL connectionUrl;
		HttpURLConnection httpConnection;

		try {
			connectionUrl = new URL(urlString);
			httpConnection = (HttpURLConnection) connectionUrl.openConnection();
		} catch (MalformedURLException e) {
			String errorMsg = "Error occured whilst trying to form HTTP-URL from string: " + urlString;
			log.error(errorMsg);
			throw new TransportHandlerException(errorMsg, e);
		} catch (IOException exception) {
			String errorMsg = "Error occured whilst trying to open a connection to: " + urlString;
			log.error(errorMsg);
			throw new TransportHandlerException(errorMsg, exception);
		}
		return httpConnection;
	}

	/**
	 * This is a utility method that reads and returns the response from a HTTP connection
	 *
	 * @param httpConnection the connection from which a response is expected
	 * @return the response (as a string) from the given HTTP connection
	 * @throws TransportHandlerException if any errors occur whilst reading the response from
	 *                                       the connection stream
	 */
	public static String readResponseFromHttpRequest(HttpURLConnection httpConnection)
			throws TransportHandlerException {
		BufferedReader bufferedReader;
		try {
			bufferedReader = new BufferedReader(new InputStreamReader(
					httpConnection.getInputStream(), StandardCharsets.UTF_8));
		} catch (IOException exception) {
			String errorMsg = "There is an issue with connecting the reader to the input stream at: " +
					httpConnection.getURL();
			log.error(errorMsg);
			throw new TransportHandlerException(errorMsg, exception);
		}

		String responseLine;
		StringBuilder completeResponse = new StringBuilder();

		try {
			while ((responseLine = bufferedReader.readLine()) != null) {
				completeResponse.append(responseLine);
			}
		} catch (IOException exception) {
			String errorMsg = "Error occured whilst trying read from the connection stream at: " +
					httpConnection.getURL();
			log.error(errorMsg);
			throw new TransportHandlerException(errorMsg, exception);
		}
		try {
			bufferedReader.close();
		} catch (IOException exception) {
			log.error("Could not succesfully close the bufferedReader to the connection at: " + httpConnection.getURL());
		}
		return completeResponse.toString();
	}

}
