// 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 (
	"errors"
	"fmt"
	"reflect"
	"strings"

	"github.com/golang/glog"
	policyv1alpha1 "github.com/open-cluster-management/hcm-compliance/pkg/apis/policy/v1alpha1"
	yaml "gopkg.in/yaml.v2"
	appsv1 "k8s.io/api/apps/v1"

	"k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type query struct {
	kind           string
	apiVersion     string
	path           []string
	attribute      string
	desiredValue   string
	parentTemplate *policyv1alpha1.GenericTemplate
	parentPolicy   *policyv1alpha1.Policy
	labels         *metav1.LabelSelector
}

const exists = "attribute exist and correctly initialized"

func ensureCaseMatch(apiVersion, kind string) (apiV, kd string) {
	//When using reflect, the method call is case sensitive. so we must ensure an exact match
	apiV, kd = apiVersion, kind
	if apiVersion == "CoreV1" || apiVersion == "corev1" || apiVersion == "Corev1" || apiVersion == "coreV1" {
		apiV = "CoreV1"
	} else if apiVersion == "Core" || apiVersion == "core" {
		apiV = "Core"
	} else if apiVersion == "AppsV1" || apiVersion == "Appsv1" || apiVersion == "appsV1" || apiVersion == "appsv1" || apiVersion == "apps" || apiVersion == "Apps" {
		apiV = "AppsV1"
	} else if apiVersion == "AppsV1beta1" || apiVersion == "appsV1beta1" || apiVersion == "Appsv1beta1" || apiVersion == "AppsV1Beta1" {
		apiV = "AppsV1beta1"
	} else if apiVersion == "AppsV1beta2" || apiVersion == "appsV1beta2" || apiVersion == "Appsv1beta2" || apiVersion == "AppsV1Beta2" {
		apiV = "AppsV1beta2"
	} else if apiVersion == "RbacV1" || apiVersion == "rbacv1" || apiVersion == "rbacV1" || apiVersion == "Rbacv1" {
		apiV = "RbacV1"
	} else if apiVersion == "RbacV1beta1" || apiVersion == "Rbacv1beta1" || apiVersion == "rbacV1beta1" || apiVersion == "rbacv1beta1" {
		apiV = "RbacV1beta1"
	} else if apiVersion == "Rbac" || apiVersion == "rbac" || apiVersion == "RBAC" {
		apiV = "Rbac"
	} else if apiVersion == "RbacV1alpha1" || apiVersion == "Rbacv1alpha1" || apiVersion == "rbacV1alpha1" || apiVersion == "rbacv1alpha1" {
		apiV = "RbacV1alpha1"
	}
	//check for kind
	if kind == "Pods" || kind == "pods" || kind == "pod" || kind == "Pod" {
		kd = "Pods"
	} else if kind == "Roles" || kind == "roles" || kind == "Role" || kind == "role" {
		kd = "Roles"
	} else if kind == "Deployments" || kind == "deployments" || kind == "Deployment" || kind == "deployment" {
		kd = "Deployments"
	} else if kind == "ReplicaSets" || kind == "Replicasets" || kind == "replicaSets" || kind == "replicasets" || kind == "replicaset" || kind == "ReplicaSet" {
		kd = "ReplicaSets"
	} else if kind == "DaemonSets" || kind == "daemonsets" || kind == "daemonSets" || kind == "Daemonsets" || kind == "daemonset" || kind == "DaemonSet" {
		kd = "DaemonSets"
	} else if kind == "StatefulSets" || kind == "Statefulsets" || kind == "statefulSets" || kind == "statefulsets" || kind == "StatefulSet" || kind == "statefulset" {
		kd = "StatefulSets"
	}
	return apiV, kd
}

