package config

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"sort"
	"strings"
)

// TLSOptions specifies several key options that create TLS Configuration
type TLSOptions struct {
	// Cert contains path to PEM encoded public key certificate
	Cert string
	// Key contains Path to private key associated with given certificate.
	Key string
	// Ciphers is a preference list of supported ciphers, empty list means default
	Ciphers []string
	// MinVersion of TLS to be negotiated with the client  ("1.0", "1.1" ...), no value means no minimum.
	MinVersion string
	// MaxVersion of TLS to be negotiated with the client, no value means no maximum.
	MaxVersion string
	// CACerts contains Path to CA certificates
	CACerts string
	// CertOptional controls whether Cert is optional or not
	CertOptional bool
}

var ciphersMap = map[string]uint16{
	// TLS 1.0 - 1.2 cipher suites.
	"TLS_RSA_WITH_RC4_128_SHA":                tls.TLS_RSA_WITH_RC4_128_SHA,
	"TLS_RSA_WITH_3DES_EDE_CBC_SHA":           tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
	"TLS_RSA_WITH_AES_128_CBC_SHA":            tls.TLS_RSA_WITH_AES_128_CBC_SHA,
	"TLS_RSA_WITH_AES_256_CBC_SHA":            tls.TLS_RSA_WITH_AES_256_CBC_SHA,
	"TLS_RSA_WITH_AES_128_CBC_SHA256":         tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
	"TLS_RSA_WITH_AES_128_GCM_SHA256":         tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
	"TLS_RSA_WITH_AES_256_GCM_SHA384":         tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
	"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA":        tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
	"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA":    tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
	"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA":    tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
	"TLS_ECDHE_RSA_WITH_RC4_128_SHA":          tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
	"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA":     tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
	"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA":      tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
	"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA":      tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
	"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
	"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256":   tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
	"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256":   tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
	"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
	"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384":   tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
	"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
	"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305":    tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
	"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305":  tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
	// TLS 1.3 cipher suites
	"TLS_AES_128_GCM_SHA256":       tls.TLS_AES_128_GCM_SHA256,
	"TLS_AES_256_GCM_SHA384":       tls.TLS_AES_256_GCM_SHA384,
	"TLS_CHACHA20_POLY1305_SHA256": tls.TLS_CHACHA20_POLY1305_SHA256,
}

var versionsMap = map[string]uint16{
	"1.0": tls.VersionTLS10,
	"1.1": tls.VersionTLS11,
	"1.2": tls.VersionTLS12,
	"1.3": tls.VersionTLS13,
}

// CreateTLSConfig creates TLS configuration out of specific TLS
func CreateTLSConfig(o TLSOptions) (out *tls.Config, err error) {
	// load key pair
	if o.Cert == "" && !o.CertOptional {
		return nil, errors.New("no TLS certificate specified")
	}
	key := o.Key
	if key == "" {
		key = o.Cert // If no key is specified, we assume it is in the cert
	}
	out = new(tls.Config)
	if o.Cert != "" {
		cert, err := tls.LoadX509KeyPair(o.Cert, key)
		if err != nil {
			return nil, err
		}
		out.Certificates = []tls.Certificate{cert}
	}

	// CA certs
	if o.CACerts != "" {
		f, err := os.Open(o.CACerts)
		if err != nil {
			return nil, err
		}
		defer f.Close()
		certPool := x509.NewCertPool()
		certs, err := ioutil.ReadAll(f)
		if err != nil {
			return nil, fmt.Errorf("error reading CA certificates: %s", err.Error())
		}

		ok := certPool.AppendCertsFromPEM(certs)
		if !ok {
			return nil, fmt.Errorf("error appending CA certificates from %s", o.CACerts)
		}
		out.RootCAs = certPool
	}

	// ciphers
	if len(o.Ciphers) > 0 {
		for _, name := range o.Ciphers {
			cipherName := strings.ToUpper(strings.Trim(name, " "))
			if len(cipherName) == 0 {
				continue
			}
			cipher, ok := ciphersMap[cipherName]
			if !ok {
				if cipherName == "HELP" {
					return nil, fmt.Errorf("available ciphers are: %s",
						availableKeys(ciphersMap))
				}
				return nil, fmt.Errorf("unknown cipher suite: %q. available ciphers: %s",
					cipherName, availableKeys(ciphersMap))
			}
			out.CipherSuites = append(out.CipherSuites, cipher)
		}
		out.PreferServerCipherSuites = true
	}

	if o.MinVersion != "" {
		version, ok := versionsMap[o.MinVersion]
		if !ok {
			return nil, fmt.Errorf("unknown minimum TLS version: %q. available versions: %s",
				o.MinVersion, availableKeys(versionsMap))

		}
		out.MinVersion = version
	}

	if o.MaxVersion != "" {
		version, ok := versionsMap[o.MaxVersion]
		if !ok {
			return nil, fmt.Errorf("unknown maximum TLS version: %q. available versions: %s",
				o.MaxVersion, availableKeys(versionsMap))
		}
		out.MaxVersion = version
	}

	return out, nil
}

func availableKeys(keySource map[string]uint16) string {
	available := make([]string, 0, len(keySource))
	for name := range keySource {
		available = append(available, name)
	}
	sort.Strings(available)
	return strings.Join(available, ", ")
}
