package aks

import (
	"context"
	"fmt"

	pcav1beta1 "github.com/cert-manager/aws-privateca-issuer/pkg/api/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/upbound/provider-aws/apis/iam/v1beta1"
	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/labels"
	"k8s.io/apimachinery/pkg/util/intstr"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/applier"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/applier/mutatefuncs"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/config"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/appbox/task"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/deploy"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/namer"
)

const (
	defaultApproverName      = "cert-manager"
	defaultApproverNamespace = task.CertManagerNamespace
	clusterIssuerName        = "cluster-issuer"
)

const (
	assumeRolePolicyDocumentTemplate = `{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::%s:oidc-provider/%s"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "%s:sub": "system:serviceaccount:%s:%s"
        }
      }
    }
  ]
}`

	pcaIssuerPolicyDocument = `{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "acm-pca:IssueCertificate",
        "acm-pca:GetCertificate",
        "acm-pca:GetCertificateAuthorityCertificate",
        "acm-pca:DescribeCertificateAuthority"
      ],
      "Resource": "*"
    }
  ]
}`
)

type PCAIssuer struct {
	*task.BaseTask
	config *PCAIssuerConfig
}

type PCAIssuerConfig struct {
	config.InfraComponentConfig

	approverNamespace string
	approverName      string
}

func NewPCAIssuer(ctx *task.Context) *PCAIssuer {
	return &PCAIssuer{
		BaseTask: &task.BaseTask{Context: ctx},
		config: &PCAIssuerConfig{
			InfraComponentConfig: ctx.InfraTmplConfig.PCAIssuer,
			approverNamespace:    defaultApproverNamespace,
			approverName:         defaultApproverName,
		},
	}
}

func (t *PCAIssuer) Name() string {
	return "PCAIssuer"
}

func (t *PCAIssuer) Create(ctx context.Context) deploy.Result {
	if shouldBypassPCAIssuerTask(t.Context) {
		return deploy.Done()
	}
	caIdentifier, region, enabled, err := t.PCAIdentifierAndRegion()
	if err != nil {
		return deploy.Error(fmt.Errorf("failed to resolve PCA config: %w", err))
	}
	if !enabled {
		return deploy.Done()
	}

	// AWSCrossplaneProviderConfig should directly be the AWS account ID
	// (following the pattern: "It should use the AWS account as the name of the provider config")
	account := t.Config.AWSCrossplaneProviderConfig

	cli, err := t.Remote.GetClient(ctx)
	if err != nil {
		return deploy.Panic(err)
	}

	crds, err := t.config.GetCRDs()
	if err != nil {
		return deploy.Error(err)
	}
	crdApplier := applier.NewApplier(cli, mutatefuncs.MutateCRD)
	for _, crd := range crds {
		if _, err := crdApplier.Expected(crd).Apply(ctx); err != nil {
			return deploy.Error(err)
		}

	}
	if _, err := applier.NewApplier(cli, mutatefuncs.MutateServiceAccount).
		Expected(t.NewServiceAccount()).
		Apply(ctx); err != nil {
		return deploy.Error(err)
	}

	roleApplier := applier.NewApplier(cli, mutatefuncs.MutateClusterRole)
	if _, err := roleApplier.Expected(t.NewClusterRole()).Apply(ctx); err != nil {
		return deploy.Error(err)
	}
	if _, err := roleApplier.Expected(t.NewApproverClusterRole()).Apply(ctx); err != nil {
		return deploy.Error(err)
	}

	rbApplier := applier.NewApplier(cli, mutatefuncs.MutateClusterRoleBinding)
	if _, err := rbApplier.Expected(t.NewClusterRoleBinding()).Apply(ctx); err != nil {
		return deploy.Error(err)
	}
	if _, err := rbApplier.Expected(t.NewApproverClusterRoleBinding()).Apply(ctx); err != nil {
		return deploy.Error(err)
	}

	if t.Cluster == nil || t.Cluster.GetOIDCURL() == "" {
		// If oidc issuer url is empty, we can not create the following resources
		return deploy.Error(fmt.Errorf("aks oidc issuer url is empty"))
	}

	if _, err := applier.NewApplier(t.Local, mutatefuncs.MutateUpboundIAMRole).
		Expected(t.NewIAMRole(false, account)).
		Apply(ctx); err != nil {
		return deploy.Error(err)
	}

	if _, err := applier.NewApplier(t.Local, mutatefuncs.MutateUpboundIAMPolicy).
		Expected(t.NewIAMPolicy(false)).
		Apply(ctx); err != nil {
		return deploy.Error(err)
	}

	if _, err := applier.NewApplier(t.Local, mutatefuncs.MutateUpboundIAMRolePolicyAttachment).
		Expected(t.NewIAMRolePolicyAttachment(false)).
		Apply(ctx); err != nil {
		return deploy.Error(err)
	}

	if _, err := applier.NewApplier(cli, mutatefuncs.MutateService).
		Expected(t.NewService()).
		Apply(ctx); err != nil {
		return deploy.Error(err)
	}

	if _, err := applier.NewApplier(cli, mutatefuncs.MutateDeployment).
		Expected(t.InjectIdentityForAWS(t.NewDeployment(), account)).
		Apply(ctx); err != nil {
		return deploy.Error(err)
	}

	issuer := &pcav1beta1.AWSPCAClusterIssuer{
		ObjectMeta: metav1.ObjectMeta{
			Name: clusterIssuerName,
		},
	}
	if _, err = controllerutil.CreateOrUpdate(ctx, cli, issuer, func() error {
		// Only set Arn and Region if they are not already set.
		// Changing these values would cause different PCA to be used for signing certificates.
		if issuer.Spec.Arn == "" {
			issuer.Spec.Arn = caIdentifier
		}
		if issuer.Spec.Region == "" {
			issuer.Spec.Region = region
		}
		return nil
	}); err != nil {
		return deploy.Error(fmt.Errorf("failed to create or update AWS PCA ClusterIssuer: %w", err))
	}

	return deploy.Done()
}

