/*
 * Copyright (c) 2004-2017, by Alex Leigh.
 * All rights reserved.
 *
 * THIS IS UNPUBLISHED PROPRIETARY SOURCE CODE
 * The copyright notice above does not evidence any
 * actual or intended publication of such source code.
 */

package lc.mecha.http.server;

import jakarta.servlet.Servlet;
import lc.BuildVersion;
import lc.mecha.log.MechaLogger;
import lc.mecha.log.MechaLoggerFactory;
import lc.mecha.security.CertificateKeyStore;
import lc.mecha.security.KeyStoreUtil;
import lc.mecha.util.UniversalJob;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
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.eclipse.jetty.websocket.server.JettyWebSocketServlet;
import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory;
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;

import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

/**
 * <code>WebServer</code> implements provides a simple API to embed
 * a Jetty HTTP engine into a running VM. Used in conjunction with
 * <code>HandlerPack</code>, end-developers can instantiate HTTP servers
 * and provide processing code without having to use a servlet engine
 * along with it's associated complexities and costs.
 *
 * @author Alex Leigh
 */

public class HttpServer {
    private final static MechaLogger logger = MechaLoggerFactory.getLogger(HttpServer.class);
    public static final String SSL_CERTIFICATE_ALIAS = "lc-mecha-http-server";
    private static final String JETTY_SSL_CERT_ALIAS = "jetty";
    private final HttpPipeline handlerPack = new HttpPipeline();
    private final int maxThreads;
    private Server server = null;
    private boolean running = false;
    private String banner = "HttpServer " + BuildVersion.MASSIVE_VERSION;
    private final HashMap<String, WebSocketConnectionFactory> socketHandlers = new HashMap<>();
    private final ArrayList<ListenerConfig> listeners = new ArrayList<>();

    private KeyStore keyStore;

    /**
     * Create a new webserver, which will listen on the provided port number. The
     * port is not reserved during construction, so no error will present if the port
     * is already in use.
     */
    public HttpServer(final int maxThreads) {
        this.maxThreads = maxThreads;
    }

    public HttpServer() {
        this(32);
    }

    public void addListener(ListenerConfig listener) {
        listeners.add(listener);
    }

    /**
     * Clone the provided keystore and perform fix-ups to solve the impedance mismatch between the ESP API and
     * the Jetty API. Namely, Jetty is hard-coded to look for a certificate named "jetty".
     *
     * @param cks The CertificateKeyStore to clone
     * @return The cloned CertificateKeyStore
     * @throws CertificateException
     * @throws KeyStoreException
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    private CertificateKeyStore fixKeyStore(CertificateKeyStore cks) throws
            CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException {
        var myKeyStore = KeyStoreUtil.cloneKeyStore(cks.getKeyStore());
        Certificate cert = myKeyStore.getCertificate(SSL_CERTIFICATE_ALIAS);
        myKeyStore.setCertificateEntry(JETTY_SSL_CERT_ALIAS, cert);

        return new CertificateKeyStore(myKeyStore, cks.getPassword());
    }

    public boolean isRunning() {
        return running;
    }

    /**
     * Add a {@link WebSocketConnection} to this webserver. For the change to take effect, the call must be made before
     * the server is started.
     */
    public void addSocketHandler(WebSocketConnectionFactory handler, String path) {
        socketHandlers.put(path, handler);
    }

    public String getBanner() {
        return banner;
    }

    public void setBanner(String banner) {
        this.banner = banner;
    }


    /**
     * Return the <code>HandlerPack</code> associated with this
     * http server instance.
     *
     * @return
     */
    public HttpPipeline getWebPipeline() {
        return handlerPack;
    }

    /**
     * Join the underlying server and wait for its completion.
     *
     * @throws InterruptedException
     */
    public void join() throws InterruptedException {
        server.join();
    }