// HandleAttrCheck handles Generic Templates
func HandleAttrCheck(key string, qryList []query) {

	slc := strings.Split(key, ".") //Key is made of: qry.apiVersion, qry.kind, namespace, genericT.Name
	if len(slc) < 4 {
		glog.Errorf("Missing element in `apiversion`.`kind`.`namespace`.`templateName` in `%v`", key)
		return
	}
	if len(qryList) == 0 {
		return
	}
	MxUpdateMap.Lock()
	var opt *metav1.ListOptions
	opt = &metav1.ListOptions{}

	for _, qry := range qryList {

		if qry.parentTemplate.Selector != nil {
			if qry.parentTemplate.Selector.MatchLabels != nil {
				lbl := createKeyValuePairs(qry.parentTemplate.Selector.MatchLabels)
				if lbl == "" {
					opt = &metav1.ListOptions{LabelSelector: lbl}
				}
			}
		}
		apiV, kind := ensureCaseMatch(slc[0], slc[1])
		genericMap := make(map[string]interface{})
		apiFuncName := reflect.ValueOf(ResClient.KubeClient).MethodByName(apiV)
		if apiFuncName.IsNil() {
			glog.Errorf("the APIVersion is not defined properly, no client function matches its name `%v`", apiV)
			return
		}
		apiFuncCallValues := apiFuncName.Call([]reflect.Value{}) //namespace scoped list
		if len(apiFuncCallValues) == 0 {
			glog.Errorf("the APIVersion is not defined properly, the call to the function `KubeClient.%v` returned empty", apiV)
			return
		}
		inputs := make([]reflect.Value, 1)
		inputs[0] = reflect.ValueOf(slc[2])
		kindFuncName := apiFuncCallValues[0].MethodByName(kind)
		if kindFuncName.IsNil() {
			glog.Errorf("the APIVersion is not defined properly, no client function matches its name `%v`", kind)
			return
		}

		kindFuncCallValues := kindFuncName.Call(inputs)
		if len(kindFuncCallValues) == 0 {
			glog.Errorf("the Kind is not defined properly, the call to the function `KubeClient.%v.%v` returned empty", apiV, kind)
			return
		}
		inputs[0] = reflect.ValueOf(*opt)

		objListFuncName := kindFuncCallValues[0].MethodByName("List")
		if objListFuncName.IsNil() {
			glog.Errorf(" no client function matches its name `%v.%v.%v`", apiV, kind, "List")
			return
		}

		objListFuncCall := objListFuncName.Call(inputs)
		if len(objListFuncCall) == 0 {
			glog.Errorf("no Objects found to list in `KubeClient.%v.%v.%v` returned empty", apiV, kind, "List")
			return
		}

		iface := objListFuncCall[0].Interface()
		var needed bool
		switch reflect.TypeOf(iface).String() {
		case "*v1.PodList":

			if _, ok := iface.(*v1.PodList); ok {
				genList := iface.(*v1.PodList)
				//fmt.Printf("\nThe pod value of v is: %v\n", reflect.TypeOf(iface).String())
				for _, item := range genList.Items {
					out, err := yaml.Marshal(item)
					if err != nil {
						panic(err)
					}
					err = yaml.Unmarshal(out, &genericMap)
					if err != nil {
						panic(err)
					}
					//pass the qry and the generic map

					res, matched := compareAttr(qry, genericMap)
					mapKey := strings.Join(slc[0:3], ".")
					if !matched { //check if the violation is already captured in the status of the genericTemplate, if not add it
						longRes := fmt.Sprintf("%v: %v in pod `%v` in namespace `%v`", qry.path, res, item.Name, slc[2])
						//qry.apiVersion, qry.kind, namespace, genericT.Name

						qry.parentTemplate, needed = updateNeeded(longRes, mapKey, item.Name, qry.parentTemplate, qry.parentPolicy)

						if needed {
							if _, ok := UpdatePolicyMap[qry.parentPolicy.Name]; !ok {

								UpdatePolicyMap[qry.parentPolicy.Name] = qry.parentPolicy

							}
						}
						glog.V(6).Infof("\nGenericTemplate name `%v` update is needed is `%v`, the status is `%v`\n", qry.parentTemplate.Name, needed, qry.parentTemplate.Status.Violations)
					} else {
						if _, ok := qry.parentTemplate.Status.Violations[mapKey][item.Name]; ok {
							delete(qry.parentTemplate.Status.Violations[mapKey], item.Name) //example map[ corev1.pod.default: map[foo-pod:[v1,v2,v3],another-pod:[v1,v2,v3]]]
							if len(qry.parentTemplate.Status.Violations[mapKey]) == 0 {
								delete(qry.parentTemplate.Status.Violations, mapKey)
							}
							UpdatePolicyMap[qry.parentPolicy.Name] = qry.parentPolicy
						}

						//it matches, but I need to check the existing value of this key, if it is different, I should remove it. Then if len violations of the GenT == 0 I set the state to compliant
					}
				}
			}
		case "*v1.RoleList":
			if _, ok := iface.(*rbacv1.RoleList); ok {
				if _, ok := iface.(*rbacv1.RoleList); ok {
					genList := iface.(*rbacv1.RoleList)
					//fmt.Printf("\nThe pod value of v is: %v\n", reflect.TypeOf(iface).String())
					for _, item := range genList.Items {
						out, err := yaml.Marshal(item)
						if err != nil {
							panic(err)
						}
						err = yaml.Unmarshal(out, &genericMap)
						if err != nil {
							panic(err)
						}
						//pass the qry and the generic map
						res, matched := compareAttr(qry, genericMap)
						if !matched { //check if the violation is already captured in the status of the genericTemplate, if not add it
							longRes := fmt.Sprintf("%v: %v in role `%v` in namespace `%v`", qry.path, res, item.Name, slc[2])
							//qry.apiVersion, qry.kind, namespace, genericT.Name
							qry.parentTemplate, needed = updateNeeded(longRes, strings.Join(slc[0:3], "."), item.Name, qry.parentTemplate, qry.parentPolicy)
							glog.V(6).Infof("\nGenericTemplate name `%v` update is needed is `%v`, the status is `%v`\n", qry.parentTemplate.Name, needed, qry.parentTemplate.Status.Violations)
						}
					}
				}
			}

		case "*v1.DeploymentList":
			if _, ok := iface.(*appsv1.DeploymentList); ok {
				if _, ok := iface.(*appsv1.DeploymentList); ok {
					genList := iface.(*appsv1.DeploymentList)
					//fmt.Printf("\nThe pod value of v is: %v\n", reflect.TypeOf(iface).String())
					for _, item := range genList.Items {
						out, err := yaml.Marshal(item)
						if err != nil {
							panic(err)
						}
						err = yaml.Unmarshal(out, &genericMap)
						if err != nil {
							panic(err)
						}
						//pass the qry and the generic map
						res, matched := compareAttr(qry, genericMap)
						if !matched { //check if the violation is already captured in the status of the genericTemplate, if not add it
							longRes := fmt.Sprintf("%v: %v in deployment `%v` in namespace `%v`", qry.path, res, item.Name, slc[2])
							//qry.apiVersion, qry.kind, namespace, genericT.Name
							qry.parentTemplate, needed = updateNeeded(longRes, strings.Join(slc[0:3], "."), item.Name, qry.parentTemplate, qry.parentPolicy)
							glog.V(6).Infof("\nGenericTemplate name `%v` update is needed is `%v`, the status is `%v`\n", qry.parentTemplate.Name, needed, qry.parentTemplate.Status.Violations)
						}
					}
				}
			}
		}
	}
	MxUpdateMap.Unlock()

}

