package tlshelpers

import (
	"context"
	"crypto/x509"
	"fmt"
	"slices"
	"time"

	"k8s.io/client-go/util/cert"

	"github.com/openshift/cluster-etcd-operator/pkg/dnshelpers"
	"github.com/openshift/cluster-etcd-operator/pkg/operator/operatorclient"
	"github.com/openshift/library-go/pkg/operator/certrotation"
	"github.com/openshift/library-go/pkg/operator/events"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apiserver/pkg/authentication/user"
	corev1informers "k8s.io/client-go/informers/core/v1"
	corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
	corev1listers "k8s.io/client-go/listers/core/v1"

	"github.com/openshift/library-go/pkg/crypto"
	"go.etcd.io/etcd/client/pkg/v3/tlsutil"
	"k8s.io/klog/v2"
)

const (
	// Given that clusters can be shutdown/hibernated for a max of ~9 months
	// this validity period leaves enough cushion to not have the
	// certs expire during shutdown/hibernation in the worst case.
	EtcdCertValidity          = 3 * 365 * 24 * time.Hour
	EtcdCertValidityRefresh   = 2.2 * 365 * 24 * time.Hour
	EtcdCaCertValidity        = 5 * 365 * 24 * time.Hour
	EtcdCaCertValidityRefresh = 4.2 * 365 * 24 * time.Hour

	EtcdJiraComponentName                  = "etcd"
	EtcdSignerCertSecretName               = "etcd-signer"
	EtcdSignerCaBundleConfigMapName        = "etcd-ca-bundle"
	EtcdMetricsSignerCertSecretName        = "etcd-metric-signer"
	EtcdMetricsSignerCaBundleConfigMapName = "etcd-metrics-ca-bundle"
	EtcdAllCertsSecretName                 = "etcd-all-certs"
	EtcdAllBundlesConfigMapName            = "etcd-all-bundles"
	EtcdClientCertSecretName               = "etcd-client"
	EtcdMetricsClientCertSecretName        = "etcd-metric-client"
)

type CertValidity struct {
	Validity  time.Duration
	RefreshAt time.Duration
}

func GetPeerClientSecretNameForNode(nodeName string) string {
	return fmt.Sprintf("etcd-peer-%s", nodeName)
}
func GetServingSecretNameForNode(nodeName string) string {
	return fmt.Sprintf("etcd-serving-%s", nodeName)
}
func GetServingMetricsSecretNameForNode(nodeName string) string {
	return fmt.Sprintf("etcd-serving-metrics-%s", nodeName)
}

func getServerHostNames(nodeInternalIPs []string) []string {
	return append([]string{
		"localhost",
		"etcd.kube-system.svc",
		"etcd.kube-system.svc.cluster.local",
		"etcd.openshift-etcd.svc",
		"etcd.openshift-etcd.svc.cluster.local",
		"127.0.0.1",
		"::1",
		// "0:0:0:0:0:0:0:1" will be automatically collapsed to "::1", so we don't have to add it on top
	}, nodeInternalIPs...)
}

func CreateSignerCertRotationBundleConfigMap(
	cmInformer corev1informers.ConfigMapInformer,
	cmLister corev1listers.ConfigMapLister,
	cmGetter corev1client.ConfigMapsGetter,
	recorder events.Recorder) certrotation.CABundleConfigMap {

	return certrotation.CABundleConfigMap{
		Name:      EtcdSignerCaBundleConfigMapName,
		Namespace: operatorclient.TargetNamespace,
		AdditionalAnnotations: certrotation.AdditionalAnnotations{
			JiraComponent: EtcdJiraComponentName,
			Description:   "Generated by cluster-etcd-operator for etcd and is used to authenticate clients and peers of etcd.",
		},
		Informer:      cmInformer,
		Lister:        cmLister,
		Client:        cmGetter,
		EventRecorder: recorder,
	}
}

