package org.jumbune.deploy;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jumbune.common.utils.Constants;

import org.jumbune.common.utils.ExtendedConstants;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.ChannelShell;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.UserInfo;

/**
 * This class is used to establish connection during deployment
 *
 */
public final class SessionEstablisher {

	static final String ECHO_HADOOP_HOME = "echo $HADOOP_HOME \n \n";
	private static final String SCP_COMMAND = "scp -f ";
	private static final Logger CONSOLE_LOGGER = LogManager.getLogger("EventLogger");
	public static final String WHERE_IS_HADOOP = "whereis hadoop";
	private static byte[] bufs;
	public static final String LS_PREFIX_PART = "ls ";
	public static final String LS_POSTFIX_COLON_PART = " -Rl | grep :";
	private static final String HADOOP_VERSION_YARN_COMMAND = "bin/yarn version";
	private static final String HADOOP_VERSION_NON_YARN_COMMAND = "bin/hadoop version";
	public static final String LL_COMMAND = "ll /usr/bin/hadoop \n";
	
	private static List<String> jars = new ArrayList<String>(2);
	
	static {
		jars.add("/lib/log4j-api-2.8.2.jar");
		jars.add("/lib/log4j-core-2.8.2.jar");
	}

	/**
	 * method for establishing connection while authentication
	 * @param username
	 * @param namenodeIP
	 * @param nnpwd
	 * @param privateKeyPath
	 * @return
	 * @throws JSchException
	 * @throws IOException
	 * @throws InterruptedException
	 */
	public static Session establishConnection(String username, String namenodeIP, String nnpwd, String privateKeyPath) {
		JSch jsch = new JSch();
		Session session = null;
		try {

			session = jsch.getSession(username, namenodeIP, Constants.TWENTY_TWO);
		} catch (JSchException e) {
			CONSOLE_LOGGER.error(e);
		}
		if(nnpwd!=null){
			session.setPassword(nnpwd);
		} else {
			try {
				jsch.addIdentity(privateKeyPath);
			} catch (JSchException e) {
				CONSOLE_LOGGER.error(e);
			}
		}
		UserInfo info = new JumbuneUserInfo();
		
		session.setUserInfo(info);
		java.util.Properties config = new java.util.Properties();
		config.put("StrictHostKeyChecking", "no");
		if(nnpwd!=null){
			config.put("PreferredAuthentications", "password");
		}
		session.setConfig(config);
		try {
			session.connect();
		} catch (JSchException e) {
			CONSOLE_LOGGER.error("Failed to authenticate, check username and password");
		}
		return session;
	}

	/**
	 * method for copying hadoop jars from namenode
	 * @param session
	 * @param username
	 * @param namenodeIP
	 * @param hadoopHome
	 * @param destinationAbsolutePath
	 * @param listOfFiles
	 * @throws JSchException
	 * @throws IOException
	 */
	public static void fetchHadoopJarsFromNamenode(Session session, String username, String namenodeIP, String hadoopHome, String destinationAbsolutePath, String hadoopDistributionType, String distributionType, String... files)
			throws JSchException, IOException {
		new File(destinationAbsolutePath).mkdirs();
		Deployer deployer = DeployerFactory.getDeployer(distributionType,hadoopDistributionType);
		String versionCommand = null ;
		String versionNumber = null ;
		if(!hadoopDistributionType.equalsIgnoreCase(ExtendedConstants.MAPR) || !hadoopDistributionType.equalsIgnoreCase(ExtendedConstants.EMRMAPR)){
		if (hadoopDistributionType.equalsIgnoreCase(ExtendedConstants.CLOUDERA)
				|| hadoopDistributionType.equalsIgnoreCase(ExtendedConstants.HORTONWORKS) || hadoopDistributionType.equalsIgnoreCase(ExtendedConstants.EMRAPACHE)) {
			versionCommand = File.separator + "usr" + File.separator + HADOOP_VERSION_YARN_COMMAND;
		}else if(distributionType.equalsIgnoreCase("Non-Yarn")){
			versionCommand = hadoopHome + File.separator + HADOOP_VERSION_NON_YARN_COMMAND;
		} else {
			versionCommand = hadoopHome + File.separator
					+ HADOOP_VERSION_YARN_COMMAND;
		}
		//begin mapr code changes
		} else{
			versionCommand = hadoopHome + File.separator + HADOOP_VERSION_YARN_COMMAND;
		}
		
		String versionResponse = executeCommand(session, versionCommand);
		versionNumber = getVersionNumber(versionResponse);
		//end mapr code changes
		String[] listOfFiles = deployer.getRelativePaths(versionNumber);
		for (String fileName : listOfFiles) {
			String command = SCP_COMMAND + hadoopHome + fileName;
			copyRemoteFile(session, command, destinationAbsolutePath);
		}
		CONSOLE_LOGGER.info("Syncing Jars from Hadoop to Jumbune.......[SUCCESS]");
	}

