package gke

import (
	"context"
	"fmt"

	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/eks-provider/controllers/network/task"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
)

// 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 {
	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.Network.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.Network.Namespace, Name: t.saKeyName()}}
	saKeyKey := types.NamespacedName{Name: t.saKeyName(), Namespace: t.Network.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 {
	return namer.LegacyBuildGCPNodePoolServiceAccountNameByNetwork(t.Network)
}

func (t *GCPServiceAccount) policyName() string {
	return buildGCPNodePoolServiceAccountPolicyNameByNetwork(t.Network)
}

func (t *GCPServiceAccount) saKeyName() string {
	return namer.LegacyBuildServiceAccountKeyNameByNetwork(t.Network)
}

// ensure service account is created and updated to network status, return nodeSA
func (t *GCPServiceAccount) ensureCreatedSA(ctx context.Context) (string, error) {
	gcpSAName := t.saName()
	refName := namer.UniqueIdentifier(t.Network.ObjectMeta)
	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: refName},
			},
			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.Network.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 network status
	if created.Status.AtProvider.Email != "" && t.Network.Status.IAM.ServiceAccountEmail == "" {
		network := t.Network
		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()
	refName := namer.UniqueIdentifier(t.Network.ObjectMeta)
	key := &iamv1alpha1.ServiceAccountKey{
		ObjectMeta: metav1.ObjectMeta{
			Name: keyName,
		},
		Spec: iamv1alpha1.ServiceAccountKeySpec{
			ResourceSpec: xpv1.ResourceSpec{
				DeletionPolicy:          xpv1.DeletionDelete,
				ProviderConfigReference: &xpv1.Reference{Name: refName},
				WriteConnectionSecretToReference: &xpv1.SecretReference{
					Namespace: t.Network.Namespace,
					Name:      keyName,
				},
			},
			ForProvider: iamv1alpha1.ServiceAccountKeyParameters{
				ServiceAccountReferer: iamv1alpha1.ServiceAccountReferer{
					ServiceAccountRef: &xpv1.Reference{
						Name: saName,
					},
				},
			},
		},
	}

	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 {
		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 {
	refName := namer.UniqueIdentifier(t.Network.ObjectMeta)
	// https://doc.crds.dev/github.com/crossplane/provider-gcp/iam.gcp.crossplane.io/ServiceAccountPolicy/v1alpha1@v0.19.0
	nodeMember := fmt.Sprintf("serviceAccount:%s", saEmail)
	policy := &iamv1alpha1.ServiceAccountPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.policyName(),
		},
		Spec: iamv1alpha1.ServiceAccountPolicySpec{
			ResourceSpec: xpv1.ResourceSpec{
				DeletionPolicy:          xpv1.DeletionDelete,
				ProviderConfigReference: &xpv1.Reference{Name: refName},
			},
			ForProvider: iamv1alpha1.ServiceAccountPolicyParameters{
				ServiceAccountReferer: iamv1alpha1.ServiceAccountReferer{
					ServiceAccountRef: &xpv1.Reference{
						Name: t.saName(),
					},
				},
				Policy: iamv1alpha1.Policy{
					Bindings: []*iamv1alpha1.Binding{
						{Role: "roles/iam.serviceAccountTokenCreator", Members: []string{nodeMember}},
						// Permissions for read-only actions that do not affect state, such as viewing (but not modifying) existing resources or data.
						{Role: "roles/viewer", Members: []string{nodeMember}},
					},
				},
			},
		},
	}

	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 {
		return deploy.Error(fmt.Errorf("service account policy %s is not ready yet", policy.Name))
	}
	return deploy.Done()
}

func buildGCPNodePoolServiceAccountPolicyNameByNetwork(network *cloudv1.Network) string {
	return namer.UniqueIdentifier(network.ObjectMeta, namer.WithPrefix("access-sa-policy"), namer.WithLimit(namer.GKEServiceAccountNameLimit))
}
