package ctbrec.recorder.server;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.BindException;
import java.net.URL;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;

import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.security.UserStore;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.handler.SecuredRedirectHandler;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.security.Credential;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Objects;

import ctbrec.Config;
import ctbrec.Version;
import ctbrec.event.EventBusHolder;
import ctbrec.event.EventHandler;
import ctbrec.event.EventHandlerConfiguration;
import ctbrec.recorder.NextGenLocalRecorder;
import ctbrec.recorder.OnlineMonitor;
import ctbrec.recorder.Recorder;
import ctbrec.servlet.StaticFileServlet;
import ctbrec.sites.Site;
import ctbrec.sites.bonga.BongaCams;
import ctbrec.sites.cam4.Cam4;
import ctbrec.sites.camsoda.Camsoda;
import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.sites.fc2live.Fc2Live;
import ctbrec.sites.flirt4free.Flirt4Free;
import ctbrec.sites.jasmin.LiveJasmin;
import ctbrec.sites.mfc.MyFreeCams;
import ctbrec.sites.showup.Showup;
import ctbrec.sites.streamate.Streamate;
import ctbrec.sites.stripchat.Stripchat;

public class HttpServer {

    private static final Logger LOG = LoggerFactory.getLogger(HttpServer.class);
    private Recorder recorder;
    private OnlineMonitor onlineMonitor;
    private Config config;
    private Server server = new Server();
    private List<Site> sites = new ArrayList<>();

    public HttpServer() throws Exception {
        logEnvironment();
        createSites();
        System.setProperty("ctbrec.server.mode", "1");
        if (System.getProperty("ctbrec.config") == null) {
            System.setProperty("ctbrec.config", "server.json");
        }
        try {
            Config.init(sites);
        } catch (Exception e) {
            LOG.error("Couldn't load config", e);
            System.exit(1);
        }

        addShutdownHook(); // for graceful termination

        registerAlertSystem();

        config = Config.getInstance();
        LOG.info("HMAC authentication is {}", config.getSettings().key != null ? "enabled" : "disabled");

        recorder = new NextGenLocalRecorder(config, sites);
        for (Site site : sites) {
            if (site.isEnabled()) {
                site.init();
                safeLogin(site);
            }
        }
        onlineMonitor = new OnlineMonitor(recorder);
        onlineMonitor.start();
        startHttpServer();
    }

    private void safeLogin(Site site) {
        if (site.credentialsAvailable()) {
            try {
                boolean success = site.login();
                if (success) {
                    LOG.info("Successfully logged in to {}", site.getName());
                } else {
                    throw new RuntimeException("Login returned false");
                }
            } catch (Exception e) {
                LOG.info("Login to {} failed", site.getName());
            }
        }
    }

    private void createSites() {
        sites.add(new BongaCams());
        sites.add(new Cam4());
        sites.add(new Camsoda());
        sites.add(new Chaturbate());
        sites.add(new Fc2Live());
        sites.add(new Flirt4Free());
        sites.add(new LiveJasmin());
        sites.add(new MyFreeCams());
        sites.add(new Showup());
        sites.add(new Streamate());
        sites.add(new Stripchat());
    }

