// 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 db

import (
	"context"

	"yunion.io/x/log"
	"yunion.io/x/pkg/errors"
	"yunion.io/x/pkg/util/rbacscope"

	"yunion.io/x/onecloud/pkg/cloudcommon/consts"
	"yunion.io/x/onecloud/pkg/cloudcommon/policy"
	"yunion.io/x/onecloud/pkg/httperrors"
	"yunion.io/x/onecloud/pkg/mcclient"
	"yunion.io/x/onecloud/pkg/util/rbacutils"
)

func IsObjectRbacAllowed(ctx context.Context, model IModel, userCred mcclient.TokenCredential, action string, extra ...string) error {
	return isObjectRbacAllowed(ctx, model, userCred, action, extra...)
}

func isObjectRbacAllowed(ctx context.Context, model IModel, userCred mcclient.TokenCredential, action string, extra ...string) error {
	_, err := isObjectRbacAllowedResult(ctx, model, userCred, action, extra...)
	return err
}

func isObjectRbacAllowedResult(ctx context.Context, model IModel, userCred mcclient.TokenCredential, action string, extra ...string) (rbacutils.SPolicyResult, error) {
	manager := model.GetModelManager()
	objOwnerId := model.GetOwnerId()

	var ownerId mcclient.IIdentityProvider
	if userCred != nil {
		ownerId = userCred
	}

	var requireScope rbacscope.TRbacScope
	resScope := manager.ResourceScope()
	switch resScope {
	case rbacscope.ScopeSystem:
		requireScope = rbacscope.ScopeSystem
	case rbacscope.ScopeDomain:
		if ownerId != nil && objOwnerId != nil && (ownerId.GetUserId() == objOwnerId.GetUserId() && action == policy.PolicyActionGet) {
			requireScope = rbacscope.ScopeUser
		} else if ownerId != nil && objOwnerId != nil && (ownerId.GetProjectDomainId() == objOwnerId.GetProjectDomainId() || objOwnerId.GetProjectDomainId() == "" || (model.IsSharable(ownerId) && action == policy.PolicyActionGet)) {
			requireScope = rbacscope.ScopeDomain
		} else {
			requireScope = rbacscope.ScopeSystem
		}
	case rbacscope.ScopeUser:
		if ownerId != nil && objOwnerId != nil && (ownerId.GetUserId() == objOwnerId.GetUserId() || objOwnerId.GetUserId() == "" || (model.IsSharable(ownerId) && action == policy.PolicyActionGet)) {
			requireScope = rbacscope.ScopeUser
		} else if ownerId != nil && objOwnerId != nil && ownerId.GetProjectId() == objOwnerId.GetProjectId() {
			requireScope = rbacscope.ScopeProject
		} else if ownerId != nil && objOwnerId != nil && ownerId.GetProjectDomainId() == objOwnerId.GetProjectDomainId() {
			requireScope = rbacscope.ScopeDomain
		} else {
			requireScope = rbacscope.ScopeSystem
		}
	default:
		// objOwnerId should not be nil
		if ownerId != nil && objOwnerId != nil && (ownerId.GetProjectId() == objOwnerId.GetProjectId() || objOwnerId.GetProjectId() == "" || (model.IsSharable(ownerId) && action == policy.PolicyActionGet)) {
			requireScope = rbacscope.ScopeProject
		} else if ownerId != nil && objOwnerId != nil && ownerId.GetProjectDomainId() == objOwnerId.GetProjectDomainId() {
			requireScope = rbacscope.ScopeDomain
		} else {
			requireScope = rbacscope.ScopeSystem
		}
	}

	scope, result := policy.PolicyManager.AllowScope(userCred, consts.GetServiceType(), manager.KeywordPlural(), action, extra...)

	if result.Result.IsAllow() && !requireScope.HigherThan(scope) {
		err := objectConfirmPolicyTags(ctx, model, result)
		if err != nil {
			return rbacutils.PolicyDeny, errors.Wrap(err, "objectConfirmPolicyTags")
		}
		return result, nil
	}
	return rbacutils.PolicyDeny, httperrors.NewForbiddenError("not enough privilege (require:%s,allow:%s:resource:%s) [tags:%s]", requireScope, scope, resScope, result.String())
}