func CreateMetricsSignerCertRotationBundleConfigMap(
	cmInformer corev1informers.ConfigMapInformer,
	cmLister corev1listers.ConfigMapLister,
	cmGetter corev1client.ConfigMapsGetter,
	recorder events.Recorder) certrotation.CABundleConfigMap {

	return certrotation.CABundleConfigMap{
		Name:      EtcdMetricsSignerCaBundleConfigMapName,
		Namespace: operatorclient.TargetNamespace,
		AdditionalAnnotations: certrotation.AdditionalAnnotations{
			JiraComponent: EtcdJiraComponentName,
			Description:   "Generated by cluster-etcd-operator for etcd and is used to authenticate Prometheus ServiceMonitors reaching etcd.",
		},
		Informer:      cmInformer,
		Lister:        cmLister,
		Client:        cmGetter,
		EventRecorder: recorder,
	}
}

func CreateSignerCert(
	secretInformer corev1informers.SecretInformer,
	secretLister corev1listers.SecretLister,
	secretGetter corev1client.SecretsGetter,
	recorder events.Recorder,
	signerValidity CertValidity) certrotation.RotatedSigningCASecret {

	return certrotation.RotatedSigningCASecret{
		Namespace: operatorclient.TargetNamespace,
		Name:      EtcdSignerCertSecretName,
		AdditionalAnnotations: certrotation.AdditionalAnnotations{
			JiraComponent: EtcdJiraComponentName,
			Description: fmt.Sprintf("Generated by cluster-etcd-operator for etcd and is used to sign peer, server and client certificates. "+
				"This certificate is valid for %d days and starts refreshing after %d days.", durationDays(signerValidity.Validity), durationDays(signerValidity.RefreshAt)),
		},
		Validity: signerValidity.Validity,
		Refresh:  signerValidity.RefreshAt,

		Informer:      secretInformer,
		Lister:        secretLister,
		Client:        secretGetter,
		EventRecorder: recorder,
	}
}

// CreateBootstrapSignerCert is a CreateSignerCert in the openshift-config namespace
func CreateBootstrapSignerCert(
	secretInformer corev1informers.SecretInformer,
	secretLister corev1listers.SecretLister,
	secretGetter corev1client.SecretsGetter,
	recorder events.Recorder,
	certValidity CertValidity) certrotation.RotatedSigningCASecret {
	secret := CreateSignerCert(secretInformer, secretLister, secretGetter, recorder, certValidity)
	secret.Namespace = operatorclient.GlobalUserSpecifiedConfigNamespace
	return secret
}

func CreateMetricsSignerCert(
	secretInformer corev1informers.SecretInformer,
	secretLister corev1listers.SecretLister,
	secretGetter corev1client.SecretsGetter,
	recorder events.Recorder,
	signerValidity CertValidity) certrotation.RotatedSigningCASecret {

	return certrotation.RotatedSigningCASecret{
		Namespace: operatorclient.TargetNamespace,
		Name:      EtcdMetricsSignerCertSecretName,
		AdditionalAnnotations: certrotation.AdditionalAnnotations{
			JiraComponent: EtcdJiraComponentName,
			Description: fmt.Sprintf("Generated by cluster-etcd-operator for etcd and is used to sign peer, server and client certificates for Prometheus ServiceMonitors. "+
				"This certificate is valid for %d days and starts refreshing after %d days.", durationDays(signerValidity.Validity), durationDays(signerValidity.RefreshAt)),
		},
		Validity: signerValidity.Validity,
		Refresh:  signerValidity.RefreshAt,

		Informer:      secretInformer,
		Lister:        secretLister,
		Client:        secretGetter,
		EventRecorder: recorder,
	}
}

// CreateBootstrapMetricsSignerCert is a CreateMetricsSignerCert in the openshift-config namespace
func CreateBootstrapMetricsSignerCert(
	secretInformer corev1informers.SecretInformer,
	secretLister corev1listers.SecretLister,
	secretGetter corev1client.SecretsGetter,
	recorder events.Recorder,
	certValidity CertValidity) certrotation.RotatedSigningCASecret {
	secret := CreateMetricsSignerCert(secretInformer, secretLister, secretGetter, recorder, certValidity)
	secret.Namespace = operatorclient.GlobalUserSpecifiedConfigNamespace
	return secret
}

