package policy

import (
	"fmt"
	"reflect"
	"strings"

	"github.com/go-logr/logr"
	wildcard "github.com/kyverno/go-wildcard"
	kyverno "github.com/kyverno/kyverno/api/kyverno/v1"
	"github.com/kyverno/kyverno/pkg/config"
	"github.com/kyverno/kyverno/pkg/utils"
	stringutils "github.com/kyverno/kyverno/pkg/utils/string"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/labels"
	listerv1 "k8s.io/client-go/listers/core/v1"
	"sigs.k8s.io/controller-runtime/pkg/log"
)

func buildPolicyLabel(policyName string) (labels.Selector, error) {
	policyLabelmap := map[string]string{"policy": policyName}
	//NOt using a field selector, as the match function will have to cast the runtime.object
	// to get the field, while it can get labels directly, saves the cast effort
	ls := &metav1.LabelSelector{}
	if err := metav1.Convert_Map_string_To_string_To_v1_LabelSelector(&policyLabelmap, ls, nil); err != nil {
		return nil, fmt.Errorf("failed to generate label sector of Policy name %s: %v", policyName, err)
	}
	policySelector, err := metav1.LabelSelectorAsSelector(ls)
	if err != nil {
		return nil, fmt.Errorf("Policy %s has invalid label selector: %v", policyName, err)
	}
	return policySelector, nil
}

func transformResource(resource unstructured.Unstructured) []byte {
	data, err := resource.MarshalJSON()
	if err != nil {
		log.Log.Error(err, "failed to marshal resource")
		return nil
	}
	return data
}

func ParseNamespacedPolicy(key string) (string, string, bool) {
	namespace := ""
	index := strings.Index(key, "/")
	if index != -1 {
		namespace = key[:index]
		key = key[index+1:]
		return namespace, key, true
	}
	return namespace, key, false
}

// MergeResources merges b into a map
func MergeResources(a, b map[string]unstructured.Unstructured) {
	for k, v := range b {
		a[k] = v
	}
}

// getNamespacesForRule gets the matched namespaces list for the given rule
func (pc *PolicyController) getNamespacesForRule(rule *kyverno.Rule, log logr.Logger) []string {
	var matchedNS []string
	if len(rule.MatchResources.Namespaces) == 0 {
		matchedNS = GetAllNamespaces(pc.nsLister, log)
		return pc.configHandler.FilterNamespaces(matchedNS)
	}

	var wildcards []string
	for _, nsName := range rule.MatchResources.Namespaces {
		if stringutils.ContainsWildcard(nsName) {
			wildcards = append(wildcards, nsName)
		}

		matchedNS = append(matchedNS, nsName)
	}

	if len(wildcards) > 0 {
		wildcardMatches := GetMatchingNamespaces(wildcards, pc.nsLister, log)
		matchedNS = append(matchedNS, wildcardMatches...)
	}

	return pc.configHandler.FilterNamespaces(matchedNS)
}

// GetMatchingNamespaces ...
func GetMatchingNamespaces(wildcards []string, nslister listerv1.NamespaceLister, log logr.Logger) []string {
	all := GetAllNamespaces(nslister, log)
	if len(all) == 0 {
		return all
	}

	var results []string
	for _, wc := range wildcards {
		for _, ns := range all {
			if wildcard.Match(wc, ns) {
				results = append(results, ns)
			}
		}
	}

	return results
}

// GetAllNamespaces gets all namespaces in the cluster
func GetAllNamespaces(nslister listerv1.NamespaceLister, log logr.Logger) []string {
	var results []string
	namespaces, err := nslister.List(labels.NewSelector())
	if err != nil {
		log.Error(err, "Failed to list namespaces")
	}
	for _, n := range namespaces {
		name := n.GetName()
		results = append(results, name)
	}

	return results
}

func (pc *PolicyController) getResourceList(kind, namespace string, labelSelector *metav1.LabelSelector, log logr.Logger) interface{} {
	resourceList, err := pc.client.ListResource("", kind, namespace, labelSelector)
	if err != nil {
		log.Error(err, "failed to list resources", "kind", kind, "namespace", namespace)
		return nil
	}

	return resourceList
}

