package knives.web;

import java.io.IOException;
import java.io.Writer;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.function.Consumer;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Path;
import javax.ws.rs.ext.Provider;

import knives.utils.Classes;
import knives.utils.Errors;
import knives.utils.Resources;
import knives.utils.Runtimes;
import knives.utils.Splitters;
import knives.web.jersey.GuiceBridgeFeature;
import knives.web.jersey.JsonExceptionMapper;

import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.Connector;
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.server.handler.ErrorHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.servlet.ErrorPageErrorHandler;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.ServerProperties;
import org.glassfish.jersey.servlet.ServletContainer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Queues;
import com.google.common.util.concurrent.AbstractService;
import com.google.inject.Injector;

/**
 * @see http://www.eclipse.org/jetty/documentation/current/embedded-examples.html
 *      https://github.com/kyle-ilantzis/simple-jersey-w-jetty-n-guice
 * @author June
 */
public class EmbeddedJetty extends AbstractService {

	private static final Logger log = LoggerFactory.getLogger(EmbeddedJetty.class);

	private static final ErrorHandler emtpyErrorPage = new ErrorPageErrorHandler() {

		@Override
		protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message)
				throws IOException {
			// XXX 에러 결과를 출력 하지 않기 위해 처리를 수행하지 않는다.
			// super.handleErrorPage(request, writer, code, message);
		}
	};

	private final Server server;

	private final QueuedThreadPool pool;
	private final HandlerCollection handlers;

	private final HttpConfiguration httpConfig;

	/**
	 * @see http://wiki.eclipse.org/Jetty/Howto/High_Load#Jetty_Tuning
	 */
	private int minThreads = Runtimes.CPU_CORE_COUNT;
	private int maxThreads = 500;
	private int capability = 60 * 100;// 100 rps

	private int idleTimeout = 1000 * 60;// 60 s
	private int stopTimeout = 1000 * 30;// 30 s

	private int connTimeout = 1000 * 3;// 3 s

	public EmbeddedJetty(int port) {
		this(false);
		addHttpConnector(port);
	}

	public EmbeddedJetty(boolean isDebugMode) {

		ArrayBlockingQueue<Runnable> queue = Queues.newArrayBlockingQueue(this.capability);
		pool = new QueuedThreadPool(this.maxThreads, this.minThreads, this.idleTimeout, queue);
		{
			pool.setName("jetty");
			pool.setDetailedDump(isDebugMode);
			pool.setStopTimeout(this.stopTimeout);
		}

		this.server = new Server(pool);
		this.server.setStopAtShutdown(true);

		// set default error page to empty
		if (false == isDebugMode) {
			this.server.addBean(emtpyErrorPage);
			emtpyErrorPage.setServer(this.server);

			this.server.setDumpAfterStart(false);
			this.server.setDumpBeforeStop(false);
		}

		this.httpConfig = new HttpConfiguration();
		{
			this.httpConfig.setSendServerVersion(false);
			this.httpConfig.setSendXPoweredBy(false);
			this.httpConfig.setSendDateHeader(true);
		}

		this.handlers = new HandlerCollection();
		this.server.setHandler(handlers);
	}

	Connector createHttpConnector(int port) {
		ConnectionFactory factory = new HttpConnectionFactory(this.httpConfig);
		return connectionWith(new ServerConnector(this.server, factory), port);
	}

	public void addHttpConnector(int port) {
		this.server.addConnector(createHttpConnector(port));
	}

	Connector createHttpsConnector(int port, SslContext context) {

		SslContextFactory sslContextFactory = new SslContextFactory();
		{
			sslContextFactory.setKeyStorePath(context.getKeystore());
			sslContextFactory.setKeyStorePassword(context.getObfKeypass());

			sslContextFactory.setTrustStorePath(context.getTruststore());
			sslContextFactory.setTrustStorePassword(context.getObfTrustpass());

			sslContextFactory.setKeyManagerPassword(context.getObfMgrpass());

			sslContextFactory.setNeedClientAuth(false);
		}

		HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig);
		{
			httpsConfig.setSecureScheme("https");
			httpsConfig.setSecurePort(port);
			httpsConfig.addCustomizer(new SecureRequestCustomizer());
		}

		ConnectionFactory[] factories = new ConnectionFactory[] {
				//
				new SslConnectionFactory(sslContextFactory, "http/1.1"),//
				new HttpConnectionFactory(httpsConfig) //
		};

		return connectionWith(new ServerConnector(this.server, factories), port);
	}

	public void addHttpsConnector(int port, SslContext context) {
		this.server.addConnector(createHttpsConnector(port, context));
	}

	ServerConnector connectionWith(ServerConnector conn, int port) {
		conn.setPort(port);
		conn.setIdleTimeout(this.connTimeout);
		conn.setReuseAddress(true);
		return conn;
	}

	// /**
	// * add configuration for common access log.
	// */
	// public void addAccessLogs() {
	//
	// RequestLogHandler logHandler = new RequestLogHandler();
	// {
	// AsyncNCSARequestLog logger = new AsyncNCSARequestLog("logs/access.log");
	// {
	// logger.setRetainDays(1);
	// logger.setAppend(true);
	// logger.setExtended(false);
	// logger.setLogTimeZone("Asia/Seoul");
	// logger.setLogLocale(Locale.ENGLISH);
	// }
	// logHandler.setRequestLog(logger);
	// }
	// handlers.addHandler(logHandler);
	// }

	public void addResources(String contextPath, String resourcePath, String welcomesCsv) {
		int opt = ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS;
		ServletContextHandler context = new ServletContextHandler(opt);
		{
			context.setContextPath(contextPath);
			context.setCompactPath(true);
			context.setAllowNullPathInfo(true);

			if (Resources.exists(resourcePath)) {
				URL url = Resources.getURL(resourcePath);
				context.setResourceBase(url.toExternalForm());
			}
			ResourceHandler h = new ResourceHandler();
			h.setMinMemoryMappedContentLength(-1);
			h.setEtags(true);
			context.setHandler(h);
			context.setWelcomeFiles(Iterables.toArray(Splitters.notEmpties(",").split(welcomesCsv), String.class));
		}
		this.handlers.addHandler(context);
	}

	public void addRestContext(String contextPath, Injector guice, String pkgCsv) throws Exception {

		// XXX handler mode = NO_SECURITY | NO_SESSIONS restfull 을 지양하므로 stateless 로 설정
		int opt = ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS;
		ServletContextHandler context = new ServletContextHandler(opt);
		{
			// set context root path
			context.setContextPath(contextPath);
			context.setCompactPath(true);
			context.setAllowNullPathInfo(true);
		}
		this.handlers.addHandler(context);

		/**
		 * XXX jersey use hk2 for DI. this code is Bridge from GUICE to HK2.
		 * 
		 * @see https://hk2.java.net/guice-bridge.html
		 * @see https://github.com/mycom-int/jersey-guice-aop
		 */
		ResourceConfig config = new ResourceConfig();
		{
			// config.register(CharsetResponseFilter.class);
			config.register(GuiceBridgeFeature.class);
			config.register(JsonExceptionMapper.class);
			config.register(JacksonJaxbJsonProvider.class);

			Consumer<Class<?>> registerClass = classOf -> {
				config.register(classOf);
				log.info("register jaxrs resource: <{}>", classOf.getName());
			};

			Splitter.on(',').split(pkgCsv).forEach(pkg -> {//
						// add jax-rs resources
						Classes.getAnnotationPresents(pkg, Path.class).forEach(registerClass);
						// add jax-rs providers
						Classes.getAnnotationPresents(pkg, Provider.class).forEach(registerClass);
					});
			/*
			 * @see https://jersey.java.net/documentation/latest/user-guide.html#appendix-properties
			 */
			Map<String, Object> p = Maps.newHashMap(config.getProperties());
			{
				p.put(ServerProperties.WADL_FEATURE_DISABLE, true);
				p.put(GuiceBridgeFeature.KEY_INJECTOR, guice.createChildInjector());
			}
			config.setProperties(p);
		}

		context.addServlet(new ServletHolder(new ServletContainer(config)), "/*");

		// add jetty filters
		/*
		 * XXX: GzipFilter.class (sync) 사용. connector 가 async 가 아닌데 AsyncGzipFilter.class 를 사용할 경우 response 를 보내지 않는 상황 발견!
		 * 안정성을 위해 sync 버전 사용.
		 */
		// context.addFilter(IncludableGzipFilter.class, "/*", null);

		/*
		 * NOTE: 내부 시스템에서 접근하는 빈도가 많은데 전부 warning 하길래 주석.
		 */
		// context.addFilter(CloseableDoSFilter.class, "/*", null);

		// context.addFilter(NoCacheFilter.class, "/*", null);
		// context.addFilter(UTF8CharsetFilter.class, "/*", null);
		// context.addFilter(Filters.hold(CrossOriginFilter.class, "allowedOrigins=*\nallowedHeaders=*\n"), "/*", null);
	}

	String lookupPorts() {
		Connector[] conns = this.server.getConnectors();
		List<String> endpoints = Lists.newArrayListWithCapacity(conns.length);

		for (Connector c : conns) {

			if (false == c instanceof ServerConnector)
				continue;

			@SuppressWarnings("resource")
			ServerConnector sc = (ServerConnector) c;
			endpoints.add(String.format("%s(%d)", Iterables.getFirst(c.getProtocols(), "?"), sc.getPort()));
		}
		return Joiner.on(',').join(endpoints);
	}

	@Override
	protected void doStart() {
		log.info("start [jetty] ...");
		try {
			this.server.start();
			log.info("start [jetty] on [{}]", lookupPorts());
		} catch (Exception e) {
			log.error("fail to start [jetty], cause: {}", Errors.rootCause(e));
			log.debug("detail :", e);
			super.notifyFailed(e);
			return;
		}
		super.notifyStarted();

	}

	@Override
	protected void doStop() {
		log.info("stop [jetty] ...");
		try {
			if (null != pool)
				pool.stop();
			if (null != server)
				this.server.stop();
			log.info("stop [jetty] gracefully");
		} catch (Exception e) {
			log.warn("fail to stop [jetty]:", Errors.rootCause(e));
			log.debug("detail :", e);
			super.notifyFailed(e);
			return;
		}
		super.notifyStopped();

	}
}
