package context

import (
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/docker/cli/cli/command"
	"github.com/docker/cli/cli/context"
	"github.com/docker/cli/cli/context/docker"
	"github.com/docker/cli/cli/context/kubernetes"
	"github.com/docker/cli/cli/context/store"
	"github.com/docker/docker/client"
	"github.com/docker/docker/pkg/homedir"
	"github.com/pkg/errors"
)

const (
	keyFrom          = "from"
	keyHost          = "host"
	keyCA            = "ca"
	keyCert          = "cert"
	keyKey           = "key"
	keySkipTLSVerify = "skip-tls-verify"
	keyKubeconfig    = "config-file"
	keyKubecontext   = "context-override"
	keyKubenamespace = "namespace-override"
)

type configKeyDescription struct {
	name        string
	description string
}

var (
	allowedDockerConfigKeys = map[string]struct{}{
		keyFrom:          {},
		keyHost:          {},
		keyCA:            {},
		keyCert:          {},
		keyKey:           {},
		keySkipTLSVerify: {},
	}
	allowedKubernetesConfigKeys = map[string]struct{}{
		keyFrom:          {},
		keyKubeconfig:    {},
		keyKubecontext:   {},
		keyKubenamespace: {},
	}
	dockerConfigKeysDescriptions = []configKeyDescription{
		{
			name:        keyFrom,
			description: "Copy named context's Docker endpoint configuration",
		},
		{
			name:        keyHost,
			description: "Docker endpoint on which to connect",
		},
		{
			name:        keyCA,
			description: "Trust certs signed only by this CA",
		},
		{
			name:        keyCert,
			description: "Path to TLS certificate file",
		},
		{
			name:        keyKey,
			description: "Path to TLS key file",
		},
		{
			name:        keySkipTLSVerify,
			description: "Skip TLS certificate validation",
		},
	}
	kubernetesConfigKeysDescriptions = []configKeyDescription{
		{
			name:        keyFrom,
			description: "Copy named context's Kubernetes endpoint configuration",
		},
		{
			name:        keyKubeconfig,
			description: "Path to a Kubernetes config file",
		},
		{
			name:        keyKubecontext,
			description: "Overrides the context set in the kubernetes config file",
		},
		{
			name:        keyKubenamespace,
			description: "Overrides the namespace set in the kubernetes config file",
		},
	}
)

func parseBool(config map[string]string, name string) (bool, error) {
	strVal, ok := config[name]
	if !ok {
		return false, nil
	}
	res, err := strconv.ParseBool(strVal)
	return res, errors.Wrap(err, name)
}

func validateConfig(config map[string]string, allowedKeys map[string]struct{}) error {
	var errs []string
	for k := range config {
		if _, ok := allowedKeys[k]; !ok {
			errs = append(errs, fmt.Sprintf("%s: unrecognized config key", k))
		}
	}
	if len(errs) == 0 {
		return nil
	}
	return errors.New(strings.Join(errs, "\n"))
}

func getDockerEndpoint(dockerCli command.Cli, config map[string]string) (docker.Endpoint, error) {
	if err := validateConfig(config, allowedDockerConfigKeys); err != nil {
		return docker.Endpoint{}, err
	}
	if contextName, ok := config[keyFrom]; ok {
		metadata, err := dockerCli.ContextStore().GetMetadata(contextName)
		if err != nil {
			return docker.Endpoint{}, err
		}
		if ep, ok := metadata.Endpoints[docker.DockerEndpoint].(docker.EndpointMeta); ok {
			return docker.Endpoint{EndpointMeta: ep}, nil
		}
		return docker.Endpoint{}, errors.Errorf("unable to get endpoint from context %q", contextName)
	}
	tlsData, err := context.TLSDataFromFiles(config[keyCA], config[keyCert], config[keyKey])
	if err != nil {
		return docker.Endpoint{}, err
	}
	skipTLSVerify, err := parseBool(config, keySkipTLSVerify)
	if err != nil {
		return docker.Endpoint{}, err
	}
	ep := docker.Endpoint{
		EndpointMeta: docker.EndpointMeta{
			Host:          config[keyHost],
			SkipTLSVerify: skipTLSVerify,
		},
		TLSData: tlsData,
	}
	// try to resolve a docker client, validating the configuration
	opts, err := ep.ClientOpts()
	if err != nil {
		return docker.Endpoint{}, errors.Wrap(err, "invalid docker endpoint options")
	}
	if _, err := client.NewClientWithOpts(opts...); err != nil {
		return docker.Endpoint{}, errors.Wrap(err, "unable to apply docker endpoint options")
	}
	return ep, nil
}

func getDockerEndpointMetadataAndTLS(dockerCli command.Cli, config map[string]string) (docker.EndpointMeta, *store.EndpointTLSData, error) {
	ep, err := getDockerEndpoint(dockerCli, config)
	if err != nil {
		return docker.EndpointMeta{}, nil, err
	}
	return ep.EndpointMeta, ep.TLSData.ToStoreTLSData(), nil
}

func getKubernetesEndpoint(dockerCli command.Cli, config map[string]string) (*kubernetes.Endpoint, error) {
	if err := validateConfig(config, allowedKubernetesConfigKeys); err != nil {
		return nil, err
	}
	if len(config) == 0 {
		return nil, nil
	}
	if contextName, ok := config[keyFrom]; ok {
		ctxMeta, err := dockerCli.ContextStore().GetMetadata(contextName)
		if err != nil {
			return nil, err
		}
		endpointMeta := kubernetes.EndpointFromContext(ctxMeta)
		if endpointMeta != nil {
			res, err := endpointMeta.WithTLSData(dockerCli.ContextStore(), dockerCli.CurrentContext())
			if err != nil {
				return nil, err
			}
			return &res, nil
		}

		// fallback to env-based kubeconfig
		kubeconfig := os.Getenv("KUBECONFIG")
		if kubeconfig == "" {
			kubeconfig = filepath.Join(homedir.Get(), ".kube/config")
		}
		ep, err := kubernetes.FromKubeConfig(kubeconfig, "", "")
		if err != nil {
			return nil, err
		}
		return &ep, nil
	}
	if config[keyKubeconfig] != "" {
		ep, err := kubernetes.FromKubeConfig(config[keyKubeconfig], config[keyKubecontext], config[keyKubenamespace])
		if err != nil {
			return nil, err
		}
		return &ep, nil
	}
	return nil, nil
}

func getKubernetesEndpointMetadataAndTLS(dockerCli command.Cli, config map[string]string) (*kubernetes.EndpointMeta, *store.EndpointTLSData, error) {
	ep, err := getKubernetesEndpoint(dockerCli, config)
	if err != nil {
		return nil, nil, err
	}
	if ep == nil {
		return nil, nil, err
	}
	return &ep.EndpointMeta, ep.TLSData.ToStoreTLSData(), nil
}
