package util

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

	"github.com/pingcap/log"
	"go.uber.org/atomic"
	"go.uber.org/zap"

	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	types "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
)

type certState struct {
	atime         time.Time
	checkInterval time.Duration
	deadline      time.Duration

	cert tls.Certificate
}

func GetTLSConfig(kubeCli kubernetes.Interface, namespace string, tcName string, secretName string) (*tls.Config, error) {
	secret, err := kubeCli.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, types.GetOptions{})
	if err != nil {
		if apierrors.IsNotFound(err) {
			// dev tier no TLS
			return nil, nil
		} else {
			return nil, fmt.Errorf("unable to load certificates from secret %s/%s: %v", namespace, secretName, err)
		}
	}

	rootCAs, cert, err := loadCertificates(secret)
	if err != nil {
		return nil, err
	}

	sp := atomic.NewPointer(&certState{
		atime:         time.Now(),
		checkInterval: time.Hour,
		deadline:      24 * time.Hour,
		cert:          cert,
	})

	loadCertFunc := func() (cert *tls.Certificate, err error) {
		s := sp.Load()
		if time.Since(s.atime) > s.checkInterval || time.Until(s.cert.Leaf.NotAfter) < s.deadline {
			nextCert := s.cert
			defer func() {
				nextState := *s
				nextState.atime = time.Now()
				nextState.cert = nextCert
				sp.Store(&nextState)
				cert = &nextCert
				err = nil
			}()
			secret, err1 := kubeCli.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, types.GetOptions{})
			log.Info("Reloading TLS certificates from secret",
				zap.String("namespace", namespace),
				zap.String("tcName", tcName),
				zap.String("secretName", secretName),
			)
			if err1 != nil {
				return
			}
			_, newCert, err1 := loadCertificates(secret)
			if err1 != nil {
				return
			}
			nextCert = newCert
		}
		return &s.cert, nil
	}

	// nolint: gosec
	// G402: TLS MinVersion too low. (gosec)
	tlsConfig := &tls.Config{
		RootCAs: rootCAs,
		GetClientCertificate: func(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) {
			return loadCertFunc()
		},
		GetCertificate: func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
			return loadCertFunc()
		},
	}

	return tlsConfig, nil
}

func loadCertificates(secret *corev1.Secret) (*x509.CertPool, tls.Certificate, error) {
	var tlsCert tls.Certificate
	rootCAs := x509.NewCertPool()

	rootCAs.AppendCertsFromPEM(secret.Data[corev1.ServiceAccountRootCAKey])

	clientCert, certExists := secret.Data[corev1.TLSCertKey]
	clientKey, keyExists := secret.Data[corev1.TLSPrivateKeyKey]
	if !certExists || !keyExists {
		return nil, tlsCert, fmt.Errorf("cert or key does not exist in secret %s/%s", secret.Namespace, secret.Name)
	}
	tlsCert, err := tls.X509KeyPair(clientCert, clientKey)
	if err != nil {
		return nil, tlsCert, fmt.Errorf("unable to load certificates from secret %s/%s: %v", secret.Namespace, secret.Name, err)
	}

	return rootCAs, tlsCert, nil
}

func LoadTlsConfigFromSecret(secret *corev1.Secret) (*tls.Config, error) {
	rootCAs, cert, err := loadCertificates(secret)
	if err != nil {
		return nil, err
	}
	// nolint: gosec
	// G402: TLS MinVersion too low. (gosec)
	return &tls.Config{
		RootCAs:      rootCAs,
		Certificates: []tls.Certificate{cert},
	}, nil
}
