package com.slack.api.bolt.jetty;

import com.slack.api.bolt.App;
import com.slack.api.bolt.WebEndpoint;
import com.slack.api.bolt.handler.WebEndpointHandler;
import com.slack.api.bolt.servlet.SlackAppServlet;
import com.slack.api.bolt.servlet.SlackOAuthAppServlet;
import com.slack.api.bolt.servlet.WebEndpointServlet;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;

/**
 * An HTTP server backed by Jetty HTTP Server that runs {@link App} apps.
 *
 * @see <a href="https://www.eclipse.org/jetty/">Jetty HTTP Server</a>
 */
@Slf4j
public class SlackAppServer {

    private final Server server;
    private final Map<String, App> pathToApp;
    private final boolean localDebug = System.getenv("SLACK_APP_LOCAL_DEBUG") != null;

    // This is intentionally mutable to allow developers to register their own one
    private ErrorHandler errorHandler = new ErrorHandler() {
        @Override
        protected void writeErrorPage(
                HttpServletRequest request,
                Writer writer,
                int code,
                String message,
                boolean showStacks) throws IOException {
            if (localDebug) {
                super.writeErrorPage(request, writer, code, message, showStacks);
            } else {
                writer.write("{\"status\":\"" + code + "\"}");
            }
        }
    };

    public SlackAppServer(App app) {
        this(app, "/slack/events", 3000);
    }

    public SlackAppServer(App app, String path) {
        this(app, path, 3000);
    }

    public SlackAppServer(App app, int port) {
        this(toApps(app, "/slack/events"), port);
    }

    public SlackAppServer(App app, String path, int port) {
        this(toApps(app, path), port);
    }

    public SlackAppServer(Map<String, App> pathToApp) {
        this(pathToApp, 3000);
    }

    public SlackAppServer(Map<String, App> pathToApp, int port) {
        this.pathToApp = pathToApp;
        server = new Server(port);
        removeServerHeader(server);

        ServletContextHandler handler = new ServletContextHandler();
        Map<String, App> addedOnes = new HashMap<>();
        for (Map.Entry<String, App> entry : this.pathToApp.entrySet()) {
            String appPath = entry.getKey();
            App theApp = entry.getValue();
            theApp.config().setAppPath(appPath);
            handler.addServlet(new ServletHolder(new SlackAppServlet(theApp)), appPath);

            if (theApp.config().isOAuthStartEnabled()) {
                if (theApp.config().isDistributedApp()) {
                    // start
                    String oAuthStartPath = appPath + theApp.config().getOauthStartPath();
                    App oAuthStartApp = theApp.toOAuthStartApp();
                    handler.addServlet(new ServletHolder(new SlackOAuthAppServlet(oAuthStartApp)), oAuthStartPath);
                    addedOnes.put(oAuthStartPath, oAuthStartApp);
                } else {
                    log.warn("The app is not ready for handling your Slack App installation URL. Make sure if you set all the necessary values in AppConfig.");
                }
            }

            if (theApp.config().isOAuthCallbackEnabled()) {
                if (theApp.config().isDistributedApp()) {
                    // callback
                    String oAuthCallbackPath = appPath + theApp.config().getOauthCallbackPath();
                    App oAuthCallbackApp = theApp.toOAuthCallbackApp();
                    handler.addServlet(new ServletHolder(new SlackOAuthAppServlet(oAuthCallbackApp)), oAuthCallbackPath);
                    addedOnes.put(oAuthCallbackPath, oAuthCallbackApp);
                } else {
                    log.warn("The app is not ready for handling OAuth callback requests. Make sure if you set all the necessary values in AppConfig.");
                }
            }

            // Register additional web endpoints
            if (theApp.getWebEndpointHandlers() != null && theApp.getWebEndpointHandlers().size() > 0) {
                for (Map.Entry<WebEndpoint, WebEndpointHandler> ee : theApp.getWebEndpointHandlers().entrySet()) {
                    WebEndpoint endpoint = ee.getKey();
                    WebEndpointHandler endpointHandler = ee.getValue();
                    ServletHolder servletHolder = new ServletHolder(
                            new WebEndpointServlet(endpoint, endpointHandler, theApp.config()));
                    handler.addServlet(servletHolder, endpoint.getPath());
                }
            }
        }
        pathToApp.putAll(addedOnes);
        server.setHandler(handler);
        server.setErrorHandler(errorHandler);
    }

    public void start() throws Exception {
        for (App app : pathToApp.values()) {
            app.start();
        }
        server.start();
        log.info("⚡️ Bolt app is running!");
        server.join();
    }

    public void stop() throws Exception {
        for (App app : pathToApp.values()) {
            app.stop();
        }
        log.info("⚡️ Your Bolt app has stopped...");
        server.stop();
    }

    public ErrorHandler getErrorHandler() {
        return errorHandler;
    }

    public void setErrorHandler(ErrorHandler errorHandler) {
        this.errorHandler = errorHandler;
    }

    // ----------------------------------------------------
    // internal methods
    // ----------------------------------------------------

    private static Map<String, App> toApps(App app, String path) {
        Map<String, App> apps = new HashMap<>();
        apps.put(path, app);
        return apps;
    }

    private static void removeServerHeader(Server server) {
        // https://stackoverflow.com/a/15675075/840108
        for (Connector y : server.getConnectors()) {
            for (ConnectionFactory x : y.getConnectionFactories()) {
                if (x instanceof HttpConnectionFactory) {
                    ((HttpConnectionFactory) x).getHttpConfiguration().setSendServerVersion(false);
                }
            }
        }
    }

}