// Copyright 1999-2020 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 isolation

import (
	"reflect"
	"regexp"
	"sync"

	"github.com/alibaba/sentinel-golang/logging"
	"github.com/alibaba/sentinel-golang/util"
	"github.com/pkg/errors"
)

var (
	ruleMap           = make(map[string][]*Rule)
	regexRuleMap      = make(map[string][]*Rule)
	regexCacheRuleMap = make(map[string][]*Rule)
	rwMux             = &sync.RWMutex{}
	currentRules      = make(map[string][]*Rule, 0)
	updateRuleMux     = new(sync.Mutex)
)

// LoadRules loads the given isolation rules to the rule manager, while all previous rules will be replaced.
// the first returned value indicates whether do real load operation, if the rules is the same with previous rules, return false
func LoadRules(rules []*Rule) (bool, error) {
	resRulesMap := make(map[string][]*Rule, 16)
	for _, rule := range rules {
		resRules, exist := resRulesMap[rule.Resource]
		if !exist {
			resRules = make([]*Rule, 0, 1)
		}
		resRulesMap[rule.Resource] = append(resRules, rule)
	}

	updateRuleMux.Lock()
	defer updateRuleMux.Unlock()
	isEqual := reflect.DeepEqual(currentRules, resRulesMap)
	if isEqual {
		logging.Info("[Isolation] Load rules is the same with current rules, so ignore load operation.")
		return false, nil
	}

	err := onRuleUpdate(resRulesMap)
	return true, err
}

func onRuleUpdate(rawResRulesMap map[string][]*Rule) (err error) {
	validResRulesMap := make(map[string][]*Rule)
	validRegexResRulesMap := make(map[string][]*Rule)
	for res, rules := range rawResRulesMap {
		validResRules := make([]*Rule, 0)
		validRegexResRules := make([]*Rule, 0)
		for _, rule := range rules {
			if err := IsValidRule(rule); err != nil {
				logging.Warn("[Isolation onRuleUpdate] Ignoring invalid isolation rule", "rule", rule, "reason", err.Error())
				continue
			}
			if rule.Regex {
				validRegexResRules = append(validRegexResRules, rule)
			} else {
				validResRules = append(validResRules, rule)
			}
		}
		if len(validResRules) > 0 {
			validResRulesMap[res] = validResRules
		}
		if len(validRegexResRules) > 0 {
			validRegexResRulesMap[res] = validRegexResRules
		}
	}

	start := util.CurrentTimeNano()
	rwMux.Lock()
	ruleMap = validResRulesMap
	regexRuleMap = validRegexResRulesMap
	regexCacheRuleMap = make(map[string][]*Rule)
	rwMux.Unlock()
	currentRules = rawResRulesMap

	logging.Debug("[Isolation onRuleUpdate] Time statistic(ns) for updating isolation rule", "timeCost", util.CurrentTimeNano()-start)
	logRuleUpdate(validResRulesMap, validRegexResRulesMap)
	return
}

// LoadRulesOfResource loads the given resource's isolation rules to the rule manager, while all previous resource's rules will be replaced.
// the first returned value indicates whether do real load operation, if the rules is the same with previous resource's rules, return false
func LoadRulesOfResource(res string, rules []*Rule) (bool, error) {
	if len(res) == 0 {
		return false, errors.New("empty resource")
	}
	updateRuleMux.Lock()
	defer updateRuleMux.Unlock()
	// clear resource rules
	if len(rules) == 0 {
		// clear resource's currentRules
		delete(currentRules, res)
		// clear ruleMap
		rwMux.Lock()
		delete(ruleMap, res)
		delete(regexRuleMap, res)
		regexCacheRuleMap = make(map[string][]*Rule)
		rwMux.Unlock()
		logging.Info("[Isolation] clear resource level rules", "resource", res)
		return true, nil
	}
	// load resource level rules
	isEqual := reflect.DeepEqual(currentRules[res], rules)
	if isEqual {
		logging.Info("[Isolation] Load resource level rules is the same with current resource level rules, so ignore load operation.")
		return false, nil
	}

	err := onResourceRuleUpdate(res, rules)
	return true, err
}