func (t *PCAIssuer) PostCreate(ctx context.Context) deploy.Result {
	if shouldBypassPCAIssuerTask(t.Context) {
		return deploy.Done()
	}
	_, _, enabled, err := t.PCAIdentifierAndRegion()
	if err != nil {
		return deploy.Error(fmt.Errorf("failed to resolve PCA config: %w", err))
	}
	if !enabled {
		return deploy.Done()
	}

	cli, err := t.Remote.GetClient(ctx)
	if err != nil {
		return deploy.Error(fmt.Errorf("failed to get remote client: %w", err))
	}

	var issuer pcav1beta1.AWSPCAClusterIssuer
	if err := cli.Get(ctx, client.ObjectKey{Name: clusterIssuerName}, &issuer); err != nil {
		return deploy.Error(fmt.Errorf("failed to get AWS PCA ClusterIssuer: %w", err))
	}
	for _, v := range issuer.Status.Conditions {
		if v.Type == "Ready" && v.Status == "True" {
			return deploy.Done()
		}
	}
	return deploy.Error(fmt.Errorf("AWS PCA ClusterIssuer is not ready yet"))
}

func (t *PCAIssuer) Delete(ctx context.Context) deploy.Result {
	if shouldBypassPCAIssuerTask(t.Context) {
		return deploy.Done()
	}
	// only local resources should be cleaned
	if err := controllers.DeleteUpboundResource(ctx, t.Local, t.NewIAMRolePolicyAttachment(true)); err != nil {
		return deploy.Error(err)
	}
	if err := controllers.DeleteUpboundResource(ctx, t.Local, t.NewIAMPolicy(true)); err != nil {
		return deploy.Error(err)
	}
	if err := controllers.DeleteUpboundResource(ctx, t.Local, t.NewIAMRole(true, "")); err != nil {
		return deploy.Error(err)
	}
	return deploy.Done()
}

func (t *PCAIssuer) NewServiceAccount() *corev1.ServiceAccount {
	return &corev1.ServiceAccount{
		ObjectMeta: metav1.ObjectMeta{
			Name:      t.config.GetName(),
			Namespace: t.config.GetNamespace(),
		},
	}
}

func (t *PCAIssuer) NewApproverClusterRoleName() string {
	return t.config.GetName() + ":" + t.config.approverName + "-approver"
}

func (t *PCAIssuer) NewApproverClusterRole() *rbacv1.ClusterRole {
	return &rbacv1.ClusterRole{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.NewApproverClusterRoleName(),
		},
		Rules: []rbacv1.PolicyRule{
			{
				APIGroups: []string{"cert-manager.io"},
				Resources: []string{"signers"},
				Verbs:     []string{"approve"},
				ResourceNames: []string{
					"awspcaclusterissuers.awspca.cert-manager.io/*",
					"awspcaissuers.awspca.cert-manager.io/*",
				},
			},
		},
	}
}

