/*
 *  Copyright 2015, Yahoo Inc.
 *  Copyrights licensed under the Apache License.
 *  See the accompanying LICENSE file for terms.
 */
package com.yahoo.dba.tools.myperfserver;

import java.io.File;
import java.io.FileInputStream;
import java.lang.management.RuntimeMXBean;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.servlet.ServletContainerInitializer;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.tomcat.InstanceManager;
import org.apache.tomcat.SimpleInstanceManager;
import org.eclipse.jetty.annotations.ServletContainerInitializersStarter;
import org.eclipse.jetty.apache.jsp.JettyJasperInitializer;
import org.eclipse.jetty.jsp.JettyJspServlet;
import org.eclipse.jetty.plus.annotation.ContainerInitializer;
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.webapp.WebAppContext;

public class App {
	public static String PID_FILE = "framework.pid";
	private static int pid = -1;

	private String configFilePath;
	private int port = 9090;
	private String contextPath = "/";
	private String logDirectoryPath = "logs";
	private String warFile;
	private String workDirectoryPath = "work";
	private String jettyHome; // it should have directory webapps which has our
								// warFile
	private boolean useHttps = false;
	private String certKeyStorePath;
	private String certKeyStorePassword;
	
	private Server jettyServer; // jetty server
	private URI serverURI;
	private String shutdownFile = "myserver.shutdown";

	/* -p --port 9090 
	 * -c --webcontextroot webapps 
	 * -l --logpath logpath 
	 * -w --war webapp war file name
	 * -f --config config file path, if provided
	 *     configuration file contains name=value pairs (java property file). If any configuration parameter is specified in command line,
	 *     it will not be overwritten by the value in file. The main purpose to use file is to avoid ssl cert store password
	 *     to be exposed in command line. The following list the configurations:
	 *     jettyHome
	 *     useHttps: value yes/y/no/n/true/false
	 *     port
	 *     webcontextroot
	 *     workdir
	 *     logpath
	 *     warfile
	 *     certKeyStorePath
	 *     certKeyStorePassword
	 */
	public static void main(String[] args) throws Exception {
		App myServer = new App();
		CommandLineParser parser = new GnuParser();
		Options options = getAvaliableCommandLineOptions();
		System.out
				.println(new Date()
						+ " Usage: java -classpath ... com.yahoo.dba.tools.myperfserver.App -f config_file_path -j jettyhome "
						+ "-p port -c webcontextroot -k workingDir -l logpath -w war_file");
		readOptionsFromCommandLine(args, parser, options, myServer);
		System.setProperty("logPath", myServer.getLogDirectoryPath());
		PID_FILE = myServer.getWarFile().substring(0,
				myServer.getWarFile().indexOf('.'))
				+ ".pid";
		checksRunningOfAnotherServerInstance();
		//for https, we have to use https for jQuery
		System.setProperty("url_protocl", myServer.useHttps?"https":"http");
		runServer(myServer);
	}

	private static void runServer(App myServer) throws Exception,
			InterruptedException {
		if (myServer.startServer()) {
			pid = getPid();
			writePid();
			myServer.waitForInterrupt();
			removePid();
		} else
			System.out.println("Server not started.");
	}

	private static void checksRunningOfAnotherServerInstance() {
		int historyPid = getHistoryPid();
		if (historyPid >= 0) {
			System.out
					.println(new Date()
							+ " *************************** WARNING *********************");
			System.out
					.println(PID_FILE
							+ " exists. Possibly another instance is still running. PID = "
							+ historyPid);
			System.out
					.println(new Date()
							+ " *************************** WARNING *********************");
		}
	}

	private static void readOptionsFromCommandLine(String[] args,
			CommandLineParser parser, Options options, App myServer) {
		try {
			CommandLine commandLine = parser.parse(options, args);
			checkCommandLineContainAvaliableOptions(myServer, commandLine);
		} catch (ParseException exp) {
			System.out.println("Unexpected exception:" + exp.getMessage());
		}
	}

