package valandur.webapi;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.jaxrs.config.BeanConfig;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.MultiException;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.slf4j.Logger;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.entity.living.player.Player;
import org.spongepowered.api.text.Text;
import org.spongepowered.api.text.format.TextColors;
import valandur.webapi.config.MainConfig;
import valandur.webapi.handler.AssetHandler;
import valandur.webapi.handler.ErrorHandler;
import valandur.webapi.serialize.SerializationFeature;
import valandur.webapi.servlet.base.BaseServlet;
import valandur.webapi.util.Constants;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.SocketException;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.Set;

public class WebServer {

    private Logger logger;

    private Server server;

    private MainConfig config;

    private byte[] apConfig;

    public String getHost() {
        return config.host;
    }
    public int getHttpPort() {
        return config.http;
    }
    public int getHttpsPort() {
        return config.https;
    }


    WebServer(Logger logger, MainConfig config) {
        this.logger = logger;
        this.config = config;

        // Process the config.js file to include data from the Web-API config files
        try {
            MainConfig.APConfig cfg = config.adminPanelConfig;
            if (cfg != null) {
                ObjectMapper om = new ObjectMapper();
                String configStr = "window.config = " + om.writeValueAsString(cfg);
                apConfig = configStr.getBytes(Charset.forName("utf-8"));
            }
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }

    public void start(Player player) {
        // Start web server
        logger.info("Starting Web Server...");

        try {
            server = new Server();

            // HTTP config
            HttpConfiguration httpConfig = new HttpConfiguration();
            httpConfig.setOutputBufferSize(32768);

            String baseUri = null;

            // HTTP
            if (config.http >= 0) {
                if (config.http < 1024) {
                    logger.warn("You are using an HTTP port < 1024 which is not recommended! \n" +
                            "This might cause errors when not running the server as root/admin. \n" +
                            "Running the server as root/admin is not recommended. " +
                            "Please use a port above 1024 for HTTP."
                    );
                }
                ServerConnector httpConn = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
                httpConn.setHost(config.host);
                httpConn.setPort(config.http);
                httpConn.setIdleTimeout(30000);
                server.addConnector(httpConn);

                baseUri = "http://" + config.host + ":" + config.http;
            }

            // HTTPS
            if (config.https >= 0) {
                if (config.https < 1024) {
                    logger.warn("You are using an HTTPS port < 1024 which is not recommended! \n" +
                            "This might cause errors when not running the server as root/admin. \n" +
                            "Running the server as root/admin is not recommended. " +
                            "Please use a port above 1024 for HTTPS."
                    );
                }

                // Update http config
                httpConfig.setSecureScheme("https");
                httpConfig.setSecurePort(config.https);

                String loc = config.customKeyStore;
                String pw = config.customKeyStorePassword;
                String mgrPw = config.customKeyStoreManagerPassword;

                if (loc == null || loc.isEmpty()) {
                    loc = Sponge.getAssetManager().getAsset(WebAPI.getInstance(), "keystore.jks")
                            .map(a -> a.getUrl().toString())
                            .orElse("../../src/main/resources/assets/webapi/keystore.jks");
                    pw = "mX4z%&uJ2E6VN#5f";
                    mgrPw = "mX4z%&uJ2E6VN#5f";
                }

                // SSL Factory
                SslContextFactory sslFactory = new SslContextFactory();
                sslFactory.setKeyStorePath(loc);
                sslFactory.setKeyStorePassword(pw);
                sslFactory.setKeyManagerPassword(mgrPw);

                // HTTPS config
                HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig);
                SecureRequestCustomizer src = new SecureRequestCustomizer();
                src.setStsMaxAge(2000);
                src.setStsIncludeSubDomains(true);
                httpsConfig.addCustomizer(src);


                ServerConnector httpsConn = new ServerConnector(server,
                        new SslConnectionFactory(sslFactory, HttpVersion.HTTP_1_1.asString()),
                        new HttpConnectionFactory(httpsConfig)
                );
                httpsConn.setHost(config.host);
                httpsConn.setPort(config.https);
                httpsConn.setIdleTimeout(30000);
                server.addConnector(httpsConn);

                baseUri = "https://" + config.host + ":" + config.https;
            }

            if (baseUri == null) {
                logger.error("You have disabled both HTTP and HTTPS - The Web-API will be unreachable!");
                baseUri = ""; // for swagger
            }

            // Collection of all handlers
            ContextHandlerCollection mainContext = new ContextHandlerCollection();

            // Asset handlers
            mainContext.addHandler(newContext("/docs", new AssetHandler("pages/redoc.html")));

            String panelPath = null;
            if (config.adminPanel) {
                // Rewrite handler
                RewriteHandler rewrite = new RewriteHandler();
                rewrite.setRewriteRequestURI(true);
                rewrite.setRewritePathInfo(true);

                panelPath = config.adminPanelConfig.basePath;
                if (!panelPath.startsWith("/")) {
                    panelPath = "/" + config.adminPanelConfig.basePath;
                }
                RedirectPatternRule redirect = new RedirectPatternRule();
                redirect.setPattern("/*");
                redirect.setLocation(panelPath);
                rewrite.addRule(redirect);
                mainContext.addHandler(newContext("/", rewrite));

                final String pPath = panelPath;
                mainContext.addHandler(newContext(panelPath, new AssetHandler("admin", path -> {
                    if (path.endsWith("config.js") && this.apConfig != null) {
                        return input -> apConfig;
                    }
                    if (path.endsWith("index.html")) {
                        return input -> new String(input).replace("<base href=\"/\">",
                                "<base href=\"" + pPath + "\">").getBytes();
                    }

                    return input -> input;
                })));
            }

            // Main servlet context
            ServletContextHandler servletsContext = new ServletContextHandler();
            servletsContext.setContextPath(Constants.BASE_PATH);

            // Resource config for jersey servlet
            ResourceConfig conf = new ResourceConfig();
            conf.packages(
                    "io.swagger.jaxrs.listing",
                    "valandur.webapi.shadow.io.swagger.jaxrs.listing",
                    "valandur.webapi.handler",
                    "valandur.webapi.security",
                    "valandur.webapi.serialize",
                    "valandur.webapi.user"
                    //"io.swagger.v3.jaxrs2.integration.resources"                      // This if for Swagger 3.0
            );
            conf.property("jersey.config.server.wadl.disableWadl", true);

            // Add error handler to jetty (will also be picked up by jersey
            ErrorHandler errHandler = new ErrorHandler();
            server.setErrorHandler(errHandler);
            server.addBean(errHandler);

            // Register all servlets. We use this instead of package scanning because we don't want the
            // integrated servlets to load unless their plugin is present. Also this gives us more control/info
            // over which servlets/endpoints are loaded.
            Set<String> servlets = new HashSet<>();
            for (Class<? extends BaseServlet> servletClass :
                    WebAPI.getServletService().getRegisteredServlets().values()) {
                conf.register(servletClass);
                String pkg = servletClass.getPackage().getName();
                servlets.add(pkg);
            }

            // Register serializer
            conf.register(SerializationFeature.class);

            // Jersey servlet
            ServletHolder jerseyServlet = new ServletHolder(new ServletContainer(conf));
            jerseyServlet.setInitOrder(1);
            // This if for Swagger 3.0
            // jerseyServlet.setInitParameter("openApi.configuration.location", assets/webapi/swagger/config.json");                                    // This is for Swagger 3.0
            servletsContext.addServlet(jerseyServlet, "/*");

            // Register swagger as bean
            // TODO: We can't set scheme and host yet because Swagger 2.0 doesn't support multiple different ones
            BeanConfig beanConfig = new BeanConfig();
            beanConfig.setBasePath(Constants.BASE_PATH);
            beanConfig.setResourcePackage("valandur.webapi.swagger," + String.join(",", servlets));
            beanConfig.setScan(true);
            if (config.devMode) {
                beanConfig.setPrettyPrint(true);
            }
            servletsContext.addBean(beanConfig);

            // Attach error handler to servlets context
            servletsContext.setErrorHandler(errHandler);
            servletsContext.addBean(errHandler);

            // Add servlets to main context
            mainContext.addHandler(servletsContext);

            // Add main context to server
            server.setHandler(mainContext);


            server.start();

            if (config.adminPanel) {
                logger.info("AdminPanel: " + baseUri + panelPath);
            }
            logger.info("API Docs: " + baseUri + "/docs");
        } catch (SocketException e) {
            logger.error("Web-API webserver could not start, probably because one of the ports needed for HTTP " +
                    "and/or HTTPS are in use or not accessible (ports below 1024 are protected)");
        } catch (MultiException e) {
            e.getThrowables().forEach(t -> {
                if (t instanceof SocketException) {
                    logger.error("Web-API webserver could not start, probably because one of the ports needed for HTTP " +
                            "and/or HTTPS are in use or not accessible (ports below 1024 are protected)");
                } else {
                    t.printStackTrace();
                    WebAPI.sentryCapture(t);
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
            WebAPI.sentryCapture(e);
        }

        if (player != null) {
            player.sendMessage(Text.builder()
                    .color(TextColors.AQUA)
                    .append(Text.of("[" + Constants.NAME + "] The web server has been restarted!"))
                    .toText()
            );
        }
    }

    public void stop() {
        if (server != null) {
            try {
                server.stop();
                server = null;
            } catch (Exception e) {
                e.printStackTrace();
                WebAPI.sentryCapture(e);
            }
        }
    }

    public void handle(String target, Request baseRequest, HttpServletRequest req, HttpServletResponse res)
            throws IOException, ServletException {
        server.handle(target, baseRequest, req, res);
    }

    private ContextHandler newContext(String path, Handler handler) {
        ContextHandler context = new ContextHandler();
        context.setContextPath(path);
        context.setHandler(handler);
        return context;
    }
}