package context

import (
	"fmt"
	"strconv"
	"strings"

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

const (
	keyFrom          = "from"
	keyHost          = "host"
	keyCA            = "ca"
	keyCert          = "cert"
	keyKey           = "key"
	keySkipTLSVerify = "skip-tls-verify"
)

type configKeyDescription struct {
	name        string
	description string
}

var (
	allowedDockerConfigKeys = map[string]struct{}{
		keyFrom:          {},
		keyHost:          {},
		keyCA:            {},
		keyCert:          {},
		keyKey:           {},
		keySkipTLSVerify: {},
	}
	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",
		},
	}
)

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(contextStore store.Reader, 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 := 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(contextStore store.Reader, config map[string]string) (docker.EndpointMeta, *store.EndpointTLSData, error) {
	ep, err := getDockerEndpoint(contextStore, config)
	if err != nil {
		return docker.EndpointMeta{}, nil, err
	}
	return ep.EndpointMeta, ep.TLSData.ToStoreTLSData(), nil
}