func updateNeeded(longRes string, key string, instance string, genericT *policyv1alpha1.GenericTemplate, plc *policyv1alpha1.Policy) (ModifiedGenericT *policyv1alpha1.GenericTemplate, needed bool) {
	// go the genT status and then into violations (Violations map[string]map[string][]string: first map for apiVersion.resource.namespace the third of the the instance of the resource and value is for violations per instance)
	// map[ corev1.pod.default: map[foo-pod:[v1,v2,v3],bar-pod:[v3,v4]] , apps.deployments.default: map[foo-dep:[v1,v2,v3],bar-dep:[v3,v4]] ]
	//If not matched, then update key and return true

	updateNeeded := false
	newSlc := strings.Split(longRes, ":")
	if len(newSlc) < 2 {
		glog.Errorf(" something is wrong in the violation format %v", longRes)
		return
	}

	if newSlc[1] != exists && genericT.Status.ComplianceState != policyv1alpha1.NonCompliant { //newSlc[1] is qry.path, if this key exists there is a violation
		genericT.Status.ComplianceState = policyv1alpha1.NonCompliant
		plc.Status.ComplianceState = policyv1alpha1.NonCompliant
		updateNeeded = true
	}

	if genericT.Status.Violations == nil {
		genericT.Status.Violations = make(map[string]map[string][]string)
	}
	//Check if the key exists, if not: create it and return true
	if _, ok := genericT.Status.Violations[key]; !ok { //example map[ corev1.pod.default: nil ]

		genericT.Status.Violations[key] = map[string][]string{instance: {longRes}}

	} else { // if the key exists, check its value, see if matches, if so, return false and same template passed in.
		if value, ok := genericT.Status.Violations[key][instance]; !ok {
			genericT.Status.Violations[key][instance] = append(genericT.Status.Violations[key][instance], longRes) //example map[ corev1.pod.default: map[foo-pod:[v1,v2,v3],another-pod:[v1,v2,v3]]]
		} else { //check its value, see if matches, if so, return false and same template passed in
			found := false
			for indx, val := range genericT.Status.Violations[key][instance] {
				exist := strings.Split(val, ":")

				if len(exist) < 2 || len(newSlc) < 2 {
					glog.Errorf("something is wrong in the violation format, %v, %v", longRes, value)
					continue
				}
				if reflect.DeepEqual(exist[0], newSlc[0]) {
					if reflect.DeepEqual(exist[1], newSlc[1]) {
						found = true

					} else {
						genericT.Status.Violations[key][instance][indx] = longRes
						found = true
						updateNeeded = true
					}

				}
			}
			if !found {
				genericT.Status.Violations[key][instance] = append(genericT.Status.Violations[key][instance], longRes)
				updateNeeded = true
			}
		}
	}
	return genericT, updateNeeded
}