	private static void checkCommandLineContainAvaliableOptions(App myServer,
			CommandLine line) {
		if(line.hasOption("f")){
			
			String argument = line.getOptionValue("f");
			if(argument != null  && !argument.trim().isEmpty())
			{
				System.out.println("Use configuration file: " + argument);
				//if we have configuration file, load it first
				//and allow command line to overwrite any specified
				myServer.setConfigFilePath(argument.trim());
				myServer.parseConfigurationFile();
			}
		}
		
		if (line.hasOption("p")) {
			try {
				myServer.setPort(Short.parseShort(line.getOptionValue("p")));
			} catch (Exception ex) {
			}
		}
		if (line.hasOption("c")) {
			String argument = line.getOptionValue("c");
			if (isOptionArgumentValid(argument))
				myServer.setContextPath(argument);
		}
		if (line.hasOption("l")) {
			String argument = line.getOptionValue("l");
			if (isOptionArgumentValid(argument))
				myServer.setLogDirectoryPath(argument);
		}
		if (line.hasOption("w")) {
			String argument = line.getOptionValue("w");
			if (isOptionArgumentValid(argument))
				myServer.setWarFile(argument);
		}
		if (line.hasOption("k")) {
			String argument = line.getOptionValue("k");
			myServer.setWorkDirectoryPath(argument);
		}
		if (line.hasOption("j")) {
			String argument = line.getOptionValue("j");
			if (isOptionArgumentValid(argument))
				myServer.setJettyHome(argument);
		}
	}

	private static boolean isOptionArgumentValid(String argument) {
		return (argument != null) && !(argument.isEmpty());
	}

	private static Options getAvaliableCommandLineOptions() {
		Options options = new Options();
		options.addOption("f", "config", true, "Configuration file path, no default.");
		options.addOption(
				"j",
				"jettyHome",
				true,
				"Jetty home, if not set, check system property jetty.home, then default to current Dir");
		options.addOption("p", "port", true,
				"http server port, default to 9090.");
		options.addOption("c", "webcontextroot", true,
				"web app url root context, defaul to /");
		options.addOption("l", "logpath", true,
				"log path, default to current directory.");
		options.addOption("w", "warfile", true,
				"war file name, default to myperf.war.");
		options.addOption("k", "workdir", true,
				"work directory for jetty, default to current dir.");
		return options;
	}

