/*
 * Copyright 1999-2019 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.adapter.gateway.common.rule;

import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.param.GatewayRegexCache;
import com.alibaba.csp.sentinel.log.RecordLog;
import com.alibaba.csp.sentinel.property.DynamicSentinelProperty;
import com.alibaba.csp.sentinel.property.PropertyListener;
import com.alibaba.csp.sentinel.property.SentinelProperty;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleUtil;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParameterMetric;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParameterMetricStorage;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.csp.sentinel.util.StringUtil;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author Eric Zhao
 * @since 1.6.0
 */
public final class GatewayRuleManager {

    /**
     * Gateway flow rule map: (resource, [rules...])
     */
    private static final Map<String, Set<GatewayFlowRule>> GATEWAY_RULE_MAP = new ConcurrentHashMap<>();

    private static final Map<String, List<ParamFlowRule>> CONVERTED_PARAM_RULE_MAP = new ConcurrentHashMap<>();

    private static final GatewayRulePropertyListener LISTENER = new GatewayRulePropertyListener();
    private static final Set<Integer> FIELD_REQUIRED_SET = new HashSet<>(
            Arrays.asList(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM,
                    SentinelGatewayConstants.PARAM_PARSE_STRATEGY_HEADER,
                    SentinelGatewayConstants.PARAM_PARSE_STRATEGY_COOKIE)
    );
    private static SentinelProperty<Set<GatewayFlowRule>> currentProperty = new DynamicSentinelProperty<>();

    static {
        currentProperty.addListener(LISTENER);
    }

    private GatewayRuleManager() {
    }

    public static void register2Property(SentinelProperty<Set<GatewayFlowRule>> property) {
        AssertUtil.notNull(property, "property cannot be null");
        synchronized (LISTENER) {
            RecordLog.info("[GatewayRuleManager] Registering new property to gateway flow rule manager");
            currentProperty.removeListener(LISTENER);
            property.addListener(LISTENER);
            currentProperty = property;
        }
    }

    /**
     * Load all provided gateway rules into memory, while
     * previous rules will be replaced.
     *
     * @param rules rule set
     * @return true if updated, otherwise false
     */
    public static boolean loadRules(Set<GatewayFlowRule> rules) {
        return currentProperty.updateValue(rules);
    }

