// Copyright 2019 Yunion
//
// 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 rbacutils

import (
	"regexp"

	"yunion.io/x/jsonutils"
	"yunion.io/x/pkg/errors"
	"yunion.io/x/pkg/util/netutils"
	"yunion.io/x/pkg/util/rbacscope"
)

type SRbacPolicy struct {
	// condition, when the policy takes effects
	// Deprecated
	Condition string

	DomainId string

	IsPublic bool

	PublicScope rbacscope.TRbacScope

	SharedDomainIds []string

	Projects []string

	Roles []string

	Ips []netutils.IPV4Prefix

	Auth bool // whether needs authentication

	// scope, the scope of the policy, system/domain/project
	Scope rbacscope.TRbacScope
	// Deprecated
	// is_admin=true means scope=system, is_admin=false means scope=project
	IsAdmin bool

	Rules TPolicy
}

func (p SRbacPolicy) needAuth() bool {
	if len(p.Condition) > 0 {
		return true
	}
	if len(p.DomainId) > 0 {
		return true
	}
	if len(p.SharedDomainIds) > 0 {
		return true
	}
	if len(p.Projects) > 0 {
		return true
	}
	if len(p.Roles) > 0 {
		return true
	}
	if p.Auth {
		return true
	}
	return false
}

var (
	tenantEqualsPattern = regexp.MustCompile(`tenant\s*==\s*['"]?(\w+)['"]?`)
	roleContainsPattern = regexp.MustCompile(`roles.contains\(['"]?(\w+)['"]?\)`)
)

func searchMatchStrings(pattern *regexp.Regexp, condstr string) []string {
	ret := make([]string, 0)
	matches := pattern.FindAllStringSubmatch(condstr, -1)
	for _, match := range matches {
		ret = append(ret, match[1])
	}
	return ret
}

func searchMatchTenants(condstr string) []string {
	return searchMatchStrings(tenantEqualsPattern, condstr)
}

func searchMatchRoles(condstr string) []string {
	return searchMatchStrings(roleContainsPattern, condstr)
}

// Deprecated
func (policy *SRbacPolicy) Decode(policyJson jsonutils.JSONObject) error {
	policy.Condition, _ = policyJson.GetString("condition")
	if policyJson.Contains("projects") {
		projectJson, _ := policyJson.GetArray("projects")
		policy.Projects = jsonutils.JSONArray2StringArray(projectJson)
	}
	if policyJson.Contains("roles") {
		roleJson, _ := policyJson.GetArray("roles")
		policy.Roles = jsonutils.JSONArray2StringArray(roleJson)
	}

	if len(policy.Projects) == 0 && len(policy.Roles) == 0 && len(policy.Condition) > 0 {
		// XXX hack
		// for smooth transtion from condition to projects&roles
		policy.Projects = searchMatchTenants(policy.Condition)
		policy.Roles = searchMatchRoles(policy.Condition)
	}
	// empty condition, no longer use this field
	policy.Condition = ""

	if policyJson.Contains("ips") {
		ipsJson, _ := policyJson.GetArray("ips")
		ipStrs := jsonutils.JSONArray2StringArray(ipsJson)
		policy.Ips = make([]netutils.IPV4Prefix, 0)
		for _, ipStr := range ipStrs {
			if len(ipStr) == 0 || ipStr == "0.0.0.0" {
				continue
			}
			prefix, err := netutils.NewIPV4Prefix(ipStr)
			if err != nil {
				continue
			}
			policy.Ips = append(policy.Ips, prefix)
		}
	}

	policy.Auth = jsonutils.QueryBoolean(policyJson, "auth", true)
	if len(policy.Ips) > 0 || len(policy.Roles) > 0 || len(policy.Projects) > 0 {
		policy.Auth = true
	}

	scopeStr, _ := policyJson.GetString("scope")
	if len(scopeStr) > 0 {
		policy.Scope = rbacscope.TRbacScope(scopeStr)
	} else {
		policy.IsAdmin = jsonutils.QueryBoolean(policyJson, "is_admin", false)
		if len(policy.Scope) == 0 {
			if policy.IsAdmin {
				policy.Scope = rbacscope.ScopeSystem
			} else {
				policy.Scope = rbacscope.ScopeProject
			}
		}
	}

	policyBody, err := policyJson.Get("policy")
	if err != nil {
		return errors.Wrap(err, "Get policy")
	}
	policy.Rules, err = decodePolicy(policyBody)
	if err != nil {
		return errors.Wrap(err, "DecodePolicy")
	}

	return nil
}

