package gardener

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

	"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2"
	cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
	"github.com/pingcap/errors"
	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/intstr"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	"github.com/tidbcloud/infra-api/pkg/consts"
	"github.com/tidbcloud/infra-api/pkg/k8s"
	"github.com/tidbcloud/infra-api/pkg/pingcap/label"
)

func init() {
	utilruntime.Must(v1alpha2.AddToScheme(scheme.Scheme))
}

const (
	certManagerGroup              = "cert-manager.io"
	webhookCertSecretName         = "webhook-cert-secret" // #nosec G101
	webhookSelfSignedCAIssuerName = "webhook-selfsigned-ca-issuer"
	webhookCAName                 = "webhook-ca"
	webhookCASecretName           = "webhook-ca-secret" // #nosec G101
	webhookIssuerName             = "webhook-issuer"

	gracefulShutdownTiDBWebhookName  = "graceful-shutdown-tidb-webhook"
	gracefulShutdownTiDBWebhookImage = "gcr.io/pingcap-public/dbaas/graceful-shutdown-tidb-webhook:20220427"
)

// only set getCaBundle as false
// Since we do not run controllers, it will failed to call webhook when create certs.
// Skip to create cert in integration test env.
func deployGracefulShutdownWebhook(ctx context.Context, ns string, cli client.Client, getCaBundle bool) (controllerutil.OperationResult, error) {
	var caBundle []byte
	var err error

	if getCaBundle {
		err = createWebhookCert(ctx, ns, cli)
		if err != nil {
			return controllerutil.OperationResultNone, err
		}

		caBundle, err = checkCertAndGetCABundle(ctx, ns, webhookCertSecretName, cli)
		if err != nil {
			return controllerutil.OperationResultNone, err
		}
	}

	return deployWebhookServer(ctx, ns, cli, caBundle)
}

func createWebhookCert(ctx context.Context, ns string, cli client.Client) error {
	err := createWebhookIssuer(ctx, ns, cli)
	if err != nil {
		return err
	}
	_, err = createWebhookServerCert(ctx, ns, cli)
	return err
}

func checkCertAndGetCABundle(ctx context.Context, ns, secretName string, cli client.Client) ([]byte, error) {
	secret := new(corev1.Secret)
	err := cli.Get(ctx, types.NamespacedName{Namespace: ns, Name: secretName}, secret)
	if err != nil {
		return nil, fmt.Errorf("unable to load certificates from secret %s/%s: %v", ns, secretName, err)
	}

	caCert, exist := secret.Data[corev1.ServiceAccountRootCAKey]
	if !exist {
		return nil, fmt.Errorf("ca cert does not exist in secret %s/%s", ns, secretName)
	}

	clientCert, certExists := secret.Data[corev1.TLSCertKey]
	clientKey, keyExists := secret.Data[corev1.TLSPrivateKeyKey]
	if !certExists || !keyExists {
		return caCert, fmt.Errorf("cert or key does not exist in secret %s/%s", ns, secretName)
	}

	_, err = tls.X509KeyPair(clientCert, clientKey)
	if err != nil {
		return caCert, fmt.Errorf("unable to load certificates from secret %s/%s: %v", ns, secretName, err)
	}
	return caCert, nil
}

