package etcdenvvar

import (
	"crypto/tls"
	"fmt"
	"net"
	"runtime"
	"sort"
	"strconv"
	"strings"

	v1 "github.com/openshift/api/config/v1"
	operatorv1 "github.com/openshift/api/operator/v1"
	configv1listers "github.com/openshift/client-go/config/listers/config/v1"
	operatorv1listers "github.com/openshift/client-go/operator/listers/operator/v1"
	"github.com/openshift/cluster-etcd-operator/pkg/backendquotahelpers"
	"github.com/openshift/cluster-etcd-operator/pkg/dnshelpers"
	"github.com/openshift/cluster-etcd-operator/pkg/hwspeedhelpers"
	"github.com/openshift/cluster-etcd-operator/pkg/operator/operatorclient"
	"github.com/openshift/cluster-etcd-operator/pkg/tlshelpers"
	"github.com/openshift/library-go/pkg/crypto"
	"github.com/openshift/library-go/pkg/operator/configobserver/featuregates"
	"go.etcd.io/etcd/client/pkg/v3/tlsutil"

	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/labels"
	corev1listers "k8s.io/client-go/listers/core/v1"
	"k8s.io/klog/v2"

	"github.com/ghodss/yaml"
)

type envVarContext struct {
	spec   operatorv1.StaticPodOperatorSpec
	status operatorv1.StaticPodOperatorStatus

	masterNodeLister        corev1listers.NodeLister
	masterNodeLabelSelector labels.Selector
	infrastructureLister    configv1listers.InfrastructureLister
	networkLister           configv1listers.NetworkLister
	configmapLister         corev1listers.ConfigMapLister
	targetImagePullSpec     string
	etcdLister              operatorv1listers.EtcdLister
	featureGateAccessor     featuregates.FeatureGateAccess
}

var FixedEtcdEnvVars = map[string]string{
	"ETCD_DATA_DIR":                       "/var/lib/etcd",
	"ETCD_INITIAL_CLUSTER_STATE":          "existing",
	"ETCD_ENABLE_PPROF":                   "true",
	"ETCD_WATCH_PROGRESS_NOTIFY_INTERVAL": "5s",
	"ETCD_SOCKET_REUSE_ADDRESS":           "true",
	"ETCD_WARNING_APPLY_DURATION":         "200ms",
}

const (
	etcdEndpointName  = "etcd-endpoints"
	clusterConfigName = "cluster-config-v1"
	clusterConfigKey  = "install-config"
)

type replicaCountDecoder struct {
	ControlPlane struct {
		Replicas string `yaml:"replicas,omitempty"`
	} `yaml:"controlPlane,omitempty"`
}

type envVarFunc func(envVarContext envVarContext) (map[string]string, error)

var envVarFns = []envVarFunc{
	getEscapedIPAddress,
	getEtcdURLHost,
	getFixedEtcdEnvVars,
	getEtcdName,
	getAllEtcdEndpoints,
	getEtcdctlEnvVars,
	getHardwareSpeedValues,
	getEtcdDBSize,
	getUnsupportedArch,
	getCipherSuites,
	getTLSMinVersion,
	getMaxLearners,
}

// getEtcdEnvVars returns the env vars that need to be set on the etcd static pods that will be rendered.
//
//	ALL_ETCD_ENDPOINTS - this is used to drive the ETCD_INITIAL_CLUSTER
//	ETCD_DATA_DIR
//	ETCD_QUOTA_BACKEND_BYTES
//	ETCD_HEARTBEAT_INTERVAL
//	ETCD_ELECTION_TIMEOUT
//	ETCD_INITIAL_CLUSTER_STATE
//	ETCD_UNSUPPORTED_ARCH
//	ETCD_CIPHER_SUITES
//	ETCD_MAX_LEARNERS
//	NODE_%s_IP
//	NODE_%s_ETCD_URL_HOST
//	NODE_%s_ETCD_NAME
func getEtcdEnvVars(envVarContext envVarContext) (map[string]string, error) {
	ret := map[string]string{}

	for _, envVarFn := range envVarFns {
		newEnvVars, err := envVarFn(envVarContext)
		if err != nil {
			return nil, err
		}
		if newEnvVars == nil {
			continue
		}
		for k, v := range newEnvVars {
			if currV, ok := ret[k]; ok {
				return nil, fmt.Errorf("key %q already set to %q", k, currV)
			}
			ret[k] = v
		}
	}

	return ret, nil
}