	/**
	 * Gets the version number after parsing the response from the hadoop version commands.
	 *
	 * @param versionResponse the version response
	 * @return the version number
	 */
	private static String getVersionNumber(String versionResponse) {
		if(versionResponse!=null){
		String[] versionResponseSplits = versionResponse.split("Subversion");
		String[] hadoopSplit = versionResponseSplits[0].split(" ");
		return hadoopSplit[1].trim();
		}
		return null;
	}

	private static void copyRemoteFile(Session session, String command, String fileLocation) throws JSchException, IOException {
		FileOutputStream fos = null;
		Channel channel = session.openChannel("exec");
		((ChannelExec) channel).setCommand(command);
		OutputStream out = channel.getOutputStream();
		InputStream in = channel.getInputStream();
		channel.connect();
		bufs = new byte[Constants.ONE_ZERO_TWO_FOUR];

		// send '\0'
		bufs[0] = 0;
		out.write(bufs, 0, 1);
		out.flush();

		while (true) {
			int c = checkAck(in);
			if (c != 'C') {
				break;
			}

			// read '0644 '
			in.read(bufs, 0, Constants.FIVE);

			long filesize = 0L;
			while (true) {
				if (in.read(bufs, 0, 1) < 0) {
					// error
					break;
				}
				if (bufs[0] == ' '){
					break;
				}
				filesize = filesize * Constants.TENL + (long) (bufs[0] - '0');
			}

			String file = null;
			for (int i = 0;; i++) {
				in.read(bufs, i, 1);
				if (bufs[i] == (byte) Constants.ZERO_CROSS_ZERO_A) {
					file = new String(bufs, 0, i);
					break;
				}
			}
			
			// send '\0'
			bufs[0] = 0;
			out.write(bufs, 0, 1);
			out.flush();

			// read a content of file
			fos = new FileOutputStream(fileLocation == null ? fileLocation : fileLocation + file);
			filesize = readFile(fos, in, filesize);
			fos.close();
			if (checkAck(in) != 0) {
				System.exit(0);
			}
			// send '\0'
			bufs[0] = 0;
			out.write(bufs, 0, 1);
			out.flush();
		}
	}

	private static long readFile(FileOutputStream fos, InputStream in,
			long filesize) throws IOException {
		int foo;
		long tempfilesize=filesize;
		while (true) {
			if (bufs.length < tempfilesize){
				foo = bufs.length;
			}else {
				foo = (int) tempfilesize;
			}
			foo = in.read(bufs, 0, foo);
			if (foo < 0) {
				// error
				break;
			}
			fos.write(bufs, 0, foo);
			tempfilesize -= foo;
			if (tempfilesize == 0L){
				break;
			}
		}
		return tempfilesize;
	}

	/**
	 * Method used to check the input
	 * b may be 0 for success,
 	 * 1 for error,
	 * 2 for fatal error,
	 * -1
	 * @param in
	 * @return
	 * @throws IOException
	 */
	static int checkAck(InputStream in) throws IOException {
		int b = in.read();
		// b may be 0 for success,
		// 1 for error,
		// 2 for fatal error,
		// -1
		if (b == 0 || b == -1){
			return b;
		}

		if (b == 1 || b == 2) {
			StringBuffer sb = new StringBuffer();
			int c;
			do {
				c = in.read();
				sb.append((char) c);
			} while (c != '\n');
			if (b == 1) { 
				CONSOLE_LOGGER.error(sb.toString());
			}
			if (b == 2) { 
				CONSOLE_LOGGER.error(sb.toString());
			}
		}
		return b;
	}