	public App() {
		this.jettyHome = System.getProperty("jetty.home", "."); // set default jetty.home
	}
	// This should be invoked before using command line options
	public void parseConfigurationFile() throws RuntimeException{
		String path = this.getConfigFilePath();
		if(path == null || path.isEmpty())
		{
			System.out.println(new Date() + ": No configuration file specified, use command line only.");
			return;
		}
		
		try{
			java.util.Properties props = new java.util.Properties();
			props.load(new FileInputStream(path));
			String useHttpsStr = props.getProperty("useHttps", "no");
			useHttpsStr = useHttpsStr.trim().toLowerCase();
			if(useHttpsStr.equals("yes") || useHttpsStr.equals("y") || useHttpsStr.equals("true"))
				this.setUseHttps(true);
			else
				this.setUseHttps(false);
			if(this.useHttps){
				this.setCertKeyStorePath(props.getProperty("certKeyStorePath", null));
				this.setCertKeyStorePassword(props.getProperty("certKeyStorePassword", null));
			}
			String prop = props.getProperty("jettyHome", null);
			if(prop!=null && !prop.isEmpty())
				this.setJettyHome(prop.trim());
			prop  = props.getProperty("port", null);
			if(prop!=null && !prop.isEmpty())
			{
				try{this.setPort(Integer.parseInt(prop.trim()));}catch(Exception ex){}
			}
			prop = props.getProperty("webcontextroot", null);
			if(prop!=null && !prop.isEmpty())
			{
				this.setContextPath(prop.trim());
			}
			
			prop = props.getProperty("workdir", null);
			if(prop!=null && !prop.isEmpty())
			{
				this.setWorkDirectoryPath(prop.trim());
			}

			prop = props.getProperty("warfile", null);
			if(prop!=null && !prop.isEmpty())
			{
				this.setWarFile(prop.trim());
			}
			
			prop = props.getProperty("logpath", null);
			if(prop!=null && !prop.isEmpty())
			{
				this.setLogDirectoryPath(prop.trim());
			}
		}catch(Exception ex)
		{
			System.out.println(new Date() + ": Failed to load configuration file " + path);
			ex.printStackTrace();
			throw new RuntimeException(ex);
		}
	}
	public boolean startServer() throws Exception {
		removeShutdownFile();
		File workDirectory = new File(this.getWorkDirectoryPath());
		File logDirectory = new File(this.getLogDirectoryPath());
		String deployedApplicationPath = this.getJettyHome()
				+ File.separatorChar + "webapps" + File.separatorChar
				+ this.getWarFile();
		if (!(isMeetingRequirementsToRunServer(workDirectory, logDirectory,
				deployedApplicationPath)))
			return false;
		WebAppContext deployedApplication = createDeployedApplicationInstance(
				workDirectory, deployedApplicationPath);

		// server = new Server(port);
		jettyServer = new Server();
		ServerConnector connector = this.isUseHttps()?this.sslConnector():connector();
		jettyServer.addConnector(connector);
		jettyServer.setHandler(deployedApplication);
		jettyServer.start();
		// server.join();

		// dump server state
		System.out.println(jettyServer.dump());
		this.serverURI = getServerUri(connector);
		return true;
	}

	private WebAppContext createDeployedApplicationInstance(File workDirectory,
			String deployedApplicationPath) {
		WebAppContext deployedApplication = new WebAppContext();
		deployedApplication.setContextPath(this.getContextPath());
		deployedApplication.setWar(deployedApplicationPath);
		deployedApplication.setAttribute("javax.servlet.context.tempdir",
				workDirectory.getAbsolutePath());
		deployedApplication
				.setAttribute(
						"org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern",
						".*/[^/]*servlet-api-[^/]*\\.jar$|.*/javax.servlet.jsp.jstl-.*\\.jar$|.*/.*taglibs.*\\.jar$");
		deployedApplication.setAttribute(
				"org.eclipse.jetty.containerInitializers", jspInitializers());
		deployedApplication.setAttribute(InstanceManager.class.getName(),
				new SimpleInstanceManager());
		deployedApplication.addBean(new ServletContainerInitializersStarter(
				deployedApplication), true);
		// webapp.setClassLoader(new URLClassLoader(new
		// URL[0],App.class.getClassLoader()));
		deployedApplication.addServlet(jspServletHolder(), "*.jsp");
		return deployedApplication;
	}

	private boolean isMeetingRequirementsToRunServer(File workDirectory,
			File logDirectory, String deployedApplicationPath) {
		if (!(createValidWorkDirectory(workDirectory)))
			return false;
		if (!(createValidLogDirectory(logDirectory)))
			return false;
		System.out.println(new Date() + " Use jetty home "
				+ this.getJettyHome());
		if (!(new File(deployedApplicationPath).exists())) {
			System.out.println(new Date()
					+ " Cannot find war file at location "
					+ deployedApplicationPath);
			return false;
		}
		System.out.println(new Date() + " Use war file "
				+ deployedApplicationPath);
		System.out.println(new Date() + " Use port " + this.getPort()
				+ ", context " + this.getContextPath() + ".");
		return true;
	}