func getFixedEtcdEnvVars(_ envVarContext) (map[string]string, error) {
	return FixedEtcdEnvVars, nil
}

func getEtcdctlEnvVars(envVarContext envVarContext) (map[string]string, error) {
	endpoints, err := getEtcdEndpoints(envVarContext.configmapLister, true)
	if err != nil {
		return nil, err
	}
	return map[string]string{
		"ETCDCTL_CACERT":    "/etc/kubernetes/static-pod-certs/configmaps/etcd-all-bundles/server-ca-bundle.crt",
		"ETCDCTL_CERT":      "/etc/kubernetes/static-pod-certs/secrets/etcd-all-certs/etcd-peer-NODE_NAME.crt",
		"ETCDCTL_KEY":       "/etc/kubernetes/static-pod-certs/secrets/etcd-all-certs/etcd-peer-NODE_NAME.key",
		"ETCDCTL_ENDPOINTS": endpoints,
		"ETCD_IMAGE":        envVarContext.targetImagePullSpec,
	}, nil
}

func getAllEtcdEndpoints(envVarContext envVarContext) (map[string]string, error) {
	endpoints, err := getEtcdEndpoints(envVarContext.configmapLister, false)
	if err != nil {
		return nil, err
	}
	return map[string]string{
		"ALL_ETCD_ENDPOINTS": endpoints,
	}, nil
}

func getEtcdName(envVarContext envVarContext) (map[string]string, error) {
	ret := map[string]string{}

	if envVarContext.status.NodeStatuses == nil || len(envVarContext.status.NodeStatuses) == 0 {
		return nil, fmt.Errorf("empty NodeStatuses, can't generate environment for getEtcdName")
	}

	for _, nodeInfo := range envVarContext.status.NodeStatuses {
		ret[fmt.Sprintf("NODE_%s_ETCD_NAME", envVarSafe(nodeInfo.NodeName))] = nodeInfo.NodeName
	}

	return ret, nil
}

func getEscapedIPAddress(envVarContext envVarContext) (map[string]string, error) {
	network, err := envVarContext.networkLister.Get("cluster")
	if err != nil {
		return nil, err
	}

	if envVarContext.status.NodeStatuses == nil || len(envVarContext.status.NodeStatuses) == 0 {
		return nil, fmt.Errorf("empty NodeStatuses, can't generate environment for getEscapedIPAddress")
	}

	ret := map[string]string{}
	for _, nodeInfo := range envVarContext.status.NodeStatuses {
		node, err := envVarContext.masterNodeLister.Get(nodeInfo.NodeName)
		if err != nil {
			return nil, err
		}

		escapedIPAddress, err := dnshelpers.GetEscapedPreferredInternalIPAddressForNodeName(network, node)
		if err != nil {
			return nil, err
		}
		ret[fmt.Sprintf("NODE_%s_IP", envVarSafe(nodeInfo.NodeName))] = escapedIPAddress
	}

	return ret, nil
}

func getEtcdURLHost(envVarContext envVarContext) (map[string]string, error) {
	ret := map[string]string{}

	network, err := envVarContext.networkLister.Get("cluster")
	if err != nil {
		return nil, err
	}

	if envVarContext.status.NodeStatuses == nil || len(envVarContext.status.NodeStatuses) == 0 {
		return nil, fmt.Errorf("empty NodeStatuses, can't generate environment for getEtcdURLHost")
	}

	for _, nodeInfo := range envVarContext.status.NodeStatuses {
		node, err := envVarContext.masterNodeLister.Get(nodeInfo.NodeName)
		if err != nil {
			return nil, err
		}
		etcdURLHost, err := dnshelpers.GetEscapedPreferredInternalIPAddressForNodeName(network, node)
		if err != nil {
			return nil, err
		}

		ret[fmt.Sprintf("NODE_%s_ETCD_URL_HOST", envVarSafe(nodeInfo.NodeName))] = etcdURLHost
	}

	return ret, nil
}