func isJointObjectRbacAllowed(ctx context.Context, item IJointModel, userCred mcclient.TokenCredential, action string, extra ...string) error {
	err1 := isObjectRbacAllowed(ctx, JointMaster(item), userCred, action, extra...)
	err2 := isObjectRbacAllowed(ctx, JointSlave(item), userCred, action, extra...)
	if err1 == nil || err2 == nil {
		return nil
	}
	return err1
}

func isClassRbacAllowed(ctx context.Context, manager IModelManager, userCred mcclient.TokenCredential, objOwnerId mcclient.IIdentityProvider, action string, extra ...string) (rbacutils.SPolicyResult, error) {
	var ownerId mcclient.IIdentityProvider
	if userCred != nil {
		ownerId = userCred
	}

	var requireScope rbacscope.TRbacScope
	resScope := manager.ResourceScope()
	switch resScope {
	case rbacscope.ScopeSystem:
		requireScope = rbacscope.ScopeSystem
	case rbacscope.ScopeDomain:
		// objOwnerId should not be nil
		if ownerId != nil && ownerId.GetProjectDomainId() == objOwnerId.GetProjectDomainId() {
			requireScope = rbacscope.ScopeDomain
		} else {
			requireScope = rbacscope.ScopeSystem
		}
	case rbacscope.ScopeUser:
		if ownerId != nil && ownerId.GetUserId() == objOwnerId.GetUserId() {
			requireScope = rbacscope.ScopeUser
		} else if ownerId != nil && ownerId.GetProjectDomainId() == objOwnerId.GetProjectDomainId() {
			requireScope = rbacscope.ScopeDomain
		} else {
			requireScope = rbacscope.ScopeSystem
		}
	default:
		// objOwnerId should not be nil
		if ownerId != nil && ownerId.GetProjectId() == objOwnerId.GetProjectId() {
			requireScope = rbacscope.ScopeProject
		} else if ownerId != nil && ownerId.GetProjectDomainId() == objOwnerId.GetProjectDomainId() {
			requireScope = rbacscope.ScopeDomain
		} else {
			requireScope = rbacscope.ScopeSystem
		}
	}

	allowScope, result := policy.PolicyManager.AllowScope(userCred, consts.GetServiceType(), manager.KeywordPlural(), action, extra...)

	if result.Result.IsAllow() && !requireScope.HigherThan(allowScope) {
		err := classConfirmPolicyTags(ctx, manager, objOwnerId, result)
		if err != nil {
			return rbacutils.PolicyDeny, errors.Wrap(err, "classConfirmPolicyTags")
		}
		return result, nil
	}
	return rbacutils.PolicyDeny, httperrors.NewForbiddenError("not enough privilege (require:%s,allow:%s)", requireScope, allowScope)
}

type IResource interface {
	KeywordPlural() string
}

func IsAllowList(scope rbacscope.TRbacScope, userCred mcclient.TokenCredential, manager IResource) rbacutils.SPolicyResult {
	if userCred == nil {
		return rbacutils.PolicyDeny
	}
	return policy.PolicyManager.Allow(scope, userCred, consts.GetServiceType(), manager.KeywordPlural(), policy.PolicyActionList)
}

func IsAdminAllowList(userCred mcclient.TokenCredential, manager IResource) rbacutils.SPolicyResult {
	return IsAllowList(rbacscope.ScopeSystem, userCred, manager)
}

func IsDomainAllowList(userCred mcclient.TokenCredential, manager IResource) rbacutils.SPolicyResult {
	return IsAllowList(rbacscope.ScopeDomain, userCred, manager)
}

func IsProjectAllowList(userCred mcclient.TokenCredential, manager IResource) rbacutils.SPolicyResult {
	return IsAllowList(rbacscope.ScopeProject, userCred, manager)
}

func IsAllowCreate(scope rbacscope.TRbacScope, userCred mcclient.TokenCredential, manager IResource) rbacutils.SPolicyResult {
	if userCred == nil {
		return rbacutils.PolicyDeny
	}
	return policy.PolicyManager.Allow(scope, userCred, consts.GetServiceType(), manager.KeywordPlural(), policy.PolicyActionCreate)
}

func IsAdminAllowCreate(userCred mcclient.TokenCredential, manager IResource) rbacutils.SPolicyResult {
	return IsAllowCreate(rbacscope.ScopeSystem, userCred, manager)
}

func IsDomainAllowCreate(userCred mcclient.TokenCredential, manager IResource) rbacutils.SPolicyResult {
	return IsAllowCreate(rbacscope.ScopeDomain, userCred, manager)
}

