package gke

import (
	"context"
	"fmt"
	"time"

	iamv1alpha1 "github.com/crossplane-contrib/provider-gcp/apis/iam/v1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

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

const (
	// nolint:gosec
	roleIAMServiceAccountTokenCreator = "roles/iam.serviceAccountTokenCreator"
	roleViewer                        = "roles/viewer"
)

// GCPServiceAccount task create gcp sa for all node pool in gke cluster.
// https://doc.crds.dev/github.com/crossplane/provider-gcp/iam.gcp.crossplane.io/ServiceAccount/v1alpha1@v0.17.1
type GCPServiceAccount struct {
	*task.BaseTask
}

func NewGCPServiceAccount(ctx *task.Context) *GCPServiceAccount {
	return &GCPServiceAccount{
		BaseTask: &task.BaseTask{Context: ctx},
	}
}

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

func (t *GCPServiceAccount) Create(ctx context.Context) deploy.Result {
	t.BeginAt = util.Pointer(time.Now())
	// do nothing for legacy data.
	if t.APPBox.Labels[v1alpha1.IsLegacyKey] == "true" {
		return deploy.Done()
	}
	nodeSA, err := t.ensureCreatedSA(ctx)
	if err != nil {
		return deploy.Error(err)
	}

	err = t.ensureCreatedSAKey(ctx)
	if err != nil {
		return deploy.Error(err)
	}

	return t.ensureCreatedSAPolicy(ctx, nodeSA)
}

func (t *GCPServiceAccount) Delete(ctx context.Context) deploy.Result {
	// delete sa
	gcpSA := &iamv1alpha1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: t.saName()}}
	if err := k8s.DeleteObject(ctx, t.Local, gcpSA); err != nil {
		return deploy.Error(err)
	}
	// refer to: https://github.com/tidbcloud/eks-provider/issues/153
	if err := k8s.PatchObject(ctx, t.Local, gcpSA, k8s.RemoveCrossplaneExternalCreatePending, k8s.DeletionTrigger); err != nil {
		return deploy.Error(err)
	}

	// delete sa key
	saKey := &iamv1alpha1.ServiceAccountKey{ObjectMeta: metav1.ObjectMeta{Namespace: t.APPBox.Namespace, Name: t.saKeyName()}}
	if err := k8s.DeleteObject(ctx, t.Local, saKey); err != nil {
		return deploy.Error(err)
	}
	// refer to: https://github.com/tidbcloud/eks-provider/issues/153
	if err := k8s.PatchObject(ctx, t.Local, saKey, k8s.RemoveCrossplaneExternalCreatePending, k8s.DeletionTrigger); err != nil {
		return deploy.Error(err)
	}

	// delete sa-policy
	policy := &iamv1alpha1.ServiceAccountPolicy{ObjectMeta: metav1.ObjectMeta{Name: t.policyName()}}
	if err := k8s.DeleteObject(ctx, t.Local, policy); err != nil {
		return deploy.Error(err)
	}
	// refer to: https://github.com/tidbcloud/eks-provider/issues/153
	if err := k8s.PatchObject(ctx, t.Local, policy, k8s.RemoveCrossplaneExternalCreatePending, k8s.DeletionTrigger); err != nil {
		return deploy.Error(err)
	}
	return deploy.Done()
}

func (t *GCPServiceAccount) PostDelete(ctx context.Context) deploy.Result {
	gcpSA := &iamv1alpha1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: t.saName()}}
	gcpKey := types.NamespacedName{Name: t.saName()}
	if err := k8s.GetDeletedObject(ctx, t.Local, gcpKey, gcpSA); err != nil {
		return deploy.ErrorOrDone(err)
	}

	policy := &iamv1alpha1.ServiceAccountPolicy{ObjectMeta: metav1.ObjectMeta{Name: t.policyName()}}
	policyKey := types.NamespacedName{Name: t.policyName()}

	saKey := &iamv1alpha1.ServiceAccountKey{ObjectMeta: metav1.ObjectMeta{Namespace: t.APPBox.Namespace, Name: t.saKeyName()}}
	saKeyKey := types.NamespacedName{Name: t.saKeyName(), Namespace: t.APPBox.Namespace}
	if err := k8s.GetDeletedObject(ctx, t.Local, saKeyKey, saKey); err != nil {
		return deploy.ErrorOrDone(err)
	}

	return deploy.ErrorOrDone(k8s.GetDeletedObject(ctx, t.Local, policyKey, policy))
}

func (t *GCPServiceAccount) saName() string {
	if name := t.APPBox.Annotations[v1alpha1.AnnotationKeyOwnCrossplaneNameServiceAccount]; name != "" {
		return name
	}
	return namer.UniqueIdentifier(t.APPBox.ObjectMeta,
		namer.WithPrefix("access-sa-"),
		namer.WithLimit(namer.GKEServiceAccountNameLimit))
}

func (t *GCPServiceAccount) policyName() string {
	if name := t.APPBox.Annotations[v1alpha1.AnnotationKeyOwnCrossplaneNameServiceAccountPolicy]; name != "" {
		return name
	}
	return namer.UniqueIdentifier(t.APPBox.ObjectMeta,
		namer.WithPrefix("access-sa-policy"),
		namer.WithLimit(namer.GKEServiceAccountNameLimit))
}

