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

import (
	"fmt"
	"hash/fnv"
	"reflect"

	"github.com/golang/glog"

	v1alpha1 "github.com/open-cluster-management/hcm-compliance/pkg/apis/accesspolicy/v1alpha1"
	"github.com/open-cluster-management/seed-sdk/pkg/context"
	corev1 "k8s.io/api/core/v1"
	networkingv1 "k8s.io/api/networking/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	kubernetes "k8s.io/client-go/kubernetes"
)

const (
	denyAllPrefix     = "deny-all-egress"
	allowEgressPrefix = "egress-rule"
)

var egressExceptionsNS = map[string]string{
	"kube-system":  "ns",
	"istio-system": "ns",
}

// policiesmapper holds a set of functions to map Application
// Policy into a NetworkPolicy
type policiesmapper struct {
	netpolmgr *networkpoliciesmanager
	svcsmgr   *servicesmanager
	clientset *kubernetes.Clientset
}

var polmapper *policiesmapper

// getPoliciesMapper will return the policiesmapper singleton
func getPoliciesMapper(ctx context.Context) *policiesmapper {
	if polmapper == nil {
		cset := ctx.KubeClientset()
		netpolmgr := getNetworkPoliciesManager(cset)
		svcsmgr := getServicesManager(cset)
		polmapper = &policiesmapper{
			netpolmgr: netpolmgr,
			svcsmgr:   svcsmgr,
			clientset: cset,
		}
	}

	return polmapper
}

// reconcileAccessPolicy handles creation and updates of an access policy
func (mapper *policiesmapper) reconcileAccessPolicy(ap *v1alpha1.AccessPolicy) {
	glog.V(3).Infof("Reconciling access policy %s.%s.%s", ap.GetNamespace(), ap.GetName(), ap.GetResourceVersion())

	sourceServiceName := ap.Spec.Source.Service
	sourceService, err := mapper.svcsmgr.get(sourceServiceName, ap.GetNamespace())
	if err != nil {
		glog.Error("Failed to get source-service object for access policy: ", err)
		return
	}
	destinationServiceName := ap.Spec.Destination.Service
	destinationService, err := mapper.svcsmgr.get(destinationServiceName, ap.GetNamespace())
	if err != nil {
		glog.Error("Failed to get target-service object for access policy: ", err)
		return
	}

	// If 'deny-all' network policy doesn't already exist, create it now.
	// If it exists just update the ownerReferences to reference this Application Policy as well.
	networkPolicy, err := mapper.denyAllPolicy(ap, sourceService)
	if err != nil {
		glog.Errorf("Failed to create/update a 'deny-all' network policy: %v", err)
		return
	}
	err = mapper.createNetworkPolicy(networkPolicy, ap)
	if err != nil {
		glog.Error("Failed to create/update a 'deny-all' network policy", err)
		return
	}

	// If egress allowance network policy doesn't already exist, create it now
	networkPolicy = allowEgressPolicy(ap, sourceService, destinationService)
	err = mapper.createNetworkPolicy(networkPolicy, ap)
	if err != nil {
		glog.Error("Failed to create/update an egress allowance network policy", err)
		return
	}
}

// Functions as defined by k8s.io/client-go/tools/cache/ResourceEventHandler. This will
// allow instance of the PoliciesMapper to be notified about resource events and handle
// those.

// OnAdd is being called when when a new resource has been added
// func (mapper *policiesmapper) onAdd(new interface{}) {
// aPolicy := new.(*v1alpha1.AccessPolicy)

// XXX
// glog.Infof("Application Policy resource has been added: %s.%s", aPolicy.GetNamespace(), aPolicy.GetName())

// XXX
// if (aPolicy.Spec.DestinationSelector.MatchLabels != nil && aPolicy.Spec.DestinationIPBlock.CIDR != "") ||
// 	(aPolicy.Spec.DestinationSelector.MatchLabels == nil && aPolicy.Spec.DestinationIPBlock.CIDR == "") {
// 	glog.Error("Exactly one of DestinationIPBlock or DestinationSelector should be specified")
// 	return
// }