func (t *PCAIssuer) NewApproverClusterRoleBinding() *rbacv1.ClusterRoleBinding {
	return &rbacv1.ClusterRoleBinding{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.NewApproverClusterRoleName(),
		},
		RoleRef: rbacv1.RoleRef{
			APIGroup: rbacv1.GroupName,
			Kind:     "ClusterRole",
			Name:     t.NewApproverClusterRoleName(),
		},
		Subjects: []rbacv1.Subject{
			{
				Kind:      rbacv1.ServiceAccountKind,
				Name:      t.config.approverName,
				Namespace: t.config.approverNamespace,
			},
		},
	}
}

func (t *PCAIssuer) NewClusterRole() *rbacv1.ClusterRole {
	return &rbacv1.ClusterRole{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.config.GetName(),
		},
		Rules: []rbacv1.PolicyRule{
			{
				APIGroups: []string{"", "coordination.k8s.io"},
				Resources: []string{"configmaps", "leases"},
				Verbs:     []string{"get", "list", "watch", "create", "update", "patch", "delete"},
			},
			{
				APIGroups: []string{""},
				Resources: []string{"events"},
				Verbs:     []string{"create", "patch"},
			},
			{
				APIGroups: []string{""},
				Resources: []string{"secrets"},
				Verbs:     []string{"get", "list", "watch"},
			},
			{
				APIGroups: []string{"awspca.cert-manager.io"},
				Resources: []string{"awspcaclusterissuers"},
				Verbs:     []string{"get", "list", "watch", "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", "update", "patch"},
			},
			{
				APIGroups: []string{"awspca.cert-manager.io"},
				Resources: []string{"awspcaissuers"},
				Verbs:     []string{"get", "list", "watch", "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", "update", "patch"},
			},
			{
				APIGroups: []string{"cert-manager.io"},
				Resources: []string{"certificaterequests"},
				Verbs:     []string{"get", "list", "watch", "update"},
			},
			{
				APIGroups: []string{"cert-manager.io"},
				Resources: []string{"certificaterequests/status"},
				Verbs:     []string{"get", "update", "patch"},
			},
		},
	}
}

func (t *PCAIssuer) NewClusterRoleBinding() *rbacv1.ClusterRoleBinding {
	return &rbacv1.ClusterRoleBinding{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.config.GetName(),
		},
		RoleRef: rbacv1.RoleRef{
			APIGroup: rbacv1.GroupName,
			Kind:     "ClusterRole",
			Name:     t.config.GetName(),
		},
		Subjects: []rbacv1.Subject{
			{
				Kind:      rbacv1.ServiceAccountKind,
				Name:      t.config.GetName(),
				Namespace: t.config.GetNamespace(),
			},
		},
	}
}

func (t *PCAIssuer) Labels() map[string]string {
	return map[string]string{
		"app.kubernetes.io/instance": t.config.GetName(),
		"app.kubernetes.io/name":     "issuer",
	}
}

func (t *PCAIssuer) NewDeployment() *appsv1.Deployment {
	imageValues := t.config.InfraComponentConfig.HelmValues["image"].(map[string]interface{})
	return &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      t.config.GetName(),
			Namespace: t.config.GetNamespace(),
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: ptr.To(int32(1)),
			Selector: metav1.SetAsLabelSelector(labels.Set(t.Labels())),
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: t.Labels(),
				},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						{
							Name: t.config.GetName(),
							// TODO: change to use a util function
							Image: fmt.Sprintf("%s:%s", imageValues["repository"], imageValues["tag"]),
							Command: []string{
								"/manager",
							},
							Args: []string{
								"--leader-elect",
							},
							Ports: []corev1.ContainerPort{
								{
									Name:          "http",
									ContainerPort: 8080,
								},
							},
							LivenessProbe: &corev1.Probe{
								ProbeHandler: corev1.ProbeHandler{
									HTTPGet: &corev1.HTTPGetAction{
										Path: "/healthz",
										Port: intstr.FromInt(8081),
									},
								},
								InitialDelaySeconds: 15,
								PeriodSeconds:       20,
							},
							ReadinessProbe: &corev1.Probe{
								ProbeHandler: corev1.ProbeHandler{
									HTTPGet: &corev1.HTTPGetAction{
										Path: "/healthz",
										Port: intstr.FromInt(8081),
									},
								},
								InitialDelaySeconds: 5,
								PeriodSeconds:       10,
							},
							SecurityContext: &corev1.SecurityContext{
								AllowPrivilegeEscalation: ptr.To(false),
							},
						},
					},
					ServiceAccountName: t.config.GetName(),
					SecurityContext: &corev1.PodSecurityContext{
						RunAsUser: ptr.To(int64(65532)),
					},
				},
			},
		},
	}
}