func (t *GCPServiceAccount) saKeyName() string {
	if name := t.APPBox.Annotations[v1alpha1.AnnotationKeyOwnCrossplaneNameServiceAccountKey]; name != "" {
		return name
	}
	return namer.UniqueIdentifier(t.APPBox.ObjectMeta,
		namer.WithPrefix("access-sa-key"),
		namer.WithLimit(namer.GKEServiceAccountNameLimit))
}

// ensure service account is created and updated to network status, return nodeSA
func (t *GCPServiceAccount) ensureCreatedSA(ctx context.Context) (string, error) {
	gcpSAName := t.saName()
	gcpSA := &iamv1alpha1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: gcpSAName}}
	mutSA := func() error {
		gcpSA.Spec = iamv1alpha1.ServiceAccountSpec{
			ResourceSpec: xpv1.ResourceSpec{
				DeletionPolicy:          xpv1.DeletionDelete,
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
			},
			ForProvider: iamv1alpha1.ServiceAccountParameters{
				DisplayName: pointer.String(gcpSAName),
				Description: pointer.String("The service account that import and backup jobs will use to access customer's data."),
			},
		}
		return nil
	}
	if _, err := controllerutil.CreateOrUpdate(ctx, t.Local, gcpSA, mutSA); err != nil {
		return "", err
	}
	key := types.NamespacedName{Namespace: t.APPBox.Namespace, Name: gcpSAName}
	var created iamv1alpha1.ServiceAccount
	if err := t.Local.Get(ctx, key, &created); err != nil {
		return "", err
	}

	var ready bool
	for _, cond := range created.Status.Conditions {
		if cond.Status == corev1.ConditionTrue && cond.Type == xpv1.TypeReady {
			ready = true
			break
		}
	}
	if !ready {
		return "", fmt.Errorf("service account is not ready yet")
	}
	// update sa to status
	if created.Status.AtProvider.Email != "" && t.APPBox.Status.IAM.ServiceAccountEmail == "" {
		network := t.APPBox
		network.Status.IAM.ServiceAccountEmail = created.Status.AtProvider.Email
		return created.Status.AtProvider.Email, t.Local.Status().Update(ctx, network)
	}
	// don't need to updated
	return created.Status.AtProvider.Email, nil
}

func (t *GCPServiceAccount) ensureCreatedSAKey(ctx context.Context) error {
	saName := t.saName()
	keyName := t.saKeyName()
	key := &iamv1alpha1.ServiceAccountKey{
		ObjectMeta: metav1.ObjectMeta{
			Name: keyName,
		},
		Spec: iamv1alpha1.ServiceAccountKeySpec{
			ResourceSpec: xpv1.ResourceSpec{
				DeletionPolicy:          xpv1.DeletionDelete,
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				WriteConnectionSecretToReference: &xpv1.SecretReference{
					Namespace: t.APPBox.Namespace,
					Name:      keyName,
				},
			},
			ForProvider: iamv1alpha1.ServiceAccountKeyParameters{
				ServiceAccountReferer: iamv1alpha1.ServiceAccountReferer{
					ServiceAccountRef: &xpv1.Reference{
						Name: saName,
					},
				},
			},
		},
	}
	// TODO: use CreateOrUpdate instead?
	if err := k8s.Apply(ctx, t.Local, key); err != nil {
		return err
	}

	var ready bool
	for _, cond := range key.Status.Conditions {
		if cond.Status == corev1.ConditionTrue && cond.Type == xpv1.TypeReady {
			ready = true
			break
		}
	}
	if !ready {
		if err := k8s.PatchObject(ctx, t.Local, key, k8s.RemoveCrossplaneExternalCreatePending, k8s.ReconcileTrigger); err != nil {
			return err
		}
		return fmt.Errorf("service account key %s is not ready yet", key.Name)
	}

	return nil
}

func (t *GCPServiceAccount) ensureCreatedSAPolicy(ctx context.Context, saEmail string) deploy.Result {
	// service accounts member pattern for iam binding.
	principalSA := serviceAccountPrefix + saEmail
	policy := &iamv1alpha1.ServiceAccountPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.policyName(),
		},
		Spec: iamv1alpha1.ServiceAccountPolicySpec{
			ResourceSpec: xpv1.ResourceSpec{
				DeletionPolicy:          xpv1.DeletionDelete,
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
			},
			ForProvider: iamv1alpha1.ServiceAccountPolicyParameters{
				ServiceAccountReferer: iamv1alpha1.ServiceAccountReferer{
					ServiceAccountRef: &xpv1.Reference{
						Name: t.saName(),
					},
				},
				Policy: iamv1alpha1.Policy{
					Bindings: []*iamv1alpha1.Binding{
						{Role: roleIAMServiceAccountTokenCreator, Members: []string{principalSA}},
						// Permissions for read-only actions that do not affect state, such as viewing (but not modifying) existing resources or data.
						{Role: roleViewer, Members: []string{principalSA}},
					},
				},
			},
		},
	}

	if err := k8s.Apply(ctx, t.Local, policy); err != nil {
		return deploy.Error(err)
	}

	var ready bool
	for _, cond := range policy.Status.Conditions {
		if cond.Status == corev1.ConditionTrue && cond.Type == xpv1.TypeReady {
			ready = true
			break
		}
	}
	if !ready {
		if err := k8s.PatchObject(ctx, t.Local, policy, k8s.RemoveCrossplaneExternalCreatePending, k8s.ReconcileTrigger); err != nil {
			return deploy.Error(err)
		}
		return deploy.Error(fmt.Errorf("service account policy %s is not ready yet", policy.Name))
	}
	return deploy.Done()
}
