/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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 org.apache.atlas;

import org.apache.atlas.security.SecurityUtil;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationConverter;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.net.URL;
import java.util.AbstractMap.SimpleEntry;
import java.util.Iterator;
import java.util.Properties;

import static org.apache.atlas.security.SecurityProperties.HADOOP_SECURITY_CREDENTIAL_PROVIDER_PATH;

/**
 * Application properties used by Atlas.
 */
public final class ApplicationProperties extends PropertiesConfiguration {
    private static final Logger LOG = LoggerFactory.getLogger(ApplicationProperties.class);

    public static final String       ATLAS_CONFIGURATION_DIRECTORY_PROPERTY = "atlas.conf";
    public static final String       ATLAS_PROPERTIES_FILENAME_SYSTEM_CONF  = "atlas.properties";
    public static final String       APPLICATION_PROPERTIES           = "atlas-application.properties";
    public static final String       GRAPHDB_BACKEND_CONF             = "atlas.graphdb.backend";
    public static final String       STORAGE_BACKEND_CONF             = "atlas.graph.storage.backend";
    public static final String       INDEX_BACKEND_CONF               = "atlas.graph.index.search.backend";
    public static final String       INDEX_MAP_NAME_CONF              = "atlas.graph.index.search.map-name";
    public static final String       SOLR_WAIT_SEARCHER_CONF          = "atlas.graph.index.search.solr.wait-searcher";
    public static final String       ELASTICSEARCH_INDEX_NAME_CONF    = "atlas.graph.index.search.elasticsearch.index-name";
    public static final String       INDEX_RECOVERY_CONF              = "atlas.index.recovery.enable";
    public static final String       ENABLE_FULLTEXT_SEARCH_CONF      = "atlas.search.fulltext.enable";
    public static final String       ENABLE_FREETEXT_SEARCH_CONF      = "atlas.search.freetext.enable";
    public static final String       ATLAS_RUN_MODE                   = "atlas.run.mode";
    public static final String       GRAPHBD_BACKEND_JANUS            = "janus";
    public static final String       DEFAULT_INDEX_NAME               = "janusgraph";
    public static final String       STORAGE_BACKEND_HBASE            = "hbase";
    public static final String       STORAGE_BACKEND_HBASE2           = "hbase2";
    public static final String       INDEX_BACKEND_SOLR               = "solr";
    public static final String       INDEX_BACKEND_ELASTICSEARCH      = "elasticsearch";
    public static final String       LDAP_TYPE                        = "atlas.authentication.method.ldap.type";
    public static final String       LDAP                             = "LDAP";
    public static final String       AD                               = "AD";
    public static final String       LDAP_AD_BIND_PASSWORD            = "atlas.authentication.method.ldap.ad.bind.password";
    public static final String       LDAP_BIND_PASSWORD               = "atlas.authentication.method.ldap.bind.password";
    public static final String       MASK_PASSWORD                    = "********";
    public static final String       DEFAULT_GRAPHDB_BACKEND          = GRAPHBD_BACKEND_JANUS;
    public static final boolean      DEFAULT_SOLR_WAIT_SEARCHER       = false;
    public static final boolean      DEFAULT_INDEX_MAP_NAME           = false;
    public static final boolean      DEFAULT_INDEX_RECOVERY           = true;
    public static final AtlasRunMode DEFAULT_ATLAS_RUN_MODE           = AtlasRunMode.PROD;
    public static final String       INDEX_SEARCH_MAX_RESULT_SET_SIZE = "atlas.graph.index.search.max-result-set-size";

    public static final SimpleEntry<String, String> DB_CACHE_CONF               = new SimpleEntry<>("atlas.graph.cache.db-cache", "true");
    public static final SimpleEntry<String, String> DB_CACHE_CLEAN_WAIT_CONF    = new SimpleEntry<>("atlas.graph.cache.db-cache-clean-wait", "20");
    public static final SimpleEntry<String, String> DB_CACHE_SIZE_CONF          = new SimpleEntry<>("atlas.graph.cache.db-cache-size", "0.5");
    public static final SimpleEntry<String, String> DB_TX_CACHE_SIZE_CONF       = new SimpleEntry<>("atlas.graph.cache.tx-cache-size", "15000");
    public static final SimpleEntry<String, String> DB_CACHE_TX_DIRTY_SIZE_CONF = new SimpleEntry<>("atlas.graph.cache.tx-dirty-size", "120");

    private static volatile Configuration instance;

    private ApplicationProperties(URL url) throws ConfigurationException {
        super(url);
    }