func getHardwareSpeedValues(envVarContext envVarContext) (map[string]string, error) {
	etcd, err := envVarContext.etcdLister.Get("cluster")
	if err != nil {
		return nil, err
	}

	speed := etcd.Spec.HardwareSpeed

	// If the speed from the api is empty, then the CEO gets to decide what to set the values to.
	//	Allows for upgrades from before this api field was added.
	if speed == "" {
		envs := hwspeedhelpers.StandardHardwareSpeed()
		infrastructure, err := envVarContext.infrastructureLister.Get("cluster")
		if err != nil {
			return nil, err
		}
		if status := infrastructure.Status.PlatformStatus; status != nil {
			switch {
			case status.Azure != nil:
				envs = hwspeedhelpers.SlowerHardwareSpeed()
			case status.IBMCloud != nil:
				if infrastructure.Status.PlatformStatus.IBMCloud.ProviderType == v1.IBMCloudProviderTypeVPC {
					envs = hwspeedhelpers.SlowerHardwareSpeed()
				}
			}
		}
		return envs, err
	}

	return hwspeedhelpers.HardwareSpeedToEnvMap(speed)
}

func getEtcdDBSize(envVarContext envVarContext) (map[string]string, error) {
	etcd, err := envVarContext.etcdLister.Get("cluster")
	if err != nil {
		return nil, fmt.Errorf("failed to retrieve etcd CR: %v", err)
	}

	backendQuotaGiB := int64(8)
	enabled, err := backendquotahelpers.IsBackendQuotaFeatureGateEnabled(envVarContext.featureGateAccessor)
	if err != nil {
		return nil, fmt.Errorf("failed check backend quota enabled: %w", err)
	}
	if enabled && etcd.Spec.BackendQuotaGiB != 0 {
		backendQuotaGiB = int64(etcd.Spec.BackendQuotaGiB)
	}

	return map[string]string{
		"ETCD_QUOTA_BACKEND_BYTES": GibibytesToBytesString(backendQuotaGiB),
	}, nil
}

func envVarSafe(nodeName string) string {
	return strings.ReplaceAll(strings.ReplaceAll(nodeName, "-", "_"), ".", "_")
}

func getUnsupportedArch(_ envVarContext) (map[string]string, error) {
	arch := runtime.GOARCH
	switch arch {
	case "arm64":
	case "s390x":
	default:
		// don't set unless it is defined.
		return nil, nil
	}
	return map[string]string{
		"ETCD_UNSUPPORTED_ARCH": arch,
	}, nil
}

func getObservedTLSMinVersion(envVarContext envVarContext) (tlsutil.TLSVersion, error) {
	var observedConfig map[string]any
	if err := yaml.Unmarshal(envVarContext.spec.ObservedConfig.Raw, &observedConfig); err != nil {
		return "", fmt.Errorf("failed to unmarshal the observedConfig: %w", err)
	}
	observedMinTLSVersion, _, err := unstructured.NestedString(observedConfig, "servingInfo", "minTLSVersion")
	if err != nil {
		return "", fmt.Errorf("couldn't get minTLSVersion from observedConfig: %w", err)
	}

	// map tls version to string recognized by etcd
	v, err := crypto.TLSVersion(observedMinTLSVersion)
	if err != nil {
		return "", fmt.Errorf("couldn't get minTLSVersion from observedConfig: %w", err)
	}
	switch v {
	case tls.VersionTLS13:
		return tlsutil.TLSVersion13, nil
	default:
		return tlsutil.TLSVersion12, nil
	}
}

func getTLSMinVersion(envVarContext envVarContext) (map[string]string, error) {
	observedMinTLSVersion, err := getObservedTLSMinVersion(envVarContext)
	if err != nil {
		return nil, fmt.Errorf("unable to compute ETCD_TLS_MIN_VERSION: %v", err)
	}
	return map[string]string{
		"ETCD_TLS_MIN_VERSION": string(observedMinTLSVersion),
	}, nil
}

