/*
 * Copyright 1999-2018 Alibaba Group Holding Ltd.
 *
 * 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.alibaba.csp.sentinel.config;

import com.alibaba.csp.sentinel.log.RecordLog;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.csp.sentinel.util.StringUtil;

import java.io.File;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

/**
 * The universal local configuration center of Sentinel. The config is retrieved from command line arguments
 * and customized properties file by default.
 *
 * @author leyou
 * @author Eric Zhao
 * @author Lin Liang
 */
public final class SentinelConfig {

    /**
     * The default application type.
     *
     * @since 1.6.0
     */
    public static final int APP_TYPE_COMMON = 0;

    /**
     * Parameter value for using context classloader.
     */
    private static final String CLASSLOADER_CONTEXT = "context";

    private static final Map<String, String> props = new ConcurrentHashMap<>();

    private static int appType = APP_TYPE_COMMON;
    private static String appName = "";

    public static final String PROJECT_NAME_PROP_KEY = "project.name";
    public static final String APP_NAME_PROP_KEY = "csp.sentinel.app.name";
    public static final String APP_TYPE_PROP_KEY = "csp.sentinel.app.type";
    public static final String CHARSET = "csp.sentinel.charset";
    public static final String SINGLE_METRIC_FILE_SIZE = "csp.sentinel.metric.file.single.size";
    public static final String TOTAL_METRIC_FILE_COUNT = "csp.sentinel.metric.file.total.count";
    public static final String COLD_FACTOR = "csp.sentinel.flow.cold.factor";
    public static final String STATISTIC_MAX_RT = "csp.sentinel.statistic.max.rt";
    public static final String SPI_CLASSLOADER = "csp.sentinel.spi.classloader";
    public static final String METRIC_FLUSH_INTERVAL = "csp.sentinel.metric.flush.interval";

    public static final String DEFAULT_CHARSET = "UTF-8";
    public static final long DEFAULT_SINGLE_METRIC_FILE_SIZE = 1024 * 1024 * 50;
    public static final int DEFAULT_TOTAL_METRIC_FILE_COUNT = 6;
    public static final int DEFAULT_COLD_FACTOR = 3;
    public static final int DEFAULT_STATISTIC_MAX_RT = 5000;
    public static final long DEFAULT_METRIC_FLUSH_INTERVAL = 1L;

    static {
        try {
            initialize();
            loadProps();
            resolveAppName();
            resolveAppType();
            RecordLog.info("[SentinelConfig] Application type resolved: {}", appType);
        } catch (Throwable ex) {
            RecordLog.warn("[SentinelConfig] Failed to initialize", ex);
            ex.printStackTrace();
        }
    }

    private static void resolveAppType() {
        try {
            String type = getConfig(APP_TYPE_PROP_KEY);
            if (type == null) {
                appType = APP_TYPE_COMMON;
                return;
            }
            appType = Integer.parseInt(type);
            if (appType < 0) {
                appType = APP_TYPE_COMMON;
            }
        } catch (Exception ex) {
            appType = APP_TYPE_COMMON;
        }
    }

    private static void initialize() {
        // Init default properties.
        setConfig(CHARSET, DEFAULT_CHARSET);
        setConfig(SINGLE_METRIC_FILE_SIZE, String.valueOf(DEFAULT_SINGLE_METRIC_FILE_SIZE));
        setConfig(TOTAL_METRIC_FILE_COUNT, String.valueOf(DEFAULT_TOTAL_METRIC_FILE_COUNT));
        setConfig(COLD_FACTOR, String.valueOf(DEFAULT_COLD_FACTOR));
        setConfig(STATISTIC_MAX_RT, String.valueOf(DEFAULT_STATISTIC_MAX_RT));
        setConfig(METRIC_FLUSH_INTERVAL, String.valueOf(DEFAULT_METRIC_FLUSH_INTERVAL));
    }

    private static void loadProps() {
        Properties properties = SentinelConfigLoader.getProperties();
        for (Object key : properties.keySet()) {
            setConfig((String) key, (String) properties.get(key));
        }
    }