func (policy *SRbacPolicy) Encode() jsonutils.JSONObject {
	ret := jsonutils.NewDict()

	if !policy.Auth && len(policy.Projects) == 0 && len(policy.Roles) == 0 && len(policy.Ips) == 0 {
		ret.Add(jsonutils.JSONFalse, "auth")
	} else {
		ret.Add(jsonutils.JSONTrue, "auth")
	}

	if len(policy.Projects) > 0 {
		ret.Add(jsonutils.NewStringArray(policy.Projects), "projects")
	}
	if len(policy.Roles) > 0 {
		ret.Add(jsonutils.NewStringArray(policy.Roles), "roles")
	}
	if len(policy.Ips) > 0 {
		ipStrs := make([]string, len(policy.Ips))
		for i := range policy.Ips {
			ipStrs[i] = policy.Ips[i].String()
		}
		ret.Add(jsonutils.NewStringArray(ipStrs), "ips")
	}

	ret.Add(jsonutils.NewString(string(policy.Scope)), "scope")

	ret.Add(policy.Rules.encode(), "policy")

	return ret
}

func (policy *SRbacPolicy) IsSystemWidePolicy() bool {
	return (len(policy.DomainId) == 0 || (policy.IsPublic && policy.PublicScope == rbacscope.ScopeSystem)) && len(policy.Roles) == 0 && len(policy.Projects) == 0
}

func (policy *SRbacPolicy) MatchDomain(domainId string) bool {
	if len(policy.DomainId) == 0 || len(domainId) == 0 {
		return true
	}
	if policy.DomainId == domainId {
		return true
	}
	if policy.IsPublic {
		if policy.PublicScope == rbacscope.ScopeSystem {
			return true
		}
		if contains(policy.SharedDomainIds, domainId) {
			return true
		}
	}
	return false
}

func (policy *SRbacPolicy) MatchProject(projectName string) bool {
	if len(policy.Projects) == 0 || len(projectName) == 0 {
		return true
	}
	if contains(policy.Projects, projectName) {
		return true
	}
	return false
}

func (policy *SRbacPolicy) MatchRoles(roleNames []string) bool {
	if len(policy.Roles) == 0 {
		return true
	}
	if intersect(policy.Roles, roleNames) {
		return true
	}
	return false
}

// check whether policy maches a userCred
// return value
// bool isMatched
// int  match weight, the higher the value, the more exact the match
// the more exact match wins
func (policy *SRbacPolicy) Match(userCred IRbacIdentity2) (bool, int) {
	if !policy.needAuth() {
		if len(policy.Ips) == 0 {
			return true, 1
		}
		if containsIp(policy.Ips, userCred.GetLoginIp()) {
			return true, 1
		}
	}
	if userCred == nil || len(userCred.GetTokenString()) == 0 || userCred.GetTokenString() == GUEST_TOKEN {
		return false, 0
	}
	weight := 0
	if policy.MatchDomain(userCred.GetProjectDomainId()) {
		if len(policy.DomainId) > 0 {
			if policy.DomainId == userCred.GetProjectDomainId() {
				weight += 30 // exact domain match
			} else if len(policy.SharedDomainIds) > 0 {
				weight += 20 // shared domain match
			} else {
				weight += 10 // else, system scope match
			}
		}
		if policy.MatchRoles(userCred.GetRoles()) {
			if len(policy.Roles) != 0 {
				weight += 100
			}
			if policy.MatchProject(userCred.GetProjectName()) {
				if len(policy.Projects) > 0 {
					weight += 1000
				}
				if len(policy.Ips) == 0 || containsIp(policy.Ips, userCred.GetLoginIp()) {
					if len(policy.Ips) > 0 {
						weight += 10000
					}
					return true, weight
				}
			}
		}
	}
	return false, 0
}