func isAWSEnv(name string) bool {

	switch name {
	case "AWS_DEFAULT_REGION", "AWS_REGION", "AWS_ROLE_ARN", "AWS_WEB_IDENTITY_TOKEN_FILE", "AWS_STS_REGIONAL_ENDPOINTS":
		return true
	}
	return false
}

func NewAWSEnv(roleARN string) []corev1.EnvVar {
	return []corev1.EnvVar{
		{
			// Now always use us-east-1
			Name:  "AWS_DEFAULT_REGION",
			Value: "us-east-1",
		},
		{
			// See AWS_DEFAULT_REGION
			Name:  "AWS_REGION",
			Value: "us-east-1",
		},
		{
			Name:  "AWS_ROLE_ARN",
			Value: roleARN,
		},
		{
			Name:  "AWS_WEB_IDENTITY_TOKEN_FILE",
			Value: "/var/run/secrets/eks.amazonaws.com/serviceaccount/token",
		},
		{
			Name:  "AWS_STS_REGIONAL_ENDPOINTS",
			Value: "regional",
		},
	}
}

func injectEnv(deploy *appsv1.Deployment, roleARN string) *appsv1.Deployment {
	for i := range deploy.Spec.Template.Spec.Containers {
		c := &deploy.Spec.Template.Spec.Containers[i]
		es := []corev1.EnvVar{}
		for _, e := range c.Env {
			if !isAWSEnv(e.Name) {
				es = append(es, e)
			}
		}
		es = append(es, NewAWSEnv(roleARN)...)
		c.Env = es
	}
	return deploy
}

func injectVolumeMount(deploy *appsv1.Deployment) *appsv1.Deployment {
	for i := range deploy.Spec.Template.Spec.Containers {
		c := &deploy.Spec.Template.Spec.Containers[i]
		exists := false
		for k := range c.VolumeMounts {
			vm := &c.VolumeMounts[k]
			if vm.Name == "aws-token" {
				exists = true
				vm.MountPath = "/var/run/secrets/eks.amazonaws.com/serviceaccount/"
			}
		}
		if !exists {
			c.VolumeMounts = append(c.VolumeMounts, corev1.VolumeMount{
				Name:      "aws-token",
				MountPath: "/var/run/secrets/eks.amazonaws.com/serviceaccount/",
			})

		}
	}
	return deploy
}

func injectVolume(deploy *appsv1.Deployment) *appsv1.Deployment {
	exists := false
	for i := range deploy.Spec.Template.Spec.Volumes {
		v := &deploy.Spec.Template.Spec.Volumes[i]
		if v.Name == "aws-token" {
			exists = true
			v.VolumeSource = corev1.VolumeSource{
				Projected: &corev1.ProjectedVolumeSource{
					Sources: []corev1.VolumeProjection{
						{
							ServiceAccountToken: &corev1.ServiceAccountTokenProjection{
								Audience:          "sts.amazonaws.com",
								ExpirationSeconds: ptr.To(int64(86400)),
								Path:              "token",
							},
						},
					},
				},
			}
		}
	}
	if !exists {
		deploy.Spec.Template.Spec.Volumes = append(deploy.Spec.Template.Spec.Volumes, corev1.Volume{
			Name: "aws-token",
			VolumeSource: corev1.VolumeSource{
				Projected: &corev1.ProjectedVolumeSource{
					Sources: []corev1.VolumeProjection{
						{
							ServiceAccountToken: &corev1.ServiceAccountTokenProjection{
								Audience:          "sts.amazonaws.com",
								ExpirationSeconds: ptr.To(int64(86400)),
								Path:              "token",
							},
						},
					},
				},
			},
		})
	}

	return deploy
}

func (t *PCAIssuer) InjectIdentityForAWS(deploy *appsv1.Deployment, account string) *appsv1.Deployment {
	name := namer.UniqueIdentifier(t.APPBox.ObjectMeta)
	roleARN := fmt.Sprintf("arn:aws:iam::%s:role/%s", account, name)
	deploy = injectEnv(deploy, roleARN)
	deploy = injectVolumeMount(deploy)
	deploy = injectVolume(deploy)

	return deploy
}