	/**
	 * This method execute the given command over SSH
	 * 
	 * @param session
	 * @param command
	 *            Command to be executed
	 * @throws JSchException
	 * @throws IOException
	 * @throws InterruptedException
	 */
	static String executeCommand(Session session, String command) throws JSchException, IOException {
		InputStream in = null;
		Channel channel = null;
		String msg = null;
		try {
			channel = session.openChannel("exec");
			((ChannelExec) channel).setCommand(command);
			channel.setInputStream(null);
			((ChannelExec) channel).setErrStream(System.err);
			in = channel.getInputStream();
			channel.connect();
			msg = validateCommandExecution(channel, in);
		} catch (InterruptedException e) {
			CONSOLE_LOGGER.error(e);
		} finally {
			if (in != null) {
				in.close();
			}
			if (channel != null) {
				channel.disconnect();
			}
		}
		return msg;
	}

	/**
	 * This method validates the executed command
	 * 
	 * @param channel
	 *            SSH channel
	 * @param in
	 *            input stream
	 * @return Error message if any

	 *             If any error occurred
	 */
	private static String validateCommandExecution(Channel channel, InputStream in) throws IOException, InterruptedException {
		StringBuilder sb = new StringBuilder();
		byte[] tmp = new byte[Constants.ONE_ZERO_TWO_FOUR];
		while (true) {
			while (in.available() > 0) {
				int i = in.read(tmp, 0,Constants.ONE_ZERO_TWO_FOUR);
				if (i < 0){
					break;
				}
				sb.append(new String(tmp, 0, i));
			}
			if (channel.isClosed()) {
				break;
			}
			Thread.sleep(Constants.THOUSAND);
		}
		return sb.toString();
	}

	private static class JumbuneUserInfo implements UserInfo {
		
		/**
		 * gets the password
		 */
		public String getPassword() {
			return null;
		}

		/**
		 * set boolean YES/NO
		 */
		public boolean promptYesNo(String str) {
			return true;
		}

		/**
		 * get the passphrase
		 */
		public String getPassphrase() {
			return null;
		}

		/**
		 * set passphrase message
		 */
		public boolean promptPassphrase(String message) {
			return true;
		}

		/**
		 * set the password
		 */
		public boolean promptPassword(String message) {
			return true;
		}

		/**
		 * set the message
		 */
		public void showMessage(String message) {
		}
	}
	
	public static String executeCommandUsingShell(Session session, String simpleCommand,String lineBreaker) throws JSchException, IOException {
		String response = null;
		ChannelShell channelShell = null;
		BufferedReader brIn = null;
		DataOutputStream dataOut = null;

		try {
			channelShell = (ChannelShell) session.openChannel("shell");
			InputStream is = channelShell.getInputStream();
			brIn = new BufferedReader(new InputStreamReader(is));
			dataOut = new DataOutputStream(channelShell.getOutputStream());
			channelShell.setPty(true);
			//Adding dumb to skip special characters like 00^m, [01;32m in the output
			channelShell.setPtyType("dumb");
			channelShell.connect();
			dataOut.writeBytes(simpleCommand);
			dataOut.flush();
			String line = null;
			StringBuilder stringBuilder = null;
			while ((line = brIn.readLine()) != null) {
				if (line.contains(lineBreaker) && !line.contains("mnt/var/log") && !line.contains("@")) {
					stringBuilder = new StringBuilder();
					stringBuilder.append(line);
					response = stringBuilder.toString();
					break;
				}
				if((line.contains(session.getUserName())) && (line.trim().endsWith("$") || line.trim().endsWith("#"))){
					break;
			}
			}
		} finally {
			if (brIn != null) {
				brIn.close();
			}
			if (dataOut != null) {
				dataOut.close();
			}
			if (channelShell != null) {
				channelShell.disconnect();
			}
		}
		return response;
	}

}