package gardener

import (
	"context"
	"fmt"
	"path/filepath"

	"github.com/cert-manager/aws-privateca-issuer/pkg/api/v1beta1"
	"github.com/pingcap/errors"
	"go.opentelemetry.io/otel/trace"
	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/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	"k8s.io/apimachinery/pkg/types"

	"github.com/tidbcloud/infra-api/controllers/utils"
	"github.com/tidbcloud/infra-api/pkg/consts"
	"github.com/tidbcloud/infra-api/pkg/k8s"
	tracerutil "github.com/tidbcloud/infra-api/pkg/tracer"
)

const (
	certManagerNamespace  = "cert-manager"
	certManagerRepository = "gcr.io/pingcap-public/cert-manager"
	AwsPcaIssuer          = "aws-pca-issuer"
	awsPcaIssuerImage     = "gcr.io/pingcap-public/dbaas/aws-pca-issuer:v0.1.2"
)

type WaitWebhookReadyError struct{}

func (err WaitWebhookReadyError) Error() string {
	return "Waiting cert manager webhook ready"
}

// depend on command: helm, sh.
func deployCertManager(
	ctx context.Context,
	providerType string,
	cli client.Client,
	kubeconfig []byte,
	manifestDirectoryPrefix string,
	pcaRoleARN string,
	pcaARN string,
	pcaRegion string,
	identityImage string,
) (op controllerutil.OperationResult, err error) {
	spanCtx, span := tracer.Start(ctx, "deployCertManager", trace.WithSpanKind(trace.SpanKindClient))
	defer func() { tracerutil.EndSpan(span, err) }()

	// 1. ensure namespace created
	span.AddEvent("ensure cert-manager's namespace is created")
	op, err = k8s.CreateIgnoreExists(spanCtx, cli, &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{Name: certManagerNamespace},
	})
	if err != nil {
		return op, errors.Annotatef(err, "failed to create namespace %s", certManagerNamespace)
	}

	kubeconfigName, err := k8s.WriteTempFile(kubeconfig)
	if err != nil {
		return op, err
	}

	// 2. run helm upgrade or install certmanager
	kubectl := k8s.NewKubectl().SetDir(manifestDirectoryPrefix).WithKubeConfig(kubeconfigName)
	helm := k8s.NewHelm().SetDir(manifestDirectoryPrefix).WithKubeConfig(kubeconfigName)

	span.AddEvent("kubectl apply cert-manager CRDs")
	_, output, err := kubectl.Apply(
		"--validate=false",
		"-f",
		filepath.Join(manifestDirectoryPrefix, "/manifests/cert-manager-crd.yaml"),
	)
	if err != nil {
		return op, errors.Annotatef(err, "failed to apply cert-manager CRDs output: %s", output)
	}

	// in test env, skip real deploying cert-manager since the webhook will block certificates creation
	if !inIntegrationTest() {
		chartPath := filepath.Join(manifestDirectoryPrefix, "/charts/cert-manager.tgz")
		span.AddEvent("helm apply cert-manager")
		_, out, err := helm.Apply(kubectl,
			"cert-manager",
			chartPath,
			"--namespace", "cert-manager",
			"--set", "extraArgs={--issuer-ambient-credentials,--dns01-recursive-nameservers=8.8.8.8:53,1.1.1.1:53}",
			"--set", fmt.Sprintf("image.repository=%s", certManagerRepository),
		)
		if err != nil {
			return op, errors.Annotatef(err, "failed helm install cert-manager chart, output: %s", out)
		}

		// Wait cert-manager webhook ready
		span.AddEvent("wait cert-manager webhook ready")
		_, err = ensureCertManagerWebhookReady(spanCtx, cli)
		if err != nil {
			return op, err
		}
	}

	// 3. apply CRDs
	span.AddEvent("kubectl apply aws-pca-clusterissuer-crd")
	_, output, err = kubectl.Apply(
		"--validate=false",
		"-f",
		filepath.Join(manifestDirectoryPrefix, "/manifests/aws-pca-clusterissuer-crd.yaml"),
	)
	if err != nil {
		return op, errors.Annotatef(err, "failed to apply aws pca cluster issuer CRD, output: %s", output)
	}

	span.AddEvent("kubectl apply aws-pca-issuer-crd")
	_, output, err = kubectl.Apply(
		"--validate=false",
		"-f",
		filepath.Join(manifestDirectoryPrefix, "/manifests/aws-pca-issuer-crd.yaml"),
	)

	if err != nil {
		return op, errors.Annotatef(err, "failed to apply aws pca issuer CRD, output: %s", output)
	}

	// 4. create needed issuers
	span.AddEvent("create needed issuers")
	err = deployAwsPACIssuer(spanCtx, providerType, cli, pcaRoleARN, identityImage)
	if err != nil {
		return op, errors.AddStack(err)
	}

	issuer := &v1beta1.AWSPCAClusterIssuer{
		ObjectMeta: metav1.ObjectMeta{
			Name: AwsPcaIssuer,
		},
		Spec: v1beta1.AWSPCAIssuerSpec{
			Arn:    pcaARN,
			Region: pcaRegion,
		},
	}

	return k8s.CreateIgnoreExists(spanCtx, cli, issuer)
}

