package local

import (
	"encoding/base64"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"

	"github.com/porter-dev/porter/internal/kubernetes"

	k8s "k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/tools/clientcmd/api"
	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
	"k8s.io/client-go/util/homedir"
)

// GetKubeconfigFromHost returns the kubeconfig for a list of contexts using default
// options set on the host, or an explicit kubeconfig path. It then strips the kubeconfig
// of contexts not specified in the contexts array, and returns generate kubeconfig.
func GetKubeconfigFromHost(kubeconfigPath string, contexts []string) ([]byte, error) {
	kubeconfigPath, err := ResolveKubeconfigPath(kubeconfigPath)
	if err != nil {
		return nil, err
	}

	loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
	loadingRules.ExplicitPath = kubeconfigPath

	clientConf := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
	rawConf, err := clientConf.RawConfig()
	if err != nil {
		return nil, err
	}

	if len(contexts) == 0 {
		contexts = []string{rawConf.CurrentContext}

		if contexts[0] == "" {
			return nil, fmt.Errorf("at least one context must be specified")
		}
	}

	conf, err := stripAndValidateClientContexts(&rawConf, contexts[0], contexts)
	if err != nil {
		return nil, err
	}

	strippedRawConf, err := conf.RawConfig()
	if err != nil {
		return nil, err
	}

	return clientcmd.Write(strippedRawConf)
}

// GetSelfAgentFromFileConfig reads a kubeconfig from a local file and generates an
// Agent from that kubeconfig
func GetSelfAgentFromFileConfig(kubeconfigPath string) (*kubernetes.Agent, error) {
	configBytes, err := GetKubeconfigFromHost(kubeconfigPath, []string{})
	if err != nil {
		return nil, err
	}

	cmdConf, err := clientcmd.NewClientConfigFromBytes(configBytes)
	if err != nil {
		return nil, err
	}

	restConf, err := cmdConf.ClientConfig()
	if err != nil {
		return nil, err
	}

	var namespace string
	cmdConfNamespace, _, err := cmdConf.Namespace()

	if err == nil && cmdConfNamespace != "" {
		namespace = cmdConfNamespace
	}

	restClientGetter := kubernetes.NewRESTClientGetterFromInClusterConfig(restConf, namespace)
	clientset, err := k8s.NewForConfig(restConf)

	return &kubernetes.Agent{
		RESTClientGetter: restClientGetter,
		Clientset:        clientset,
	}, nil
}

// ResolveKubeconfigPath finds the path to a kubeconfig, first searching for the
// passed string, then in the home directory, then as an env variable.
func ResolveKubeconfigPath(kubeconfigPath string) (string, error) {
	envVarName := clientcmd.RecommendedConfigPathEnvVar

	if kubeconfigPath != "" {
		if _, err := os.Stat(kubeconfigPath); os.IsNotExist(err) {
			// the specified kubeconfig does not exist, throw error
			return "", fmt.Errorf("kubeconfig not found: %s does not exist", kubeconfigPath)
		}
	}

	if kubeconfigPath == "" {
		if os.Getenv(envVarName) == "" {
			if home := homedir.HomeDir(); home != "" {
				kubeconfigPath = filepath.Join(home, ".kube", "config")
			}
		} else {
			kubeconfigPath = os.Getenv(envVarName)
		}
	}

	return kubeconfigPath, nil
}

// GetConfigFromHostWithCertData gets the kubeconfig using default options set on the host:
// the kubeconfig can either be retrieved from a specified path or an environment variable.
// This function only outputs a clientcmd that uses the allowedContexts.
//
// This function also populates all of the certificate data that's specified as a filepath.
func GetConfigFromHostWithCertData(kubeconfigPath string, allowedContexts []string) (clientcmd.ClientConfig, error) {
	envVarName := clientcmd.RecommendedConfigPathEnvVar

	if kubeconfigPath != "" {
		if _, err := os.Stat(kubeconfigPath); os.IsNotExist(err) {
			// the specified kubeconfig does not exist so fallback to other options
			kubeconfigPath = ""
		}
	}

	if kubeconfigPath == "" && os.Getenv(envVarName) == "" {
		if home := homedir.HomeDir(); home != "" {
			kubeconfigPath = filepath.Join(home, ".kube", "config")
		}
	}

	loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
	loadingRules.ExplicitPath = kubeconfigPath

	clientConf := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
	rawConf, err := clientConf.RawConfig()
	if err != nil {
		return nil, err
	}

	populateCertificateRefs(&rawConf)
	populateOIDCPluginCerts(&rawConf)

	if len(allowedContexts) == 0 {
		allowedContexts = []string{rawConf.CurrentContext}

		if allowedContexts[0] == "" {
			return nil, fmt.Errorf("at least one context must be specified")
		}
	}

	res, err := stripAndValidateClientContexts(&rawConf, allowedContexts[0], allowedContexts)
	if err != nil {
		return nil, err
	}

	return res, nil
}