    private void addShutdownHook() {
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                LOG.info("Shutting down");
                if (onlineMonitor != null) {
                    onlineMonitor.shutdown();
                }
                if (recorder != null) {
                    recorder.shutdown();
                }
                try {
                    server.stop();
                } catch (Exception e) {
                    LOG.error("Couldn't stop HTTP server", e);
                }
                try {
                    Config.getInstance().save();
                } catch (IOException e) {
                    LOG.error("Couldn't save configuration", e);
                }
                LOG.info("Goodbye!");
            }
        });
    }

    private void startHttpServer() throws Exception {
        server = new Server();

        HttpConfiguration httpConfig = new HttpConfiguration();
        httpConfig.setSendServerVersion(false);
        httpConfig.setSecurePort(this.config.getSettings().httpSecurePort);
        httpConfig.setSecureScheme("https");
        HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpConfig);

        SslContextFactory sslContextFactory = new SslContextFactory.Server();
        URL keyStoreUrl = getClass().getResource("/keystore.pkcs12");
        String keyStoreSrc = System.getProperty("keystore.file", keyStoreUrl.toExternalForm());
        String keyStorePassword = System.getProperty("keystore.password", "ctbrecsucks");
        sslContextFactory.setKeyStorePath(keyStoreSrc);
        sslContextFactory.setKeyStorePassword(keyStorePassword);
        sslContextFactory.setTrustStorePath(keyStoreSrc);
        sslContextFactory.setTrustStorePassword(keyStorePassword);

        try (ServerConnector http = new ServerConnector(server, httpConnectionFactory);
                ServerConnector https = new ServerConnector(server, sslContextFactory, httpConnectionFactory)) {

            // connector for http
            http.setPort(this.config.getSettings().httpPort);
            http.setIdleTimeout(this.config.getSettings().httpTimeout);

            // connector for https (TLS)
            https.setPort(this.config.getSettings().httpSecurePort);
            https.setIdleTimeout(this.config.getSettings().httpTimeout);

            String contextPath = Config.getInstance().getContextPath();
            ServletContextHandler basicAuthContext = new ServletContextHandler(ServletContextHandler.SESSIONS);
            basicAuthContext.setContextPath(contextPath + "/secured");

            ServletContextHandler defaultContext = new ServletContextHandler(ServletContextHandler.SESSIONS);
            defaultContext.setContextPath(contextPath);
            addHttpHeaderFilter(defaultContext);

            RecorderServlet recorderServlet = new RecorderServlet(recorder, sites);
            ServletHolder holder = new ServletHolder(recorderServlet);
            defaultContext.addServlet(holder, "/rec");

            HlsServlet hlsServlet = new HlsServlet(this.config);
            holder = new ServletHolder(hlsServlet);
            defaultContext.addServlet(holder, "/hls/*");

            if (this.config.getSettings().webinterface) {
                StaticFileServlet staticFileServlet = new StaticFileServlet("/html");
                holder = new ServletHolder(staticFileServlet);
                String staticFileContext = "/static/*";
                defaultContext.addServlet(holder, staticFileContext);
                LOG.info("Register static file servlet under {}", defaultContext.getContextPath()+staticFileContext);

                // servlet to retrieve the HMAC secured by basic auth
                String username = this.config.getSettings().webinterfaceUsername;
                String password = this.config.getSettings().webinterfacePassword;
                basicAuthContext.setSecurityHandler(basicAuth(username, password, "CTB Recorder"));
                basicAuthContext.addServlet(new ServletHolder(new HttpServlet() {
                    @Override
                    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException {
                        if (Objects.equal(username, req.getRemoteUser())) {
                            resp.setStatus(HttpServletResponse.SC_OK);
                            resp.setContentType("application/json");
                            byte[] hmac = Optional.ofNullable(HttpServer.this.config.getSettings().key).orElse(new byte[0]);
                            try {
                                JSONObject response = new JSONObject();
                                response.put("hmac", new String(hmac, "utf-8"));
                                resp.getOutputStream().println(response.toString());
                            } catch (Exception e) {
                                resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                            }
                        }
                    }
                }), "/hmac");
            }

            server.addConnector(http);
            HandlerList handlers = new HandlerList();
            if (this.config.getSettings().transportLayerSecurity) {
                server.addConnector(https);
                handlers.setHandlers(new Handler[] { new SecuredRedirectHandler(), basicAuthContext, defaultContext });
            } else {
                handlers.setHandlers(new Handler[] { basicAuthContext, defaultContext });
            }
            server.setHandler(handlers);

            try {
                server.start();
                server.join();
            } catch (BindException e) {
                LOG.error("Port {} is already in use", http.getPort(), e);
                System.exit(1);
            } catch (Exception e) {
                LOG.error("Server start failed", e);
                System.exit(1);
            }
        }
    }

    private void addHttpHeaderFilter(ServletContextHandler defaultContext) {
        FilterHolder httpHeaderFilter = new FilterHolder(new Filter() {
            @Override
            public void init(FilterConfig filterConfig) throws ServletException {
                // noop
            }

            @Override
            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
                ((HttpServletResponse)response).addHeader("Server", "CTB Recorder/" + getVersion());
                chain.doFilter(request, response);
            }

            @Override
            public void destroy() {
                // noop
            }
        });
        defaultContext.addFilter(httpHeaderFilter, "/*", EnumSet.of(DispatcherType.REQUEST, DispatcherType.INCLUDE));
    }

    private static final SecurityHandler basicAuth(String username, String password, String realm) {
        UserStore userStore = new UserStore();
        userStore.addUser(username, Credential.getCredential(password), new String[] { "user" });
        HashLoginService l = new HashLoginService();
        l.setUserStore(userStore);
        l.setName(realm);

        Constraint constraint = new Constraint();
        constraint.setName(Constraint.__BASIC_AUTH);
        constraint.setRoles(new String[] { "user" });
        constraint.setAuthenticate(true);

        ConstraintMapping cm = new ConstraintMapping();
        cm.setConstraint(constraint);
        cm.setPathSpec("/*");

        ConstraintSecurityHandler csh = new ConstraintSecurityHandler();
        csh.setAuthenticator(new BasicAuthenticator());
        csh.setRealmName("myrealm");
        csh.addConstraintMapping(cm);
        csh.setLoginService(l);

        return csh;
    }

    private void registerAlertSystem() {
        for (EventHandlerConfiguration ehconfig : Config.getInstance().getSettings().eventHandlers) {
            EventHandler handler = new EventHandler(ehconfig);
            EventBusHolder.register(handler);
            LOG.debug("Registered event handler for {} {}", ehconfig.getEvent(), ehconfig.getName());
        }
        LOG.debug("Alert System registered");
    }

    private void logEnvironment() {
        LOG.debug("OS:\t{} {}", System.getProperty("os.name"), System.getProperty("os.version"));
        LOG.debug("Java:\t{} {} {}", System.getProperty("java.vendor"), System.getProperty("java.vm.name"), System.getProperty("java.version"));
        try {
            LOG.debug("ctbrec server {}", getVersion());
        } catch (IOException e) {
            // nothing to do here
        }
    }

    private Version getVersion() throws IOException {
        try (InputStream is = getClass().getClassLoader().getResourceAsStream("version")) {
            BufferedReader reader = new BufferedReader(new InputStreamReader(is));
            String versionString = reader.readLine();
            Version version = Version.of(versionString);
            return version;
        }
    }

    public static void main(String[] args) throws Exception {
        new HttpServer();
    }
}