    public static Set<GatewayFlowRule> getRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();
        for (Set<GatewayFlowRule> ruleSet : GATEWAY_RULE_MAP.values()) {
            rules.addAll(ruleSet);
        }
        return rules;
    }

    public static Set<GatewayFlowRule> getRulesForResource(String resourceName) {
        if (StringUtil.isBlank(resourceName)) {
            return new HashSet<>();
        }
        Set<GatewayFlowRule> set = GATEWAY_RULE_MAP.get(resourceName);
        if (set == null) {
            return new HashSet<>();
        }
        return new HashSet<>(set);
    }

    /**
     * <p>Get all converted parameter rules.</p>
     * <p>Note: caller SHOULD NOT modify the list and rules.</p>
     *
     * @param resourceName valid resource name
     * @return converted parameter rules
     */
    public static List<ParamFlowRule> getConvertedParamRules(String resourceName) {
        if (StringUtil.isBlank(resourceName)) {
            return new ArrayList<>();
        }
        return CONVERTED_PARAM_RULE_MAP.get(resourceName);
    }

    public static boolean isValidRule(GatewayFlowRule rule) {
        if (rule == null || StringUtil.isBlank(rule.getResource()) || rule.getResourceMode() < 0
                || rule.getGrade() < 0 || rule.getCount() < 0 || rule.getBurst() < 0 || rule.getControlBehavior() < 0) {
            return false;
        }
        if (rule.getControlBehavior() == RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER
                && rule.getMaxQueueingTimeoutMs() < 0) {
            return false;
        }
        if (rule.getIntervalSec() <= 0) {
            return false;
        }
        GatewayParamFlowItem item = rule.getParamItem();
        if (item != null) {
            return isValidParamItem(item);
        }
        return true;
    }

    static boolean isValidParamItem(/*@NonNull*/ GatewayParamFlowItem item) {
        if (item.getParseStrategy() < 0) {
            return false;
        }
        // Check required field name for item types.
        if (FIELD_REQUIRED_SET.contains(item.getParseStrategy()) && StringUtil.isBlank(item.getFieldName())) {
            return false;
        }
        return StringUtil.isEmpty(item.getPattern()) || item.getMatchStrategy() >= 0;
    }

    private static final class GatewayRulePropertyListener implements PropertyListener<Set<GatewayFlowRule>> {

        @Override
        public void configUpdate(Set<GatewayFlowRule> conf) {
            applyGatewayRuleInternal(conf);
            RecordLog.info("[GatewayRuleManager] Gateway flow rules received: {}", GATEWAY_RULE_MAP);
        }

        @Override
        public void configLoad(Set<GatewayFlowRule> conf) {
            applyGatewayRuleInternal(conf);
            RecordLog.info("[GatewayRuleManager] Gateway flow rules loaded: {}", GATEWAY_RULE_MAP);
        }

        private int getIdxInternal(Map<String, Integer> idxMap, String resourceName) {
            // Prepare index map.
            if (!idxMap.containsKey(resourceName)) {
                idxMap.put(resourceName, 0);
            }
            return idxMap.get(resourceName);
        }

        private void cacheRegexPattern(/*@NonNull*/ GatewayParamFlowItem item) {
            String pattern = item.getPattern();
            if (StringUtil.isNotEmpty(pattern) &&
                    item.getMatchStrategy() == SentinelGatewayConstants.PARAM_MATCH_STRATEGY_REGEX) {
                if (GatewayRegexCache.getRegexPattern(pattern) == null) {
                    GatewayRegexCache.addRegexPattern(pattern);
                }
            }
        }

        private synchronized void applyGatewayRuleInternal(Set<GatewayFlowRule> conf) {
            if (conf == null || conf.isEmpty()) {
                applyToConvertedParamMap(new HashSet<ParamFlowRule>());
                GATEWAY_RULE_MAP.clear();
                return;
            }
            Map<String, Set<GatewayFlowRule>> gatewayRuleMap = new ConcurrentHashMap<>();
            Map<String, Integer> idxMap = new HashMap<>();
            Set<ParamFlowRule> paramFlowRules = new HashSet<>();
            Map<String, List<GatewayFlowRule>> noParamMap = new HashMap<>();

            for (GatewayFlowRule rule : conf) {
                if (!isValidRule(rule)) {
                    RecordLog.warn("[GatewayRuleManager] Ignoring invalid rule when loading new rules: " + rule);
                    continue;
                }
                String resourceName = rule.getResource();
                if (rule.getParamItem() == null) {
                    // Cache the rules with no parameter config, then skip.
                    List<GatewayFlowRule> noParamList = noParamMap.get(resourceName);
                    if (noParamList == null) {
                        noParamList = new ArrayList<>();
                        noParamMap.put(resourceName, noParamList);
                    }
                    noParamList.add(rule);
                } else {
                    int idx = getIdxInternal(idxMap, resourceName);
                    // Convert to parameter flow rule.
                    if (paramFlowRules.add(GatewayRuleConverter.applyToParamRule(rule, idx))) {
                        idxMap.put(rule.getResource(), idx + 1);
                    }
                    cacheRegexPattern(rule.getParamItem());
                }
                // Apply to the gateway rule map.
                Set<GatewayFlowRule> ruleSet = gatewayRuleMap.get(resourceName);
                if (ruleSet == null) {
                    ruleSet = new HashSet<>();
                    gatewayRuleMap.put(resourceName, ruleSet);
                }
                ruleSet.add(rule);
            }
            // Handle non-param mode rules.
            for (Map.Entry<String, List<GatewayFlowRule>> e : noParamMap.entrySet()) {
                List<GatewayFlowRule> rules = e.getValue();
                if (rules == null || rules.isEmpty()) {
                    continue;
                }
                for (GatewayFlowRule rule : rules) {
                    int idx = getIdxInternal(idxMap, e.getKey());
                    // Always use the same index (the last position).
                    paramFlowRules.add(GatewayRuleConverter.applyNonParamToParamRule(rule, idx));
                }
            }

            applyToConvertedParamMap(paramFlowRules);

            GATEWAY_RULE_MAP.clear();
            GATEWAY_RULE_MAP.putAll(gatewayRuleMap);
        }

        private void applyToConvertedParamMap(Set<ParamFlowRule> paramFlowRules) {
            Map<String, List<ParamFlowRule>> newRuleMap = ParamFlowRuleUtil.buildParamRuleMap(
                    new ArrayList<>(paramFlowRules));
            if (newRuleMap == null || newRuleMap.isEmpty()) {
                // No parameter flow rules, so clear all the metrics.
                for (String resource : CONVERTED_PARAM_RULE_MAP.keySet()) {
                    ParameterMetricStorage.clearParamMetricForResource(resource);
                }
                RecordLog.info("[GatewayRuleManager] No gateway rules, clearing parameter metrics of previous rules");
                CONVERTED_PARAM_RULE_MAP.clear();
                return;
            }

            // Clear unused parameter metrics.
            for (Map.Entry<String, List<ParamFlowRule>> entry : CONVERTED_PARAM_RULE_MAP.entrySet()) {
                String resource = entry.getKey();
                if (!newRuleMap.containsKey(resource)) {
                    ParameterMetricStorage.clearParamMetricForResource(resource);
                    continue;
                }
                List<ParamFlowRule> newRuleList = newRuleMap.get(resource);
                List<ParamFlowRule> oldRuleList = new ArrayList<>(entry.getValue());
                oldRuleList.removeAll(newRuleList);
                for (ParamFlowRule rule : oldRuleList) {
                    ParameterMetric metric = ParameterMetricStorage.getParamMetricForResource(resource);
                    if (null != metric) {
                        metric.clearForRule(rule);
                    }
                }
            }

            // Apply to converted rule map.
            CONVERTED_PARAM_RULE_MAP.clear();
            CONVERTED_PARAM_RULE_MAP.putAll(newRuleMap);

            RecordLog.info("[GatewayRuleManager] Converted internal param rules: {}", CONVERTED_PARAM_RULE_MAP);
        }
    }
}
