// Licensed Materials - Property of IBM
// (c) Copyright IBM Corporation 2018, 2019. All Rights Reserved.
// Note to U.S. Government Users Restricted Rights:
// Use, duplication or disclosure restricted by GSA ADP Schedule
// Contract with IBM Corp.

// Package policy handles policy controller logic
package policy

import (
	"fmt"
	"strings"
	"sync"

	"github.com/golang/glog"
	policyv1alpha1 "github.com/open-cluster-management/hcm-compliance/pkg/apis/policy/v1alpha1"
	resourceClient "github.com/open-cluster-management/seed-sdk/pkg/client"
	rbacv1 "k8s.io/api/rbac/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var roleBindingsMap map[string]rbacv1.RoleBinding

type roleBindingOrigin struct {
	roleBindingTemplate *policyv1alpha1.RoleBindingTemplate
	policy              *policyv1alpha1.Policy
	namespace           string
}
type syncedMustHaveRBMap struct {
	mustHaveRoleBinding map[string]roleBindingOrigin
	//Mx for making the map thread safe
	Mx sync.RWMutex
}

type syncedMustNotHaveRBMap struct {
	mustNotHaveRoleBinding map[string]roleBindingOrigin
	//Mx for making the map thread safe
	Mx sync.RWMutex
}

func (sm *syncedMustHaveRBMap) addMustHaveRB(rbNs string, rbo roleBindingOrigin) {
	//the key of the <rolebinding name>-<policy name> e.g. rb1-pl1
	//the value is rolebinding object
	sm.Mx.Lock()
	defer sm.Mx.Unlock()
	//check if the map is initialized, if not initilize it
	if sm.mustHaveRoleBinding == nil {
		sm.mustHaveRoleBinding = make(map[string]roleBindingOrigin)
	}
	sm.mustHaveRoleBinding[rbNs] = rbo
}

func (sm *syncedMustHaveRBMap) getMustHaveRB(rbNs string, rbo roleBindingOrigin) (value *rbacv1.RoleBinding, found bool) {
	//the key of the <rolebinding name>-<policy name> e.g. rb1-pl1
	//the value is rolebinding object
	sm.Mx.Lock()
	defer sm.Mx.Unlock()
	//check if the map is initialized, if not initilize it
	if sm.mustHaveRoleBinding == nil {
		return nil, false
	}
	if val, ok := sm.mustHaveRoleBinding[rbNs]; ok {
		return &val.roleBindingTemplate.RoleBinding, true
	}
	return nil, false
}

func (sm *syncedMustNotHaveRBMap) getMustNotHaveRB(rbNs string, rbo roleBindingOrigin) (value *rbacv1.RoleBinding, found bool) {
	//the key of the <rolebinding name>-<policy name> e.g. rb1-pl1
	//the value is rolebinding object
	sm.Mx.Lock()
	defer sm.Mx.Unlock()
	//check if the map is initialized, if not initilize it
	if sm.mustNotHaveRoleBinding == nil {
		return nil, false
	}
	if val, ok := sm.mustNotHaveRoleBinding[rbNs]; ok {
		return &val.roleBindingTemplate.RoleBinding, true
	}
	return nil, false
}

func (sm *syncedMustHaveRBMap) removeMustHaveRB(rbNs string) {
	//the key of the <rolebinding name>-<policy name> e.g. rb1-pl1
	//the value is rolebinding object
	sm.Mx.Lock()
	defer sm.Mx.Unlock()
	//check if the map is initialized, if not return
	if sm.mustHaveRoleBinding == nil {
		return
	}
	if _, ok := sm.mustHaveRoleBinding[rbNs]; ok {
		delete(sm.mustHaveRoleBinding, rbNs)
	}
}

func (sm *syncedMustNotHaveRBMap) addMustNotHaveRB(rbNs string, rbo roleBindingOrigin) {
	//the key of the <rolebinding name>-<policy name> e.g. rb1-pl1
	//the value is rolebinding object
	sm.Mx.Lock()
	defer sm.Mx.Unlock()
	//check if the map is initialized, if not initilize it
	if sm.mustNotHaveRoleBinding == nil {
		sm.mustNotHaveRoleBinding = make(map[string]roleBindingOrigin)
	}
	sm.mustNotHaveRoleBinding[rbNs] = rbo
}

func (sm *syncedMustNotHaveRBMap) removeMustNotHaveRB(rbNs string) {
	//the key of the <rolebinding name>-<policy name> e.g. rb1-pl1
	//the value is rolebinding object
	sm.Mx.Lock()
	defer sm.Mx.Unlock()
	//check if the map is initialized, if not return
	if sm.mustNotHaveRoleBinding == nil {
		return
	}
	if _, ok := sm.mustNotHaveRoleBinding[rbNs]; ok {
		delete(sm.mustNotHaveRoleBinding, rbNs)
	}
}

func handleMustHaveRoleBinding(rbValue roleBindingOrigin, ResClient *resourceClient.ResourceClient, smMustHaveRoleB *syncedMustHaveRBMap) {
	var updateNeeded bool
	//I create convert it into a map, where the key is the same and the value is the roleBinding
	//Get the list of roles that satisfy the label:
	opt := &metav1.ListOptions{}
	//For now we don't use label selectors, in the future we might
	/*
		if rtValue.roleTemplate.Selector != nil {
			if rtValue.roleTemplate.Selector.MatchLabels != nil {
				lbl := createKeyValuePairs(rtValue.roleTemplate.Selector.MatchLabels)
				if lbl == "" {
					opt = &metav1.ListOptions{LabelSelector: lbl}
				}
			}
		}
	*/
	roleBindingList, err := ResClient.KubeClient.RbacV1().RoleBindings(rbValue.namespace).List(*opt) //namespace scoped list
	if err == nil {
		createRoleBindingMap(roleBindingList)
	} else {
		glog.Errorf("Error listing rolebindings from the API server,%v", err)
	}
	//for each elem, check if the elem is in a must have, then do nothing, else create it and report it.
	smMustHaveRoleB.Mx.Lock()
	defer smMustHaveRoleB.Mx.Unlock()
	for key, val := range smMustHaveRoleB.mustHaveRoleBinding {
		updateNeeded = false
		if _, ok := roleBindingsMap[key]; !ok {
			updateNeeded = true
			if strings.ToLower(string(val.policy.Spec.RemediationAction)) == strings.ToLower(string(policyv1alpha1.Enforce)) {
				_, err := ResClient.KubeClient.RbacV1().RoleBindings(val.namespace).Create(&val.roleBindingTemplate.RoleBinding)
				if err != nil {
					val.policy.Status.ComplianceState = policyv1alpha1.NonCompliant
					val.roleBindingTemplate.Status.ComplianceState = policyv1alpha1.NonCompliant
					glog.Errorf("Error creating role binding %v specified in policy template %v, the error is: %v", val.roleBindingTemplate.RoleBinding.Name, val.policy.Name, err)
				}
				glog.Infof("created role binding %v specified in policy template %v", val.roleBindingTemplate.RoleBinding.Name, val.policy.Name)
				val.policy.Status.Message = fmt.Sprintf("created roleBinding `%v`", val.roleBindingTemplate.RoleBinding.Name)
				val.roleBindingTemplate.Status.ComplianceState = policyv1alpha1.Compliant
			} else {
				val.roleBindingTemplate.Status.ComplianceState = policyv1alpha1.NonCompliant
				val.policy.Status.ComplianceState = policyv1alpha1.NonCompliant
				val.policy.Status.Message = fmt.Sprintf("missing roleBinding `%v`", val.roleBindingTemplate.RoleBinding.Name)

			}
		} else { //the rolebinding exists
			val.roleBindingTemplate.Status.ComplianceState = policyv1alpha1.Compliant
		}
		if updateNeeded {
			UpdatePolicyMap[val.policy.Name] = val.policy
		}
	}
}

func handleMustNotHaveRoleBinding(rbValue roleBindingOrigin, ResClient *resourceClient.ResourceClient, smMustNotHaveRoleB *syncedMustNotHaveRBMap) {
	var updateNeeded bool
	opt := &metav1.ListOptions{}
	//For now we don't use label selectors, in the future we might
	/*
		if rtValue.roleTemplate.Selector != nil {
			if rtValue.roleTemplate.Selector.MatchLabels != nil {
				lbl := createKeyValuePairs(rtValue.roleTemplate.Selector.MatchLabels)
				if lbl == "" {
					opt = &metav1.ListOptions{LabelSelector: lbl}
				}
			}
		}
	*/
	roleBindingList, err := ResClient.KubeClient.RbacV1().RoleBindings(rbValue.namespace).List(*opt) //namespace scoped list
	if err == nil {
		createRoleBindingMap(roleBindingList)
	} else {
		glog.Errorf("Error listing rolebindings from the API server,%v", err)
	}
	//for each elem, check if the elem is in a must have, then do nothing, else create it and report it.
	smMustNotHaveRoleB.Mx.Lock()
	defer smMustNotHaveRoleB.Mx.Unlock()
	delOpt := &metav1.DeleteOptions{}
	for key, val := range smMustNotHaveRoleB.mustNotHaveRoleBinding {
		updateNeeded = false
		if _, ok := roleBindingsMap[key]; ok {
			updateNeeded = true
			if strings.ToLower(string(val.policy.Spec.RemediationAction)) == strings.ToLower(string(policyv1alpha1.Enforce)) {
				err := ResClient.KubeClient.RbacV1().RoleBindings(val.namespace).Delete(val.roleBindingTemplate.RoleBinding.Name, delOpt)
				if err != nil {
					val.policy.Status.ComplianceState = policyv1alpha1.NonCompliant
					val.roleBindingTemplate.Status.ComplianceState = policyv1alpha1.NonCompliant
					glog.Errorf("Error deleting role binding %v specified in policy template %v, the error is: %v", val.roleBindingTemplate.RoleBinding.Name, val.policy.Name, err)
				}
				glog.Infof("deleted role binding %v specified in policy template %v", val.roleBindingTemplate.RoleBinding.Name, val.policy.Name)
				val.policy.Status.Message = fmt.Sprintf("deleted roleBinding `%v`", val.roleBindingTemplate.RoleBinding.Name)
			} else {
				val.policy.Status.ComplianceState = policyv1alpha1.NonCompliant
				val.roleBindingTemplate.Status.ComplianceState = policyv1alpha1.NonCompliant
				val.policy.Status.Message = fmt.Sprintf("additional roleBinding `%v`", val.roleBindingTemplate.RoleBinding.Name)
			}
		} else { //the rolebinding does not exists
			val.roleBindingTemplate.Status.ComplianceState = policyv1alpha1.Compliant
		}
		if updateNeeded {
			UpdatePolicyMap[val.policy.Name] = val.policy
		}
	}
}

func createRoleBindingMap(roleBindingList *rbacv1.RoleBindingList) {
	roleBindingsMap = nil
	roleBindingsMap = make(map[string]rbacv1.RoleBinding)

	if len(roleBindingList.Items) == 0 {
		return
	}
	for _, roleB := range roleBindingList.Items {
		roleBName := []string{roleB.Name, roleB.Namespace}
		roleBNamespace := strings.Join(roleBName, "-")
		roleBindingsMap[roleBNamespace] = roleB
	}
}

/*
I need to process the policy, and add the RB to the correspondong map
in the map I need to keep track of the parent policy, the verify its compliance

the syncedMap should have the functions to add/remove items safely
*/
