package cert

import (
	"context"
	"encoding/base64"
	"fmt"
	"hash/fnv"
	"time"

	"github.com/Rican7/retry"
	"github.com/Rican7/retry/strategy"
	"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2"
	cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
	"github.com/pingcap/log"
	"go.uber.org/zap"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/pingcap/mimir/pkg/k8s"
	"github.com/pingcap/mimir/pkg/protos"
)

const (
	defaultOrganization = "PingCAP"
	certManagerGroup    = "cert-manager.io"

	defaultCertificateDuration = 3 * time.Hour    // 3 hours
	defaultRenewBefore         = 15 * time.Minute // 15 minutes
)

func CreateCertIfAbsent(cli client.Client, namespace string, userEmail string, issuer string) error {
	email := fnvHash(userEmail)

	cert := &v1alpha2.Certificate{
		ObjectMeta: metav1.ObjectMeta{
			Name:      email,
			Namespace: namespace,
			Annotations: map[string]string{
				"email": userEmail,
			},
		},

		Spec: v1alpha2.CertificateSpec{
			SecretName: email,
			Duration: &metav1.Duration{
				Duration: defaultCertificateDuration,
			},
			RenewBefore: &metav1.Duration{
				Duration: defaultRenewBefore,
			},
			CommonName:   email,
			Organization: []string{defaultOrganization},
			Usages: []v1alpha2.KeyUsage{
				v1alpha2.UsageClientAuth,
			},
			IssuerRef: cmmeta.ObjectReference{
				Name:  issuer,
				Kind:  v1alpha2.IssuerKind,
				Group: certManagerGroup,
			},
		},
	}

	return k8s.CreateResourceIfAbsent(cli, cert)
}

func FetchCert(cli client.Client, namespace string, server string, userEmail string) (*protos.ClientAuthData, error) {
	certData := &protos.ClientAuthData{
		Server: server,
	}

	email := fnvHash(userEmail)

	return certData, retry.Retry(func(attempt uint) error {
		var secret v1.Secret

		err := cli.Get(context.TODO(), types.NamespacedName{
			Name:      email,
			Namespace: namespace,
		}, &secret)
		if err != nil {
			return err
		}

		ca, err := encodeToString(secret.Data["ca.crt"])
		if err != nil {
			return err
		}

		cert, err := encodeToString(secret.Data["tls.crt"])
		if err != nil {
			return err
		}

		key, err := encodeToString(secret.Data["tls.key"])
		if err != nil {
			return err
		}

		certData.CA = ca
		certData.Client = cert
		certData.ClientKey = key

		return nil
	}, strategy.Limit(10), strategy.Wait(time.Second*3))
}

func encodeToString(data []byte) (string, error) {
	if data == nil {
		return "", nil
	}

	return base64.StdEncoding.EncodeToString(data), nil
}

func fnvHash(raw string) string {
	hash := fnv.New32()
	if _, err := hash.Write([]byte(raw)); err != nil {
		// impossible path
		log.Error("failed to write fnv hash", zap.Error(err))
	}
	return fmt.Sprintf("%x", hash.Sum32())
}