    /**
     * Attempt to start the instance.
     */
    public void start() {
        if (running)
            return;

        QueuedThreadPool pool = new QueuedThreadPool(this.maxThreads, 32);

        new Thread(() -> {
            while (true) {
                logger.info("HTTP Server Status: [busy: {}] [idle: {}] [max: {}]",
                        pool.getBusyThreads(),
                        pool.getIdleThreads(),
                        pool.getMaxThreads());
                try {
                    Thread.sleep(10 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        server = new Server(pool);

        for (ListenerConfig listener : listeners) {
            if (listener.isSecure()) {
                server.addConnector(makeHttpsConnector(listener.getPort(), listener.getKeyStore()));
            } else {
                server.addConnector(makeHttpConnector(listener.getPort()));
            }
        }

        ContextHandlerCollection handlers = new ContextHandlerCollection();

        for (Map.Entry<String, WebSocketConnectionFactory> entry : socketHandlers.entrySet()) {
            logger.info("Adding WebSocket handler. [path: {}] [handler: {}]", entry.getKey(), entry.getValue());
            ServletContextHandler servletContextHandler = new ServletContextHandler();
            JettyWebSocketServletContainerInitializer.configure(servletContextHandler, null);

            Servlet websocketServlet = new JettyWebSocketServlet() {
                @Override
                protected void configure(JettyWebSocketServletFactory factory) {
                    //factory.addMapping("/", (req, res) -> entry.getValue());
                    factory.setCreator(entry.getValue());
                }
            };
            servletContextHandler.addServlet(new ServletHolder(websocketServlet), entry.getKey());
            handlers.addHandler(servletContextHandler);
        }

        handlers.addHandler(handlerPack);
        server.setHandler(handlers);

        logger.info("Starting web server. [listeners: {}] [maxThreads: {}]", new Object[]{listeners, maxThreads});

        try {
            server.start();
            final Connector[] c = server.getConnectors();
            for (final Connector a : c) {
                logger.info("Connector2: {}", a);
            }
        } catch (Exception e) {
            logger.warn("Failure to start Jetty", e);
            System.exit(UniversalJob.RET_ERROR);
        }
    }

    private Connector makeHttpConnector(int port) {
        logger.info("Setting up HTTP listen port: [port: {}]", port);
        final HttpConfiguration http_config = new HttpConfiguration();
        http_config.setSendServerVersion(false);
        http_config.setSendXPoweredBy(false);

        final ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(http_config));
        http.setPort(port);
        http.setIdleTimeout(30000);

        return http;
    }

    private Connector makeHttpsConnector(int port, CertificateKeyStore keyStore) {
        final HttpConfiguration http_config = new HttpConfiguration();
        http_config.setSecureScheme("https");
        http_config.setOutputBufferSize(32768);
        http_config.setSendServerVersion(false);
        http_config.setSendXPoweredBy(false);
        http_config.setIdleTimeout(500000);

        SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
        sslContextFactory.setKeyStore(keyStore.getKeyStore());

        // sslContextFactory.setKeyStorePath(keystoreFile.getAbsolutePath());
        // sslContextFactory.setKeyStorePassword(keyStore.getPassword());
        sslContextFactory.setKeyManagerPassword(keyStore.getPassword());

        ServerConnector https = new ServerConnector(server,
                new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()),
                new HttpConnectionFactory(http_config));
        https.setPort(port);

        return https;
    }

    @Override
    public String toString() {
        return "WebServer{" +
                "handlerPack=" + handlerPack +
                ", maxThreads=" + maxThreads +
                ", server=" + server +
                ", running=" + running +
                '}';
    }

    /**
     * Stop the running instance. This will block until Jetty claims
     * it has successfully shutdown; after such time, the port should
     * be released and be available for use.
     *
     * @throws Exception
     */
    public void stop() throws Exception {
        logger.info("Stopping server...");
        if (!running)
            return;

        server.stop();

        while (server.isStopping()) {
            try {
                Thread.sleep(250);
            } catch (InterruptedException e) {
                logger.warn("Timer failed: {}", e);
            }
        }

        running = false;

        logger.info("Server stopped.");

        System.exit(0);
    }

}
