package azkaban.webapp;

import java.io.File;
import java.lang.reflect.Constructor;
import java.util.Arrays;

import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;

import org.apache.log4j.Logger;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.log.Log4JLogChute;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.bio.SocketConnector;
import org.mortbay.jetty.security.SslSocketConnector;
import org.mortbay.jetty.servlet.Context;
import org.mortbay.jetty.servlet.DefaultServlet;
import org.mortbay.jetty.servlet.ServletHolder;

import azkaban.user.UserManager;
import azkaban.user.XmlUserManager;
import azkaban.utils.Props;
import azkaban.utils.Utils;
import azkaban.webapp.servlet.AzkabanServletContextListener;
import azkaban.webapp.servlet.admin.InitialSetupServlet;
import azkaban.webapp.session.SessionCache;

public class AzkabanAdminServer implements AzkabanServer {
	private static final Logger logger = Logger.getLogger(AzkabanAdminServer.class);
	private static AzkabanAdminServer app;
	private static final int NUM_CONNECTIONS = 10;
	private static final String AZKABAN_DEFAULT_ADMIN_DIR = "admin";
	private static final String AZKABAN_DEFAULT_ADMIN_PROPERTIES = "admin.properties";
	private static final String AZKABAN_DEFAULT_WEB_DIR = "web";
	private static final String AZKABAN_DEFAULT_PLUGIN_DIR = "pluginlib";
	private static final int AZKABAN_DEFAULT_ADMIN_PORT = 12233;
	
	private static final String USER_MANAGER_CLASS_PARAM = "user.manager.class";
	private final VelocityEngine velocityEngine;
	private final SessionCache sessionCache;
	private static Server server;
	private String pluginLibDirectory;
	private File adminPropertiesFile;

	private Props props;
	
	public AzkabanAdminServer(Props props, File propFile, boolean initialSetup) throws Exception {
		this.props = props;
		velocityEngine = configureVelocityEngine(true);
		sessionCache = initialSetup ? null : new SessionCache(props);
		adminPropertiesFile = propFile;
		
		pluginLibDirectory = props.get("pluginLibDirectory");
	}
	
	public File getPropFile() {
		return adminPropertiesFile;
	}
	
	/**
	 * Creates and configures the velocity engine.
	 * 
	 * @param devMode
	 * @return
	 */
	private VelocityEngine configureVelocityEngine(final boolean devMode) {
		VelocityEngine engine = new VelocityEngine();
		engine.setProperty("resource.loader", "classpath");
		engine.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
		engine.setProperty("classpath.resource.loader.cache", !devMode);
		engine.setProperty("classpath.resource.loader.modificationCheckInterval", 5L);
		engine.setProperty("resource.manager.logwhenfound", false);
		engine.setProperty("input.encoding", "UTF-8");
		engine.setProperty("output.encoding", "UTF-8");
		engine.setProperty("directive.set.null.allowed", true);
		engine.setProperty("resource.manager.logwhenfound", false);
		engine.setProperty("velocimacro.permissions.allow.inline", true);
		engine.setProperty("velocimacro.library.autoreload", devMode);
		engine.setProperty("velocimacro.library", "/azkaban/webapp/servlet/velocity/macros.vm");
		engine.setProperty("velocimacro.permissions.allow.inline.to.replace.global", true);
		engine.setProperty("velocimacro.arguments.strict", true);
		engine.setProperty("runtime.log.invalid.references", devMode);
		engine.setProperty("runtime.log.logsystem.class", Log4JLogChute.class);
		engine.setProperty("runtime.log.logsystem.log4j.logger", Logger.getLogger("org.apache.velocity.Logger"));
		engine.setProperty("parser.pool.size", 3);
		return engine;
	}
	
