package framework

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	awsconfig "github.com/aws/aws-sdk-go-v2/config"
	"github.com/go-logr/logr"
	"github.com/onsi/ginkgo"
	"github.com/onsi/gomega"
	"go.uber.org/zap"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	apitypes "k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"
	zapopt "sigs.k8s.io/controller-runtime/pkg/log/zap"

	"github.com/tidbcloud/shared-provider/pkg/scheme"
	"github.com/tidbcloud/shared-provider/pkg/types"
	"github.com/tidbcloud/shared-provider/pkg/utils/labels"
	"github.com/tidbcloud/shared-provider/pkg/utils/random"
	"github.com/tidbcloud/shared-provider/tests/framework/cleanup"
	"github.com/tidbcloud/shared-provider/tests/framework/wait"
)

const (
	defaultSuffixLen = 5

	// SuffixUpgrade means a kube cluster have new manager version
	SuffixUpgrade = "upgrade"
	// SuffixAnother means a kube cluster for testing reschedule
	SuffixAnother = "1"
)

var (
	cleanupTimeout = 10 * time.Minute
)

type Framework struct {
	Client      client.Client
	InfraClient client.Client

	AWSConfig aws.Config

	Logger logr.Logger

	once sync.Once

	InfraNamespace string

	KubeClusterNameBase string

	AWSRegion string
	GCPRegion string
	// TODO(liubo02): change to use aws sdk's iam.GetCallerIdentity
	AWSCallerIdentity string

	Provider string

	cleanupAction cleanup.Action
}

func New() *Framework {
	lvl := zap.NewAtomicLevelAt(zap.FatalLevel)
	zapOpts := zapopt.Options{
		Development:     true,
		StacktraceLevel: &lvl,
		DestWriter:      ginkgo.GinkgoWriter,
	}
	f := &Framework{
		KubeClusterNameBase: globalSuite.config.KubeClusterNameBase,
		AWSRegion:           globalSuite.config.AWSRegion,
		GCPRegion:           globalSuite.config.GCPRegion,
		AWSCallerIdentity:   globalSuite.config.AWSCallerIdentity,
		Logger:              zapopt.New(zapopt.UseFlagOptions(&zapOpts)),
	}

	switch globalSuite.config.Cloud {
	case CloudEKS:
		f.Provider = types.ProviderTypeAWSSharedTier
	case CloudGKE:
		f.Provider = types.ProviderTypeGCPSharedTier
	default:
		// use AWS as default provider
		f.Provider = types.ProviderTypeAWSSharedTier
	}

	ginkgo.BeforeEach(f.BeforeEach)
	ginkgo.AfterEach(f.AfterEach)

	return f
}

type testClient struct {
	client.Client
	key string
}

func (c *testClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
	obj.SetLabels(NewE2ELabels(obj.GetLabels(), c.key))
	return c.Client.Create(ctx, obj, opts...)
}

func (c *testClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
	obj.SetLabels(NewE2ELabels(obj.GetLabels(), c.key))
	return c.Client.Update(ctx, obj, opts...)
}

// NOTE(liubo02): some patches can delete label but is not used normally. Just ignore these cases.
func (c *testClient) Patch(
	ctx context.Context,
	obj client.Object,
	patch client.Patch,
	opts ...client.PatchOption,
) error {
	obj.SetLabels(NewE2ELabels(obj.GetLabels(), c.key))
	return c.Client.Patch(ctx, obj, patch, opts...)
}

func newTestClient(cfg *rest.Config, key string) (client.Client, error) {
	c, err := newClient(cfg)
	if err != nil {
		return nil, err
	}
	return &testClient{
		Client: c,
		key:    key,
	}, nil
}

func newClient(cfg *rest.Config) (client.Client, error) {
	c, err := client.New(cfg, client.Options{
		Scheme: scheme.Scheme,
	})
	if err != nil {
		return nil, fmt.Errorf("new client for framework failed: %w", err)
	}

	return c, nil
}

func (f *Framework) BeforeEach() {
	f.once.Do(func() {
		var err error

		usrCfg, err := globalSuite.config.Environment.LoadUserConfig()
		gomega.Expect(err).NotTo(gomega.HaveOccurred())
		f.Client, err = newTestClient(usrCfg, f.KubeClusterNameBase)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		infraCfg, err := globalSuite.config.Environment.LoadInfraAPIConfig()
		gomega.Expect(err).NotTo(gomega.HaveOccurred())
		f.InfraClient, err = newTestClient(infraCfg, f.KubeClusterNameBase)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		awsCfg, err := awsconfig.LoadDefaultConfig(context.TODO())
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		awsCfg.Region = globalSuite.config.AWSRegion
		f.AWSConfig = awsCfg
	})

	ctx := context.Background()

	f.cleanupAction = AddCleanupAction(f.DeleteInfraNamespace)
	f.CreateInfraNamespace(ctx, globalSuite.config.NamespaceBaseName+globalSuite.config.KubeClusterNameBase)
}

func (f *Framework) AfterEach() {
	f.DeleteInfraNamespace()
	RemoveCleanupAction(f.cleanupAction)
}

func (f *Framework) CreateInfraNamespace(ctx context.Context, baseName string) {
	obj := &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name:   baseName + "-" + random.Random(defaultSuffixLen),
			Labels: NewE2ELabels(nil, f.KubeClusterNameBase),
		},
	}
	err := f.InfraClient.Create(ctx, obj)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())

	f.InfraNamespace = obj.GetName()
}

func (f *Framework) DeleteInfraNamespace() {
	if ginkgo.CurrentGinkgoTestDescription().Failed {
		f.Logger.Info("Skip cleanup because the case is failed", "namespace", f.InfraNamespace)
		return
	}
	ctx := context.Background()

	obj := &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: f.InfraNamespace,
		},
	}

	ginkgo.By("try to clean infra namespace")
	err := f.InfraClient.Delete(ctx, obj)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())

	ginkgo.By("wait for infra namespace cleaned")
	err = wait.WaitForObjectDeleted(ctx, f.InfraClient, obj, cleanupTimeout)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())

	ginkgo.By("infra namespace is cleaned")
}

// Schedule always schedule obj to first cluster
func (f *Framework) Schedule(ctx context.Context, obj client.Object) {
	f.ScheduleTo(ctx, obj, "")
}

// ScheduleTo is used to schedule object to specified kube cluster
func (f *Framework) ScheduleTo(ctx context.Context, obj client.Object, suffix string) {
	err := f.InfraClient.Patch(ctx, obj,
		client.RawPatch(apitypes.MergePatchType, []byte(fmt.Sprintf(
			`{"metadata":{"labels":{"%s":"%s"}}}`,
			labels.LabelKeyKubernetesCluster,
			f.KubeClusterName(suffix),
		))),
	)

	gomega.Expect(err).NotTo(gomega.HaveOccurred())
}

func (f *Framework) KubeClusterName(suffix string) string {
	if suffix == "" {
		return f.KubeClusterNameBase
	}

	return f.KubeClusterNameBase + "-" + suffix
}

func (f *Framework) NetworkRegion() string {
	switch f.Provider {
	case types.ProviderTypeAWSSharedTier:
		return f.AWSRegion
	case types.ProviderTypeGCPSharedTier:
		return f.GCPRegion
	}
	return ""
}