// If 'deny-all' network policy doesn't already exist, create it now.
// If it exists just update the ownerReferences to reference this Application Policy as well.
// XXX
// policy := denyAllPolicy(aPolicy)
// err := mapper.createNetworkPolicy(policy, aPolicy)
// if err != nil {
// 	glog.Error("Failed to create/update a 'deny-all' network policy", err)
// 	return
// }

// If egress allowance network policy doesn't already exist, create it now
// XXX
// policy = allowEgressPolicy(aPolicy)
// err = mapper.createNetworkPolicy(policy, aPolicy)
// if err != nil {
// 	glog.Error("Failed to create/update an egress allowance network policy", err)
// 	return
// }
// }

// OnDelete is being called when a resource has been deleted
func (mapper *policiesmapper) onDelete(old interface{}) {
	aPolicy := old.(*v1alpha1.AccessPolicy)

	glog.Infof("Application Policy resource has been deleted: %s.%s", aPolicy.GetNamespace(), aPolicy.GetName())

	// Kuberenetes will automatically delete network policies that have this deleted policy as an owner reference and
	// if they don't reference any other application policy.
}

// OnUpdate is being called when a resource has been updated
func (mapper *policiesmapper) onUpdate(old, new interface{}) {
	oldPolicy := old.(*v1alpha1.AccessPolicy)
	newPolicy := new.(*v1alpha1.AccessPolicy)

	if oldPolicy.GetResourceVersion() == newPolicy.GetResourceVersion() {
		return
	}

	glog.Infof("Application Policy resource has been updated: %s.%s", oldPolicy.GetNamespace(), oldPolicy.GetName())

	// XXX
	// oldHash := policyHash(oldPolicy, true)
	// newHash := policyHash(newPolicy, true)
	// if oldHash == newHash {
	// 	glog.Info("The application policy update isn't affecting network policies")
	// 	return
	// }

	// TODO Implement update
	glog.Error("Update of an application policy not yet supported")
}

func updateExistingPolicy(policy *networkingv1.NetworkPolicy, existing *networkingv1.NetworkPolicy, owner *v1alpha1.AccessPolicy) *networkingv1.NetworkPolicy {
	updated := false

	if !reflect.DeepEqual(policy.Spec.PodSelector, existing.Spec.PodSelector) {
		in, out := &policy.Spec.PodSelector, new(metav1.LabelSelector)
		(*in).DeepCopyInto(out)
		existing.Spec.PodSelector = *out
		glog.V(5).Infof("Updating %s.%s	podselector rule to: %v", existing.GetName(), existing.GetNamespace(), existing.Spec.PodSelector)
		updated = true
	}

	eI, pI := existing.Spec.Ingress, policy.Spec.Ingress
	if !((len(eI) == 0 && len(pI) == 0) || reflect.DeepEqual(eI, pI)) {
		ingress := []networkingv1.NetworkPolicyIngressRule{}
		for _, rule := range pI {
			ingress = append(ingress, *(rule.DeepCopy()))
		}
		existing.Spec.Ingress = ingress
		glog.V(5).Infof("Updating %s.%s	ingress rule to: %v", existing.GetName(), existing.GetNamespace(), existing.Spec.Ingress)
		updated = true
	}

	eE, pE := existing.Spec.Egress, policy.Spec.Egress
	if !((len(eE) == 0 && len(pE) == 0) || reflect.DeepEqual(eE, pE)) {
		egress := []networkingv1.NetworkPolicyEgressRule{}
		for _, rule := range pE {
			egress = append(egress, *(rule.DeepCopy()))
		}
		existing.Spec.Egress = egress
		glog.V(5).Infof("Updating %s.%s	egress rule to: %v", existing.GetName(), existing.GetNamespace(), existing.Spec.Egress)
		updated = true
	}

	ownerRefs := existing.GetOwnerReferences()
	newRef := policyOwnerRef(owner)
	refExists := false
	for _, ref := range ownerRefs {
		if ref.UID == newRef.UID {
			refExists = true
			glog.Info("    found owner referenced in list of existing owners")
			break
		}
	}
	if !refExists {
		glog.Info("A network policy already exists. Updating its ownerReferences with new Application Policy")
		ownerRefs = append(ownerRefs, newRef)
		existing.OwnerReferences = ownerRefs
		updated = true
	}

	if updated {
		return existing
	}
	return nil
}

