package config

import (
	"os"
	"reflect"
	"regexp"
	"strings"
	"sync"

	"github.com/go-logr/logr"
	"github.com/minio/minio/pkg/wildcard"
	v1 "k8s.io/api/core/v1"
	informers "k8s.io/client-go/informers/core/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/cache"
)

// read the conifgMap with name in env:INIT_CONFIG
// this configmap stores the resources that are to be filtered
const cmNameEnv string = "INIT_CONFIG"

// ConfigData stores the configuration
type ConfigData struct {
	client kubernetes.Interface
	// configMap Name
	cmName string
	// lock configuration
	mux sync.RWMutex
	// configuration data
	filters []k8Resource
	// hasynced
	cmSycned cache.InformerSynced
	log      logr.Logger
}

// ToFilter checks if the given resource is set to be filtered in the configuration
func (cd *ConfigData) ToFilter(kind, namespace, name string) bool {
	cd.mux.RLock()
	defer cd.mux.RUnlock()
	for _, f := range cd.filters {
		if wildcard.Match(f.Kind, kind) && wildcard.Match(f.Namespace, namespace) && wildcard.Match(f.Name, name) {
			return true
		}
	}
	return false
}

// Interface to be used by consumer to check filters
type Interface interface {
	ToFilter(kind, namespace, name string) bool
}

// NewConfigData ...
func NewConfigData(rclient kubernetes.Interface, cmInformer informers.ConfigMapInformer, filterK8Resources string, log logr.Logger) *ConfigData {
	// environment var is read at start only
	if cmNameEnv == "" {
		log.Info("ConfigMap name not defined in env:INIT_CONFIG: loading no default configuration")
	}
	cd := ConfigData{
		client:   rclient,
		cmName:   os.Getenv(cmNameEnv),
		cmSycned: cmInformer.Informer().HasSynced,
		log:      log,
	}
	//TODO: this has been added to backward support command line arguments
	// will be removed in future and the configuration will be set only via configmaps
	if filterK8Resources != "" {
		cd.log.Info("init configuration from commandline arguments")
		cd.initFilters(filterK8Resources)
	}

	cmInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc:    cd.addCM,
		UpdateFunc: cd.updateCM,
		DeleteFunc: cd.deleteCM,
	})
	return &cd
}

//Run checks syncing
func (cd *ConfigData) Run(stopCh <-chan struct{}) {
	logger := cd.log
	// wait for cache to populate first time
	if !cache.WaitForCacheSync(stopCh, cd.cmSycned) {
		logger.Info("configuration: failed to sync informer cache")
	}
}

func (cd *ConfigData) addCM(obj interface{}) {
	cm := obj.(*v1.ConfigMap)
	if cm.Name != cd.cmName {
		return
	}
	cd.load(*cm)
	// else load the configuration
}

func (cd *ConfigData) updateCM(old, cur interface{}) {
	cm := cur.(*v1.ConfigMap)
	if cm.Name != cd.cmName {
		return
	}
	// if data has not changed then dont load configmap
	cd.load(*cm)
}

func (cd *ConfigData) deleteCM(obj interface{}) {
	logger := cd.log
	cm, ok := obj.(*v1.ConfigMap)
	if !ok {
		tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
		if !ok {
			logger.Info("failed to get object from tombstone")
			return
		}
		_, ok = tombstone.Obj.(*v1.ConfigMap)
		if !ok {
			logger.Info("Tombstone contained object that is not a ConfigMap", "object", obj)
			return
		}
	}

	if cm.Name != cd.cmName {
		return
	}
	// remove the configuration parameters
	cd.unload(*cm)
}

func (cd *ConfigData) load(cm v1.ConfigMap) {
	logger := cd.log.WithValues("name", cm.Name, "namespace", cm.Namespace)
	if cm.Data == nil {
		logger.V(4).Info("configuration: No data defined in ConfigMap")
		return
	}
	// get resource filters
	filters, ok := cm.Data["resourceFilters"]
	if !ok {
		logger.V(4).Info("configuration: No resourceFilters defined in ConfigMap")
		return
	}
	// filters is a string
	if filters == "" {
		logger.V(4).Info("configuration: resourceFilters is empty in ConfigMap")
		return
	}
	// parse and load the configuration
	cd.mux.Lock()
	defer cd.mux.Unlock()

	newFilters := parseKinds(filters)
	if reflect.DeepEqual(newFilters, cd.filters) {
		logger.V(4).Info("resourceFilters did not change")
		return
	}
	logger.V(2).Info("Updated resource filters", "oldFilters", cd.filters, "newFilters", newFilters)
	// update filters
	cd.filters = newFilters
}

//TODO: this has been added to backward support command line arguments
// will be removed in future and the configuration will be set only via configmaps
func (cd *ConfigData) initFilters(filters string) {
	logger := cd.log
	// parse and load the configuration
	cd.mux.Lock()
	defer cd.mux.Unlock()

	newFilters := parseKinds(filters)
	logger.V(2).Info("Init resource filters", "filters", newFilters)
	// update filters
	cd.filters = newFilters
}

func (cd *ConfigData) unload(cm v1.ConfigMap) {
	logger := cd.log
	logger.Info("ConfigMap deleted, removing configuration filters", "name", cm.Name, "namespace", cm.Namespace)
	cd.mux.Lock()
	defer cd.mux.Unlock()
	cd.filters = []k8Resource{}
}

type k8Resource struct {
	Kind      string //TODO: as we currently only support one GVK version, we use the kind only. But if we support multiple GVK, then GV need to be added
	Namespace string
	Name      string
}

//ParseKinds parses the kinds if a single string contains comma separated kinds
// {"1,2,3","4","5"} => {"1","2","3","4","5"}
func parseKinds(list string) []k8Resource {
	resources := []k8Resource{}
	var resource k8Resource
	re := regexp.MustCompile(`\[([^\[\]]*)\]`)
	submatchall := re.FindAllString(list, -1)
	for _, element := range submatchall {
		element = strings.Trim(element, "[")
		element = strings.Trim(element, "]")
		elements := strings.Split(element, ",")
		//TODO: wildcards for namespace and name
		if len(elements) == 0 {
			continue
		}
		if len(elements) == 3 {
			resource = k8Resource{Kind: elements[0], Namespace: elements[1], Name: elements[2]}
		}
		if len(elements) == 2 {
			resource = k8Resource{Kind: elements[0], Namespace: elements[1]}
		}
		if len(elements) == 1 {
			resource = k8Resource{Kind: elements[0]}
		}
		resources = append(resources, resource)
	}
	return resources
}