func IsProjectAllowCreate(userCred mcclient.TokenCredential, manager IResource) rbacutils.SPolicyResult {
	return IsAllowCreate(rbacscope.ScopeProject, userCred, manager)
}

func IsAllowClassPerform(scope rbacscope.TRbacScope, userCred mcclient.TokenCredential, manager IResource, action string) rbacutils.SPolicyResult {
	if userCred == nil {
		return rbacutils.PolicyDeny
	}
	return policy.PolicyManager.Allow(scope, userCred, consts.GetServiceType(), manager.KeywordPlural(), policy.PolicyActionPerform, action)
}

func IsAdminAllowClassPerform(userCred mcclient.TokenCredential, manager IResource, action string) rbacutils.SPolicyResult {
	return IsAllowClassPerform(rbacscope.ScopeSystem, userCred, manager, action)
}

func IsDomainAllowClassPerform(userCred mcclient.TokenCredential, manager IResource, action string) rbacutils.SPolicyResult {
	return IsAllowClassPerform(rbacscope.ScopeDomain, userCred, manager, action)
}

func IsProjectAllowClassPerform(userCred mcclient.TokenCredential, manager IResource, action string) rbacutils.SPolicyResult {
	return IsAllowClassPerform(rbacscope.ScopeProject, userCred, manager, action)
}

func IsAllowGet(ctx context.Context, scope rbacscope.TRbacScope, userCred mcclient.TokenCredential, obj IModel) bool {
	if userCred == nil {
		return false
	}
	result := policy.PolicyManager.Allow(scope, userCred, consts.GetServiceType(), obj.KeywordPlural(), policy.PolicyActionGet)
	err := objectConfirmPolicyTags(ctx, obj, result)
	if err != nil {
		log.Errorf("IsAllowGet %s", err)
		return false
	} else {
		return true
	}
}

func IsAdminAllowGet(ctx context.Context, userCred mcclient.TokenCredential, obj IModel) bool {
	return IsAllowGet(ctx, rbacscope.ScopeSystem, userCred, obj)
}

func IsDomainAllowGet(ctx context.Context, userCred mcclient.TokenCredential, obj IModel) bool {
	return IsAllowGet(ctx, rbacscope.ScopeDomain, userCred, obj)
}

func IsProjectAllowGet(ctx context.Context, userCred mcclient.TokenCredential, obj IModel) bool {
	return IsAllowGet(ctx, rbacscope.ScopeProject, userCred, obj)
}

func IsAllowGetSpec(ctx context.Context, scope rbacscope.TRbacScope, userCred mcclient.TokenCredential, obj IModel, spec string) bool {
	if userCred == nil {
		return false
	}
	result := policy.PolicyManager.Allow(scope, userCred, consts.GetServiceType(), obj.KeywordPlural(), policy.PolicyActionGet, spec)
	err := objectConfirmPolicyTags(ctx, obj, result)
	if err != nil {
		log.Errorf("IsAllowGetSpec %s", err)
		return false
	} else {
		return true
	}
}

func IsAdminAllowGetSpec(ctx context.Context, userCred mcclient.TokenCredential, obj IModel, spec string) bool {
	return IsAllowGetSpec(ctx, rbacscope.ScopeSystem, userCred, obj, spec)
}

func IsDomainAllowGetSpec(ctx context.Context, userCred mcclient.TokenCredential, obj IModel, spec string) bool {
	return IsAllowGetSpec(ctx, rbacscope.ScopeDomain, userCred, obj, spec)
}

func IsProjectAllowGetSpec(ctx context.Context, userCred mcclient.TokenCredential, obj IModel, spec string) bool {
	return IsAllowGetSpec(ctx, rbacscope.ScopeProject, userCred, obj, spec)
}

func IsAllowPerform(ctx context.Context, scope rbacscope.TRbacScope, userCred mcclient.TokenCredential, obj IModel, action string) bool {
	if userCred == nil {
		return false
	}
	result := policy.PolicyManager.Allow(scope, userCred, consts.GetServiceType(), obj.KeywordPlural(), policy.PolicyActionPerform, action)
	err := objectConfirmPolicyTags(ctx, obj, result)
	if err != nil {
		log.Errorf("IsAllowPerform %s", err)
		return false
	} else {
		return true
	}
}

