/**
  * Copyright 2017 Hortonworks.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at

  *   http://www.apache.org/licenses/LICENSE-2.0

  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
 **/


package com.hortonworks.streamline.webservice;

import com.hortonworks.registries.auth.Login;
import com.hortonworks.registries.common.ServletFilterConfiguration;
import com.hortonworks.registries.common.util.FileStorage;
import com.hortonworks.registries.storage.NOOPTransactionManager;
import com.hortonworks.registries.storage.Storable;
import com.hortonworks.registries.storage.StorageManager;
import com.hortonworks.registries.storage.StorageManagerAware;
import com.hortonworks.registries.storage.TransactionManager;
import com.hortonworks.registries.storage.TransactionManagerAware;
import com.hortonworks.registries.storage.transaction.TransactionEventListener;
import com.hortonworks.streamline.common.Constants;
import com.hortonworks.streamline.common.ModuleRegistration;
import com.hortonworks.streamline.common.exception.ConfigException;
import com.hortonworks.streamline.common.util.ReflectionHelper;
import com.hortonworks.streamline.streams.security.StreamlineAuthorizer;
import com.hortonworks.streamline.streams.security.authentication.StreamlineKerberosRequestFilter;
import com.hortonworks.streamline.streams.security.impl.DefaultStreamlineAuthorizer;
import com.hortonworks.streamline.streams.security.service.SecurityCatalogService;
import com.hortonworks.streamline.streams.service.GenericExceptionMapper;
import com.hortonworks.streamline.webservice.configurations.AuthorizerConfiguration;
import com.hortonworks.streamline.webservice.configurations.LoginConfiguration;
import com.hortonworks.streamline.webservice.configurations.ModuleConfiguration;
import com.hortonworks.streamline.webservice.configurations.StorageProviderConfiguration;
import com.hortonworks.streamline.webservice.configurations.StreamlineConfiguration;
import com.hortonworks.streamline.webservice.resources.StreamlineConfigurationResource;
import com.hortonworks.streamline.webservice.filters.StreamlineResponseHeaderFilter;
import io.dropwizard.Application;
import io.dropwizard.assets.AssetsBundle;
import io.dropwizard.jetty.HttpConnectorFactory;
import io.dropwizard.server.AbstractServerFactory;
import io.dropwizard.server.DefaultServerFactory;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jetty.servlet.ErrorPageErrorHandler;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.security.auth.Subject;
import javax.security.auth.login.LoginException;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterRegistration;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static com.hortonworks.registries.storage.util.StorageUtils.getStorableEntities;

public class StreamlineApplication extends Application<StreamlineConfiguration> {
    private static final Logger LOG = LoggerFactory.getLogger(StreamlineApplication.class);

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

    @Override
    public String getName() {
        return "Streamline Web Service";
    }

    @Override
    public void initialize(Bootstrap<StreamlineConfiguration> bootstrap) {
        bootstrap.addBundle(new AssetsBundle("/assets", "/", "index.html", "static"));
        super.initialize(bootstrap);
    }

    @Override
    public void run(StreamlineConfiguration configuration, Environment environment) throws Exception {
        AbstractServerFactory sf = (AbstractServerFactory) configuration.getServerFactory();
        // disable all default exception mappers
        sf.setRegisterDefaultExceptionMappers(false);

        environment.jersey().register(GenericExceptionMapper.class);

        registerResources(configuration, environment, getSubjectFromLoginImpl(configuration));

        if (configuration.isEnableCors()) {
            List<String> urlPatterns = configuration.getCorsUrlPatterns();
            if (urlPatterns != null && !urlPatterns.isEmpty()) {
                enableCORS(environment, urlPatterns);
            }
        }

        setupCustomTrustStore(configuration);

        addSecurityHeaders(environment);

        addServletFilters(configuration, environment);

    }

    @SuppressWarnings("unchecked")
    private void addServletFilters(StreamlineConfiguration configuration, Environment environment) {
        List<ServletFilterConfiguration> servletFilterConfigurations = configuration.getServletFilters();
        if (servletFilterConfigurations != null && !servletFilterConfigurations.isEmpty()) {
            for (ServletFilterConfiguration servletFilterConfiguration: servletFilterConfigurations) {
                try {
                    addServletFilter(environment, servletFilterConfiguration.getClassName(),
                            (Class<? extends Filter>) Class.forName(servletFilterConfiguration.getClassName()),
                            servletFilterConfiguration.getParams());
                } catch (Exception e) {
                    LOG.error("Error occurred while adding servlet filter {}", servletFilterConfiguration);
                    throw new RuntimeException(e);
                }
            }
        } else {
            LOG.info("No servlet filters configured");
        }
    }

    private void addServletFilter(Environment environment, String name, Class<? extends Filter> filter, Map<String, String> params) {
        FilterRegistration.Dynamic dynamic = environment.servlets().addFilter(name, filter);
        dynamic.setInitParameters(params);
        dynamic.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*");
        LOG.info("Added servlet filter '{}' with configuration {}", filter, params);
    }