// Creates the provided network policy in the cluster if there is no such already.
// If a similar network policy already exists, the ownerReferences field of the existing one will be
// updated to also reference the new owner.
func (mapper *policiesmapper) createNetworkPolicy(policy *networkingv1.NetworkPolicy, owner *v1alpha1.AccessPolicy) error {
	policyName := policy.GetName()
	existing, err := mapper.netpolmgr.get(policyName, policy.GetNamespace())
	if err != nil {
		glog.Error("Failed to check whether a network policy already exists", err)
		return err
	}

	if existing != nil {
		updatedExisting := updateExistingPolicy(policy, existing, owner)
		if updatedExisting != nil {
			glog.Infof("updating NetworkPolicy %s.%s", updatedExisting.GetNamespace(), updatedExisting.GetName())
			err = mapper.netpolmgr.update(updatedExisting)
			if err != nil {
				glog.Errorf("Error updating NetworkPolicy %v", err)
			}
		}
	} else {
		//The same network policy hasn't been deployed before. Create it now.
		glog.Infof("creating NetworkPolicy %s.%s", policy.GetNamespace(), policy.GetName())
		err = mapper.netpolmgr.create(policy)
		if err != nil {
			glog.Errorf("Error creating NetworkPolicy %v", err)
		}
	}
	return nil
}

/*
	The function will return a NetworkPolicy instance similar to the following
	YAML while replacing the tokens with the provided value.

		apiVersion: networking.k8s.io/v1
		kind: NetworkPolicy
		metadata:
			name: <name>
			namespace: <ns>
		spec:
			podSelector:
				matchLabels:
					<fromLabel>
			policyTypes:
			- Ingress
			- Egress
			egress:
			- to:
				- namespaceSelector:
					matchLabels:
						<egressExceptionsNS[0]>
			- to:
				- namespaceSelector:
					matchLabels:
						<egressExceptionsNS[1]>
						.
						.
						.
*/
func (mapper *policiesmapper) denyAllPolicy(ap *v1alpha1.AccessPolicy, svc *corev1.Service) (*networkingv1.NetworkPolicy, error) {
	name := fmt.Sprintf("%s-%d", denyAllPrefix, policyHash(ap, false))

	serviceSelector := svc.Spec.Selector
	podLabelSelector := metav1.LabelSelector{}
	metav1.Convert_Map_string_To_string_To_v1_LabelSelector(&serviceSelector, &podLabelSelector, nil)
	egress := []networkingv1.NetworkPolicyEgressRule{}
	for key, value := range egressExceptionsNS {
		toNSLabelSelector := metav1.LabelSelector{}
		metav1.AddLabelToSelector(&toNSLabelSelector, key, value)
		egressRule := networkingv1.NetworkPolicyEgressRule{
			To: []networkingv1.NetworkPolicyPeer{
				networkingv1.NetworkPolicyPeer{
					NamespaceSelector: &toNSLabelSelector,
				},
			},
		}
		egress = append(egress, egressRule)
	}
	policy := &networkingv1.NetworkPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: ap.GetNamespace(),
			OwnerReferences: []metav1.OwnerReference{
				policyOwnerRef(ap),
			},
		},
		Spec: networkingv1.NetworkPolicySpec{
			PodSelector: podLabelSelector,
			Ingress:     []networkingv1.NetworkPolicyIngressRule{},
			Egress:      egress,
		},
	}
	return policy, nil
}

