package framework

import (
	"context"
	"fmt"
	"os"
	"strconv"
	"sync"
	"time"

	awsconfig "github.com/aws/aws-sdk-go-v2/config"
	"github.com/go-logr/logr"
	"github.com/onsi/ginkgo/v2"
	"github.com/onsi/gomega"
	"github.com/tidbcloud/aws-shared-provider/pkg/scheme"
	"github.com/tidbcloud/aws-shared-provider/pkg/utils/random"
	"github.com/tidbcloud/aws-shared-provider/tests/framework/wait"
	"github.com/tidbcloud/pkgv2/gormutil"
	infra "github.com/tidbcloud/tidb-management-service/api/serverless_infra"
	infraclient "github.com/tidbcloud/tidb-management-service/api/serverless_infra/client"
	inframysql "github.com/tidbcloud/tidb-management-service/api/serverless_infra/client/mysql"
	"github.com/tidbcloud/tidb-management-service/api/serverless_infra/meta"
	"go.uber.org/zap"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"
	zapopt "sigs.k8s.io/controller-runtime/pkg/log/zap"
)

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 = 30 * time.Minute
)

type Framework struct {
	InfraClusterCli infraclient.Client
	Client          client.Client

	Logger logr.Logger

	once sync.Once

	InfraNamespace string

	KubeClusterNameBase string

	AWSRegion string

	TiDBPoolNamespace string
}

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,
		TiDBPoolNamespace:   globalSuite.config.TiDBPoolNamespace,
		Logger:              zapopt.New(zapopt.UseFlagOptions(&zapOpts)),
	}

	ginkgo.BeforeEach(f.BeforeEach)
	AddCleanupAction(f.DeleteInfraNamespace)

	return f
}

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 = newClient(usrCfg)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

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

		user := os.Getenv("RDS_USERNAME")
		password := os.Getenv("RDS_PASSWORD")
		dbname := os.Getenv("RDS_DBNAME")
		portStr := os.Getenv("RDS_PORT")
		dbHost := os.Getenv("RDS_CONNECT_HOST")
		if dbHost == "" {
			dbHost = "127.0.0.1"
		}

		port, err := strconv.ParseUint(portStr, 10, 16)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())
		connCfg := &gormutil.Config{
			User:     user,
			Host:     dbHost,
			Database: dbname,
			Password: password,
			Port:     uint16(port),
		}

		indexField2name := map[string]string{
			"status.zone_redundant_enabled-index": "cluster_statuses:zone_redundant_enabled",
			"status.keyspace_token-index":         "clusters:keyspace_token",
			"namespace-index":                     "clusters:namespace",
		}
		infraClusterCli, err := inframysql.NewMySQLClient(connCfg, "E2E", f.KubeClusterNameBase, indexField2name, 0, false)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())
		f.InfraClusterCli = infraClusterCli
	})

	ctx := context.Background()

	f.CreateInfraNamespace(ctx, globalSuite.config.NamespaceBaseName+globalSuite.config.KubeClusterNameBase)
}

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

	f.InfraNamespace = obj.GetName()
}

func (f *Framework) DeleteInfraNamespace() {
	if ginkgo.CurrentSpecReport().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.Client.Delete(ctx, obj)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())

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

	clusterKeys := make([]meta.ObjectKey, 0)
	err = f.InfraClusterCli.GetIndex(ctx, f.InfraNamespace, "namespace-index", &clusterKeys)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())
	for _, clusterKey := range clusterKeys {
		cluster := &infra.Cluster{ObjectMeta: meta.ObjectMeta{Namespace: clusterKey.Namespace, Name: clusterKey.Name}}
		err = f.InfraClusterCli.Get(ctx, cluster)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())
		err = f.InfraClusterCli.Delete(ctx, cluster)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())
	}

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