func getCipherSuites(envVarContext envVarContext) (map[string]string, error) {
	var observedConfig map[string]any
	if err := yaml.Unmarshal(envVarContext.spec.ObservedConfig.Raw, &observedConfig); err != nil {
		return nil, fmt.Errorf("failed to unmarshal the observedConfig: %w", err)
	}
	observedCipherSuites, _, err := unstructured.NestedStringSlice(observedConfig, "servingInfo", "cipherSuites")
	if err != nil {
		return nil, fmt.Errorf("couldn't get cipherSuites from observedConfig: %w", err)
	}

	actualCipherSuites := tlshelpers.SupportedEtcdCiphers(observedCipherSuites)

	if len(actualCipherSuites) == 0 {
		return nil, fmt.Errorf("no supported cipherSuites not found in observedConfig")
	}

	observedMinTLSVersion, err := getObservedTLSMinVersion(envVarContext)
	if err != nil {
		return nil, fmt.Errorf("unable to compute ETCD_CIPHER_SUITES: %v", err)
	}

	envName := "ETCD_CIPHER_SUITES"
	if observedMinTLSVersion == tlsutil.TLSVersion13 {
		// When --tls-min-version is set to 'TLS1.3', etcd does not allow --cipher-suites to also be specified.
		// We still outout an env var for informational purposes.
		envName = "CIPHER_SUITES"
	}

	return map[string]string{
		envName: strings.Join(actualCipherSuites, ","),
	}, nil
}

// getMaxLearners reads the control-plane replicas from the install-config. We tolerate max learners equal to the
// desired control-plane replicas so that the admin can surge up N x 2 in case of vertical scaling/replacement.
func getMaxLearners(envVarContext envVarContext) (map[string]string, error) {
	clusterConfig, err := envVarContext.configmapLister.ConfigMaps(operatorclient.TargetNamespace).Get(clusterConfigName)
	if err != nil {
		errMsg := fmt.Errorf("failed to get configmap %s/%s :%v", operatorclient.TargetNamespace, clusterConfigName, err)
		klog.Error(errMsg)
		return nil, errMsg
	}

	d := replicaCountDecoder{}
	if err := yaml.Unmarshal([]byte(clusterConfig.Data[clusterConfigKey]), &d); err != nil {
		errMsg := fmt.Errorf("%s key doesn't exist in configmap %s/%s :%w", clusterConfigKey, operatorclient.TargetNamespace, clusterConfigName, err)
		klog.Error(errMsg)
		return nil, errMsg
	}

	replicaCount, err := strconv.Atoi(d.ControlPlane.Replicas)
	if err != nil {
		errMsg := fmt.Errorf("failed to convert replica %s: %w", d.ControlPlane.Replicas, err)
		klog.Error(errMsg)
		return nil, errMsg
	}

	return map[string]string{
		"ETCD_MAX_LEARNERS": fmt.Sprint(replicaCount),
	}, nil
}

func getEtcdEndpoints(configmapLister corev1listers.ConfigMapLister, skipBootstrap bool) (string, error) {
	etcdEndpoints, err := configmapLister.ConfigMaps(operatorclient.TargetNamespace).Get(etcdEndpointName)
	if err != nil {
		return "", err
	}
	var etcdURLs []string
	bootstrapAddress := etcdEndpoints.Annotations["alpha.installer.openshift.io/etcd-bootstrap"]
	// In some cases we want to exclude the ephemeral bootstrap-etcd member from the endpoint list.
	if !skipBootstrap && len(bootstrapAddress) > 0 {
		if ip := net.ParseIP(bootstrapAddress); ip == nil {
			return "", fmt.Errorf("configmaps/%s contains invalid bootstrap ip address: %s: %v", etcdEndpointName, bootstrapAddress, err)
		}
		etcdURLs = append(etcdURLs, fmt.Sprintf("https://%s", net.JoinHostPort(bootstrapAddress, "2379")))
	}
	for k := range etcdEndpoints.Data {
		address := etcdEndpoints.Data[k]
		if ip := net.ParseIP(address); ip == nil {
			return "", fmt.Errorf("configmaps/%s contains invalid ip address: %s: %v", etcdEndpointName, address, err)
		}
		etcdURLs = append(etcdURLs, fmt.Sprintf("https://%s", net.JoinHostPort(address, "2379")))
	}
	sort.Strings(etcdURLs)

	return strings.Join(etcdURLs, ","), nil
}

func GibibytesToBytesString(dbSize int64) string {
	etcdDBSize := dbSize * 1024 * 1024 * 1024
	return strconv.FormatInt(etcdDBSize, 10)
}