func deployAwsPACIssuer(ctx context.Context, providerType string, cli client.Client, pcaARN string, identityImage string) (errResult error) {
	spanCtx, span := tracer.Start(ctx, "deployAwsPACIssuer", trace.WithSpanKind(trace.SpanKindClient))
	defer func() { tracerutil.EndSpan(span, errResult) }()

	cr := &rbacv1.ClusterRole{
		ObjectMeta: metav1.ObjectMeta{
			Name: AwsPcaIssuer,
		},
	}
	_, err := controllerutil.CreateOrUpdate(spanCtx, cli, cr, func() error {
		cr.Rules = []rbacv1.PolicyRule{
			{
				APIGroups: []string{"", "coordination.k8s.io"},
				Resources: []string{"configmaps", "leases"},
				Verbs:     []string{"get", "watch", "list", "create", "update", "patch", "delete"},
			},
			{
				APIGroups: []string{""},
				Resources: []string{"events"},
				Verbs:     []string{"create", "patch"},
			},
			{
				APIGroups: []string{""},
				Resources: []string{"secrets"},
				Verbs:     []string{"get", "watch", "list"},
			},
			{
				APIGroups: []string{"awspca.cert-manager.io"},
				Resources: []string{"awspcaclusterissuers"},
				Verbs:     []string{"get", "watch", "list", "create", "update", "patch", "delete"},
			},
			{
				APIGroups: []string{"awspca.cert-manager.io"},
				Resources: []string{"awspcaclusterissuers/finalizers"},
				Verbs:     []string{"update"},
			},
			{
				APIGroups: []string{"awspca.cert-manager.io"},
				Resources: []string{"awspcaclusterissuers/status"},
				Verbs:     []string{"get", "patch", "update"},
			},
			{
				APIGroups: []string{"cert-manager.io"},
				Resources: []string{"certificaterequests"},
				Verbs:     []string{"get", "watch", "list", "update"},
			},
			{
				APIGroups: []string{"cert-manager.io"},
				Resources: []string{"certificaterequests/status"},
				Verbs:     []string{"get", "watch", "update"},
			},
			{
				APIGroups: []string{"awspca.cert-manager.io"},
				Resources: []string{"awspcaissuers"},
				Verbs:     []string{"get", "watch", "list", "create", "update", "patch", "delete"},
			},
			{
				APIGroups: []string{"awspca.cert-manager.io"},
				Resources: []string{"awspcaissuers/finalizers"},
				Verbs:     []string{"update"},
			},
			{
				APIGroups: []string{"awspca.cert-manager.io"},
				Resources: []string{"awspcaissuers/status"},
				Verbs:     []string{"get", "patch", "update"},
			},
		}
		return nil
	})
	if err != nil {
		return err
	}

	sa := &corev1.ServiceAccount{
		ObjectMeta: metav1.ObjectMeta{
			Name:      AwsPcaIssuer,
			Namespace: "cert-manager",
		},
	}
	if _, err := k8s.CreateIgnoreExists(spanCtx, cli, sa); err != nil {
		return err
	}

	crb := &rbacv1.ClusterRoleBinding{
		ObjectMeta: metav1.ObjectMeta{
			Name: AwsPcaIssuer,
		},
		Subjects: []rbacv1.Subject{{
			Kind:      rbacv1.ServiceAccountKind,
			Name:      AwsPcaIssuer,
			Namespace: "cert-manager",
		}},
		RoleRef: rbacv1.RoleRef{
			Kind:     "ClusterRole",
			Name:     AwsPcaIssuer,
			APIGroup: rbacv1.GroupName,
		},
	}
	if _, err := k8s.CreateIgnoreExists(spanCtx, cli, crb); err != nil {
		return err
	}

	labels := map[string]string{
		"app.kubernetes.io/instance": AwsPcaIssuer,
	}
	deploy := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      AwsPcaIssuer,
			Namespace: "cert-manager",
		},
		Spec: appsv1.DeploymentSpec{
			Selector: &metav1.LabelSelector{
				MatchLabels: labels,
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: labels,
				},
			},
		},
	}

	_, err = controllerutil.CreateOrUpdate(spanCtx, cli, deploy, func() error {
		deploy.Spec.Replicas = pointer.Int32(1)
		deploy.Spec.Template.Spec = corev1.PodSpec{
			RestartPolicy:      corev1.RestartPolicyAlways,
			ServiceAccountName: AwsPcaIssuer,
		}
		pcmContainer := corev1.Container{
			Name:  "aws-pca-issuer",
			Image: awsPcaIssuerImage,
			Command: []string{
				"/manager",
			},
			Args: []string{
				"--leader-elect",
			},
			Resources: corev1.ResourceRequirements{
				Requests: corev1.ResourceList{
					corev1.ResourceCPU:    resource.MustParse("20m"),
					corev1.ResourceMemory: resource.MustParse("40Mi"),
				},
			},
		}

		if providerType == consts.ProviderTypeGCP {
			sidecar, vol, volMount, envs := k8s.PrepareGCP2AWSIdentitySidecar(pcaARN, identityImage)
			deploy.Spec.Template.Spec.Volumes = append(deploy.Spec.Template.Spec.Volumes, vol)
			pcmContainer.Env = append(pcmContainer.Env, envs...)
			pcmContainer.VolumeMounts = append(pcmContainer.VolumeMounts, volMount)
			deploy.Spec.Template.Spec.Containers = append(deploy.Spec.Template.Spec.Containers, sidecar)
		}
		deploy.Spec.Template.Spec.Containers = append(deploy.Spec.Template.Spec.Containers, pcmContainer)
		return nil
	})

	return err
}

func ensureCertManagerWebhookReady(ctx context.Context, cli client.Client) (bool, error) {
	app := new(appsv1.Deployment)
	err := cli.Get(ctx, types.NamespacedName{
		Namespace: "cert-manager",
		Name:      "cert-manager-webhook",
	}, app)
	if err != nil {
		return false, err
	}
	if utils.IsStatusConditionTrue(app.Status.Conditions, appsv1.DeploymentAvailable) {
		return true, nil
	}
	return false, WaitWebhookReadyError{}
}
