package gke

import (
	"context"
	"fmt"
	"time"

	"github.com/crossplane-contrib/provider-gcp/apis/storage/v1alpha1"
	xpstoragev1alpha1 "github.com/crossplane-contrib/provider-gcp/apis/storage/v1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"github.com/tidbcloud/eks-provider/controllers/cluster/task"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/util"
)

// GCRBucketPolicy modify the bucket policy of the `gcr.io` registry bucket to
// allow the node service account to pull private images from the registry.
type GCRBucketPolicy struct {
	*task.BaseTask
}

func NewGCRBucketPolicy(ctx *task.Context) *GCRBucketPolicy {
	return &GCRBucketPolicy{BaseTask: &task.BaseTask{Context: ctx}}
}

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

func (t *GCRBucketPolicy) Create(ctx context.Context) deploy.Result {
	if !t.IsRegionalManaged() {
		return deploy.Done()
	}

	t.BeginAt = util.Pointer(time.Now())

	// build iam policy member
	policy, err := t.buildBucketPolicyMember(false)
	if err != nil {
		return deploy.Error(err)
	}
	err = k8s.CreateIfNotExist(ctx, t.Local, policy)
	if err != nil {
		return deploy.Error(err)
	}

	// wait for iam policy member to be ready
	ready := policy.GetCondition(xpv1.TypeReady)
	if ready.Status != corev1.ConditionTrue {
		return deploy.Error(fmt.Errorf("bucket policy not ready")).WithRequeuAfter(time.Minute)
	}

	return deploy.Done()
}

// TODO(cluster-controller): implement deletion of this resource
func (t *GCRBucketPolicy) Delete(ctx context.Context) deploy.Result {
	return deploy.Done()
}

// buildBucketPolicyMember builds a `BucketPolicyMember`, which is used to allow
// node service account to pull private images from the registry.  If `onlyMeta` is true, the function
// will only return the `BucketPolicyMember` with the metadata
//
// If the service account email is not ready or invalid, the function will return an error.
func (t *GCRBucketPolicy) buildBucketPolicyMember(onlyMeta bool) (*v1alpha1.BucketPolicyMember, error) {
	var (
		saName  = t.GetGCPNodePoolServiceAccountName()
		saEmail = t.GetGCPNodePoolServiceAccountEmail()
	)
	policy := &xpstoragev1alpha1.BucketPolicyMember{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.policyMemberNameForSA(saName),
		},
	}

	if onlyMeta {
		return policy, nil
	}

	policy.Spec = xpstoragev1alpha1.BucketPolicyMemberSpec{
		ResourceSpec: xpv1.ResourceSpec{
			DeletionPolicy:          xpv1.DeletionDelete,
			ProviderConfigReference: &xpv1.Reference{Name: t.GetGCPProviderConfigName()},
		},
		ForProvider: xpstoragev1alpha1.BucketPolicyMemberParameters{
			// the project is something like `pingcap-gardener`, not `tidbcloud-dev-000`
			Bucket: util.Pointer(gcrBucketName(t.Config.FullManage.GCPProjectID)),
			Role:   "roles/storage.objectViewer",
			Member: util.Pointer(fmt.Sprintf("serviceAccount:%s", saEmail)),
		},
	}

	return policy, nil
}

func (t *GCRBucketPolicy) policyMemberNameForSA(saName string) string {
	return fmt.Sprintf("view-gcr-%s", saName)
}

// gcrBucketName returns the bucket name of the `gcr.io` registry.
func gcrBucketName(projectID string) string {
	// ref: https://cloud.google.com/container-registry/docs/access-control#grant
	return fmt.Sprintf("artifacts.%s.appspot.com", projectID)
}
