package collect

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"path/filepath"
	"strings"

	"github.com/pkg/errors"
	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
	"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
	corev1 "k8s.io/api/core/v1"
	kuberneteserrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/klog/v2"
)

type ConfigMapOutput struct {
	Namespace       string            `json:"namespace"`
	Name            string            `json:"name"`
	Key             string            `json:"key"`
	ConfigMapExists bool              `json:"configMapExists"`
	KeyExists       bool              `json:"keyExists"`
	Value           string            `json:"value,omitempty"`
	Data            map[string]string `json:"data,omitonempty"`
}

type CollectConfigMap struct {
	Collector    *troubleshootv1beta2.ConfigMap
	BundlePath   string
	Namespace    string
	ClientConfig *rest.Config
	Client       kubernetes.Interface
	Context      context.Context
	RBACErrors
}

func (c *CollectConfigMap) Title() string {
	return getCollectorName(c)
}

func (c *CollectConfigMap) IsExcluded() (bool, error) {
	return isExcluded(c.Collector.Exclude)
}

func (c *CollectConfigMap) Collect(progressChan chan<- interface{}) (CollectorResult, error) {
	output := NewResult()

	configMaps := []corev1.ConfigMap{}
	namespace := c.Collector.Namespace

	if c.Collector.Name != "" {
		if namespace == "" {
			kubeconfig := k8sutil.GetKubeconfig()
			ns, _, err := kubeconfig.Namespace()
			klog.V(2).Infof("no namespace was set for configmap '%s': using namespace '%s' from current kubeconfig context", c.Collector.Name, ns)
			if err != nil {
				return nil, errors.Wrapf(err, "a namespace was not specified for configmap '%s' and could not be discovered from kubeconfig", c.Collector.Name)
			}
			namespace = ns
		}
		klog.V(1).Infof("looking for configmap '%s' in namespace '%s'", c.Collector.Name, namespace)
		configMap, err := c.Client.CoreV1().ConfigMaps(namespace).Get(c.Context, c.Collector.Name, metav1.GetOptions{})
		if err != nil {
			if kuberneteserrors.IsNotFound(err) {
				filePath, encoded, err := configMapToOutput(c.Collector, nil, c.Collector.Name)
				if err != nil {
					return output, errors.Wrapf(err, "collect secret %s", c.Collector.Name)
				}
				output.SaveResult(c.BundlePath, filePath, bytes.NewBuffer(encoded))
			}
			output.SaveResult(c.BundlePath, GetConfigMapErrorsFileName(c.Collector), marshalErrors([]string{err.Error()}))
			return output, nil
		}
		configMaps = append(configMaps, *configMap)
	} else if len(c.Collector.Selector) > 0 {
		cms, err := listConfigMapsForSelector(c.Context, c.Client, c.Collector.Namespace, c.Collector.Selector)
		if err != nil {
			output.SaveResult(c.BundlePath, GetConfigMapErrorsFileName(c.Collector), marshalErrors([]string{err.Error()}))
			return output, nil
		}
		configMaps = append(configMaps, cms...)
	} else {
		return nil, errors.New("either name or selector must be specified")
	}

	for _, configMap := range configMaps {
		filePath, encoded, err := configMapToOutput(c.Collector, &configMap, configMap.Name)
		if err != nil {
			return output, errors.Wrapf(err, "collect configMap %s", configMap.Name)
		}
		output.SaveResult(c.BundlePath, filePath, bytes.NewBuffer(encoded))
	}

	return output, nil
}

func configMapToOutput(configMapCollector *troubleshootv1beta2.ConfigMap, configMap *corev1.ConfigMap, configMapName string) (string, []byte, error) {
	foundConfigMap := ConfigMapOutput{
		Namespace: configMapCollector.Namespace,
		Name:      configMapName,
		Key:       configMapCollector.Key,
	}

	if configMap != nil {
		foundConfigMap.ConfigMapExists = true
		if configMapCollector.IncludeAllData {
			foundConfigMap.Data = configMap.Data
		}
		if configMapCollector.Key != "" {
			if val, ok := configMap.Data[configMapCollector.Key]; ok {
				foundConfigMap.KeyExists = true
				if configMapCollector.IncludeValue {
					foundConfigMap.Value = string(val)
				}
			}
		}
	}

	return marshalConfigMapOutput(configMapCollector, foundConfigMap)
}

func listConfigMapsForSelector(ctx context.Context, client kubernetes.Interface, namespace string, selector []string) ([]corev1.ConfigMap, error) {
	serializedLabelSelector := strings.Join(selector, ",")

	listOptions := metav1.ListOptions{
		LabelSelector: serializedLabelSelector,
	}

	configMaps, err := client.CoreV1().ConfigMaps(namespace).List(ctx, listOptions)
	if err != nil {
		return nil, err
	}

	return configMaps.Items, nil
}

func marshalConfigMapOutput(configMapCollector *troubleshootv1beta2.ConfigMap, configMap ConfigMapOutput) (string, []byte, error) {
	path := GetConfigMapFileName(configMapCollector, configMap.Name)

	b, err := json.MarshalIndent(configMap, "", "  ")
	if err != nil {
		return path, nil, err
	}

	return path, b, nil
}

func GetConfigMapFileName(configMapCollector *troubleshootv1beta2.ConfigMap, name string) string {
	parts := []string{"configmaps", configMapCollector.Namespace, name}
	if configMapCollector.Key != "" {
		parts = append(parts, configMapCollector.Key)
	}
	return fmt.Sprintf("%s.json", filepath.Join(parts...))
}

func GetConfigMapErrorsFileName(configMapCollector *troubleshootv1beta2.ConfigMap) string {
	parts := []string{"configmaps-errors", configMapCollector.Namespace}
	if configMapCollector.Name != "" {
		parts = append(parts, configMapCollector.Name)
	} else {
		parts = append(parts, selectorToString(configMapCollector.Selector))
	}
	if configMapCollector.Key != "" {
		parts = append(parts, configMapCollector.Key)
	}
	return fmt.Sprintf("%s.json", filepath.Join(parts...))
}