/*
	The function will return a NetworkPolicy instance similar to the following
	YAML while replacing the 'ns' and labels tokens with the provided values.

		apiVersion: networking.k8s.io/v1
		kind: NetworkPolicy
		metadata:
			name: <name>
			namespace: <ns>
		spec:
			podSelector:
				matchLabels:
					<fromLabel>
			policyTypes:
			- Egress
			egress:
			- to:
				- podSelector:
					matchLabels:
						<toLabel>
*/
func allowEgressPolicy(ap *v1alpha1.AccessPolicy, source *corev1.Service, destination *corev1.Service) *networkingv1.NetworkPolicy {
	// Add an egress rule network policy to allow egress between source and destination
	name := fmt.Sprintf("%s-%d", allowEgressPrefix, policyHash(ap, true))

	fromLabel := source.Spec.Selector
	fromLabelSelector := metav1.LabelSelector{}
	metav1.Convert_Map_string_To_string_To_v1_LabelSelector(&fromLabel, &fromLabelSelector, nil)

	// Print debugging information
	glog.V(3).Infof("NetworkPolicy full name: %s.%s", ap.GetNamespace(), name)
	glog.V(3).Info("NetworkPolicy source pod selection labels:")
	for key, value := range fromLabel {
		glog.Infof("\t%s=%s", key, value)
	}

	var destNetworkPolicyPeer networkingv1.NetworkPolicyPeer

	toLabelSelector := metav1.LabelSelector{}
	toLabel := destination.Spec.Selector
	metav1.Convert_Map_string_To_string_To_v1_LabelSelector(&toLabel, &toLabelSelector, nil)
	destNetworkPolicyPeer = networkingv1.NetworkPolicyPeer{
		PodSelector: &toLabelSelector,
	}

	// Generate a NetworkPolicy instance based on the data above
	policy := &networkingv1.NetworkPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: ap.GetNamespace(),
			OwnerReferences: []metav1.OwnerReference{
				policyOwnerRef(ap),
			},
		},
		Spec: networkingv1.NetworkPolicySpec{
			PodSelector: fromLabelSelector,
			PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeEgress},
			Egress: []networkingv1.NetworkPolicyEgressRule{
				networkingv1.NetworkPolicyEgressRule{
					To: []networkingv1.NetworkPolicyPeer{
						destNetworkPolicyPeer,
					},
				},
			},
		},
	}
	return policy
}

// The function will generate a unique policy hash based on selected attributes from the
// provided application policy.
// Current attributes used for the hash calculation:
//  - Namespace
//  - Source
//  - Destination
func policyHash(ap *v1alpha1.AccessPolicy, useDestLabels bool) uint32 {
	h := fnv.New32a()
	h.Write([]byte(ap.GetNamespace()))
	h.Write([]byte(ap.Spec.Source.Service))
	h.Write([]byte(ap.Spec.Destination.Service))
	return h.Sum32()
}

// Remove a string from the provided slice and return the updated slice
func remove(from []string, toRemove string) []string {
	for i, v := range from {
		if v == toRemove {
			return append(from[:i], from[i+1:]...)
		}
	}
	return from
}

// Create a OwnerReference that references the provided object
func policyOwnerRef(policy *v1alpha1.AccessPolicy) metav1.OwnerReference {
	// metav1.NewContollerRef(policy, ???)
	blockOwnerDeletion := true
	isController := true
	return metav1.OwnerReference{
		Kind:               "AccessPolicy",
		UID:                policy.UID,
		Name:               policy.Name,
		APIVersion:         "accesspolicy.mcm.ibm.com/v1alpha1",
		BlockOwnerDeletion: &blockOwnerDeletion,
		Controller:         &isController,
	}
}
