package userinfo

import (
	"fmt"
	"strings"

	"github.com/kyverno/kyverno/pkg/config"
	"github.com/kyverno/kyverno/pkg/utils"
	admissionv1 "k8s.io/api/admission/v1"
	authenticationv1 "k8s.io/api/authentication/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	labels "k8s.io/apimachinery/pkg/labels"
	rbaclister "k8s.io/client-go/listers/rbac/v1"
	"sigs.k8s.io/controller-runtime/pkg/log"
)

const (
	clusterrolekind = "ClusterRole"
	rolekind        = "Role"
	// SaPrefix represents service account prefix in admission requests
	SaPrefix = "system:serviceaccount:"
	// KyvernoSuffix ...
	KyvernoSuffix = "kyverno:"
)

type allRolesStruct struct {
	RoleType string
	Role     []string
}

var allRoles []allRolesStruct

//GetRoleRef gets the list of roles and cluster roles for the incoming api-request
func GetRoleRef(rbLister rbaclister.RoleBindingLister, crbLister rbaclister.ClusterRoleBindingLister, request *admissionv1.AdmissionRequest, dynamicConfig config.Interface) (roles []string, clusterRoles []string, err error) {
	keys := append(request.UserInfo.Groups, request.UserInfo.Username)
	if utils.SliceContains(keys, dynamicConfig.GetExcludeGroupRole()...) {
		return
	}

	// rolebindings
	roleBindings, err := rbLister.List(labels.NewSelector())
	if err != nil {
		return roles, clusterRoles, fmt.Errorf("failed to list rolebindings: %v", err)
	}

	rs, crs, err := getRoleRefByRoleBindings(roleBindings, request.UserInfo)
	if err != nil {
		return roles, clusterRoles, err
	}
	roles = append(roles, rs...)
	clusterRoles = append(clusterRoles, crs...)

	// clusterrolebindings
	clusterroleBindings, err := crbLister.List(labels.NewSelector())
	if err != nil {
		return roles, clusterRoles, fmt.Errorf("failed to list clusterrolebindings: %v", err)
	}

	crs, err = getRoleRefByClusterRoleBindings(clusterroleBindings, request.UserInfo)
	if err != nil {
		return roles, clusterRoles, err
	}
	clusterRoles = append(clusterRoles, crs...)

	return roles, clusterRoles, nil
}

func getRoleRefByRoleBindings(roleBindings []*rbacv1.RoleBinding, userInfo authenticationv1.UserInfo) (roles []string, clusterRoles []string, err error) {
	for _, rolebinding := range roleBindings {
		for _, subject := range rolebinding.Subjects {
			if !matchSubjectsMap(subject, userInfo) {
				continue
			}

			switch rolebinding.RoleRef.Kind {
			case rolekind:
				roles = append(roles, rolebinding.Namespace+":"+rolebinding.RoleRef.Name)
			case clusterrolekind:
				clusterRoles = append(clusterRoles, rolebinding.RoleRef.Name)
			}
		}
	}

	return roles, clusterRoles, nil
}

// RoleRef in ClusterRoleBindings can only reference a ClusterRole in the global namespace
func getRoleRefByClusterRoleBindings(clusterroleBindings []*rbacv1.ClusterRoleBinding, userInfo authenticationv1.UserInfo) (clusterRoles []string, err error) {
	for _, clusterRoleBinding := range clusterroleBindings {
		for _, subject := range clusterRoleBinding.Subjects {
			if !matchSubjectsMap(subject, userInfo) {
				continue
			}

			if clusterRoleBinding.RoleRef.Kind == clusterrolekind {
				clusterRoles = append(clusterRoles, clusterRoleBinding.RoleRef.Name)
			}
		}
	}
	return clusterRoles, nil
}

