package org.inria.bmajwatcher.server.launcher;

import java.io.File;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.TimeUnit;

import javax.servlet.ServletContext;

import org.apache.log4j.Logger;
import org.inria.bmajwatcher.server.utils.Utils;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;


/**
 * Launcher that runs commands on a remote server via ssh.
 * Localhost and remote host must have the same :
 * - BIOMAJ_ROOT
 * - data.dir
 * - tmp directory (defined in general.conf)
 * 
 * @author rsabas
 *
 */
public class RemoteLauncher implements Launcher {

	public static String LOGIN_AUTH = "login";
	public static String KEY_AUTH = "key";
	
	private String authType = null;
	private String userName = null;
	private String password = null;
	private String keyFilePath = null;
	private String passPhrase = null;
	
	private static final String BACKGROUND_COMMAND = " > /dev/null 2>&1 &";
	
	private String biomajRoot;
	
	private int connectionTimeout = 3000; // 3s
	
	private Logger log = Logger.getLogger(RemoteLauncher.class);
	
	// Doesnt need to be static as only one remotelauncher is created over the application
	private Vector<HostAvailable> hostsAvailability = new Vector<HostAvailable>();
	private int currentHostPosition = 0;
	
	/**
	 * Binds host availability
	 */
	class HostAvailable {
		public String host = "";
		public boolean available = true;
		
		public HostAvailable(String host, boolean available) {
			this.host = host;
			this.available = available;
		}
	}
	
	/**
	 * SSH session attributes
	 */
	class SessionInfo {
		public Session session;
		public Channel channel;
		public String host;
		
		public SessionInfo(Session session, Channel channel, String host) {
			this.session = session;
			this.channel = channel;
			this.host = host;
		}
	}
	
	/**
	 * RemoteLauncher should only be created via LauncherFactory as only
	 * on remoteLauncher is kept for the application.
	 * 
	 * @param context
	 */
	protected RemoteLauncher(ServletContext context) {
		biomajRoot = context.getInitParameter("BIOMAJ_ROOT");
		
		authType = context.getInitParameter("SSH_AUTH_TYPE");
		userName = context.getInitParameter("SSH_LOGIN");
		password = context.getInitParameter("SSH_PASSWD");
		keyFilePath = context.getInitParameter("SSH_KEY_PATH");
		passPhrase = context.getInitParameter("SSH_PASSPHRASE");
		
		String value = context.getInitParameter("SSH_HOSTS");
		if (value != null && !value.trim().isEmpty()) {
			String[] hosts = value.split(",");
			for (String host : hosts) {
				if (!host.trim().isEmpty()) {
					hostsAvailability.add(new HostAvailable(host.trim(), true));
				}
			}
		}
		
	}
	
	/**
	 * Returns the first available host in the list.
	 * If no hosts are available, return the next one in the list.
	 * An available host in an host where no updates are currently running.
	 * 
	 * @param nextOne	If true, return the next host in the list, regardless of its availability.
	 * 					This parameter is useful to force the connection to a new host if the connection
	 * 					failed for the previous one.
	 * 
	 * @return
	 */
	private synchronized String getHost(boolean nextOne) {
		if (!nextOne) {
			for (int i = 0; i < hostsAvailability.size(); i++) {
				if (hostsAvailability.get(i).available) {
					currentHostPosition = i;
					return hostsAvailability.get(i).host;
				}
			}
		}
		if (hostsAvailability.size() > 0) {
			// Loop with max = host count
			currentHostPosition = (currentHostPosition + 1) % hostsAvailability.size();
			String host = hostsAvailability.get(currentHostPosition).host;
			return host;
		}
		return "";
	}
	
	/**
	 * Opens an SSH session with command to execute.
	 * 
	 * @param command
	 * @return
	 */
	private synchronized SessionInfo connect(String command) {
		int tries = 0;
		
		while (tries++ < hostsAvailability.size()) {
			// If we loop, it's because we got ssh exception. Otherwise, method would have returned.
			// So we try to connect to a different host until it works or we run out of hosts.
			try {
				JSch sshClient = new JSch();
				String host = getHost(tries != 1); // If more than one try, then connection failed. Try to connect to another host.
				if (!host.isEmpty()) {
					log.debug("Trying to connect to : " + userName + "@" + host);
					Session session = sshClient.getSession(userName, host, 22);
					
					if (authType.equals(KEY_AUTH)) {
						if (keyFilePath != null && passPhrase != null && !passPhrase.trim().isEmpty()) {
							log.debug("ssh authentication with key and passphrase");
							if (new File(keyFilePath).exists()) {
								sshClient.addIdentity(keyFilePath, passPhrase);
							} else {
								log.error("Key does not exist : " + keyFilePath);
								return null;
							}
						} else if (keyFilePath != null) {
							log.debug("ssh authentication with key and no passphrase");
							sshClient.addIdentity(keyFilePath);
						} else {
							log.error("Invalid key : " + keyFilePath);
							return null;
						}
					} else if (authType.equals(LOGIN_AUTH)) {
						session.setPassword(password);
					}
					
					session.setPassword(password);
					session.setConfig("StrictHostKeyChecking", "no");
					session.connect();
					Channel channel = session.openChannel("exec");
					
					((ChannelExec) channel).setCommand(command);
					
//					channel.setOutputStream(System.out);
					
					System.out.println("Let's go");
					channel.connect(connectionTimeout);
					
					log.debug("Connected to " + host + " with user " + userName);
					setHostAvailable(host, false);
					
					return new SessionInfo(session, channel, host);
				} else {
					log.error("No ssh host.");
					return null;
				}
			} catch (JSchException e) {
				log.error(e);
			}
		}
		return null;
	}
	