func CreatePeerCertificate(node *corev1.Node,
	secretInformer corev1informers.SecretInformer,
	secretLister corev1listers.SecretLister,
	secretGetter corev1client.SecretsGetter,
	recorder events.Recorder,
	certValidity CertValidity) (*certrotation.RotatedSelfSignedCertKeySecret, error) {
	return createCertForNode(
		fmt.Sprintf("Peer (client and server) certificate for node %s, generated by cluster-etcd-operator for etcd. "+
			"This certificate is valid for %d days and starts refreshing after %d days.", node.Name, durationDays(certValidity.Validity), durationDays(certValidity.RefreshAt)),
		GetPeerClientSecretNameForNode(node.Name),
		node, secretInformer, secretLister, secretGetter, recorder, certValidity)
}

func CreateServingCertificate(node *corev1.Node,
	secretInformer corev1informers.SecretInformer,
	secretLister corev1listers.SecretLister,
	secretGetter corev1client.SecretsGetter,
	recorder events.Recorder,
	certValidity CertValidity) (*certrotation.RotatedSelfSignedCertKeySecret, error) {
	return createCertForNode(
		fmt.Sprintf("Serving (client and server) certificate for node %s, generated by cluster-etcd-operator for etcd. "+
			"This certificate is valid for %d days and starts refreshing after %d days.", node.Name, durationDays(certValidity.Validity), durationDays(certValidity.RefreshAt)),
		GetServingSecretNameForNode(node.Name),
		node, secretInformer, secretLister, secretGetter, recorder, certValidity)
}

func CreateMetricsServingCertificate(node *corev1.Node,
	secretInformer corev1informers.SecretInformer,
	secretLister corev1listers.SecretLister,
	secretGetter corev1client.SecretsGetter,
	recorder events.Recorder,
	certValidity CertValidity) (*certrotation.RotatedSelfSignedCertKeySecret, error) {
	return createCertForNode(
		fmt.Sprintf("Serving (client and server) certificate for node %s, generated by cluster-etcd-operator for etcd. "+
			"This certificate is valid for %d days and starts refreshing after %d days.", node.Name, durationDays(certValidity.Validity), durationDays(certValidity.RefreshAt)),
		GetServingMetricsSecretNameForNode(node.Name),
		node, secretInformer, secretLister, secretGetter, recorder, certValidity)
}

func createCertForNode(description, secretName string, node *corev1.Node,
	secretInformer corev1informers.SecretInformer,
	secretLister corev1listers.SecretLister,
	secretGetter corev1client.SecretsGetter,
	recorder events.Recorder,
	certValidity CertValidity) (*certrotation.RotatedSelfSignedCertKeySecret, error) {

	ipAddresses, err := dnshelpers.GetInternalIPAddressesForNodeName(node)
	if err != nil {
		return nil, fmt.Errorf("could not retrieve internal IP addresses for node: %w", err)
	}
	hostNames := getServerHostNames(ipAddresses)

	// MakeServerCertForDuration sorts hostnames (via sets.List) before
	// choosing the first as Subject CN. UserInfo.Name must equal that
	// value or cert generation aborts.
	// Sort so hostNames[0] is the lexically first hostname
	slices.Sort(hostNames)
	creator := &CARotatingTargetCertCreator{
		&certrotation.PeerRotation{
			Hostnames: func() []string {
				return hostNames
			},
			UserInfo: &user.DefaultInfo{
				Name: hostNames[0],
			},
		},
	}

	return &certrotation.RotatedSelfSignedCertKeySecret{
		Namespace: operatorclient.TargetNamespace,
		Name:      secretName,
		AdditionalAnnotations: certrotation.AdditionalAnnotations{
			JiraComponent:                    EtcdJiraComponentName,
			Description:                      description,
			AutoRegenerateAfterOfflineExpiry: "",
		},
		Validity:    certValidity.Validity,
		Refresh:     certValidity.RefreshAt,
		CertCreator: creator,

		Informer:      secretInformer,
		Lister:        secretLister,
		Client:        secretGetter,
		EventRecorder: recorder,
	}, nil
}