	/**
	 * Azkaban using Jetty
	 * 
	 * @param args
	 */
	public static void main(String[] args) throws Exception {
		OptionParser parser = new OptionParser();

		OptionSpec<Integer> portArg = parser
				.acceptsAll(Arrays.asList("p", "port"), "The port for azkaban.")
				.withRequiredArg()
				.describedAs("port").ofType(Integer.class);
		
		OptionSpec<String> adminDirectory = parser
				.acceptsAll(Arrays.asList("d", "dir"), "The admin directory")
				.withRequiredArg()
				.describedAs("dir").ofType(String.class);

		OptionSpec<String> webDirectory = parser
				.acceptsAll(Arrays.asList("w", "web"), "The web resource directory")
				.withRequiredArg()
				.describedAs("web").ofType(String.class);
		
		OptionSpec<String> pluginLibDirectory = parser
				.acceptsAll(Arrays.asList("l", "pluginlib"), "The plugin lib directory")
				.withRequiredArg()
				.describedAs("plugin").ofType(String.class);
		
		OptionSet options = parser.parse(args);

		String adminDir = options.has(adminDirectory) ? options.valueOf(adminDirectory) : AZKABAN_DEFAULT_ADMIN_DIR;
		int port = options.has(portArg) ? options.valueOf(portArg) : AZKABAN_DEFAULT_ADMIN_PORT;
		String webDir = options.has(webDirectory) ? options.valueOf(webDirectory) : AZKABAN_DEFAULT_WEB_DIR;
		String pluginLib = options.has(pluginLibDirectory) ? options.valueOf(pluginLibDirectory) : AZKABAN_DEFAULT_PLUGIN_DIR;
		
		Props props = null;
		server = new Server();
		File adminDirFile = new File(adminDir);
		if (!adminDirFile.exists()) {
			adminDirFile.mkdirs();
		}
		File adminPropertiesFile = new File(adminDirFile, AZKABAN_DEFAULT_ADMIN_PROPERTIES);

		boolean secureMode = false;
		if (adminPropertiesFile.exists()) {
			props = new Props(null, adminPropertiesFile);
			if (props.containsKey("mysql.host") && props.containsKey("user.manager.class")) {
				secureMode = true;
			}
		}
		else {
			props = new Props();
		}

		if (secureMode) {
			app = new AzkabanAdminServer(props, adminPropertiesFile, false);
			
			SslSocketConnector secureConnector = new SslSocketConnector();
			secureConnector.setPort(port);
			server.addConnector(secureConnector);
		}
		else {
			logger.info("Server settings not found. Running setup wizard.");
			props.put("pluginLibDirectory", pluginLib);
			app = new AzkabanAdminServer(props, adminPropertiesFile, true);
			SocketConnector connector = new SocketConnector();
			connector.setPort(port);
			server.addConnector(connector);
		}
		
		Context root = new Context(server, "/", Context.SESSIONS);
		String staticDir = webDir;
		root.setResourceBase(staticDir);
		
		root.setAttribute(AzkabanServletContextListener.AZKABAN_SERVLET_CONTEXT_KEY, app);
		ServletHolder executorHolder = new ServletHolder(new InitialSetupServlet());
		root.addServlet(executorHolder, "/");
	
		ServletHolder staticServlet = new ServletHolder(new DefaultServlet());
		root.addServlet(staticServlet, "/css/*");
		root.addServlet(staticServlet, "/js/*");
		root.addServlet(staticServlet, "/images/*");
		root.addServlet(staticServlet, "/favicon.ico");
		
		try {
			server.start();
		} 
		catch (Exception e) {
			logger.warn(e);
			Utils.croak(e.getMessage(), 1);
		}
		
		Runtime.getRuntime().addShutdownHook(new Thread() {
			public void run() {
				logger.info("Shutting down http server...");
				try {
					app.stopServer();
				} catch (Exception e) {
					logger.error("Error while shutting down http server.", e);
				}
				logger.info("kk thx bye.");
			}
		});
	}
	
	public void stopServer() throws Exception {
		server.stop();
		server.destroy();
	}

	@Override
	public Props getServerProps() {
		return props;
	}

	@Override
	public VelocityEngine getVelocityEngine() {
		return velocityEngine;
	}

	@Override
	public SessionCache getSessionCache() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public UserManager getUserManager() {
		// TODO Auto-generated method stub
		return null;
	}
	
	public String getPluginLibDirectory() {
		return pluginLibDirectory;
	}

	private UserManager loadUserManager(Props props) {
		Class<?> userManagerClass = props.getClass(USER_MANAGER_CLASS_PARAM, null);
		logger.info("Loading user manager class " + userManagerClass.getName());
		UserManager manager = null;

		if (userManagerClass != null && userManagerClass.getConstructors().length > 0) {

			try {
				Constructor<?> userManagerConstructor = userManagerClass.getConstructor(Props.class);
				manager = (UserManager) userManagerConstructor.newInstance(props);
			} 
			catch (Exception e) {
				logger.error("Could not instantiate UserManager "+ userManagerClass.getName());
				throw new RuntimeException(e);
			}

		} 
		else {
			manager = new XmlUserManager(props);
		}

		return manager;
	}
}