	private void disconnect(SessionInfo sshInfo) {
		sshInfo.channel.disconnect();
		sshInfo.session.disconnect();
	}
	
	
	/**
	 * Exports environment variables needed for biomaj to work.
	 * 
	 * @return
	 */
	private String getEnvCommand() {
		String command = "source " + biomajRoot + "/bin/env.sh;";
		return command;
	}
	
	/**
	 * Connects to the server and launches the given command.
	 * 
	 * @param command
	 * @return
	 */
	private boolean launchRemoteCommand(String command) {
		log.debug("Remote command : " + command);
		SessionInfo sshInfo = null;
		if ((sshInfo = connect(command)) != null) {
			while (!sshInfo.channel.isClosed()) {
				try {
					TimeUnit.MILLISECONDS.sleep(200);
				} catch (InterruptedException ex) {
					ex.printStackTrace();
				}
			}
			disconnect(sshInfo);
			setHostAvailable(sshInfo.host, true);
			return true;
		} else {
			log.error("Connection failed !");
			return false;
		}
	}
	
	private synchronized void setHostAvailable(String host, boolean available) {
		for (HostAvailable h : hostsAvailability) {
			if (h.host.equals(host)) {
				h.available = available;
				break;
			}
		}
	}
	
	@Override
	public boolean rebuild(String bankName) {
		String command = getEnvCommand() + "nohup nohup bash $BIOMAJ_ROOT/bin/biomaj.sh --rebuild " + bankName + BACKGROUND_COMMAND;
		return launchRemoteCommand(command);
	}

	@Override
	public boolean removeAll(String bankName, boolean noHistory, boolean keepProd) {
		String command = "";
		if (noHistory)
			command = getEnvCommand() + "nohup bash $BIOMAJ_ROOT/bin/biomaj.sh --remove " + bankName + " --all-no-history";
		else
			command = getEnvCommand() + "nohup bash $BIOMAJ_ROOT/bin/biomaj.sh --remove " + bankName + " --all";
		
		if (keepProd)
			command += " --keep-dir-prod";
		
		command += BACKGROUND_COMMAND;
		
		return launchRemoteCommand(command);
	}

	@Override
	public boolean removeDirs(List<String> paths, String bankName, boolean keepProd) {
		StringBuilder dirs = new StringBuilder();
		for (String p : paths) {
			dirs.append(p + " ");
		}
		String command = getEnvCommand() + "nohup bash $BIOMAJ_ROOT/bin/biomaj.sh --remove " + bankName + " --paths " + dirs.toString().trim();
		if (keepProd)
			command += " --keep-dir-prod";
		
		command += BACKGROUND_COMMAND;
		
		return launchRemoteCommand(command);
	}

	@Override
	public boolean startUpdate(String bankName) {
		String command = getEnvCommand() + "nohup bash $BIOMAJ_ROOT/bin/biomaj.sh --update " + bankName + BACKGROUND_COMMAND;
		return launchRemoteCommand(command);
	}

	@Override
	public boolean startUpdateFromScratch(String bankName) {
		String command = getEnvCommand() + "nohup bash $BIOMAJ_ROOT/bin/biomaj.sh --update " + bankName + " --fromscratch" + BACKGROUND_COMMAND;
		return launchRemoteCommand(command);
	}

	@Override
	public boolean startUpdateNew(String bankName) {
		String command = getEnvCommand() + "nohup bash $BIOMAJ_ROOT/bin/biomaj.sh --update " + bankName + " --new" + BACKGROUND_COMMAND;
		return launchRemoteCommand(command);
	}

	@Override
	public boolean stopUpdate(String bankName) {
		return false;
	}
	
	public static void main(String[] args) {
		System.out.println(Utils.millisToHumanReadableDuration(581273900));
	}

}