    public static void forceReload() {
        if (instance != null) {
            synchronized (ApplicationProperties.class) {
                if (instance != null) {
                    instance = null;
                }
            }
        }
    }

    public static Configuration get() throws AtlasException {
        Configuration me = instance;

        if (me == null) {
            synchronized (ApplicationProperties.class) {
                me = instance;

                if (me == null) {
                    String propertyFilename = System.getProperty(ATLAS_PROPERTIES_FILENAME_SYSTEM_CONF, APPLICATION_PROPERTIES);
                    set(get(propertyFilename));

                    me = instance;
                }
            }
        }

        return me;
    }

    public static Configuration set(Configuration configuration) throws AtlasException {
        synchronized (ApplicationProperties.class) {
            instance = configuration;
        }

        return instance;
    }

    public static Configuration getConfFromAbsolutePath(String absoluteFilePath) throws AtlasException {
        if (instance == null) {
            synchronized (ApplicationProperties.class) {
                if (instance == null) {
                    set(get(absoluteFilePath));
                }
            }
        }
        return instance;
    }

    public static Configuration get(PropertiesConfiguration clientConf) throws AtlasException {
        if (instance == null) {
            synchronized (ApplicationProperties.class) {
                if (instance == null) {
                    set(clientConf);
                }
            }
        }
        return instance;
    }

    public static Configuration get(String fileName) throws AtlasException {
        try {
            URL url = null;

            if (StringUtils.isNotBlank(fileName)) {
                File file = new File(fileName);
                if (file.exists()) {
                    url = file.toURI().toURL();
                    LOG.info("Loading {} from {}", fileName, url);
                }
            }
            if (url == null) {
                String confLocation = System.getProperty(ATLAS_CONFIGURATION_DIRECTORY_PROPERTY);

                if (confLocation == null) {
                    LOG.info("Looking for {} in classpath", fileName);

                    url = ApplicationProperties.class.getClassLoader().getResource(fileName);

                    if (url == null) {
                        LOG.info("Looking for /{} in classpath", fileName);
                        url = ApplicationProperties.class.getClassLoader().getResource("/" + fileName);
                    }
                } else {
                    url = new File(confLocation, fileName).toURI().toURL();
                }

                LOG.info("Loading {} from {}", fileName, url);
            }

            ApplicationProperties appProperties = new ApplicationProperties(url);

            appProperties.setDefaults();

            setLdapPasswordFromKeystore(appProperties);

            Configuration configuration = appProperties.interpolatedConfiguration();

            logConfiguration(configuration);

            return configuration;
        } catch (Exception e) {
            throw new AtlasException("Failed to load application properties", e);
        }
    }

    public static Configuration getSubsetConfiguration(Configuration inConf, String prefix) {
        return inConf.subset(prefix);
    }

    public static Properties getSubsetAsProperties(Configuration inConf, String prefix) {
        Configuration subset = inConf.subset(prefix);

        return ConfigurationConverter.getProperties(subset);
    }

    public static Class getClass(Configuration configuration, String propertyName, String defaultValue, Class assignableClass) throws AtlasException {
        try {
            String   propertyValue = configuration.getString(propertyName, defaultValue);
            Class<?> clazz         = Class.forName(propertyValue);

            if (assignableClass == null || assignableClass.isAssignableFrom(clazz)) {
                return clazz;
            } else {
                String message = "Class " + clazz.getName() + " specified in property " + propertyName + " is not assignable to class " + assignableClass.getName();

                LOG.error(message);

                throw new AtlasException(message);
            }
        } catch (Exception e) {
            throw new AtlasException(e);
        }
    }

    public static Class getClass(String fullyQualifiedClassName, Class assignableClass) throws AtlasException {
        try {
            Class<?> clazz = Class.forName(fullyQualifiedClassName);

            if (assignableClass == null || assignableClass.isAssignableFrom(clazz)) {
                return clazz;
            } else {
                String message = "Class " + clazz.getName() + " is not assignable to class " + assignableClass.getName();

                LOG.error(message);

                throw new AtlasException(message);
            }
        } catch (Exception e) {
            throw new AtlasException(e);
        }
    }