func compareAttr(qry query, podMap map[string]interface{}) (res string, matched bool) {
	if _, ok := podMap[qry.path[0]]; !ok {
		s := fmt.Sprintf("attribute `%v` is missing", qry.path[0])
		return s, false
	}
	if reflect.DeepEqual(podMap[qry.path[0]], reflect.Zero(reflect.TypeOf(podMap[qry.path[0]])).Interface()) {
		s := fmt.Sprintf("attribute `%v` is missing", qry.path[0])
		return s, false
	}
	if qry.attribute == qry.path[0] {
		return exists, true
	}

	return ensureAttribute(podMap[qry.path[0]], 0, qry)
}

func ensureAttribute(podMap interface{}, indx int, qry query) (res string, matched bool) {

	if podMap == nil {
		s := fmt.Sprintf("attribute `%v` is missing", qry.path[indx])
		return s, false
	}

	//if the passed interface has a zero value => attribute value is missing, but the attribute exists
	if reflect.DeepEqual(podMap, reflect.Zero(reflect.TypeOf(podMap)).Interface()) {
		//fmt.Printf("attribute `%v` value is missing %v", qry.path[indx], podMap)
		s := fmt.Sprintf("attribute `%v` value is missing", qry.path[indx])
		return s, false
	}
	//else the value exists and we reached the end of path, so we found a value for the attribute
	if qry.attribute == qry.path[indx] {
		//check the value if it matches
		if qry.desiredValue != "" {
			if reflect.DeepEqual((reflect.TypeOf(podMap).Kind()), reflect.String) {
				if !FindAttrPattern(qry.desiredValue, strings.ToLower(podMap.(string))) {
					//	if !reflect.DeepEqual(qry.desiredValue, strings.ToLower(podMap.(string))) {

					return fmt.Sprintf("attribute initialized but we have a value mismatch. desired = `%v`, actual = `%v`",
						qry.desiredValue, strings.ToLower(podMap.(string))), false
				}
				return exists, true
			}
		}

		return exists, true
	}
	// the value is not zero, the name mathes the one found in path, but this is not the end of path, so we need to dig deeper
	typ := reflect.TypeOf(podMap).Kind()
	if typ == reflect.Slice {
		// for each element call with indx + 1
		if indx+1 < len(qry.path) {
			var res string
			for _, item := range podMap.([]interface{}) {
				//return ensureAttribute(item, indx, qry) //works for a single container
				res, _ = ensureAttribute(item, indx, qry)
				if res != exists {
					return res, false
				}
			}
		}
		return exists, true
	} else if typ == reflect.Map {
		// call with indx + 1
		if indx+1 < len(qry.path) {
			deepPmap := podMap.(map[interface{}]interface{})
			deepPmapIface := deepPmap[qry.path[indx+1]]
			return ensureAttribute(deepPmapIface, indx+1, qry)
		}
	}

	return fmt.Sprintf("desired attribute `%v` is not found for the object `%v`", strings.Join(qry.path, "."), qry.kind), false
}