func (t *PCAIssuer) NewService() *corev1.Service {
	return &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      t.config.GetName(),
			Namespace: t.config.GetNamespace(),
		},
		Spec: corev1.ServiceSpec{
			Selector: t.Labels(),
			Ports: []corev1.ServicePort{
				{
					Port:       8080,
					TargetPort: intstr.FromString("http"),
					Protocol:   corev1.ProtocolTCP,
					Name:       "http",
				},
			},
		},
	}
}

func (t *PCAIssuer) NewIAMRole(onlyMeta bool, account string) *v1beta1.Role {
	name := namer.UniqueIdentifier(t.APPBox.ObjectMeta)
	if t.APPBox.Annotations[v1alpha1.AnnotationKeyOwnCrossplaneNameIAMRolePCAIssuer] != "" {
		name = t.APPBox.Annotations[v1alpha1.AnnotationKeyOwnCrossplaneNameIAMRolePCAIssuer]
	}
	role := &v1beta1.Role{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}
	if onlyMeta {
		return role
	}

	url := getOIDCIssuerURLWithoutScheme(t.Cluster.GetOIDCURL())
	role.Spec = v1beta1.RoleSpec{
		ForProvider: v1beta1.RoleParameters{
			AssumeRolePolicy: ptr.To(fmt.Sprintf(assumeRolePolicyDocumentTemplate,
				account,
				url, url,
				t.config.GetNamespace(),
				t.config.GetName(),
			)),
			Description: ptr.To(fmt.Sprintf("role for aws pca issuer in aks cluster %s", name)),
		},
		ResourceSpec: xpv1.ResourceSpec{
			ProviderConfigReference: &xpv1.Reference{Name: t.Config.AWSCrossplaneProviderConfig},
			ManagementPolicies:      controllers.GetResourceManagementPolicies(),
		},
	}

	return role
}

func (t *PCAIssuer) NewIAMPolicy(onlyMeta bool) *v1beta1.Policy {
	name := namer.UniqueIdentifier(t.APPBox.ObjectMeta)
	// iampolicy has same name as iamrole
	if t.APPBox.Annotations[v1alpha1.AnnotationKeyOwnCrossplaneNameIAMRolePCAIssuer] != "" {
		name = t.APPBox.Annotations[v1alpha1.AnnotationKeyOwnCrossplaneNameIAMRolePCAIssuer]
	}
	policy := &v1beta1.Policy{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}
	if onlyMeta {
		return policy
	}
	policy.Spec = v1beta1.PolicySpec{
		ForProvider: v1beta1.PolicyParameters{
			Policy:      ptr.To(pcaIssuerPolicyDocument),
			Description: ptr.To(fmt.Sprintf("policy for aws pca issuer in aks cluster %s", name)),
		},
		ResourceSpec: xpv1.ResourceSpec{
			ProviderConfigReference: &xpv1.Reference{Name: t.Config.AWSCrossplaneProviderConfig},
			ManagementPolicies:      controllers.GetResourceManagementPolicies(),
		},
	}

	return policy

}

func (t *PCAIssuer) NewIAMRolePolicyAttachment(onlyMeta bool) *v1beta1.RolePolicyAttachment {
	name := namer.UniqueIdentifier(t.APPBox.ObjectMeta)
	// iamrolepolicyattachment has same name as iamrole
	if t.APPBox.Annotations[v1alpha1.AnnotationKeyOwnCrossplaneNameIAMRolePCAIssuer] != "" {
		name = t.APPBox.Annotations[v1alpha1.AnnotationKeyOwnCrossplaneNameIAMRolePCAIssuer]
	}
	rp := &v1beta1.RolePolicyAttachment{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}
	if onlyMeta {
		return rp

	}

	rp.Spec = v1beta1.RolePolicyAttachmentSpec{
		ForProvider: v1beta1.RolePolicyAttachmentParameters{
			RoleRef: &xpv1.Reference{
				Name: name,
			},
			PolicyArnRef: &xpv1.Reference{
				Name: name,
			},
		},
		ResourceSpec: xpv1.ResourceSpec{
			ProviderConfigReference: &xpv1.Reference{Name: t.Config.AWSCrossplaneProviderConfig},
			ManagementPolicies:      controllers.GetResourceManagementPolicies(),
		},
	}

	return rp
}
