/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 rbac

import (
	"context"
	"fmt"

	"github.com/apache/servicecomb-service-center/datasource/rbac"
	"github.com/apache/servicecomb-service-center/pkg/log"
	"github.com/apache/servicecomb-service-center/server/plugin/auth"
	rbacmodel "github.com/go-chassis/cari/rbac"
)

// Allow return: matched labels(empty if no label defined), error
func Allow(ctx context.Context, _ string, roleList []string,
	targetResource *auth.ResourceScope) ([]map[string]string, error) {
	//TODO check project
	allPerms, err := getPermsByRoles(ctx, roleList)
	if err != nil {
		log.Error("get role list errors", err)
		return nil, err
	}
	if len(allPerms) == 0 {
		log.Warn("role list has no any permissions")
		return nil, rbacmodel.NewError(rbacmodel.ErrNoPermission, "role has no any permissions")
	}
	allow, labelList := GetLabel(allPerms, targetResource.Type, targetResource.Verb)
	if !allow {
		return nil, rbacmodel.NewError(rbacmodel.ErrNoPermission,
			fmt.Sprintf("role has no permissions[%s:%s]", targetResource.Type, targetResource.Verb))
	}
	// allow, but no label found, means we can ignore the labels
	if len(labelList) == 0 {
		return nil, nil
	}
	// target resource needs no label, return without filter
	if len(targetResource.Labels) == 0 {
		return labelList, nil
	}
	// allow, and labels found, filter the labels
	filteredLabelList := FilterLabel(targetResource.Labels, labelList)
	// target resource label matches no label in permission, means not allow
	if len(filteredLabelList) == 0 {
		return nil, rbacmodel.NewError(rbacmodel.ErrNoPermission,
			fmt.Sprintf("role has no permissions[%s:%s] for labels %v",
				targetResource.Type, targetResource.Verb, targetResource.Labels))
	}
	return filteredLabelList, nil
}

func FilterLabel(targetResourceLabel []map[string]string, permLabelList []map[string]string) []map[string]string {
	l := make([]map[string]string, 0)
	for _, resourceLabel := range targetResourceLabel {
		for _, label := range permLabelList {
			if LabelMatched(resourceLabel, label) {
				l = append(l, label)
			}
		}
	}
	return l
}

func LabelMatched(targetResourceLabel map[string]string, permLabel map[string]string) bool {
	for k, v := range permLabel {
		if vv := targetResourceLabel[k]; vv != v {
			return false
		}
	}
	return true
}

func getPermsByRoles(ctx context.Context, roleList []string) ([]*rbacmodel.Permission, error) {
	var allPerms = make([]*rbacmodel.Permission, 0)
	for _, name := range roleList {
		r, err := rbac.Instance().GetRole(ctx, name)
		if err == nil {
			allPerms = append(allPerms, r.Perms...)
			continue
		}
		if err == rbac.ErrRoleNotExist {
			log.Warn(fmt.Sprintf("role [%s] not exist", name))
			continue
		}
		log.Error(fmt.Sprintf("get role [%s] failed", name), err)
		return nil, err
	}
	return allPerms, nil
}

// GetLabel checks if the perms have permission to operate the resource(ignore label),
// if one perm have the permission, add it's label to the result.
func GetLabel(perms []*rbacmodel.Permission, targetResource, verb string) (allow bool, labelList []map[string]string) {
	for _, perm := range perms {
		a, l := GetLabelFromSinglePerm(perm, targetResource, verb)
		if !a {
			continue
		}
		allow = true
		// allow and has no label, return fast
		if len(l) == 0 {
			return true, nil
		}
		labelList = append(labelList, l...)
	}
	return
}

// GetLabel checks if the perm have permission to operate the resource(ignore label),
// if the perm have the permission, return it's label.
func GetLabelFromSinglePerm(perm *rbacmodel.Permission, targetResource, verb string) (allow bool, labelList []map[string]string) {
	if !allowVerb(perm.Verbs, verb) {
		return false, nil
	}

	return getResourceLabel(perm.Resources, targetResource)
}

func allowVerb(haystack []string, needle string) bool {
	for _, e := range haystack {
		if e == "*" || e == needle {
			return true
		}
	}
	return false
}

func getResourceLabel(resources []*rbacmodel.Resource, needle string) (allow bool, labelList []map[string]string) {
	for _, resource := range resources {
		// filter the same resource
		if resource.Type != needle {
			continue
		}
		// has no label, return fast
		if len(resource.Labels) == 0 {
			return true, nil
		}
		labelList = append(labelList, resource.Labels)
		allow = true
	}
	return
}