// matchSubjectsMap checks if userInfo found in subject
// return true directly if found a match
// subject.kind can only be ServiceAccount, User and Group
func matchSubjectsMap(subject rbacv1.Subject, userInfo authenticationv1.UserInfo) bool {
	// ServiceAccount
	if strings.Contains(userInfo.Username, SaPrefix) {
		return matchServiceAccount(subject, userInfo)
	}

	// User or Group
	return matchUserOrGroup(subject, userInfo)
}

// matchServiceAccount checks if userInfo sa matche the subject sa
// serviceaccount represents as saPrefix:namespace:name in userInfo
func matchServiceAccount(subject rbacv1.Subject, userInfo authenticationv1.UserInfo) bool {
	subjectServiceAccount := subject.Namespace + ":" + subject.Name
	if userInfo.Username[len(SaPrefix):] != subjectServiceAccount {
		return false
	}

	log.Log.V(3).Info(fmt.Sprintf("found a matched service account not match: %s", subjectServiceAccount))
	return true
}

// matchUserOrGroup checks if userInfo contains user or group info in a subject
func matchUserOrGroup(subject rbacv1.Subject, userInfo authenticationv1.UserInfo) bool {
	keys := append(userInfo.Groups, userInfo.Username)
	for _, key := range keys {
		if subject.Name == key {
			log.Log.V(3).Info(fmt.Sprintf("found a matched user/group '%v' in request userInfo: %v", subject.Name, keys))
			return true
		}
	}

	return false
}

//IsRoleAuthorize is role authorize or not
func IsRoleAuthorize(rbLister rbaclister.RoleBindingLister, crbLister rbaclister.ClusterRoleBindingLister, rLister rbaclister.RoleLister, crLister rbaclister.ClusterRoleLister, request *admissionv1.AdmissionRequest, dynamicConfig config.Interface) (bool, error) {
	if strings.Contains(request.UserInfo.Username, SaPrefix) {
		roles, clusterRoles, err := GetRoleRef(rbLister, crbLister, request, dynamicConfig)
		if err != nil {
			return false, err
		}
		allRoles := append(allRoles, allRolesStruct{
			RoleType: "ClusterRole",
			Role:     clusterRoles,
		}, allRolesStruct{
			RoleType: "Role",
			Role:     roles,
		})
		for _, r := range allRoles {
			for _, e := range r.Role {
				if strings.Contains(e, KyvernoSuffix) {
					return true, nil
				}
				var labels map[string]string
				if r.RoleType == "Role" {
					roleData := strings.Split(e, ":")
					role, err := rLister.Roles(roleData[0]).Get(strings.Join(roleData[1:], ":"))
					if err != nil {
						return false, err
					}
					labels = role.GetLabels()
				} else {
					role, err := crLister.Get(e)
					if err != nil {
						return false, err
					}
					labels = role.GetLabels()
				}
				if !strings.Contains(e, KyvernoSuffix) {
					if labels["kubernetes.io/bootstrapping"] == "rbac-defaults" {
						return true, nil
					}
				}
			}
		}
		return true, nil
	}
	// User or Group
	for _, e := range dynamicConfig.GetExcludeUsername() {
		if strings.Contains(request.UserInfo.Username, e) {
			return true, nil
		}
	}

	// Restrict Development Roles
	for _, e := range dynamicConfig.RestrictDevelopmentUsername() {
		if strings.Contains(request.UserInfo.Username, strings.TrimSpace(e)) {
			return false, nil
		}
	}

	var matchedRoles []bool
	excludeGroupRule := append(dynamicConfig.GetExcludeGroupRole(), KyvernoSuffix)
	for _, e := range request.UserInfo.Groups {
		for _, defaultSuffix := range excludeGroupRule {

			if strings.Contains(strings.TrimSpace(e), strings.TrimSpace(defaultSuffix)) {
				matchedRoles = append(matchedRoles, true)
				break
			}
		}
	}
	if len(matchedRoles) == len(request.UserInfo.Groups) {
		return true, nil
	}

	return false, nil
}