// GetResourcesPerNamespace returns
// - Namespaced resources across all namespaces if namespace is set to empty "", for Namespaced Kind
// - Namespaced resources in the given namespace
// - Cluster-wide resources for Cluster-wide Kind
func (pc *PolicyController) getResourcesPerNamespace(kind string, namespace string, rule kyverno.Rule, log logr.Logger) map[string]unstructured.Unstructured {
	resourceMap := map[string]unstructured.Unstructured{}

	if kind == "Namespace" {
		namespace = ""
	}

	list := pc.getResourceList(kind, namespace, rule.MatchResources.Selector, log)
	switch typedList := list.(type) {
	case []*unstructured.Unstructured:
		for _, r := range typedList {
			if pc.match(*r, rule) {
				resourceMap[string(r.GetUID())] = *r
			}
		}
	case *unstructured.UnstructuredList:
		for _, r := range typedList.Items {
			if pc.match(r, rule) {
				resourceMap[string(r.GetUID())] = r
			}
		}
	}

	// skip resources to be filtered
	excludeResources(resourceMap, rule.ExcludeResources.ResourceDescription, pc.configHandler, log)
	return resourceMap
}

func (pc *PolicyController) match(r unstructured.Unstructured, rule kyverno.Rule) bool {
	if r.GetDeletionTimestamp() != nil {
		return false
	}

	if r.GetKind() == "Pod" {
		if !isRunningPod(r) {
			return false
		}
	}

	// match name
	if rule.MatchResources.Name != "" {
		if !wildcard.Match(rule.MatchResources.Name, r.GetName()) {
			return false
		}
	}
	// Skip the filtered resources
	if pc.configHandler.ToFilter(r.GetKind(), r.GetNamespace(), r.GetName()) {
		return false
	}

	return true
}

// ExcludeResources ...
func excludeResources(included map[string]unstructured.Unstructured, exclude kyverno.ResourceDescription, configHandler config.Interface, log logr.Logger) {
	if reflect.DeepEqual(exclude, (kyverno.ResourceDescription{})) {
		return
	}
	excludeName := func(name string) Condition {
		if exclude.Name == "" {
			return NotEvaluate
		}
		if wildcard.Match(exclude.Name, name) {
			return Skip
		}
		return Process
	}

	excludeNamespace := func(namespace string) Condition {
		if len(exclude.Namespaces) == 0 {
			return NotEvaluate
		}
		if utils.ContainsNamepace(exclude.Namespaces, namespace) {
			return Skip
		}
		return Process
	}

	excludeSelector := func(labelsMap map[string]string) Condition {
		if exclude.Selector == nil {
			return NotEvaluate
		}
		selector, err := metav1.LabelSelectorAsSelector(exclude.Selector)
		// if the label selector is incorrect, should be fail or
		if err != nil {
			log.Error(err, "failed to build label selector")
			return Skip
		}
		if selector.Matches(labels.Set(labelsMap)) {
			return Skip
		}
		return Process
	}

	findKind := func(kind string, kinds []string) bool {
		for _, k := range kinds {
			if k == kind {
				return true
			}
		}
		return false
	}

	excludeKind := func(kind string) Condition {
		if len(exclude.Kinds) == 0 {
			return NotEvaluate
		}

		if findKind(kind, exclude.Kinds) {
			return Skip
		}

		return Process
	}

	// check exclude condition for each resource
	for uid, resource := range included {
		// 0 -> don't check
		// 1 -> is not to be exclude
		// 2 -> to be exclude
		excludeEval := []Condition{}

		if ret := excludeName(resource.GetName()); ret != NotEvaluate {
			excludeEval = append(excludeEval, ret)
		}
		if ret := excludeNamespace(resource.GetNamespace()); ret != NotEvaluate {
			excludeEval = append(excludeEval, ret)
		}
		if ret := excludeSelector(resource.GetLabels()); ret != NotEvaluate {
			excludeEval = append(excludeEval, ret)
		}
		if ret := excludeKind(resource.GetKind()); ret != NotEvaluate {
			excludeEval = append(excludeEval, ret)
		}
		// exclude the filtered resources
		if configHandler.ToFilter(resource.GetKind(), resource.GetNamespace(), resource.GetName()) {
			delete(included, uid)
			continue
		}

		func() {
			for _, ret := range excludeEval {
				if ret == Process {
					// Process the resources
					continue
				}
				// Skip the resource from processing
				delete(included, uid)
			}
		}()
	}
}