func stripAndValidateClientContexts(
	rawConf *clientcmdapi.Config,
	currentContext string,
	allowedContexts []string,
) (clientcmd.ClientConfig, error) {
	// grab a copy to get the pointer and set clusters, authinfos, and contexts to empty
	copyConf := rawConf.DeepCopy()

	copyConf.Clusters = make(map[string]*api.Cluster)
	copyConf.AuthInfos = make(map[string]*api.AuthInfo)
	copyConf.Contexts = make(map[string]*api.Context)
	copyConf.CurrentContext = currentContext

	// put allowed clusters in a map
	aContextMap := kubernetes.CreateAllowedContextMap(allowedContexts)

	for contextName, context := range rawConf.Contexts {
		userName := context.AuthInfo
		clusterName := context.Cluster
		authInfo, userFound := rawConf.AuthInfos[userName]
		cluster, clusterFound := rawConf.Clusters[clusterName]

		// make sure the cluster is "allowed"
		_, isAllowed := aContextMap[contextName]

		if userFound && clusterFound && isAllowed {
			copyConf.Clusters[clusterName] = cluster
			copyConf.AuthInfos[userName] = authInfo
			copyConf.Contexts[contextName] = context
		}
	}

	// validate the copyConf and create a ClientConfig
	err := clientcmd.Validate(*copyConf)
	if err != nil {
		return nil, err
	}

	clientConf := clientcmd.NewDefaultClientConfig(*copyConf, &clientcmd.ConfigOverrides{})

	return clientConf, nil
}

func populateCertificateRefs(config *clientcmdapi.Config) {
	for _, cluster := range config.Clusters {
		refs := clientcmd.GetClusterFileReferences(cluster)
		for _, str := range refs {
			// only write certificate if the file reference is CA
			if *str != cluster.CertificateAuthority {
				break
			}

			fileBytes, err := ioutil.ReadFile(*str)
			if err != nil {
				continue
			}

			cluster.CertificateAuthorityData = fileBytes
			cluster.CertificateAuthority = ""
		}
	}

	for _, authInfo := range config.AuthInfos {
		refs := clientcmd.GetAuthInfoFileReferences(authInfo)
		for _, str := range refs {
			if *str == "" {
				continue
			}

			var refType int

			if authInfo.ClientCertificate == *str {
				refType = 0
			} else if authInfo.ClientKey == *str {
				refType = 1
			} else if authInfo.TokenFile == *str {
				refType = 2
			}

			fileBytes, err := ioutil.ReadFile(*str)
			if err != nil {
				continue
			}

			if refType == 0 {
				authInfo.ClientCertificateData = fileBytes
				authInfo.ClientCertificate = ""
			} else if refType == 1 {
				authInfo.ClientKeyData = fileBytes
				authInfo.ClientKey = ""
			} else if refType == 2 {
				authInfo.Token = base64.StdEncoding.EncodeToString(fileBytes)
				authInfo.TokenFile = ""
			}
		}
	}
}

func populateOIDCPluginCerts(config *clientcmdapi.Config) {
	for _, authInfo := range config.AuthInfos {
		if authInfo.AuthProvider != nil && authInfo.AuthProvider.Name == "oidc" {
			if ca, ok := authInfo.AuthProvider.Config["idp-certificate-authority"]; ok && ca != "" {
				fileBytes, err := ioutil.ReadFile(ca)
				if err != nil {
					continue
				}

				authInfo.AuthProvider.Config["idp-certificate-authority-data"] = base64.StdEncoding.EncodeToString(fileBytes)
				delete(authInfo.AuthProvider.Config, "idp-certificate-authority")
			}
		}
	}
}