    /**
     * Get config value of the specific key.
     *
     * @param key config key
     * @return the config value.
     */
    public static String getConfig(String key) {
        AssertUtil.notNull(key, "key cannot be null");
        return props.get(key);
    }
    
    /**
     * Get config value of the specific key.
     *
     * @param key config key
     * @param envVariableKey Get the value of the environment variable with the given key
     * @return the config value.
     */
    public static String getConfig(String key, boolean envVariableKey) {
        AssertUtil.notNull(key, "key cannot be null");
        if (envVariableKey) {
            String value = System.getenv(key);
            if (StringUtil.isNotEmpty(value)) {
                return value;
            }
        }
        return getConfig(key);
    }

    public static void setConfig(String key, String value) {
        AssertUtil.notNull(key, "key cannot be null");
        AssertUtil.notNull(value, "value cannot be null");
        props.put(key, value);
    }

    public static String removeConfig(String key) {
        AssertUtil.notNull(key, "key cannot be null");
        return props.remove(key);
    }

    public static void setConfigIfAbsent(String key, String value) {
        AssertUtil.notNull(key, "key cannot be null");
        AssertUtil.notNull(value, "value cannot be null");
        String v = props.get(key);
        if (v == null) {
            props.put(key, value);
        }
    }

    public static String getAppName() {
        return appName;
    }

    /**
     * Get application type.
     *
     * @return application type, common (0) by default
     * @since 1.6.0
     */
    public static int getAppType() {
        return appType;
    }

    public static String charset() {
        return props.get(CHARSET);
    }
    
    /**
     * Get the metric log flush interval in second
     * @return  the metric log flush interval in second
     * @since 1.8.1
     */
    public static long metricLogFlushIntervalSec() {
        String flushIntervalStr = SentinelConfig.getConfig(METRIC_FLUSH_INTERVAL);
        if (flushIntervalStr == null) {
            return DEFAULT_METRIC_FLUSH_INTERVAL;
        }
        try {
            return Long.parseLong(flushIntervalStr);
        } catch (Throwable throwable) {
            RecordLog.warn("[SentinelConfig] Parse the metricLogFlushInterval fail, use default value: "
                    + DEFAULT_METRIC_FLUSH_INTERVAL, throwable);
            return DEFAULT_METRIC_FLUSH_INTERVAL;
        }
    }

    public static long singleMetricFileSize() {
        try {
            return Long.parseLong(props.get(SINGLE_METRIC_FILE_SIZE));
        } catch (Throwable throwable) {
            RecordLog.warn("[SentinelConfig] Parse singleMetricFileSize fail, use default value: "
                    + DEFAULT_SINGLE_METRIC_FILE_SIZE, throwable);
            return DEFAULT_SINGLE_METRIC_FILE_SIZE;
        }
    }

    public static int totalMetricFileCount() {
        try {
            return Integer.parseInt(props.get(TOTAL_METRIC_FILE_COUNT));
        } catch (Throwable throwable) {
            RecordLog.warn("[SentinelConfig] Parse totalMetricFileCount fail, use default value: "
                    + DEFAULT_TOTAL_METRIC_FILE_COUNT, throwable);
            return DEFAULT_TOTAL_METRIC_FILE_COUNT;
        }
    }

    public static int coldFactor() {
        try {
            int coldFactor = Integer.parseInt(props.get(COLD_FACTOR));
            // check the cold factor larger than 1
            if (coldFactor <= 1) {
                coldFactor = DEFAULT_COLD_FACTOR;
                RecordLog.warn("cold factor=" + coldFactor + ", should be larger than 1, use default value: "
                        + DEFAULT_COLD_FACTOR);
            }
            return coldFactor;
        } catch (Throwable throwable) {
            RecordLog.warn("[SentinelConfig] Parse coldFactor fail, use default value: "
                    + DEFAULT_COLD_FACTOR, throwable);
            return DEFAULT_COLD_FACTOR;
        }
    }

