package gardener

import (
	"context"
	"fmt"

	"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2"
	cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
	"github.com/pingcap/errors"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/intstr"
	"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/config"
	"github.com/tidbcloud/infra-api/pkg/consts"
	"github.com/tidbcloud/infra-api/pkg/k8s"
)

func deployLocalProxy(
	ctx context.Context,
	cli client.Client,
	image string,
	region string,
	providerType string,
	loadBalancerSourceRanges []string,
	auth0Audience string,
	auth0Domain string,
) (controllerutil.OperationResult, error) {
	var (
		op  controllerutil.OperationResult
		err error
	)
	labels := map[string]string{
		"app": consts.LocalClusterProxyName,
	}
	shootOperatorCr := &rbacv1.ClusterRole{
		ObjectMeta: metav1.ObjectMeta{
			Name:   shootClusterOperatorRole,
			Labels: labels,
		},
	}

	_, err = controllerutil.CreateOrUpdate(ctx, cli, shootOperatorCr, func() error {
		shootOperatorCr.Rules = []rbacv1.PolicyRule{
			{
				APIGroups: []string{""},
				Resources: []string{"pods", "services", "endpoints", "persistentvolumeclaims", "configmaps"},
				Verbs:     []string{"get", "watch", "list", "create", "delete", "update"},
			},
			{
				APIGroups: []string{"batch"},
				Resources: []string{"jobs"},
				Verbs:     []string{"get", "list", "watch", "create", "update", "delete"},
			},
			// for controller objects, do not allow human deletion in favor of safety
			{
				APIGroups: []string{"apps"},
				Resources: []string{"deployments", "statefulsets", "controllerrevisions"},
				Verbs:     []string{"get", "list", "watch", "create", "update"},
			},
			{
				APIGroups: []string{"apps.pingcap.com"},
				Resources: []string{"statefulsets", "statefulsets/status"},
				Verbs:     []string{"get", "list", "watch", "create", "update"},
			},
			{
				APIGroups: []string{"pingcap.com"},
				Resources: []string{"*"},
				Verbs:     []string{"get", "list", "watch", "create", "update"},
			},
		}
		return nil
	})
	if err != nil {
		return op, errors.AddStack(err)
	}

	cr := &rbacv1.ClusterRole{
		ObjectMeta: metav1.ObjectMeta{
			Name:   consts.LocalClusterProxyName,
			Labels: labels,
		},
	}
	_, err = controllerutil.CreateOrUpdate(ctx, cli, cr, func() error {
		cr.Rules = []rbacv1.PolicyRule{
			{
				APIGroups: []string{""},
				Resources: []string{"secrets", "services", "endpoints", "serviceaccounts"},
				Verbs:     []string{"get", "watch", "list", "create", "delete", "update"},
			},
			{
				APIGroups: []string{"batch"},
				Resources: []string{"jobs"},
				Verbs:     []string{"get", "list", "watch", "create", "update", "delete"},
			},
			// Allow proxy to bind subjects to the shoot-operator role
			{
				APIGroups: []string{"rbac.authorization.k8s.io"},
				Resources: []string{"clusterrolebindings"},
				Verbs:     []string{"create", "get", "update"},
			},
			{
				APIGroups:     []string{"rbac.authorization.k8s.io"},
				Resources:     []string{"clusterroles"},
				Verbs:         []string{"bind"},
				ResourceNames: []string{shootClusterOperatorRole},
			},
		}
		return nil
	})
	if err != nil {
		return op, errors.AddStack(err)
	}

	sa := &corev1.ServiceAccount{
		ObjectMeta: metav1.ObjectMeta{
			Name:      consts.LocalClusterProxyName,
			Namespace: consts.LocalOperatorNamespace,
			Labels:    labels,
		},
	}
	_, err = k8s.CreateIgnoreExists(ctx, cli, sa)
	if err != nil {
		return op, err
	}

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

	deploy := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      consts.LocalClusterProxyName,
			Namespace: consts.LocalOperatorNamespace,
		},
		Spec: appsv1.DeploymentSpec{
			Selector: &metav1.LabelSelector{
				MatchLabels: labels,
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: labels,
				},
			},
		},
	}
	_, err = controllerutil.CreateOrUpdate(ctx, cli, deploy, func() error {
		deploy.Spec.Replicas = pointer.Int32(2)
		deploy.Spec.Template.Spec = corev1.PodSpec{
			ServiceAccountName: consts.LocalClusterProxyName,
			Volumes: []corev1.Volume{
				{
					Name: "tls-certs",
					VolumeSource: corev1.VolumeSource{
						Secret: &corev1.SecretVolumeSource{
							SecretName:  "proxy-certificate-secret",
							DefaultMode: pointer.Int32(0422),
						},
					},
				},
			},
			Affinity: &corev1.Affinity{
				PodAntiAffinity: &corev1.PodAntiAffinity{
					PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{
						{
							Weight: 100,
							PodAffinityTerm: corev1.PodAffinityTerm{
								LabelSelector: &metav1.LabelSelector{
									MatchExpressions: []metav1.LabelSelectorRequirement{
										{
											Key:      "app",
											Operator: "In",
											Values:   []string{consts.LocalClusterProxyName},
										},
									},
								},
								TopologyKey: "kubernetes.io/hostname",
							},
						},
					},
				},
			},
			Containers: []corev1.Container{{
				Name:  "proxy",
				Image: image,
				Command: []string{"/proxy",
					"--port", "443",
					"--tls-dir", "/proxy-tls",
					"--gotty-image", config.GetGottyImage(),
					"--gotty-cmd", "/gotty -w --max-connection 1 --timeout 60 --reconnect /usql --password",
				},
				Env: []corev1.EnvVar{
					{
						Name:  "AUTH0-AUDIENCE",
						Value: auth0Audience,
					},
					{
						Name:  "AUTH0-DOMAIN",
						Value: auth0Domain,
					},
				},
				Resources: corev1.ResourceRequirements{
					Limits: corev1.ResourceList{
						corev1.ResourceCPU:    resource.MustParse("100m"),
						corev1.ResourceMemory: resource.MustParse("256Mi"),
					},
					Requests: corev1.ResourceList{
						corev1.ResourceCPU:    resource.MustParse("20m"),
						corev1.ResourceMemory: resource.MustParse("80Mi"),
					},
				},
				VolumeMounts: []corev1.VolumeMount{
					{
						Name:      "tls-certs",
						MountPath: "/proxy-tls",
					},
				},
			}},
		}
		return nil
	})
	if err != nil {
		return op, err
	}

	svc := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      consts.LocalClusterProxyName,
			Namespace: consts.LocalOperatorNamespace,
		},
	}
	return controllerutil.CreateOrUpdate(context.TODO(), cli, svc, func() error {
		svc.Spec.Ports = []corev1.ServicePort{{
			Port:       443,
			TargetPort: intstr.FromInt(443),
			Protocol:   corev1.ProtocolTCP,
		}}
		svc.Spec.Selector = labels
		svc.Spec.Type = corev1.ServiceTypeLoadBalancer
		svc.Spec.LoadBalancerSourceRanges = loadBalancerSourceRanges
		// Set externalTrafficPolicy to Local to avoid internet traffic for tidb nodes
		// Otherwise it would affect current billing metering
		svc.Spec.ExternalTrafficPolicy = corev1.ServiceExternalTrafficPolicyTypeLocal
		return nil
	})
}

func createOrUpdateProxyCert(cli client.Client, ns, region, cloudProvider string) error {
	certificate := &v1alpha2.Certificate{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "proxy-certifcate",
			Namespace: ns,
		},
		Spec: v1alpha2.CertificateSpec{
			SecretName: "proxy-certificate-secret",
			DNSNames: []string{
				"dbaas-proxy",
				"dbaas-proxy.tidb-admin",
				"dbaas-proxy.tidb-admin.svc",
			},
			Duration: &metav1.Duration{
				Duration: consts.CertificateDuration,
			},
			RenewBefore: &metav1.Duration{
				Duration: consts.RenewBefore,
			},
			Organization: []string{consts.CertificateOrg},
			IssuerRef: cmmeta.ObjectReference{
				Name:  consts.AwsPcaIssuer,
				Kind:  consts.AwsPcaIssuerKind,
				Group: consts.CertManagerGroup,
			},
		},
	}
	if cloudProvider == consts.ProviderTypeAWS {
		certificate.Spec.DNSNames = append(certificate.Spec.DNSNames, fmt.Sprintf("*.%s.elb.amazonaws.com", region))
	}
	_, err := controllerutil.CreateOrUpdate(context.TODO(), cli, certificate, func() error { return nil })
	return err
}
