package kubernetes

import (
	"encoding/json"
	"errors"
	"net/url"

	"github.com/porter-dev/porter/api/types"
	"github.com/porter-dev/porter/internal/models"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/tools/clientcmd/api"
)

// GetClusterCandidatesFromKubeconfig parses a kubeconfig for a list of cluster
// candidates.
//
// The local boolean represents whether the auth mechanism should be designated as
// "local": if so, the auth mechanism uses local plugins/mechanisms purely from the
// kubeconfig.
func GetClusterCandidatesFromKubeconfig(
	kubeconfig []byte,
	projectID uint,
	local bool,
) ([]*models.ClusterCandidate, error) {
	config, err := clientcmd.NewClientConfigFromBytes(kubeconfig)
	if err != nil {
		return nil, err
	}

	rawConf, err := config.RawConfig()
	if err != nil {
		return nil, err
	}

	res := make([]*models.ClusterCandidate, 0)

	for contextName, context := range rawConf.Contexts {
		clusterName := context.Cluster
		awsClusterID := ""
		authInfoName := context.AuthInfo

		resolvers := make([]models.ClusterResolver, 0)
		var authMechanism models.ClusterAuth

		if local {
			authMechanism = models.Local
		} else {
			// get the resolvers, if needed
			authMechanism, resolvers = parseAuthInfoForResolvers(rawConf.AuthInfos[authInfoName])
			clusterResolvers := parseClusterForResolvers(rawConf.Clusters[clusterName])
			resolvers = append(resolvers, clusterResolvers...)

			if authMechanism == models.AWS {
				// if the auth mechanism is AWS, we need to parse more explicitly
				// for the cluster id
				awsClusterID = parseAuthInfoForAWSClusterID(rawConf.AuthInfos[authInfoName], clusterName)
			}
		}

		// construct the raw kubeconfig that's relevant for that context
		contextConf, err := getConfigForContext(&rawConf, contextName)
		if err != nil {
			continue
		}

		rawBytes, err := clientcmd.Write(*contextConf)

		if err == nil {
			// create the candidate cluster
			res = append(res, &models.ClusterCandidate{
				AuthMechanism:     authMechanism,
				ProjectID:         projectID,
				Resolvers:         resolvers,
				ContextName:       contextName,
				Name:              clusterName,
				Server:            rawConf.Clusters[clusterName].Server,
				AWSClusterIDGuess: []byte(awsClusterID),
				Kubeconfig:        rawBytes,
			})
		}
	}

	return res, nil
}

// GetRawConfigFromBytes returns the clientcmdapi.Config from kubeconfig
// bytes
func GetRawConfigFromBytes(kubeconfig []byte) (*api.Config, error) {
	config, err := clientcmd.NewClientConfigFromBytes(kubeconfig)
	if err != nil {
		return nil, err
	}

	rawConf, err := config.RawConfig()
	if err != nil {
		return nil, err
	}

	return &rawConf, nil
}