    /**
     * <p>Get the max RT value that Sentinel could accept for system BBR strategy.</p>
     *
     * @return the max allowed RT value
     * @since 1.4.1
     */
    public static int statisticMaxRt() {
        String v = props.get(STATISTIC_MAX_RT);
        try {
            if (StringUtil.isEmpty(v)) {
                return DEFAULT_STATISTIC_MAX_RT;
            }
            return Integer.parseInt(v);
        } catch (Throwable throwable) {
            RecordLog.warn("[SentinelConfig] Invalid statisticMaxRt value: {}, using the default value instead: "
                    + DEFAULT_STATISTIC_MAX_RT, v, throwable);
            SentinelConfig.setConfig(STATISTIC_MAX_RT, String.valueOf(DEFAULT_STATISTIC_MAX_RT));
            return DEFAULT_STATISTIC_MAX_RT;
        }
    }

    /**
     * Function for resolving project name. The order is elaborated below:
     *
     * <ol>
     * <li>Resolve the value from {@code CSP_SENTINEL_APP_NAME} system environment;</li>
     * <li>Resolve the value from {@code csp.sentinel.app.name} system property;</li>
     * <li>Resolve the value from {@code project.name} system property (for compatibility);</li>
     * <li>Resolve the value from {@code sun.java.command} system property, then remove path, arguments and ".jar" or ".JAR"
     * suffix, use the result as app name. Note that whitespace in file name or path is not allowed, or a
     * wrong app name may be gotten, For example:
     * <p>
     * <code>
     * "test.Main" -> test.Main<br/>
     * "/target/test.Main" -> test.Main<br/>
     * "/target/test.Main args1" -> test.Main<br/>
     * "Main.jar" -> Main<br/>
     * "/target/Main.JAR args1" -> Main<br/>
     * "Mai n.jar" -> Mai // whitespace in file name is not allowed<br/>
     * </code>
     * </p>
     * </li>
     * </ol>
     */
    private static void resolveAppName() {
        // Priority: system env -> csp.sentinel.app.name -> project.name -> main class (or jar) name
        String envKey = toEnvKey(APP_NAME_PROP_KEY);
        String n = System.getenv(envKey);
        if (!StringUtil.isBlank(n)) {
            appName = n;
            RecordLog.info("App name resolved from system env {}: {}", envKey, appName);
            return;
        }
        n = props.get(APP_NAME_PROP_KEY);
        if (!StringUtil.isBlank(n)) {
            appName = n;
            RecordLog.info("App name resolved from property {}: {}", APP_NAME_PROP_KEY, appName);
            return;
        }
        n = props.get(PROJECT_NAME_PROP_KEY);
        if (!StringUtil.isBlank(n)) {
            appName = n;
            RecordLog.info("App name resolved from property {}: {}", PROJECT_NAME_PROP_KEY, appName);
            return;
        }
        // Parse sun.java.command property by default.
        String command = System.getProperty("sun.java.command");
        if (StringUtil.isBlank(command)) {
            RecordLog.warn("Cannot resolve default appName from property sun.java.command");
            return;
        }
        command = command.split("\\s")[0];
        String separator = File.separator;
        if (command.contains(separator)) {
            String[] strs;
            if ("\\".equals(separator)) {
                // Handle separator in Windows.
                strs = command.split("\\\\");
            } else {
                strs = command.split(separator);
            }
            command = strs[strs.length - 1];
        }
        if (command.toLowerCase().endsWith(".jar")) {
            command = command.substring(0, command.length() - 4);
        }
        appName = command;
        RecordLog.info("App name resolved from default: {}", appName);
    }

    private static String toEnvKey(/*@NotBlank*/ String propKey) {
        return propKey.toUpperCase().replace('.', '_');
    }
    /**
     * Whether use context classloader via config parameter
     *
     * @return Whether use context classloader
     */
    public static boolean shouldUseContextClassloader() {
        String classloaderConf = SentinelConfig.getConfig(SentinelConfig.SPI_CLASSLOADER);
        return CLASSLOADER_CONTEXT.equalsIgnoreCase(classloaderConf);
    }

    private SentinelConfig() {}
}