func CreateMetricsClientCert(
	secretInformer corev1informers.SecretInformer,
	secretLister corev1listers.SecretLister,
	secretGetter corev1client.SecretsGetter,
	recorder events.Recorder,
	certValidity CertValidity) certrotation.RotatedSelfSignedCertKeySecret {
	creator := &CARotatingTargetCertCreator{
		&certrotation.ClientRotation{
			UserInfo: &user.DefaultInfo{
				Name:   "etcd-metric",
				Groups: []string{"system:etcd", "etcd-metric"},
			},
		},
	}

	return certrotation.RotatedSelfSignedCertKeySecret{
		Namespace: operatorclient.TargetNamespace,
		Name:      EtcdMetricsClientCertSecretName,
		AdditionalAnnotations: certrotation.AdditionalAnnotations{
			JiraComponent: EtcdJiraComponentName,
			Description: fmt.Sprintf("Client certificate for Prometheus ServiceMonitors to reach etcd grpc-proxy to retrieve metrics. "+
				"Generated by cluster-etcd-operator for etcd. "+
				"This certificate is valid for %d days and starts refreshing after %d days.", durationDays(certValidity.Validity), durationDays(certValidity.RefreshAt)),
		},
		Validity:    certValidity.Validity,
		Refresh:     certValidity.RefreshAt,
		CertCreator: creator,

		Informer:      secretInformer,
		Lister:        secretLister,
		Client:        secretGetter,
		EventRecorder: recorder,
	}
}

func CreateEtcdClientCert(
	secretInformer corev1informers.SecretInformer,
	secretLister corev1listers.SecretLister,
	secretGetter corev1client.SecretsGetter,
	recorder events.Recorder,
	certValidity CertValidity) certrotation.RotatedSelfSignedCertKeySecret {
	creator := &CARotatingTargetCertCreator{
		&certrotation.ClientRotation{
			UserInfo: &user.DefaultInfo{
				Name:   "etcd-client",
				Groups: []string{"system:etcd", "etcd-client"},
			},
		},
	}

	return certrotation.RotatedSelfSignedCertKeySecret{
		Namespace: operatorclient.TargetNamespace,
		Name:      EtcdClientCertSecretName,
		AdditionalAnnotations: certrotation.AdditionalAnnotations{
			JiraComponent: EtcdJiraComponentName,
			Description: fmt.Sprintf("Client certificate for apiserver, cluster-etcd-operator and etcdctl to reach etcd. "+
				"Generated by cluster-etcd-operator for etcd. "+
				"This certificate is valid for %d days and starts refreshing after %d days.", durationDays(certValidity.Validity), durationDays(certValidity.RefreshAt)),
		},
		Validity:    certValidity.Validity,
		Refresh:     certValidity.RefreshAt,
		CertCreator: creator,

		Informer:      secretInformer,
		Lister:        secretLister,
		Client:        secretGetter,
		EventRecorder: recorder,
	}
}

func ReadSignerCaCert(ctx context.Context, secretClient corev1client.SecretsGetter, name string) (*crypto.CA, error) {
	signingCertKeyPairSecret, err := secretClient.Secrets(operatorclient.TargetNamespace).Get(ctx, name, metav1.GetOptions{})
	if err != nil {
		return nil, fmt.Errorf("error getting %s/%s: %w", operatorclient.TargetNamespace, name, err)
	}

	return crypto.GetCAFromBytes(signingCertKeyPairSecret.Data["tls.crt"], signingCertKeyPairSecret.Data["tls.key"])
}

func ReadSignerCaBundle(ctx context.Context, cmClient corev1client.ConfigMapsGetter, name string) ([]*x509.Certificate, error) {
	bundle, err := cmClient.ConfigMaps(operatorclient.TargetNamespace).Get(ctx, name, metav1.GetOptions{})
	if err != nil {
		return nil, fmt.Errorf("error getting %s/%s: %w", operatorclient.TargetNamespace, name, err)
	}

	return cert.ParseCertsPEM([]byte(bundle.Data["ca-bundle.crt"]))
}

func SupportedEtcdCiphers(cipherSuites []string) []string {
	allowedCiphers := []string{}
	for _, cipher := range cipherSuites {
		_, ok := tlsutil.GetCipherSuite(cipher)
		if !ok {
			// skip and log unsupported ciphers
			klog.Warningf("cipher is not supported for use with etcd, skipping: %q", cipher)
			continue
		}
		allowedCiphers = append(allowedCiphers, cipher)
	}
	return allowedCiphers

}

func durationDays(d time.Duration) int {
	return int(d.Hours() / 24)
}