func deployWebhookServer(ctx context.Context, ns string, cli client.Client, caBundle []byte) (controllerutil.OperationResult, error) {
	var (
		op  controllerutil.OperationResult
		err error
	)
	webhookLabels := map[string]string{
		"app": gracefulShutdownTiDBWebhookName,
	}
	sa := &corev1.ServiceAccount{
		ObjectMeta: metav1.ObjectMeta{
			Name:      gracefulShutdownTiDBWebhookName,
			Namespace: ns,
			Labels:    webhookLabels,
		},
	}
	if _, err := k8s.CreateIgnoreExists(ctx, cli, sa); err != nil {
		return op, err
	}

	cr := &rbacv1.ClusterRole{
		ObjectMeta: metav1.ObjectMeta{
			Name:   gracefulShutdownTiDBWebhookName,
			Labels: webhookLabels,
		},
	}
	_, err = controllerutil.CreateOrUpdate(context.TODO(), cli, cr, func() error {
		cr.Rules = []rbacv1.PolicyRule{
			{
				APIGroups: []string{""},
				Resources: []string{"pods"},
				Verbs:     []string{"get", "update"},
			},
			{
				APIGroups: []string{"pingcap.com"},
				Resources: []string{"tidbclusters"},
				Verbs:     []string{"get"},
			},
		}
		return nil
	})
	if err != nil {
		return op, errors.AddStack(err)
	}

	crb := &rbacv1.ClusterRoleBinding{
		ObjectMeta: metav1.ObjectMeta{
			Name:   gracefulShutdownTiDBWebhookName,
			Labels: webhookLabels,
		},
		Subjects: []rbacv1.Subject{{
			Kind:      rbacv1.ServiceAccountKind,
			Name:      gracefulShutdownTiDBWebhookName,
			Namespace: ns,
		}},
		RoleRef: rbacv1.RoleRef{
			Kind:     "ClusterRole",
			Name:     gracefulShutdownTiDBWebhookName,
			APIGroup: rbacv1.GroupName,
		},
	}
	if _, err := k8s.CreateIgnoreExists(ctx, cli, crb); err != nil {
		return op, err
	}

	deploy := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      gracefulShutdownTiDBWebhookName,
			Namespace: ns,
			Labels:    webhookLabels,
		},
		Spec: appsv1.DeploymentSpec{
			Selector: &metav1.LabelSelector{
				MatchLabels: webhookLabels,
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: webhookLabels,
				},
			},
		},
	}
	_, err = controllerutil.CreateOrUpdate(ctx, cli, deploy, func() error {
		deploy.Spec.Replicas = pointer.Int32Ptr(1)
		deploy.Spec.Template.Spec = corev1.PodSpec{
			ServiceAccountName: gracefulShutdownTiDBWebhookName,
			Containers: []corev1.Container{
				{
					Name:  "server",
					Image: gracefulShutdownTiDBWebhookImage,
					Ports: []corev1.ContainerPort{
						{
							Name:          "webhook-api",
							ContainerPort: int32(8443),
						},
					},
					VolumeMounts: []corev1.VolumeMount{
						{
							Name:      "webhook-tls-certs",
							MountPath: "/run/secrets/tls",
							ReadOnly:  true,
						},
					},
				},
			},
			Volumes: []corev1.Volume{
				{
					Name: "webhook-tls-certs",
					VolumeSource: corev1.VolumeSource{
						Secret: &corev1.SecretVolumeSource{
							SecretName: webhookCertSecretName,
						},
					},
				},
			},
		}
		return nil
	})
	if err != nil {
		return op, errors.AddStack(err)
	}

	svc := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      gracefulShutdownTiDBWebhookName,
			Namespace: ns,
		},
		Spec: corev1.ServiceSpec{
			Ports: []corev1.ServicePort{
				{
					Port:       443,
					TargetPort: intstr.FromString("webhook-api"),
					Protocol:   corev1.ProtocolTCP,
				},
			},
			Selector: webhookLabels,
		},
	}
	_, err = k8s.CreateIgnoreExists(ctx, cli, svc)
	if err != nil {
		return op, err
	}

	podLabelsRequirements := []metav1.LabelSelectorRequirement{
		{
			Key:      label.ManagedByLabelKey,
			Operator: metav1.LabelSelectorOpIn,
			Values:   []string{"tidb-operator"},
		},
		{
			Key:      label.ComponentLabelKey,
			Operator: metav1.LabelSelectorOpIn,
			Values:   []string{"tidb"},
		},
		{
			Key:      label.InstanceLabelKey,
			Operator: metav1.LabelSelectorOpExists,
		},
	}
	sideEffectClassNone := admissionregistrationv1.SideEffectClassNone
	failPolicy := admissionregistrationv1.Ignore
	webhookConfig := &admissionregistrationv1.ValidatingWebhookConfiguration{
		ObjectMeta: metav1.ObjectMeta{
			Name: gracefulShutdownTiDBWebhookName,
		},
		Webhooks: []admissionregistrationv1.ValidatingWebhook{
			{
				Name: fmt.Sprintf("%s.%s.svc", gracefulShutdownTiDBWebhookName, ns),
				ObjectSelector: &metav1.LabelSelector{
					MatchExpressions: podLabelsRequirements,
				},
				AdmissionReviewVersions: []string{"v1", "v1beta1"},
				// timeout should be 1~30s, configure max timeout for best effort graceful shutdown
				TimeoutSeconds: pointer.Int32Ptr(30),
				SideEffects:    &sideEffectClassNone,
				FailurePolicy:  &failPolicy,
				ClientConfig: admissionregistrationv1.WebhookClientConfig{
					Service: &admissionregistrationv1.ServiceReference{
						Name:      gracefulShutdownTiDBWebhookName,
						Namespace: ns,
						Path:      pointer.StringPtr("/validate"),
					},
					CABundle: caBundle,
				},
				Rules: []admissionregistrationv1.RuleWithOperations{
					{
						Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Delete},
						Rule: admissionregistrationv1.Rule{
							APIGroups:   []string{""},
							APIVersions: []string{"v1"},
							Resources:   []string{"pods"},
						},
					},
				},
			},
		},
	}
	return k8s.CreateIgnoreExists(ctx, cli, webhookConfig)
}