    /**
     * Get the specified property as an {@link InputStream}.
     * If the property is not set, then the specified default filename
     * is searched for in the following locations, in order of precedence:
     * 1. Atlas configuration directory specified by the {@link #ATLAS_CONFIGURATION_DIRECTORY_PROPERTY} system property
     * 2. relative to the working directory if {@link #ATLAS_CONFIGURATION_DIRECTORY_PROPERTY} is not set
     * 3. as a classloader resource
     *
     * @param configuration
     * @param propertyName
     * @param defaultFileName name of file to use by default if specified property is not set in the configuration- if null,
     * an {@link AtlasException} is thrown if the property is not set
     * @return an {@link InputStream}
     * @throws AtlasException if no file was found or if there was an error loading the file
     */
    public static InputStream getFileAsInputStream(Configuration configuration, String propertyName, String defaultFileName) throws AtlasException {
        File   fileToLoad;
        String fileName   = configuration.getString(propertyName);

        if (fileName == null) {
            if (defaultFileName == null) {
                throw new AtlasException(propertyName + " property not set and no default value specified");
            }

            LOG.info("{} property not set; defaulting to {}", propertyName, defaultFileName);

            fileName = defaultFileName;

            String atlasConfDir = System.getProperty(ATLAS_CONFIGURATION_DIRECTORY_PROPERTY);

            if (atlasConfDir != null) {
                // Look for default filename in Atlas config directory
                fileToLoad = new File(atlasConfDir, fileName);
            } else {
                // Look for default filename under the working directory
                fileToLoad = new File(fileName);
            }
        } else {
            // Look for configured filename
            fileToLoad = new File(fileName);
        }

        InputStream inStr;

        if (fileToLoad.exists()) {
            try {
                LOG.info("Loading file {} from {}", fileName, fileToLoad.getPath());

                inStr = new FileInputStream(fileToLoad);
            } catch (FileNotFoundException e) {
                throw new AtlasException("Error loading file " + fileName, e);
            }
        } else {
            // Look for file as class loader resource
            inStr = Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName);

            if (inStr == null) {
                String msg = fileName + " not found in file system or as class loader resource";

                LOG.error(msg);

                throw new AtlasException(msg);
            }

            LOG.info("Loaded {} as resource from {}", fileName, Thread.currentThread().getContextClassLoader().getResource(fileName));
        }

