package eks

import (
	"context"
	"fmt"

	s3v1alpha3 "github.com/crossplane-contrib/provider-aws/apis/s3/v1alpha3"
	s3v1beta1 "github.com/crossplane-contrib/provider-aws/apis/s3/v1beta1"
	"github.com/crossplane-contrib/provider-aws/apis/v1beta1"
	v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/pingcap/errors"

	"github.com/tidbcloud/eks-provider/api/v1alpha1"
	"github.com/tidbcloud/eks-provider/controllers/network/task"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/log"
)

type ProviderConfig struct {
	task.BaseTask
	crossplaneIgnoreDelete []string
}

// taskToCrossplaneKind generates the crossplane objects list
// associating to the specified tasks. For example, a Bucket task will
// associate to the buckets.s3.aws.crossplane.io object
func taskToCrossplaneKind(ignoreDeletes []string) []string {
	ret := []string{}
	for _, task := range ignoreDeletes {
		switch task {
		case "Bucket":
			ret = append(ret, s3v1beta1.BucketKind, s3v1alpha3.BucketPolicyKind)
		default:
			// ... more crossplane objects to be added.
		}
	}
	return ret
}

func NewProviderConfig(ctx *task.Context) *ProviderConfig {
	cpid := taskToCrossplaneKind(ctx.Config.Network.IgnoreDelete)
	return &ProviderConfig{
		BaseTask:               task.BaseTask{Context: ctx},
		crossplaneIgnoreDelete: cpid,
	}
}

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

// This task must succeed to enable following tasks to execute
func (t *ProviderConfig) Create(ctx context.Context) deploy.Result {
	networkSpec, err := v1alpha1.EKSNetworkSpecForNetwork(t.Network)
	if err != nil {
		return deploy.Panic(err)
	}

	if networkSpec.AssumeRoleArn == "" {
		return deploy.Panic(fmt.Errorf("network assume role arn is empty"))
	}

	// ProviderConfig is a cluster resource instead of namespaced, so we have to construct a unique
	// name for the object.
	name := namer.UniqueIdentifier(t.Network.ObjectMeta)
	config := &v1beta1.ProviderConfig{
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: t.Namer.Labels(),
		},
	}
	_, err = controllerutil.CreateOrUpdate(ctx, t.Local, config, func() error {
		if config.Annotations == nil {
			config.Annotations = make(map[string]string)
		}
		config.Annotations["assume-role"] = networkSpec.AssumeRoleArn
		config.Annotations["external-id"] = networkSpec.AssumeExternalId
		config.Spec = v1beta1.ProviderConfigSpec{
			AssumeRoleARN: pointer.String(networkSpec.AssumeRoleArn),
			ExternalID:    pointer.String(networkSpec.AssumeExternalId),
			Credentials: v1beta1.ProviderCredentials{
				Source: v1.CredentialsSourceInjectedIdentity,
			},
		}
		return nil
	})
	return deploy.PanicOrDone(err)
}

// We already make sure the Create succeed by returning Panic error when failed,
// so we can get rid of the PostCreate check
func (t *ProviderConfig) PostCreate(ctx context.Context) deploy.Result {
	return deploy.Done()
}

func (t *ProviderConfig) ownedProviderConfigUsage(
	ctx context.Context,
	name string,
) (
	[]v1beta1.ProviderConfigUsage,
	error,
) {
	pcul := v1beta1.ProviderConfigUsageList{}
	if err := t.Local.List(ctx, &pcul,
		&client.MatchingLabels{
			consts.CrossplaneProviderConfigLabel: name,
		},
	); err != nil {
		return nil, err
	}
	return pcul.Items, nil
}

// Delete deletes the ProviderConfig resource if there are no crossplane
// resources that need to be ignored for deletion. Otherwise, ensuring all
// dependent ProviderConfigUsages have been removed before returning
// deploy.Done.
func (t *ProviderConfig) Delete(ctx context.Context) deploy.Result {
	if len(t.crossplaneIgnoreDelete) == 0 {
		// delete the ProviderConfig if there is no dependent usages
		name := namer.UniqueIdentifier(t.Network.ObjectMeta)
		config := &v1beta1.ProviderConfig{
			ObjectMeta: metav1.ObjectMeta{
				Name: name,
			},
		}
		return deploy.ErrorOrDone(k8s.DeleteObject(ctx, t.Local, config))
	}
	return deploy.Done()
}

func (t *ProviderConfig) PostDelete(ctx context.Context) deploy.Result {
	name := namer.UniqueIdentifier(t.Network.ObjectMeta)
	if len(t.crossplaneIgnoreDelete) == 0 {
		var c v1beta1.ProviderConfig
		key := types.NamespacedName{
			Name: name,
		}
		return deploy.ErrorOrDone(k8s.GetDeletedObject(ctx, t.Local, key, &c))
	}

	lgr := log.FromContext(ctx)
	usages, err := t.ownedProviderConfigUsage(ctx, name)
	if err != nil {
		return deploy.Error(err)
	}
	// wait until all ProviderConfigUsages to be deleted
	ignoreDelete := func(r string) bool {
		for _, t := range t.crossplaneIgnoreDelete {
			if r == t {
				return true
			}
		}
		return false
	}
	for _, usage := range usages {
		kind := usage.ResourceReference.Kind
		if !ignoreDelete(kind) {
			return deploy.Error(
				errors.Errorf(
					"In used by ProviderConfigUsage(%s/%s)",
					usage.ResourceReference.Kind,
					usage.ResourceReference.Name,
				),
			)
		}
		lgr.Info(
			"ignore the ProviderConfigUsage",
			"ProviderConfig", name,
			"ProviderConfigUsage", usage.GetName(),
			"UsageKind", kind,
		)
	}
	// the ProviderConfig won't be deleted if there are objects
	// that need to be ignored,
	return deploy.Done()
}