func createWebhookIssuer(ctx context.Context, ns string, cli client.Client) error {
	selfSignedCAIssuer := &v1alpha2.Issuer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      webhookSelfSignedCAIssuerName,
			Namespace: ns,
		},
		Spec: v1alpha2.IssuerSpec{
			IssuerConfig: v1alpha2.IssuerConfig{
				SelfSigned: &v1alpha2.SelfSignedIssuer{},
			},
		},
	}

	webhookCA := &v1alpha2.Certificate{
		ObjectMeta: metav1.ObjectMeta{
			Name:      webhookCAName,
			Namespace: ns,
		},
		Spec: v1alpha2.CertificateSpec{
			SecretName: webhookCASecretName,
			CommonName: "Webhook CA",
			IsCA:       true,
			Duration: &metav1.Duration{
				Duration: consts.CaCertificateDuration,
			},
			// NOTE: The RenewBefore does not work for CA,
			// as CA certificate can not be renewed by cert-manager
			// https://github.com/jetstack/cert-manager/issues/2478
			RenewBefore: &metav1.Duration{
				Duration: consts.RenewBefore,
			},
			IssuerRef: cmmeta.ObjectReference{
				Name: selfSignedCAIssuer.GetName(),
				Kind: v1alpha2.IssuerKind,
			},
		},
	}

	webhookIssuer := &v1alpha2.Issuer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      webhookIssuerName,
			Namespace: ns,
		},
		Spec: v1alpha2.IssuerSpec{
			IssuerConfig: v1alpha2.IssuerConfig{
				CA: &v1alpha2.CAIssuer{
					SecretName: webhookCASecretName,
				},
			},
		},
	}

	for _, obj := range []client.Object{selfSignedCAIssuer, webhookCA, webhookIssuer} {
		_, err := k8s.CreateIgnoreExists(ctx, cli, obj)
		if err != nil {
			return errors.AddStack(err)
		}
	}

	return nil
}

func createWebhookServerCert(ctx context.Context, ns string, cli client.Client) (controllerutil.OperationResult, error) {
	certDNSNames := []string{
		fmt.Sprintf("graceful-shutdown-tidb-webhook.%s.svc", ns),
	}

	webhookCertificate := &v1alpha2.Certificate{
		ObjectMeta: metav1.ObjectMeta{
			Name:      webhookCertSecretName,
			Namespace: ns,
		},
		Spec: v1alpha2.CertificateSpec{
			SecretName: webhookCertSecretName,
			Duration: &metav1.Duration{
				Duration: consts.CertificateDuration,
			},
			RenewBefore: &metav1.Duration{
				Duration: consts.RenewBefore,
			},
			Organization: []string{consts.CertificateOrg},
			Usages: []v1alpha2.KeyUsage{
				v1alpha2.UsageServerAuth,
				v1alpha2.UsageClientAuth,
			},
			DNSNames: certDNSNames,
			IPAddresses: []string{
				"127.0.0.1",
				"::1",
			},
			IssuerRef: cmmeta.ObjectReference{
				Name:  webhookIssuerName,
				Kind:  v1alpha2.IssuerKind,
				Group: certManagerGroup,
			},
		},
	}

	return k8s.CreateIgnoreExists(ctx, cli, webhookCertificate)
}