//FindAttrPattern checks if a value has a pattern: e.g. pattern = nginx* , value = nginx-1.9.0
func FindAttrPattern(pattern string, value string) bool {

	//if pattern = "*" => all namespaces are included
	if pattern == "*" {
		return true //anyvalue is accpetable
	}

	if pattern == "" {
		if reflect.DeepEqual(pattern, value) {
			return true
		}
		return false
	}

	//if the pattern has NO "*" => do an exact search
	if !strings.Contains(pattern, "*") {
		if reflect.DeepEqual(pattern, value) {
			return true
		}
		return false
	}

	// if there is a * something, we need to figure out where: it can be a leading, ending or leading and ending
	if strings.LastIndex(pattern, "*") == 0 {
		// check for has suffix of pattern - *
		substring := strings.TrimPrefix(pattern, "*")

		if strings.HasSuffix(value, substring) {
			return true
		}
		return false
	}

	if strings.Index(pattern, "*") == len(pattern)-1 {
		// check for has prefix of pattern - *
		substring := strings.TrimSuffix(pattern, "*")
		if strings.HasPrefix(value, substring) {
			return true
		}
		return false

	}

	if strings.LastIndex(pattern, "*") == len(pattern)-1 && strings.Index(pattern, "*") == 0 {
		substring := strings.TrimPrefix(pattern, "*")
		substring = strings.TrimSuffix(substring, "*")

		if strings.Contains(value, substring) {
			return true
		}
		return false
	}
	return false
}

// api/rbac/v1
// corev1.pod.spec.containers[].imagePullPolicy: always
//https://$ENDPOINT/api/v1/namespaces/$NAMESPACE/pods

func parseQuery(path string, ls *metav1.LabelSelector) (q query, err error) {
	path = strings.ToLower(path)
	path = strings.Replace(path, " ", "", -1)
	//"corev1.pod.spec.containers.imagePullPolicy: always"
	qry := query{}
	qry.labels = ls
	slc := strings.Split(path, ".")
	if len(slc) > 2 {
		qry.apiVersion = slc[0]
		qry.kind = slc[1]
		slc = slc[2:]
		for indx := range slc {
			slc[indx] = strings.ToLower(slc[indx])
		}
		if strings.Contains(slc[len(slc)-1], ":") {
			val := strings.Split(slc[len(slc)-1], ":")
			qry.desiredValue = val[1]
			qry.attribute = val[0]
			qry.path = append(slc[:len(slc)-1], val[:1]...)
		} else {
			qry.path = slc
			qry.attribute = slc[len(slc)-1]
		}
	} else {
		glog.V(2).Infof("invalid path `%v` is too short", path)
		s := fmt.Sprintf("invalid path `%v` is too short", path)
		return qry, errors.New(s)
	}
	return qry, nil
}