    // Add security headers. If needed these may be overridden via custom 'servletFilters'.
    private void addSecurityHeaders(Environment environment) {
        Map<String, String> params = new HashMap<>();
        params.put("X-Frame-Options", "SAMEORIGIN");
        params.put("X-XSS-Protection", "1; mode=block");
        params.put("X-Content-Type-Options", "nosniff");
        params.put("Content-Security-Policy", "script-src 'self'; object-src 'self'");
        params.put("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
        Class<? extends Filter> filter = StreamlineResponseHeaderFilter.class;
        addServletFilter(environment, filter.getName() + ".internal", filter, params);
    }

    private Subject getSubjectFromLoginImpl (StreamlineConfiguration streamlineConfiguration) {
        LoginConfiguration loginConfiguration = streamlineConfiguration.getLoginConfiguration();
        if (loginConfiguration == null) {
            return null;
        }
        try {
            Login login = (Login) Class.forName(loginConfiguration.getClassName()).newInstance();
            login.configure(loginConfiguration.getParams() != null ? loginConfiguration.getParams() : new HashMap<String, Object>(), "StreamlineServer");
            try {
                return login.login().getSubject();
            } catch (LoginException e) {
                LOG.error("Unable to login using login configuration {}", loginConfiguration);
                throw new RuntimeException(e);
            }
        } catch (InstantiationException|IllegalAccessException|ClassNotFoundException e) {
            LOG.error("Unable to instantiate loginImpl using login configuration {}", loginConfiguration);
            throw new RuntimeException(e);
        }
    }

    private void setupCustomTrustStore(StreamlineConfiguration configuration) {
        if (StringUtils.isNotEmpty(configuration.getTrustStorePath())) {
            System.setProperty("javax.net.ssl.trustStore", configuration.getTrustStorePath());
            if (StringUtils.isNotEmpty(configuration.getTrustStorePassword())) {
                System.setProperty("javax.net.ssl.trustStorePassword", configuration.getTrustStorePassword());
            }
        }
    }

    private void enableCORS(Environment environment, List<String> urlPatterns) {
        // Enable CORS headers
        final FilterRegistration.Dynamic cors = environment.servlets().addFilter("CORS", CrossOriginFilter.class);

        // Configure CORS parameters
        cors.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*");
        cors.setInitParameter(CrossOriginFilter.ALLOWED_HEADERS_PARAM, "X-Requested-With,Authorization,Content-Type,Accept,Origin");
        cors.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "OPTIONS,GET,PUT,POST,DELETE,HEAD");

        // Add URL mapping
        String[] urls = urlPatterns.toArray(new String[urlPatterns.size()]);
        cors.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, urls);
    }

    private StorageManager getDao(StreamlineConfiguration configuration) {
        StorageProviderConfiguration storageProviderConfiguration = configuration.getStorageProviderConfiguration();
        return getStorageManager(storageProviderConfiguration);
    }

    private StorageManager getStorageManager(StorageProviderConfiguration storageProviderConfiguration) {
        final String providerClass = storageProviderConfiguration.getProviderClass();
        StorageManager storageManager = null;
        try {
            storageManager = (StorageManager) Class.forName(providerClass).newInstance();
        } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
        storageManager.init(storageProviderConfiguration.getProperties());

        return storageManager;
    }

    private FileStorage getJarStorage (StreamlineConfiguration configuration, StorageManager storageManager) {
        FileStorage fileStorage = null;
        try {
            fileStorage = ReflectionHelper.newInstance(configuration.getFileStorageConfiguration().getClassName());
            fileStorage.init(configuration.getFileStorageConfiguration().getProperties());
            if (fileStorage instanceof StorageManagerAware) {
                ((StorageManagerAware) fileStorage).setStorageManager(storageManager);
            }
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
            throw new RuntimeException(e);
        }
        return fileStorage;
    }

    private void registerResources(StreamlineConfiguration configuration, Environment environment, Subject subject) throws ConfigException,
            ClassNotFoundException, IllegalAccessException, InstantiationException {
        StorageManager storageManager = getDao(configuration);
        TransactionManager transactionManager;
        if (storageManager instanceof TransactionManager) {
            transactionManager = (TransactionManager) storageManager;
        } else {
            transactionManager = new NOOPTransactionManager();
        }
        environment.jersey().register(new TransactionEventListener(transactionManager, true));
        Collection<Class<? extends Storable>> streamlineEntities = getStorableEntities();
        storageManager.registerStorables(streamlineEntities);
        LOG.info("Registered streamline entities {}", streamlineEntities);
        FileStorage fileStorage = this.getJarStorage(configuration, storageManager);
        int appPort = ((HttpConnectorFactory) ((DefaultServerFactory) configuration.getServerFactory()).getApplicationConnectors().get(0)).getPort();
        String catalogRootUrl = configuration.getCatalogRootUrl().replaceFirst("8080", appPort + "");
        List<ModuleConfiguration> modules = configuration.getModules();
        List<Object> resourcesToRegister = new ArrayList<>();

        // add StreamlineConfigResource
        resourcesToRegister.add(new StreamlineConfigurationResource(configuration));

        // authorizer
        StreamlineAuthorizer authorizer;
        AuthorizerConfiguration authorizerConf = configuration.getAuthorizerConfiguration();
        SecurityCatalogService securityCatalogService = new SecurityCatalogService(storageManager);
        if (authorizerConf != null) {
            authorizer = ((Class<StreamlineAuthorizer>) Class.forName(authorizerConf.getClassName())).newInstance();
            Map<String, Object> authorizerConfig = new HashMap<>();
            authorizerConfig.put(DefaultStreamlineAuthorizer.CONF_CATALOG_SERVICE, securityCatalogService);
            authorizerConfig.put(DefaultStreamlineAuthorizer.CONF_ADMIN_PRINCIPALS, authorizerConf.getAdminPrincipals());
            authorizer.init(authorizerConfig);
            String filterClazzName = authorizerConf.getContainerRequestFilter();
            ContainerRequestFilter filter;
            if (StringUtils.isEmpty(filterClazzName)) {
                filter = new StreamlineKerberosRequestFilter(); // default
            } else {
                filter = ((Class<ContainerRequestFilter>) Class.forName(filterClazzName)).newInstance();
            }
            LOG.info("Registering ContainerRequestFilter: {}", filter.getClass().getCanonicalName());
            environment.jersey().register(filter);
        } else {
            LOG.info("Authorizer config not set, setting noop authorizer");
            String noopAuthorizerClassName = "com.hortonworks.streamline.streams.security.impl.NoopAuthorizer";
            authorizer = ((Class<StreamlineAuthorizer>) Class.forName(noopAuthorizerClassName)).newInstance();
        }

        for (ModuleConfiguration moduleConfiguration: modules) {
            String moduleName = moduleConfiguration.getName();
            String moduleClassName = moduleConfiguration.getClassName();
            LOG.info("Registering module [{}] with class [{}]", moduleName, moduleClassName);
            ModuleRegistration moduleRegistration = (ModuleRegistration) Class.forName(moduleClassName).newInstance();
            if (moduleConfiguration.getConfig() == null) {
                moduleConfiguration.setConfig(new HashMap<String, Object>());
            }
            if (moduleName.equals(Constants.CONFIG_STREAMS_MODULE)) {
                moduleConfiguration.getConfig().put(Constants.CONFIG_CATALOG_ROOT_URL, catalogRootUrl);
            }
            Map<String, Object> initConfig = new HashMap<>(moduleConfiguration.getConfig());
            initConfig.put(Constants.CONFIG_AUTHORIZER, authorizer);
            initConfig.put(Constants.CONFIG_SECURITY_CATALOG_SERVICE, securityCatalogService);
            initConfig.put(Constants.CONFIG_SUBJECT, subject);
            if ((initConfig.get("proxyUrl") != null) && (configuration.getHttpProxyUrl() == null || configuration.getHttpProxyUrl().isEmpty())) {
                LOG.warn("Please move proxyUrl, proxyUsername and proxyPassword configuration properties under streams module to httpProxyUrl, " +
                        "httpProxyUsername and httpProxyPassword respectively at top level in your streamline.yaml");
                configuration.setHttpProxyUrl((String) initConfig.get("proxyUrl"));
                configuration.setHttpProxyUsername((String) initConfig.get("proxyUsername"));
                configuration.setHttpProxyPassword((String) initConfig.get("proxyPassword"));
            }
            // pass http proxy information from top level config to each module. Up to them how they want to use it. Currently used in StreamsModule
            initConfig.put(Constants.CONFIG_HTTP_PROXY_URL, configuration.getHttpProxyUrl());
            initConfig.put(Constants.CONFIG_HTTP_PROXY_USERNAME, configuration.getHttpProxyUsername());
            initConfig.put(Constants.CONFIG_HTTP_PROXY_PASSWORD, configuration.getHttpProxyPassword());
            moduleRegistration.init(initConfig, fileStorage);
            if (moduleRegistration instanceof StorageManagerAware) {
                LOG.info("Module [{}] is StorageManagerAware and setting StorageManager.", moduleName);
                StorageManagerAware storageManagerAware = (StorageManagerAware) moduleRegistration;
                storageManagerAware.setStorageManager(storageManager);
            }
            if (moduleRegistration instanceof TransactionManagerAware) {
                LOG.info("Module [{}] is TransactionManagerAware and setting TransactionManager.", moduleName);
                TransactionManagerAware transactionManagerAware = (TransactionManagerAware) moduleRegistration;
                transactionManagerAware.setTransactionManager(transactionManager);
            }
            resourcesToRegister.addAll(moduleRegistration.getResources());

        }

        LOG.info("Registering resources to Jersey environment: [{}]", resourcesToRegister);
        for(Object resource : resourcesToRegister) {
            environment.jersey().register(resource);
        }
        environment.jersey().register(MultiPartFeature.class);

        final ErrorPageErrorHandler errorPageErrorHandler = new ErrorPageErrorHandler();
        errorPageErrorHandler.addErrorPage(Response.Status.UNAUTHORIZED.getStatusCode(), "/401.html");
        environment.getApplicationContext().setErrorHandler(errorPageErrorHandler);
    }
}