package viper

import (
	"context"
	"errors"
	"fmt"
	"log"
	"strings"
	"time"

	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	watch2 "k8s.io/apimachinery/pkg/watch"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
)

var defaultNamespace = "default"

const (
	versionKey = "oneconfig.tidbcloud.com/version"
)

type ConfigMapConfigManager struct {
	Client kubernetes.Interface
}

func (kcm ConfigMapConfigManager) Get(key string) ([]byte, error) {
	ns, name, configKey := parsePath(key)
	cm, err := kcm.Client.CoreV1().ConfigMaps(ns).Get(context.TODO(), name, metav1.GetOptions{})
	if err != nil {
		return nil, err
	}
	log.Printf("Get config: %s/%s, version: %s", cm.Namespace, cm.Name, cm.Annotations[versionKey])
	return extractKeyFromConfigMap(cm, configKey)
}

func (kcm ConfigMapConfigManager) Watch(key string, stop chan bool) <-chan *Response {
	ns, name, configKey := parsePath(key)
	resp := make(chan *Response, 0)
	backoff := NewBackOff(100*time.Millisecond, 10*time.Second, 0.5)
	lastVersion := ""

	go func(cmName string, key string, stop <-chan bool, resp chan *Response) {
		defer close(resp)

		for {
			watch, err := kcm.Client.CoreV1().ConfigMaps(ns).Watch(context.TODO(), metav1.ListOptions{})
			if err != nil {
				resp <- &Response{Error: err}
				log.Printf("oneconfig watch failed, restart watch")
				time.Sleep(backoff.Next())
				continue
			} else {
				backoff.Reset()
			}
		ResultLoop:
			for {
				select {
				case <-stop:
					watch.Stop()
					return
				case event, ok := <-watch.ResultChan():
					if !ok {
						watch.Stop()
						log.Printf("oneconfig watch channel closed, restart watch")
						break ResultLoop
					}
					if event.Type == watch2.Modified || event.Type == watch2.Added {
						cm, ok := event.Object.(*v1.ConfigMap)
						if !ok {
							resp <- &Response{Error: fmt.Errorf("received unexpected object: %v", event.Object)}
						}
						if cm.Name == cmName {
							if data, err := extractKeyFromConfigMap(cm, key); err != nil {
								resp <- &Response{Error: err}
							} else {
								version := cm.Annotations[versionKey]
								if version != lastVersion {
									log.Printf("Config %s/%s changed, new version: %s", cm.Namespace, cm.Name, version)
									lastVersion = version
									resp <- &Response{Value: data}
								}
							}
						}
					}
				}
			}
		}

	}(name, configKey, stop, resp)

	return resp
}

func extractKeyFromConfigMap(configMap *v1.ConfigMap, key string) ([]byte, error) {
	if val, ok := configMap.Data[key]; ok {
		return []byte(val), nil
	}

	if val, ok := configMap.BinaryData[key]; ok {
		return val, nil
	}

	return nil, errors.New("Missing file in config map")
}

func parsePath(path string) (ns string, name string, key string) {
	values := strings.Split(path, "/")
	switch len(values) {
	case 3:
		ns = values[0]
		name = values[1]
		key = values[2]
	case 2:
		ns = defaultNamespace
		name = values[0]
		key = values[1]
	default:
	}
	return
}

func NewConfigMapConfigManager(configPath string) (ConfigManager, error) {
	config, err := GetConfigFromReader(configPath)
	if err != nil {
		return nil, err
	}
	config.Timeout = 0
	// creates the clientset
	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		return nil, err
	}
	return &ConfigMapConfigManager{Client: clientset}, nil
}

func GetConfigFromReader(configPath string) (*rest.Config, error) {
	loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
	// if you want to change the loading rules (which files in which order), you can do so here
	if configPath != "" {
		loadingRules.Precedence = append([]string{configPath}, loadingRules.Precedence...)
	}
	configOverrides := &clientcmd.ConfigOverrides{}
	kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
	ns, _, err := kubeConfig.Namespace()
	if err == nil {
		defaultNamespace = ns
	}
	return kubeConfig.ClientConfig()
}