        return inStr;
    }

    void setDefault(SimpleEntry<String, String> keyValueDefault, String currentValue) {
        if (StringUtils.isNotEmpty(currentValue)) {
            return;
        }

        clearPropertyDirect(keyValueDefault.getKey());
        addPropertyDirect(keyValueDefault.getKey(), keyValueDefault.getValue());

        LOG.info("Property (set to default) {} = {}", keyValueDefault.getKey(), keyValueDefault.getValue());
    }

    private static void logConfiguration(Configuration configuration) {
        if (LOG.isDebugEnabled()) {
            Iterator<String> keys = configuration.getKeys();

            LOG.debug("Configuration loaded:");

            while (keys.hasNext()) {
                String key = keys.next();

                LOG.debug("{} = {}", key, configuration.getProperty(key));
            }
        }
    }

    private static void setLdapPasswordFromKeystore(Configuration configuration) {
        String ldapType = configuration.getString(LDAP_TYPE);

        if (StringUtils.isNotEmpty(ldapType)) {
            try {
                if (ldapType.equalsIgnoreCase(LDAP)) {
                    String maskPasssword = configuration.getString(LDAP_BIND_PASSWORD);

                    if (MASK_PASSWORD.equals(maskPasssword)) {
                        String password = SecurityUtil.getPassword(configuration, LDAP_BIND_PASSWORD, HADOOP_SECURITY_CREDENTIAL_PROVIDER_PATH);

                        configuration.clearProperty(LDAP_BIND_PASSWORD);
                        configuration.addProperty(LDAP_BIND_PASSWORD, password);
                    }
                } else if (ldapType.equalsIgnoreCase(AD)) {
                    String maskPasssword = configuration.getString(LDAP_AD_BIND_PASSWORD);

                    if (MASK_PASSWORD.equals(maskPasssword)) {
                        String password = SecurityUtil.getPassword(configuration, LDAP_AD_BIND_PASSWORD, HADOOP_SECURITY_CREDENTIAL_PROVIDER_PATH);

                        configuration.clearProperty(LDAP_AD_BIND_PASSWORD);
                        configuration.addProperty(LDAP_AD_BIND_PASSWORD, password);
                    }
                }
            } catch (Exception e) {
                LOG.error("Error in getting secure password ", e);
            }
        }
    }

    public static String getDecryptedPassword(Configuration configuration, String propertyKey) {
        String configuredValue = configuration != null ? configuration.getString(propertyKey) : null;

        if (configuredValue != null && MASK_PASSWORD.equals(configuredValue)) {
            try {
                return SecurityUtil.getPassword(configuration, propertyKey, HADOOP_SECURITY_CREDENTIAL_PROVIDER_PATH);
            } catch (Exception e) {
                LOG.error("Error in getting secure password ", e);
            }
        }

        return configuredValue;
    }

    private void setDefaults() {
        AtlasRunMode runMode = AtlasRunMode.valueOf(getString(ATLAS_RUN_MODE, DEFAULT_ATLAS_RUN_MODE.name()));

        // setting value for 'atlas.graphdb.backend' (default = 'janus')
        String graphDbBackend = getString(GRAPHDB_BACKEND_CONF);

        if (StringUtils.isEmpty(graphDbBackend)) {
            graphDbBackend = DEFAULT_GRAPHDB_BACKEND;
        }

        clearPropertyDirect(GRAPHDB_BACKEND_CONF);
        addPropertyDirect(GRAPHDB_BACKEND_CONF, graphDbBackend);

        LOG.info("Using graphdb backend '{}'", graphDbBackend);

        // setting value for 'atlas.graph.storage.backend' (default = 'hbase2')
        String storageBackend = getString(STORAGE_BACKEND_CONF);

        if (StringUtils.isEmpty(storageBackend) || storageBackend.equalsIgnoreCase(STORAGE_BACKEND_HBASE)) {
            storageBackend = STORAGE_BACKEND_HBASE2;
        }

        clearPropertyDirect(STORAGE_BACKEND_CONF);
        addPropertyDirect(STORAGE_BACKEND_CONF, storageBackend);

        LOG.info("Using storage backend '{}'", storageBackend);

        // setting value for 'atlas.graph.index.search.backend' (default = 'solr')
        String indexBackend = getString(INDEX_BACKEND_CONF);

        if (StringUtils.isEmpty(indexBackend)) {
            indexBackend = INDEX_BACKEND_SOLR;
        }

        clearPropertyDirect(INDEX_BACKEND_CONF);
        addPropertyDirect(INDEX_BACKEND_CONF, indexBackend);

        LOG.info("Using index backend '{}'", indexBackend);

        // set the following if indexing backend is 'solr'
        if (indexBackend.equalsIgnoreCase(INDEX_BACKEND_SOLR)) {
            LOG.info("Atlas is running in MODE: {}.", runMode.name());

            if (runMode == AtlasRunMode.PROD) {
                if (!containsKey(SOLR_WAIT_SEARCHER_CONF)) {
                    addPropertyDirect(SOLR_WAIT_SEARCHER_CONF, DEFAULT_SOLR_WAIT_SEARCHER);
                }

                LOG.info("Setting {} = {}", SOLR_WAIT_SEARCHER_CONF, getBoolean(SOLR_WAIT_SEARCHER_CONF));

                clearPropertyDirect(INDEX_MAP_NAME_CONF);
                addPropertyDirect(INDEX_MAP_NAME_CONF, DEFAULT_INDEX_MAP_NAME);

                LOG.info("Setting index.search.map-name property '{}'", DEFAULT_INDEX_MAP_NAME);
            }
        } else if (indexBackend.equalsIgnoreCase(INDEX_BACKEND_ELASTICSEARCH)) {
            addPropertyDirect(ELASTICSEARCH_INDEX_NAME_CONF, DEFAULT_INDEX_NAME);

            LOG.info("Setting elasticsearch.index-name property '{}'", DEFAULT_INDEX_NAME);
        }

        // setting value for 'atlas.graph.index.search.max-result-set-size' (default = 500000)
        int indexMaxResultSetSize = getInt(INDEX_SEARCH_MAX_RESULT_SET_SIZE, 500000);

        clearPropertyDirect(INDEX_SEARCH_MAX_RESULT_SET_SIZE);
        addPropertyDirect(INDEX_SEARCH_MAX_RESULT_SET_SIZE, indexMaxResultSetSize);

        LOG.info("Setting {} = {}", INDEX_SEARCH_MAX_RESULT_SET_SIZE, indexMaxResultSetSize);

        setDbCacheConfDefaults();
    }

    private void setDbCacheConfDefaults() {
        SimpleEntry<String, String>[] keyValues = new SimpleEntry[] {DB_CACHE_CONF, DB_CACHE_CLEAN_WAIT_CONF, DB_CACHE_SIZE_CONF, DB_TX_CACHE_SIZE_CONF, DB_CACHE_TX_DIRTY_SIZE_CONF};

        for (SimpleEntry<String, String> kv : keyValues) {
            String currentValue = getString(kv.getKey());

            setDefault(kv, currentValue);
        }
    }

    public enum AtlasRunMode {
        PROD,
        DEV
    }
}