	private boolean createValidLogDirectory(File logDirectory) {
		if (!logDirectory.exists()) {
			System.out.println(new Date() + " Create log directory at "
					+ logDirectory.getAbsolutePath());
			logDirectory.mkdir();
		}
		if (!logDirectory.exists()) {
			System.out.println(new Date() + " Invalid log directory "
					+ logDirectory.getAbsolutePath());
			return false;
		}
		System.out.println(new Date() + " Use log directory "
				+ logDirectory.getAbsolutePath());
		return true;
	}

	private boolean createValidWorkDirectory(File workDirectory) {
		if (!workDirectory.exists()) {
			System.out.println(new Date() + " Create working dir at "
					+ workDirectory.getAbsolutePath());
			workDirectory.mkdir();
		}
		if (!workDirectory.exists()) {
			System.out.println(new Date() + " Invalid working directory "
					+ workDirectory.getAbsolutePath());
			return false;
		}
		System.out.println(new Date() + " Use working directory "
				+ workDirectory.getAbsolutePath());
		return true;
	}

	private ServerConnector connector() {
		ServerConnector connector = new ServerConnector(jettyServer);
		connector.setPort(port);
		return connector;
	}

	/**
	 * Create ssl connector if https is used
	 * @return
	 */
	private ServerConnector sslConnector() {
		HttpConfiguration http_config = new HttpConfiguration();
		http_config.setSecureScheme("https");
		http_config.setSecurePort(this.getPort());
		
		HttpConfiguration https_config = new HttpConfiguration(http_config);
		https_config.addCustomizer(new SecureRequestCustomizer());
		
		SslContextFactory sslContextFactory = new SslContextFactory(this.getCertKeyStorePath());
		sslContextFactory.setKeyStorePassword(this.getCertKeyStorePassword());
		//exclude weak ciphers
		sslContextFactory.setExcludeCipherSuites("^.*_(MD5|SHA|SHA1)$");
		//only support tlsv1.2
		sslContextFactory.addExcludeProtocols("SSL", "SSLv2", "SSLv2Hello", "SSLv3", "TLSv1", "TLSv1.1");
		
		ServerConnector connector = new ServerConnector(jettyServer, 
				new SslConnectionFactory(sslContextFactory, "http/1.1"),
				new HttpConnectionFactory(https_config));
		connector.setPort(this.getPort());
		connector.setIdleTimeout(50000);
		return connector;
	}
	
	private URI getServerUri(ServerConnector connector)
			throws URISyntaxException {
		String scheme = "http";
		for (ConnectionFactory connectFactory : connector
				.getConnectionFactories()) {
			if (connectFactory.getProtocol().startsWith("SSL-http")) {
				scheme = "https";
			}
		}
		String host = connector.getHost();
		if (host == null) {
			try{
				host = InetAddress.getLocalHost().getHostName();
			}catch(Exception ex){}
		}
		if (host == null){
			host = "localhost";			
		}
		int myport = connector.getLocalPort();
		serverURI = new URI(String.format("%s://%s:%d", scheme, host, myport));
		System.out.println(new Date() + " Server URI: " + serverURI + this.contextPath);
		return serverURI;
	}

	public void stop() throws Exception {
		if (jettyServer != null)
			jettyServer.stop();
	}