// Parsing rules are:
//
// (1) If a client certificate + client key exist, uses x509 auth mechanism
// (2) If an oidc/gcp/aws plugin exists, uses that auth mechanism
// (3) If a bearer token exists, uses bearer token auth mechanism
// (4) If a username/password exist, uses basic auth mechanism
// (5) Otherwise, the config gets skipped
func parseAuthInfoForResolvers(authInfo *api.AuthInfo) (authMechanism models.ClusterAuth, resolvers []models.ClusterResolver) {
	resolvers = make([]models.ClusterResolver, 0)

	if (authInfo.ClientCertificate != "" || len(authInfo.ClientCertificateData) != 0) &&
		(authInfo.ClientKey != "" || len(authInfo.ClientKeyData) != 0) {
		if len(authInfo.ClientCertificateData) == 0 {
			fn := map[string]string{
				"filename": authInfo.ClientCertificate,
			}

			fnBytes, _ := json.Marshal(&fn)

			resolvers = append(resolvers, models.ClusterResolver{
				Name:     types.ClientCertData,
				Resolved: false,
				Data:     fnBytes,
			})
		}

		if len(authInfo.ClientKeyData) == 0 {
			fn := map[string]string{
				"filename": authInfo.ClientKey,
			}

			fnBytes, _ := json.Marshal(&fn)

			resolvers = append(resolvers, models.ClusterResolver{
				Name:     types.ClientKeyData,
				Resolved: false,
				Data:     fnBytes,
			})
		}

		return models.X509, resolvers
	}

	if authInfo.AuthProvider != nil {
		switch authInfo.AuthProvider.Name {
		case "oidc":
			filename, isFile := authInfo.AuthProvider.Config["idp-certificate-authority"]
			data, isData := authInfo.AuthProvider.Config["idp-certificate-authority-data"]

			if isFile && (!isData || data == "") {
				fn := map[string]string{
					"filename": filename,
				}

				fnBytes, _ := json.Marshal(&fn)

				return models.OIDC, []models.ClusterResolver{
					{
						Name:     types.OIDCIssuerData,
						Resolved: false,
						Data:     fnBytes,
					},
				}
			}

			return models.OIDC, resolvers
		case "gcp":
			return models.GCP, []models.ClusterResolver{
				{
					Name:     types.GCPKeyData,
					Resolved: false,
				},
			}
		}
	}

	if authInfo.Exec != nil {
		if authInfo.Exec.Command == "aws" || authInfo.Exec.Command == "aws-iam-authenticator" {
			return models.AWS, []models.ClusterResolver{
				{
					Name:     types.AWSData,
					Resolved: false,
				},
			}
		}
	}

	if authInfo.Token != "" || authInfo.TokenFile != "" {
		if authInfo.Token == "" {
			fn := map[string]string{
				"filename": authInfo.TokenFile,
			}

			fnBytes, _ := json.Marshal(&fn)

			return models.Bearer, []models.ClusterResolver{
				{
					Name:     types.TokenData,
					Resolved: false,
					Data:     fnBytes,
				},
			}
		}

		return models.Bearer, resolvers
	}

	if authInfo.Username != "" && authInfo.Password != "" {
		return models.Basic, resolvers
	}

	return models.X509, resolvers
}

// Parses the cluster object to determine resolvers -- only currently supported resolver is
// population of the cluster certificate authority data
func parseClusterForResolvers(cluster *api.Cluster) (resolvers []models.ClusterResolver) {
	resolvers = make([]models.ClusterResolver, 0)

	if cluster.CertificateAuthority != "" && len(cluster.CertificateAuthorityData) == 0 {
		fn := map[string]string{
			"filename": cluster.CertificateAuthority,
		}

		fnBytes, _ := json.Marshal(&fn)

		resolvers = append(resolvers, models.ClusterResolver{
			Name:     types.ClusterCAData,
			Resolved: false,
			Data:     fnBytes,
		})
	}

	serverURL, err := url.Parse(cluster.Server)

	if err == nil {
		if hostname := serverURL.Hostname(); hostname == "127.0.0.1" || hostname == "localhost" {
			resolvers = append(resolvers, models.ClusterResolver{
				Name:     types.ClusterLocalhost,
				Resolved: false,
			})
		}
	}

	return resolvers
}

func parseAuthInfoForAWSClusterID(authInfo *api.AuthInfo, fallback string) string {
	if authInfo.Exec != nil {
		if authInfo.Exec.Command == "aws" {
			// look for --cluster-name flag
			for i, arg := range authInfo.Exec.Args {
				if arg == "--cluster-name" && len(authInfo.Exec.Args) > i+1 {
					return authInfo.Exec.Args[i+1]
				}
			}
		} else if authInfo.Exec.Command == "aws-iam-authenticator" {
			// look for -i or --cluster-id flag
			for i, arg := range authInfo.Exec.Args {
				if (arg == "-i" || arg == "--cluster-id") && len(authInfo.Exec.Args) > i+1 {
					return authInfo.Exec.Args[i+1]
				}
			}
		}
	}

	return fallback
}

// getConfigForContext returns the raw kubeconfig associated with only a
// single context of the raw config
func getConfigForContext(
	rawConf *api.Config,
	contextName string,
) (*api.Config, error) {
	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 = contextName

	context, ok := rawConf.Contexts[contextName]

	if ok {
		userName := context.AuthInfo
		clusterName := context.Cluster
		authInfo, userFound := rawConf.AuthInfos[userName]
		cluster, clusterFound := rawConf.Clusters[clusterName]

		if userFound && clusterFound {
			copyConf.Clusters[clusterName] = cluster
			copyConf.AuthInfos[userName] = authInfo
			copyConf.Contexts[contextName] = context
		} else {
			return nil, errors.New("linked user and cluster not found")
		}
	} else {
		return nil, errors.New("context not found")
	}

	return copyConf, nil
}

// CreateAllowedContextMap creates a dummy map from context name to context name
func CreateAllowedContextMap(contexts []string) map[string]string {
	aContextMap := make(map[string]string)

	for _, context := range contexts {
		aContextMap[context] = context
	}

	return aContextMap
}