func onResourceRuleUpdate(res string, rawResRules []*Rule) (err error) {
	validResRules := make([]*Rule, 0)
	validRegexResRules := make([]*Rule, 0)
	for _, rule := range rawResRules {
		if err := IsValidRule(rule); err != nil {
			logging.Warn("[Isolation onResourceRuleUpdate] Ignoring invalid isolation rule", "rule", rule, "reason", err.Error())
			continue
		}
		if rule.Regex {
			validRegexResRules = append(validRegexResRules, rule)
		} else {
			validResRules = append(validResRules, rule)
		}
	}

	start := util.CurrentTimeNano()
	rwMux.Lock()
	if len(validResRules) == 0 {
		delete(ruleMap, res)
		delete(regexRuleMap, res)
	} else {
		ruleMap[res] = validResRules
		regexRuleMap[res] = validRegexResRules
	}
	regexCacheRuleMap = make(map[string][]*Rule)
	rwMux.Unlock()
	currentRules[res] = rawResRules
	logging.Debug("[Isolation onResourceRuleUpdate] Time statistic(ns) for updating isolation rule", "timeCost", util.CurrentTimeNano()-start)
	logging.Info("[Isolation] load resource level rules", "resource", res, "validResRules", validResRules)
	return nil
}

// ClearRules clears all the rules in isolation module.
func ClearRules() error {
	_, err := LoadRules(nil)
	return err
}

// ClearRulesOfResource clears resource level rules in isolation module.
func ClearRulesOfResource(res string) error {
	_, err := LoadRulesOfResource(res, nil)
	return err
}

// GetRules returns all the rules based on copy.
// It doesn't take effect for isolation module if user changes the rule.
func GetRules() []Rule {
	rules := getRules()
	ret := make([]Rule, 0, len(rules))
	for _, rule := range rules {
		ret = append(ret, *rule)
	}
	return ret
}

// GetRulesOfResource returns specific resource's rules based on copy.
// It doesn't take effect for isolation module if user changes the rule.
func GetRulesOfResource(res string) []Rule {
	rules := getRulesOfResource(res)
	ret := make([]Rule, 0, len(rules))
	for _, rule := range rules {
		ret = append(ret, *rule)
	}
	return ret
}

// getRules returns all the rules。Any changes of rules take effect for isolation module
// getRules is an internal interface.
func getRules() []*Rule {
	rwMux.RLock()
	defer rwMux.RUnlock()

	return rulesFrom(ruleMap)
}

// getRulesOfResource returns specific resource's rules。Any changes of rules take effect for isolation module
// getRulesOfResource is an internal interface.
func getRulesOfResource(res string) []*Rule {
	rwMux.RLock()
	resRules := make([]*Rule, 0)
	if rules, ok := ruleMap[res]; ok {
		resRules = append(resRules, rules...)
	}
	regexCacheRules, ok := regexCacheRuleMap[res]
	rwMux.RUnlock()

	if ok {
		resRules = append(resRules, regexCacheRules...)
	} else {
		rwMux.Lock()
		// double check
		if regexCacheRules, ok = regexCacheRuleMap[res]; ok {
			resRules = append(resRules, regexCacheRules...)
		} else {
			regexRules := regexRuleMatches(res)
			regexCacheRuleMap[res] = regexRules
			resRules = append(resRules, regexRules...)
		}
		rwMux.Unlock()
	}

	ret := make([]*Rule, 0, len(resRules))
	for _, r := range resRules {
		ret = append(ret, r)
	}
	return ret
}

func rulesFrom(m map[string][]*Rule) []*Rule {
	rules := make([]*Rule, 0, 8)
	if len(m) == 0 {
		return rules
	}
	for _, rs := range m {
		for _, r := range rs {
			if r != nil {
				rules = append(rules, r)
			}
		}
	}
	return rules
}

func logRuleUpdate(m map[string][]*Rule, regexM map[string][]*Rule) {
	rs := rulesFrom(m)
	if len(rs) == 0 {
		logging.Info("[IsolationRuleManager] Isolation rules were cleared")
	} else {
		logging.Info("[IsolationRuleManager] Isolation rules were loaded", "rules", rs)
	}
	reRs := rulesFrom(regexM)
	if len(reRs) == 0 {
		logging.Info("[IsolationRuleManager] Isolation regex rules were cleared")
	} else {
		logging.Info("[IsolationRuleManager] Isolation regex rules were loaded", "rules", reRs)
	}
}

// IsValidRule checks whether the given Rule is valid.
func IsValidRule(r *Rule) error {
	if r == nil {
		return errors.New("nil isolation rule")
	}
	if len(r.Resource) == 0 {
		return errors.New("empty resource of isolation rule")
	}
	if r.MetricType != Concurrency {
		return errors.Errorf("unsupported metric type: %d", r.MetricType)
	}
	if r.Threshold == 0 {
		return errors.New("zero threshold")
	}
	return nil
}

func regexRuleMatches(resource string) []*Rule {
	result := make([]*Rule, 0)
	for pattern, regexRules := range regexRuleMap {
		re := regexp.MustCompile(pattern)
		if !re.MatchString(resource) {
			continue
		}
		for _, rule := range regexRules {
			result = append(result, rule)
		}
	}
	return result
}