	/**
	 * Get process id of current running starloader
	 * 
	 * @return OS process id
	 */
	public static int getPid() {
		try {
			RuntimeMXBean runtime = java.lang.management.ManagementFactory
					.getRuntimeMXBean();
			String jvmName = runtime.getName();
			String ss = jvmName.substring(0, jvmName.indexOf('@'));
			return Integer.parseInt(ss);
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		return -1;// no valid number
	}

	private static void writePid() {
		if (pid < 0)
			return;// not supported
		try {
			java.io.FileWriter fw = new java.io.FileWriter(PID_FILE);
			fw.write(String.valueOf(pid));
			fw.close();
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}

	private static int getHistoryPid() {
		try {
			java.io.BufferedReader fr = new java.io.BufferedReader(
					new java.io.FileReader(PID_FILE));
			String line = fr.readLine();
			fr.close();
			return Integer.parseInt(line);
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		return -1;
	}

	private static void removePid() {
		try {
			new File(PID_FILE).delete();
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}

	private void removeShutdownFile() {
		try {
			new File(this.shutdownFile).delete();
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}

	private List<ContainerInitializer> jspInitializers() {
		JettyJasperInitializer sci = new JettyJasperInitializer();
		ContainerInitializer initializer = new ContainerInitializer(
				(ServletContainerInitializer) sci, null);
		List<ContainerInitializer> initializers = new ArrayList<ContainerInitializer>();
		initializers.add(initializer);
		return initializers;
	}

	private ServletHolder jspServletHolder() {
		ServletHolder holderJsp = new ServletHolder("jsp",
				JettyJspServlet.class);
		holderJsp.setInitOrder(0);
		holderJsp.setInitParameter("logVerbosityLevel", "DEBUG");
		holderJsp.setInitParameter("fork", "false");
		holderJsp.setInitParameter("xpoweredBy", "false");
		holderJsp.setInitParameter("compilerTargetVM", "1.8");
		holderJsp.setInitParameter("compilerSourceVM", "1.8");
		holderJsp.setInitParameter("keepgenerated", "true");
		return holderJsp;
	}

	public int getPort() {
		return port;
	}

	public void setPort(int port) {
		this.port = port;
	}

	public String getContextPath() {
		return contextPath;
	}

	public void setContextPath(String contextPath) {
		this.contextPath = contextPath;
	}

	public String getLogDirectoryPath() {
		return logDirectoryPath;
	}

	public void setLogDirectoryPath(String logPath) {
		this.logDirectoryPath = logPath;
	}

	public String getWarFile() {
		return warFile;
	}

	public void setWarFile(String warFile) {
		this.warFile = warFile;
	}

	public String getWorkDirectoryPath() {
		return workDirectoryPath;
	}

	public void setWorkDirectoryPath(String workDir) {
		this.workDirectoryPath = workDir;
	}

	public String getJettyHome() {
		return jettyHome;
	}

	public void setJettyHome(String jettyHome) {
		this.jettyHome = jettyHome;
	}

	public void waitForInterrupt() throws InterruptedException {
		waitToShutdownSignal();
		shutdown();
		removeShutdownFile();
		System.out.println(new Date() + " Wait for shutdown to complete");
		if (jettyServer != null)
			jettyServer.join(); // wait for it to complete
		System.out.println(new Date() + " Shutdown is completed.");
	}

	private void waitToShutdownSignal() {
		while (true) {
			// sleep 5 seconds, then check shutdown file
			try {
				Thread.sleep(5000);
				if (new File(this.shutdownFile).exists()) {
					// we are suppose to shutdown
					System.out.println(new Date()
							+ " Receive soft shutdown signal.");
					break;
				}
			} catch (Throwable th) {
				System.out.println(new Date() + " Receive shutdown signal.");
				break;
			}
		}
	}

	private void shutdown() {
		try { // shutdown
			System.out.println(new Date() + " Shutdown server");
			stop();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public String getConfigFilePath() {
		return configFilePath;
	}

	public void setConfigFilePath(String configFilePath) {
		this.configFilePath = configFilePath;
	}

	public boolean isUseHttps() {
		return useHttps;
	}

	public void setUseHttps(boolean useHttps) {
		this.useHttps = useHttps;
	}

	public String getCertKeyStorePath() {
		return certKeyStorePath;
	}

	public void setCertKeyStorePath(String certKeyStorePath) {
		this.certKeyStorePath = certKeyStorePath;
	}

	public String getCertKeyStorePassword() {
		return certKeyStorePassword;
	}

	public void setCertKeyStorePassword(String certKeyStorePassword) {
		this.certKeyStorePassword = certKeyStorePassword;
	}
}