func IsAdminAllowPerform(ctx context.Context, userCred mcclient.TokenCredential, obj IModel, action string) bool {
	return IsAllowPerform(ctx, rbacscope.ScopeSystem, userCred, obj, action)
}

func IsDomainAllowPerform(ctx context.Context, userCred mcclient.TokenCredential, obj IModel, action string) bool {
	return IsAllowPerform(ctx, rbacscope.ScopeDomain, userCred, obj, action)
}

func IsProjectAllowPerform(ctx context.Context, userCred mcclient.TokenCredential, obj IModel, action string) bool {
	return IsAllowPerform(ctx, rbacscope.ScopeProject, userCred, obj, action)
}

func IsAllowUpdate(ctx context.Context, scope rbacscope.TRbacScope, userCred mcclient.TokenCredential, obj IModel) bool {
	if userCred == nil {
		return false
	}
	result := policy.PolicyManager.Allow(scope, userCred, consts.GetServiceType(), obj.KeywordPlural(), policy.PolicyActionUpdate)
	err := objectConfirmPolicyTags(ctx, obj, result)
	if err != nil {
		log.Errorf("IsAllowUpdate %s", err)
		return false
	} else {
		return true
	}
}

func IsAdminAllowUpdate(ctx context.Context, userCred mcclient.TokenCredential, obj IModel) bool {
	return IsAllowUpdate(ctx, rbacscope.ScopeSystem, userCred, obj)
}

func IsDomainAllowUpdate(ctx context.Context, userCred mcclient.TokenCredential, obj IModel) bool {
	return IsAllowUpdate(ctx, rbacscope.ScopeDomain, userCred, obj)
}

func IsProjectAllowUpdate(ctx context.Context, userCred mcclient.TokenCredential, obj IModel) bool {
	return IsAllowUpdate(ctx, rbacscope.ScopeProject, userCred, obj)
}

func IsAllowUpdateSpec(ctx context.Context, scope rbacscope.TRbacScope, userCred mcclient.TokenCredential, obj IModel, spec string) bool {
	if userCred == nil {
		return false
	}
	result := policy.PolicyManager.Allow(scope, userCred, consts.GetServiceType(), obj.KeywordPlural(), policy.PolicyActionUpdate, spec)
	err := objectConfirmPolicyTags(ctx, obj, result)
	if err != nil {
		log.Errorf("IsAllowUpdateSpec %s", err)
		return false
	} else {
		return true
	}
}

func IsAdminAllowUpdateSpec(ctx context.Context, userCred mcclient.TokenCredential, obj IModel, spec string) bool {
	return IsAllowUpdateSpec(ctx, rbacscope.ScopeSystem, userCred, obj, spec)
}

func IsDomainAllowUpdateSpec(ctx context.Context, userCred mcclient.TokenCredential, obj IModel, spec string) bool {
	return IsAllowUpdateSpec(ctx, rbacscope.ScopeDomain, userCred, obj, spec)
}

func IsProjectAllowUpdateSpec(ctx context.Context, userCred mcclient.TokenCredential, obj IModel, spec string) bool {
	return IsAllowUpdateSpec(ctx, rbacscope.ScopeProject, userCred, obj, spec)
}

func IsAllowDelete(ctx context.Context, scope rbacscope.TRbacScope, userCred mcclient.TokenCredential, obj IModel) bool {
	if userCred == nil {
		return false
	}
	result := policy.PolicyManager.Allow(scope, userCred, consts.GetServiceType(), obj.KeywordPlural(), policy.PolicyActionDelete)
	err := objectConfirmPolicyTags(ctx, obj, result)
	if err != nil {
		log.Errorf("IsAllowDelete %s", err)
		return false
	} else {
		return true
	}
}

func IsAdminAllowDelete(ctx context.Context, userCred mcclient.TokenCredential, obj IModel) bool {
	return IsAllowDelete(ctx, rbacscope.ScopeSystem, userCred, obj)
}

func IsDomainAllowDelete(ctx context.Context, userCred mcclient.TokenCredential, obj IModel) bool {
	return IsAllowDelete(ctx, rbacscope.ScopeDomain, userCred, obj)
}

func IsProjectAllowDelete(ctx context.Context, userCred mcclient.TokenCredential, obj IModel) bool {
	return IsAllowDelete(ctx, rbacscope.ScopeProject, userCred, obj)
}
